Passed
Pull Request — master (#181)
by
unknown
02:20
created

Signer::canonize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

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