Passed
Pull Request — master (#174)
by
unknown
05:42
created

Signer::existsSignature()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2.0054

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 8
cts 9
cp 0.8889
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 9
nc 2
nop 1
crap 2.0054
1
<?php
2
3
namespace NFePHP\Common;
4
5
/**
6
 * Class to signner a Xml
7
 * Meets packages:
8
 *     sped-nfe,
9
 *     sped-cte,
10
 *     sped-mdfe,
11
 *     sped-nfse,
12
 *     sped-efinanceira
13
 *     sped-esocial
14
 *     sped-efdreinf
15
 *     e sped-esfinge
16
 *
17
 * @category  NFePHP
18
 * @package   NFePHP\Common\Signer
19
 * @copyright NFePHP Copyright (c) 2016
20
 * @license   http://www.gnu.org/licenses/lgpl.txt LGPLv3+
21
 * @license   https://opensource.org/licenses/MIT MIT
22
 * @license   http://www.gnu.org/licenses/gpl.txt GPLv3+
23
 * @author    Roberto L. Machado <linux.rlm at gmail dot com>
24
 * @link      http://github.com/nfephp-org/sped-common for the canonical source repository
25
 */
26
27
use NFePHP\Common\Certificate\PublicKey;
28
use NFePHP\Common\Exception\SignerException;
29
use DOMDocument;
30
use DOMNode;
31
use DOMElement;
32
33
class Signer
34
{
35
    const CANONICAL = array(true,false,null,null);
36
    private static $canonical = self::CANONICAL;
37
38
    /**
39
     * Make Signature for multiple tags within the document.
40
     *
41
     * @param Certificate $certificate
42
     * @param \DOMDocument $doc The XML to sign.
43
     * @param array $tags the tags to include signature.
44
     * @param string $mark for URI (optional).
45
     * @param int $algorithm (optional).
46
     * @param array $canonical parameters to format node for signature (optional).
47
     *
48
     * @return \DOMDocument
49
     *
50
     * @throws SignerException
51
     */
52
    public static function signMultipleTags(
53
        Certificate $certificate,
54
        DOMDocument $doc,
55
        array $tags,
56
        string $mark = 'Id',
57
        int $algorithm = OPENSSL_ALGO_SHA1,
58
        $canonical = self::CANONICAL
59
    ): DOMDocument {
60
        $signed = $doc;
61
62
        foreach ($tags as $tagName) {
63
            $node = $signed->getElementsByTagName($tagName)->item(0); //TODO: loop here too when multiple tags found.
64
            
65
            $signed = self::sign(
66
                $certificate,
67
                $signed,
68
                $node,
69
                $mark,
70
                $algorithm,
71
                $canonical
72
            );
73
74
            if (!Signer::isSigned($signed->saveXML(), $tagName)) {
75
                throw SignerException::signatureComparisonFailed();
76
            }
77
        }
78
79
        return $signed;
80
    }
81
    
82
    /**
83
     * Make the Signature tag.
84
     *
85
     * @param Certificate $certificate
86
     * @param \DOMDocument $content xml to sign
87
     * @param \DOMNode $tagNode The tagNode to be signed
88
     * @param string $mark for URI (optional)
89
     * @param int $algorithm (optional)
90
     * @param array $canonical parameters to format node for signature (optional)
91
     *
92
     * @return \DOMDocument
93
     *
94
     * @throws SignerException
95
     */
96 1
    public static function sign(
97
        Certificate $certificate,
98
        DOMDocument $content,
99
        DOMNode $tagNode,
100
        string $mark = 'Id',
101
        int $algorithm = OPENSSL_ALGO_SHA1,
102
        $canonical = self::CANONICAL
103
    ): DOMDocument {
104 1
        if (!empty($canonical)) {
105 1
            self::$canonical = $canonical;
106
        }
107
        
108 1
        return self::createSignature(
109
            $certificate,
110
            $content,
111
            $tagNode,
0 ignored issues
show
Compatibility introduced by
$tagNode of type object<DOMNode> is not a sub-type of object<DOMElement>. It seems like you assume a child class of the class DOMNode to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
112
            $mark,
113
            $algorithm,
114
            $canonical
115
        );
116
    }
117
    
118
    /**
119
     * Method that provides the signature of xml as standard SEFAZ.
120
     *
121
     * @param Certificate $certificate
122
     * @param \DOMDocument $dom The original document
123
     * @param \DOMElement $node node to be signed
124
     * @param string $mark Marker signed attribute
125
     * @param int $algorithm cryptographic algorithm (optional)
126
     * @param array $canonical parameters to format node for signature (optional)
127
     *
128
     * @return \DOMDocument
129
     */
130 1
    private static function createSignature(
131
        Certificate $certificate,
132
        DOMDocument $dom,
133
        DOMElement $node,
134
        string $mark,
135
        int $algorithm = OPENSSL_ALGO_SHA1,
136
        $canonical = self::CANONICAL
137
    ): DOMDocument {
138 1
        $nsDSIG = 'http://www.w3.org/2000/09/xmldsig#';
139 1
        $nsCannonMethod = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
140 1
        $nsSignatureMethod = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
141 1
        $nsDigestMethod = 'http://www.w3.org/2000/09/xmldsig#sha1';
142 1
        $digestAlgorithm = 'sha1';
143 1
        if ($algorithm == OPENSSL_ALGO_SHA256) {
144
            $digestAlgorithm = 'sha256';
145
            $nsSignatureMethod = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256';
146
            $nsDigestMethod = 'http://www.w3.org/2001/04/xmlenc#sha256';
147
        }
148 1
        $nsTransformMethod1 ='http://www.w3.org/2000/09/xmldsig#enveloped-signature';
149 1
        $nsTransformMethod2 = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
150 1
        $idSigned = trim($node->getAttribute($mark));
151 1
        $signatureNode = $dom->createElementNS($nsDSIG, 'Signature');
152 1
        $signatureNode->setAttribute('Id', "Ass_$idSigned");
153 1
        $node->parentNode->appendChild($signatureNode);
154 1
        $signedInfoNode = $dom->createElement('SignedInfo');
155 1
        $signatureNode->appendChild($signedInfoNode);
156 1
        $canonicalNode = $dom->createElement('CanonicalizationMethod');
157 1
        $signedInfoNode->appendChild($canonicalNode);
158 1
        $canonicalNode->setAttribute('Algorithm', $nsCannonMethod);
159 1
        $signatureMethodNode = $dom->createElement('SignatureMethod');
160 1
        $signedInfoNode->appendChild($signatureMethodNode);
161 1
        $signatureMethodNode->setAttribute('Algorithm', $nsSignatureMethod);
162 1
        $referenceNode = $dom->createElement('Reference');
163 1
        $signedInfoNode->appendChild($referenceNode);
164
        
165 1
        if (!empty($idSigned)) {
166 1
            $idSigned = "#$idSigned";
167
        }
168
        
169 1
        $referenceNode->setAttribute('URI', $idSigned);
170 1
        $transformsNode = $dom->createElement('Transforms');
171 1
        $referenceNode->appendChild($transformsNode);
172 1
        $transfNode1 = $dom->createElement('Transform');
173 1
        $transformsNode->appendChild($transfNode1);
174 1
        $transfNode1->setAttribute('Algorithm', $nsTransformMethod1);
175 1
        $transfNode2 = $dom->createElement('Transform');
176 1
        $transformsNode->appendChild($transfNode2);
177 1
        $transfNode2->setAttribute('Algorithm', $nsTransformMethod2);
178 1
        $digestMethodNode = $dom->createElement('DigestMethod');
179 1
        $referenceNode->appendChild($digestMethodNode);
180 1
        $digestMethodNode->setAttribute('Algorithm', $nsDigestMethod);
181 1
        $digestValue = self::makeDigest($node, $digestAlgorithm, $canonical);
182 1
        $digestValueNode = $dom->createElement('DigestValue', $digestValue);
183 1
        $referenceNode->appendChild($digestValueNode);
184 1
        $c14n = self::canonize($signedInfoNode, $canonical);
185 1
        $signature = $certificate->sign($c14n, $algorithm);
186 1
        $signatureValue = base64_encode($signature);
187 1
        $signatureValueNode = $dom->createElement('SignatureValue', $signatureValue);
188 1
        $signatureNode->appendChild($signatureValueNode);
189 1
        $keyInfoNode = $dom->createElement('KeyInfo');
190 1
        $signatureNode->appendChild($keyInfoNode);
191 1
        $x509DataNode = $dom->createElement('X509Data');
192 1
        $keyInfoNode->appendChild($x509DataNode);
193 1
        $pubKeyClean = $certificate->publicKey->unFormated();
194 1
        $x509CertificateNode = $dom->createElement('X509Certificate', $pubKeyClean);
195 1
        $x509DataNode->appendChild($x509CertificateNode);
196
197 1
        return $dom;
198
    }
199
200
    /**
201
     * Remove old signature from document to replace it
202
     * @param string $content
203
     * @return string
204
     */
205 1
    public static function removeSignature(string $content): string
206
    {
207 1
        if (!self::existsSignature($content)) {
208
            return $content;
209
        }
210
211 1
        $dom = new \DOMDocument('1.0', 'utf-8');
212 1
        $dom->formatOutput = false;
213 1
        $dom->preserveWhiteSpace = false;
214 1
        $dom->loadXML($content);
215 1
        $node = $dom->documentElement;
216 1
        $signature = $node->getElementsByTagName('Signature')->item(0);
217
218 1
        if (!empty($signature)) {
219 1
            $parent = $signature->parentNode;
220 1
            $parent->removeChild($signature);
221
        }
222
223 1
        return $dom->saveXML();
224
    }
225
226
    /**
227
     * Verify if xml signature is valid
228
     * @param string $content
229
     * @param string $tagname tag for sign (optional)
230
     * @param array $canonical parameters to format node for signature (optional)
231
     * @return bool
232
     * @throws SignerException Not is a XML, Digest or Signature dont match
233
     */
234 6
    public static function isSigned(string $content, string $tagname = '', $canonical = self::CANONICAL): bool
235
    {
236 6
        if (!self::existsSignature($content)) {
237 1
            return false;
238
        }
239
240 5
        if (!self::digestCheck($content, $tagname, $canonical)) {
241
            return false;
242
        }
243
244 3
        return self::signatureCheck($content, $canonical);
245
    }
246
    
247
    /**
248
     * Check if Signature tag already exists
249
     * @param string $content
250
     * @return boolean
251
     */
252 6
    public static function existsSignature(string $content): bool
253
    {
254 6
        if (!Validator::isXML($content)) {
255
            throw SignerException::isNotXml();
256
        }
257
258 6
        $dom = new \DOMDocument('1.0', 'utf-8');
259 6
        $dom->formatOutput = false;
260 6
        $dom->preserveWhiteSpace = false;
261 6
        $dom->loadXML($content);
262 6
        $signature = $dom->getElementsByTagName('Signature')->item(0);
263
264 6
        return !empty($signature);
265
    }
266
    
267
    /**
268
     * Verify signature value from SignatureInfo node and public key
269
     * @param string $xml
270
     * @param array $canonical
271
     * @return boolean
272
     */
273 3
    private static function signatureCheck(string $xml, $canonical = self::CANONICAL): bool
274
    {
275 3
        $dom = new \DOMDocument('1.0', 'utf-8');
276 3
        $dom->formatOutput = false;
277 3
        $dom->preserveWhiteSpace = false;
278 3
        $dom->loadXML($xml);
279
        
280 3
        $signature = $dom->getElementsByTagName('Signature')->item(0);
281 3
        $sigMethAlgo = $signature->getElementsByTagName('SignatureMethod')->item(0)->getAttribute('Algorithm');
282 3
        $algorithm = OPENSSL_ALGO_SHA256;
283
284 3
        if ($sigMethAlgo == 'http://www.w3.org/2000/09/xmldsig#rsa-sha1') {
285 3
            $algorithm = OPENSSL_ALGO_SHA1;
286
        }
287
288 3
        $certificateContent = $signature->getElementsByTagName('X509Certificate')->item(0)->nodeValue;
289 3
        $publicKey = PublicKey::createFromContent($certificateContent);
290 3
        $signInfoNode = self::canonize(
291 3
            $signature->getElementsByTagName('SignedInfo')->item(0),
292
            $canonical
293
        );
294 3
        $signatureValue = $signature->getElementsByTagName('SignatureValue')->item(0)->nodeValue;
295 3
        $decodedSignature = base64_decode(
296 3
            str_replace(array("\r", "\n"), '', $signatureValue)
297
        );
298
299 3
        if (!$publicKey->verify($signInfoNode, $decodedSignature, $algorithm)) {
300 1
            throw SignerException::signatureComparisonFailed();
301
        }
302
303 2
        return true;
304
    }
305
    
306
    /**
307
     * Verify digest value of data node
308
     * @param string $xml
309
     * @param string $tagname
310
     * @param array $canonical
311
     * @return bool
312
     * @throws SignerException
313
     */
314 5
    private static function digestCheck(string $xml, string $tagname = '', $canonical = self::CANONICAL): bool
315
    {
316 5
        $dom = new \DOMDocument('1.0', 'utf-8');
317 5
        $dom->formatOutput = false;
318 5
        $dom->preserveWhiteSpace = false;
319 5
        $dom->loadXML($xml);
320
321 5
        $signature = null;
0 ignored issues
show
Unused Code introduced by
$signature is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
322 5
        $sigURI = null;
0 ignored issues
show
Unused Code introduced by
$sigURI is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
323 5
        $node = null;
0 ignored issues
show
Unused Code introduced by
$node is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
324
325 5
        if (empty($tagname)) {
326 4
            $signature = $dom->getElementsByTagName('Signature')->item(0);
327 4
            $sigURI = $signature->getElementsByTagName('Reference')->item(0)->getAttribute('URI');
328 4
            if (empty($sigURI)) {
329
                $tagname = $dom->documentElement->nodeName;
330
            } else {
331 4
                $xpath = new \DOMXPath($dom);
332 4
                $entries = $xpath->query('//@Id');
333 4
                foreach ($entries as $entry) {
334 4
                    $tagname = $entry->ownerElement->nodeName;
335 4
                    break;
336
                }
337
            }
338 4
            $node = $dom->getElementsByTagName($tagname)->item(0);
339 4
            if (empty($node)) {
340 4
                throw SignerException::tagNotFound($tagname);
341
            }
342
        } else {
343 1
            $node = $dom->getElementsByTagName($tagname)->item(0);
344 1
            if (empty($node)) {
345 1
                throw SignerException::tagNotFound($tagname);
346
            }
347
            
348
            $signature = $node->nextSibling;
349
            if ($signature->nodeName !== 'Signature') {
350
                throw SignerException::tagNotFound('Signature');
351
            }
352
            
353
            $sigURI = $signature->getElementsByTagName('Reference')->item(0)->getAttribute('URI');
354
        }
355
356 4
        $sigMethAlgo = $signature->getElementsByTagName('SignatureMethod')->item(0)->getAttribute('Algorithm');
357 4
        $algorithm = 'sha256';
358
359 4
        if ($sigMethAlgo == 'http://www.w3.org/2000/09/xmldsig#rsa-sha1') {
360 4
            $algorithm = 'sha1';
361
        }
362
363 4
        if ($sigURI == '') {
364
            $node->removeChild($signature);
365
        }
366
367 4
        $calculatedDigest = self::makeDigest($node, $algorithm, $canonical);
368 4
        $informedDigest = $signature->getElementsByTagName('DigestValue')->item(0)->nodeValue;
369
370 4
        if ($calculatedDigest != $informedDigest) {
371 1
            throw SignerException::digestComparisonFailed();
372
        }
373
        
374 3
        return true;
375
    }
376
    
377
    /**
378
     * Calculate digest value for given node
379
     * @param DOMNode $node
380
     * @param string $algorithm
381
     * @param array $canonical
382
     * @return string
383
     */
384 4
    private static function makeDigest(DOMNode $node, string $algorithm, $canonical = self::CANONICAL): string
385
    {
386 4
        $c14n = self::canonize($node, $canonical);
387 4
        $hashValue = hash($algorithm, $c14n, true);
388
389 4
        return base64_encode($hashValue);
390
    }
391
    
392
    /**
393
     * Reduced to the canonical form
394
     * @param DOMNode $node
395
     * @param array $canonical
396
     * @return string
397
     */
398 4
    private static function canonize(DOMNode $node, $canonical = self::CANONICAL): string
399
    {
400 4
        return $node->C14N(
401 4
            $canonical[0],
402 4
            $canonical[1],
403 4
            $canonical[2],
404 4
            $canonical[3]
405
        );
406
    }
407
}
408