Passed
Push — master ( c8d807...914b45 )
by Roberto
57s
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
 *     e sped-esfinge
14
 *
15
 * @category  NFePHP
16
 * @package   NFePHP\Common\Signer
17
 * @copyright NFePHP Copyright (c) 2016
18
 * @license   http://www.gnu.org/licenses/lgpl.txt LGPLv3+
19
 * @license   https://opensource.org/licenses/MIT MIT
20
 * @license   http://www.gnu.org/licenses/gpl.txt GPLv3+
21
 * @author    Roberto L. Machado <linux.rlm at gmail dot com>
22
 * @link      http://github.com/nfephp-org/sped-common for the canonical source repository
23
 */
24
25
use NFePHP\Common\Certificate;
26
use NFePHP\Common\Certificate\PublicKey;
27
use NFePHP\Common\Exception\SignerException;
28
use NFePHP\Common\Strings;
29
use NFePHP\Common\Validator;
30
use DOMDocument;
31
use DOMNode;
32
use DOMElement;
33
34
class Signer
35
{
36
    private static $canonical = [true,false,null,null];
37
    
38
    /**
39
     * Make Signature tag
40
     * @param Certificate $certificate
41
     * @param string $content xml for signed
42
     * @param string $tagname
43
     * @param string $marker for URI (opcional)
0 ignored issues
show
Bug introduced by
There is no parameter named $marker. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

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