1 | <?php |
||
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, |
||
|
|||
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( |
|
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( |
|
372 | } |
||
373 |
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.