Passed
Push — master ( 767d4f...898805 )
by Roberto
44s
created

src/Signer.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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