Completed
Pull Request — master (#174)
by
unknown
05:20
created

Signer::makeDigest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 3
crap 1
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
    {
61
        $signed = $doc;
62
63
        foreach ($tags as $tagName) {
64
            $node = $signed->getElementsByTagName($tagName)->item(0); //TODO: loop here too when multiple tags found.
65
            
66
            $signed = self::sign(
67
                $certificate,
68
                $signed,
69
                $node,
70
                $mark,
71
                $algorithm,
72
                $canonical
73
            );
74
75
            if (!Signer::isSigned($signed->saveXML(), $tagName)) {
76
                throw SignerException::signatureComparisonFailed();
77
            }
78
        }
79
80
        return $signed;
81
    }
82
    
83
    /**
84
     * Make the Signature tag.
85
     *
86
     * @param Certificate $certificate
87
     * @param \DOMDocument $content xml to sign
88
     * @param \DOMNode $tagNode The tagNode to be signed
89
     * @param string $mark for URI (optional)
90
     * @param int $algorithm (optional)
91
     * @param array $canonical parameters to format node for signature (optional)
92
     *
93
     * @return \DOMDocument
94
     *
95
     * @throws SignerException
96
     */
97 1
    public static function sign(
98
        Certificate $certificate,
99
        DOMDocument $content,
100
        DOMNode $tagNode,
101
        string $mark = 'Id',
102
        int $algorithm = OPENSSL_ALGO_SHA1,
103
        $canonical = self::CANONICAL
104
    ): DOMDocument
105
    {
106 1
        if (!empty($canonical)) {
107 1
            self::$canonical = $canonical;
108
        }
109
        
110 1
        return self::createSignature(
111
            $certificate,
112
            $content,
113
            $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...
114
            $mark,
115
            $algorithm,
116
            $canonical
117
        );
118
    }
119
    
120
    /**
121
     * Method that provides the signature of xml as standard SEFAZ.
122
     *
123
     * @param Certificate $certificate
124
     * @param \DOMDocument $dom The original document
125
     * @param \DOMElement $node node to be signed
126
     * @param string $mark Marker signed attribute
127
     * @param int $algorithm cryptographic algorithm (optional)
128
     * @param array $canonical parameters to format node for signature (optional)
129
     *
130
     * @return \DOMDocument
131
     */
132 1
    private static function createSignature(
133
        Certificate $certificate,
134
        DOMDocument $dom,
135
        DOMElement $node,
136
        string $mark,
137
        int $algorithm = OPENSSL_ALGO_SHA1,
138
        $canonical = self::CANONICAL
139
    ): DOMDocument {
140 1
        $nsDSIG = 'http://www.w3.org/2000/09/xmldsig#';
141 1
        $nsCannonMethod = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
142 1
        $nsSignatureMethod = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
143 1
        $nsDigestMethod = 'http://www.w3.org/2000/09/xmldsig#sha1';
144 1
        $digestAlgorithm = 'sha1';
145 1
        if ($algorithm == OPENSSL_ALGO_SHA256) {
146
            $digestAlgorithm = 'sha256';
147
            $nsSignatureMethod = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256';
148
            $nsDigestMethod = 'http://www.w3.org/2001/04/xmlenc#sha256';
149
        }
150 1
        $nsTransformMethod1 ='http://www.w3.org/2000/09/xmldsig#enveloped-signature';
151 1
        $nsTransformMethod2 = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
152 1
        $idSigned = trim($node->getAttribute($mark));
153 1
        $signatureNode = $dom->createElementNS($nsDSIG, 'Signature');
154 1
        $signatureNode->setAttribute('Id', "Ass_$idSigned");
155 1
        $node->parentNode->appendChild($signatureNode);
156 1
        $signedInfoNode = $dom->createElement('SignedInfo');
157 1
        $signatureNode->appendChild($signedInfoNode);
158 1
        $canonicalNode = $dom->createElement('CanonicalizationMethod');
159 1
        $signedInfoNode->appendChild($canonicalNode);
160 1
        $canonicalNode->setAttribute('Algorithm', $nsCannonMethod);
161 1
        $signatureMethodNode = $dom->createElement('SignatureMethod');
162 1
        $signedInfoNode->appendChild($signatureMethodNode);
163 1
        $signatureMethodNode->setAttribute('Algorithm', $nsSignatureMethod);
164 1
        $referenceNode = $dom->createElement('Reference');
165 1
        $signedInfoNode->appendChild($referenceNode);
166
        
167 1
        if (!empty($idSigned)) {
168 1
            $idSigned = "#$idSigned";
169
        }
170
        
171 1
        $referenceNode->setAttribute('URI', $idSigned);
172 1
        $transformsNode = $dom->createElement('Transforms');
173 1
        $referenceNode->appendChild($transformsNode);
174 1
        $transfNode1 = $dom->createElement('Transform');
175 1
        $transformsNode->appendChild($transfNode1);
176 1
        $transfNode1->setAttribute('Algorithm', $nsTransformMethod1);
177 1
        $transfNode2 = $dom->createElement('Transform');
178 1
        $transformsNode->appendChild($transfNode2);
179 1
        $transfNode2->setAttribute('Algorithm', $nsTransformMethod2);
180 1
        $digestMethodNode = $dom->createElement('DigestMethod');
181 1
        $referenceNode->appendChild($digestMethodNode);
182 1
        $digestMethodNode->setAttribute('Algorithm', $nsDigestMethod);
183 1
        $digestValue = self::makeDigest($node, $digestAlgorithm, $canonical);
184 1
        $digestValueNode = $dom->createElement('DigestValue', $digestValue);
185 1
        $referenceNode->appendChild($digestValueNode);
186 1
        $c14n = self::canonize($signedInfoNode, $canonical);
187 1
        $signature = $certificate->sign($c14n, $algorithm);
188 1
        $signatureValue = base64_encode($signature);
189 1
        $signatureValueNode = $dom->createElement('SignatureValue', $signatureValue);
190 1
        $signatureNode->appendChild($signatureValueNode);
191 1
        $keyInfoNode = $dom->createElement('KeyInfo');
192 1
        $signatureNode->appendChild($keyInfoNode);
193 1
        $x509DataNode = $dom->createElement('X509Data');
194 1
        $keyInfoNode->appendChild($x509DataNode);
195 1
        $pubKeyClean = $certificate->publicKey->unFormated();
196 1
        $x509CertificateNode = $dom->createElement('X509Certificate', $pubKeyClean);
197 1
        $x509DataNode->appendChild($x509CertificateNode);
198
199 1
        return $dom;
200
    }
201
202
    /**
203
     * Remove old signature from document to replace it
204
     * @param string $content
205
     * @return string
206
     */
207 1
    public static function removeSignature(string $content): string
208
    {
209 1
        if (!self::existsSignature($content)) {
210
            return $content;
211
        }
212
213 1
        $dom = new \DOMDocument('1.0', 'utf-8');
214 1
        $dom->formatOutput = false;
215 1
        $dom->preserveWhiteSpace = false;
216 1
        $dom->loadXML($content);
217 1
        $node = $dom->documentElement;
218 1
        $signature = $node->getElementsByTagName('Signature')->item(0);
219
220 1
        if (!empty($signature)) {
221 1
            $parent = $signature->parentNode;
222 1
            $parent->removeChild($signature);
223
        }
224
225 1
        return $dom->saveXML();
226
    }
227
228
    /**
229
     * Verify if xml signature is valid
230
     * @param string $content
231
     * @param string $tagname tag for sign (optional)
232
     * @param array $canonical parameters to format node for signature (optional)
233
     * @return bool
234
     * @throws SignerException Not is a XML, Digest or Signature dont match
235
     */
236 6
    public static function isSigned(string $content, string $tagname = '', $canonical = self::CANONICAL): bool
237
    {
238 6
        if (!self::existsSignature($content)) {
239 1
            return false;
240
        }
241
242 5
        if (!self::digestCheck($content, $tagname, $canonical)) {
243
            return false;
244
        }
245
246 3
        return self::signatureCheck($content, $canonical);
247
    }
248
    
249
    /**
250
     * Check if Signature tag already exists
251
     * @param string $content
252
     * @return boolean
253
     */
254 6
    public static function existsSignature(string $content): bool
255
    {
256 6
        if (!Validator::isXML($content)) {
257
            throw SignerException::isNotXml();
258
        }
259
260 6
        $dom = new \DOMDocument('1.0', 'utf-8');
261 6
        $dom->formatOutput = false;
262 6
        $dom->preserveWhiteSpace = false;
263 6
        $dom->loadXML($content);
264 6
        $signature = $dom->getElementsByTagName('Signature')->item(0);
265
266 6
        return !empty($signature);
267
    }
268
    
269
    /**
270
     * Verify signature value from SignatureInfo node and public key
271
     * @param string $xml
272
     * @param array $canonical
273
     * @return boolean
274
     */
275 3
    private static function signatureCheck(string $xml, $canonical = self::CANONICAL): bool
276
    {
277 3
        $dom = new \DOMDocument('1.0', 'utf-8');
278 3
        $dom->formatOutput = false;
279 3
        $dom->preserveWhiteSpace = false;
280 3
        $dom->loadXML($xml);
281
        
282 3
        $signature = $dom->getElementsByTagName('Signature')->item(0);
283 3
        $sigMethAlgo = $signature->getElementsByTagName('SignatureMethod')->item(0)->getAttribute('Algorithm');
284 3
        $algorithm = OPENSSL_ALGO_SHA256;
285
286 3
        if ($sigMethAlgo == 'http://www.w3.org/2000/09/xmldsig#rsa-sha1') {
287 3
            $algorithm = OPENSSL_ALGO_SHA1;
288
        }
289
290 3
        $certificateContent = $signature->getElementsByTagName('X509Certificate')->item(0)->nodeValue;
291 3
        $publicKey = PublicKey::createFromContent($certificateContent);
292 3
        $signInfoNode = self::canonize(
293 3
            $signature->getElementsByTagName('SignedInfo')->item(0),
294
            $canonical
295
        );
296 3
        $signatureValue = $signature->getElementsByTagName('SignatureValue')->item(0)->nodeValue;
297 3
        $decodedSignature = base64_decode(
298 3
            str_replace(array("\r", "\n"), '', $signatureValue)
299
        );
300
301 3
        if (!$publicKey->verify($signInfoNode, $decodedSignature, $algorithm)) {
302 1
            throw SignerException::signatureComparisonFailed();
303
        }
304
305 2
        return true;
306
    }
307
    
308
    /**
309
     * Verify digest value of data node
310
     * @param string $xml
311
     * @param string $tagname
312
     * @param array $canonical
313
     * @return bool
314
     * @throws SignerException
315
     */
316 5
    private static function digestCheck(string $xml, string $tagname = '', $canonical = self::CANONICAL): bool
317
    {
318 5
        $dom = new \DOMDocument('1.0', 'utf-8');
319 5
        $dom->formatOutput = false;
320 5
        $dom->preserveWhiteSpace = false;
321 5
        $dom->loadXML($xml);
322
323 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...
324 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...
325 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...
326
327 5
        if (empty($tagname)) {
328 4
            $signature = $dom->getElementsByTagName('Signature')->item(0);
329 4
            $sigURI = $signature->getElementsByTagName('Reference')->item(0)->getAttribute('URI');
330 4
            if (empty($sigURI)) {
331
                $tagname = $dom->documentElement->nodeName;
332
            } else {
333 4
                $xpath = new \DOMXPath($dom);
334 4
                $entries = $xpath->query('//@Id');
335 4
                foreach ($entries as $entry) {
336 4
                    $tagname = $entry->ownerElement->nodeName;
337 4
                    break;
338
                } 
339
            }
340 4
            $node = $dom->getElementsByTagName($tagname)->item(0);            
341 4
            if (empty($node)) {
342 4
                throw SignerException::tagNotFound($tagname);
343
            }
344
        }
345
        else {
346 1
            $node = $dom->getElementsByTagName($tagname)->item(0);
347 1
            if (empty($node)) {
348 1
                throw SignerException::tagNotFound($tagname);
349
            }
350
            
351
            $signature = $node->nextSibling;
352
            if ($signature->nodeName !== 'Signature') {
353
                throw SignerException::tagNotFound('Signature');
354
            }
355
            
356
            $sigURI = $signature->getElementsByTagName('Reference')->item(0)->getAttribute('URI');
357
        }
358
359 4
        $sigMethAlgo = $signature->getElementsByTagName('SignatureMethod')->item(0)->getAttribute('Algorithm');
360 4
        $algorithm = 'sha256';
361
362 4
        if ($sigMethAlgo == 'http://www.w3.org/2000/09/xmldsig#rsa-sha1') {
363 4
            $algorithm = 'sha1';
364
        }
365
366 4
        if ($sigURI == '') {
367
            $node->removeChild($signature);
368
        }
369
370 4
        $calculatedDigest = self::makeDigest($node, $algorithm, $canonical);
371 4
        $informedDigest = $signature->getElementsByTagName('DigestValue')->item(0)->nodeValue;
372
373 4
        if ($calculatedDigest != $informedDigest) {
374 1
            throw SignerException::digestComparisonFailed();
375
        }
376
        
377 3
        return true;
378
    }
379
    
380
    /**
381
     * Calculate digest value for given node
382
     * @param DOMNode $node
383
     * @param string $algorithm
384
     * @param array $canonical
385
     * @return string
386
     */
387 4
    private static function makeDigest(DOMNode $node, string $algorithm, $canonical = self::CANONICAL): string
388
    {
389 4
        $c14n = self::canonize($node, $canonical);
390 4
        $hashValue = hash($algorithm, $c14n, true);
391
392 4
        return base64_encode($hashValue);
393
    }
394
    
395
    /**
396
     * Reduced to the canonical form
397
     * @param DOMNode $node
398
     * @param array $canonical
399
     * @return string
400
     */
401 4
    private static function canonize(DOMNode $node, $canonical = self::CANONICAL): string
402
    {
403 4
        return $node->C14N(
404 4
            $canonical[0],
405 4
            $canonical[1],
406 4
            $canonical[2],
407 4
            $canonical[3]
408
        );
409
    }
410
}
411