Passed
Pull Request — master (#174)
by
unknown
11:30
created

Signer   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 358
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 87.88%

Importance

Changes 0
Metric Value
wmc 31
lcom 1
cbo 4
dl 0
loc 358
ccs 145
cts 165
cp 0.8788
rs 9.8
c 0
b 0
f 0

10 Methods

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