Passed
Push — master ( e62ad0...8c869c )
by Roberto
02:36
created

Signer::existsSignature()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3.0067

Importance

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