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
|
|||
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 |
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.