1 | <?php |
||
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, |
||
|
|||
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 | 5 | if (self::digestCheck($content, $tagname, $canonical)) { |
|
207 | 3 | if (self::signatureCheck($content, $canonical)) { |
|
208 | 2 | return true; |
|
209 | } |
||
210 | } |
||
211 | } |
||
212 | 1 | return false; |
|
213 | } |
||
214 | |||
215 | /** |
||
216 | * Check if Signature tag already exists |
||
217 | * @param string $content |
||
218 | * @return boolean |
||
219 | */ |
||
220 | 6 | public static function existsSignature($content) |
|
221 | { |
||
222 | 6 | if (! Validator::isXML($content)) { |
|
223 | throw SignerException::isNotXml(); |
||
224 | } |
||
225 | 6 | $dom = new \DOMDocument('1.0', 'utf-8'); |
|
226 | 6 | $dom->formatOutput = false; |
|
227 | 6 | $dom->preserveWhiteSpace = false; |
|
228 | 6 | $dom->loadXML($content); |
|
229 | 6 | $signature = $dom->getElementsByTagName('Signature')->item(0); |
|
230 | 6 | if (empty($signature)) { |
|
231 | 2 | return false; |
|
232 | } |
||
233 | 6 | return true; |
|
234 | } |
||
235 | |||
236 | /** |
||
237 | * Verify signature value from SignatureInfo node and public key |
||
238 | * @param string $xml |
||
239 | * @param array $canonical |
||
240 | * @return boolean |
||
241 | */ |
||
242 | 3 | public static function signatureCheck( |
|
243 | $xml, |
||
244 | $canonical = self::CANONICAL |
||
245 | ) { |
||
246 | 3 | $dom = new \DOMDocument('1.0', 'utf-8'); |
|
247 | 3 | $dom->formatOutput = false; |
|
248 | 3 | $dom->preserveWhiteSpace = false; |
|
249 | 3 | $dom->loadXML($xml); |
|
250 | |||
251 | 3 | $signature = $dom->getElementsByTagName('Signature')->item(0); |
|
252 | 3 | $sigMethAlgo = $signature->getElementsByTagName('SignatureMethod') |
|
253 | 3 | ->item(0)->getAttribute('Algorithm'); |
|
254 | 3 | $algorithm = OPENSSL_ALGO_SHA256; |
|
255 | 3 | if ($sigMethAlgo == 'http://www.w3.org/2000/09/xmldsig#rsa-sha1') { |
|
256 | 3 | $algorithm = OPENSSL_ALGO_SHA1; |
|
257 | } |
||
258 | 3 | $certificateContent = $signature->getElementsByTagName('X509Certificate') |
|
259 | 3 | ->item(0)->nodeValue; |
|
260 | 3 | $publicKey = PublicKey::createFromContent($certificateContent); |
|
261 | 3 | $signInfoNode = self::canonize( |
|
262 | 3 | $signature->getElementsByTagName('SignedInfo')->item(0), |
|
263 | $canonical |
||
264 | ); |
||
265 | 3 | $signatureValue = $signature->getElementsByTagName('SignatureValue') |
|
266 | 3 | ->item(0)->nodeValue; |
|
267 | 3 | $decodedSignature = base64_decode( |
|
268 | 3 | str_replace(array("\r", "\n"), '', $signatureValue) |
|
269 | ); |
||
270 | 3 | if (! $publicKey->verify($signInfoNode, $decodedSignature, $algorithm)) { |
|
271 | 1 | throw SignerException::signatureComparisonFailed(); |
|
272 | } |
||
273 | 2 | return true; |
|
274 | } |
||
275 | |||
276 | /** |
||
277 | * Verify digest value of data node |
||
278 | * @param string $xml |
||
279 | * @param string $tagname |
||
280 | * @param array $canonical |
||
281 | * @return bool |
||
282 | * @throws SignerException |
||
283 | */ |
||
284 | 5 | public static function digestCheck( |
|
285 | $xml, |
||
286 | $tagname = '', |
||
287 | $canonical = self::CANONICAL |
||
288 | ) { |
||
289 | 5 | $dom = new \DOMDocument('1.0', 'utf-8'); |
|
290 | 5 | $dom->formatOutput = false; |
|
291 | 5 | $dom->preserveWhiteSpace = false; |
|
292 | 5 | $dom->loadXML($xml); |
|
293 | 5 | $root = $dom->documentElement; |
|
294 | 5 | $signature = $dom->getElementsByTagName('Signature')->item(0); |
|
295 | 5 | $sigURI = $signature->getElementsByTagName('Reference') |
|
296 | 5 | ->item(0) |
|
297 | 5 | ->getAttribute('URI'); |
|
298 | 5 | if (empty($tagname)) { |
|
299 | 4 | if (empty($sigURI)) { |
|
300 | $tagname = $root->nodeName; |
||
301 | } else { |
||
302 | 4 | $xpath = new \DOMXPath($dom); |
|
303 | 4 | $entries = $xpath->query('//@Id'); |
|
304 | 4 | foreach ($entries as $entry) { |
|
305 | 4 | $tagname = $entry->ownerElement->nodeName; |
|
306 | 4 | break; |
|
307 | } |
||
308 | } |
||
309 | } |
||
310 | 5 | $node = $dom->getElementsByTagName($tagname)->item(0); |
|
311 | 5 | if (empty($node)) { |
|
312 | 1 | throw SignerException::tagNotFound($tagname); |
|
313 | } |
||
314 | 4 | $sigMethAlgo = $signature->getElementsByTagName('SignatureMethod') |
|
315 | 4 | ->item(0) |
|
316 | 4 | ->getAttribute('Algorithm'); |
|
317 | 4 | $algorithm = 'sha256'; |
|
318 | 4 | if ($sigMethAlgo == 'http://www.w3.org/2000/09/xmldsig#rsa-sha1') { |
|
319 | 4 | $algorithm = 'sha1'; |
|
320 | } |
||
321 | 4 | if ($sigURI == '') { |
|
322 | $node->removeChild($signature); |
||
323 | } |
||
324 | 4 | $calculatedDigest = self::makeDigest($node, $algorithm, $canonical); |
|
325 | 4 | $informedDigest = $signature->getElementsByTagName('DigestValue') |
|
326 | 4 | ->item(0) |
|
327 | 4 | ->nodeValue; |
|
328 | 4 | if ($calculatedDigest != $informedDigest) { |
|
329 | 1 | throw SignerException::digestComparisonFailed(); |
|
330 | } |
||
331 | 3 | return true; |
|
332 | } |
||
333 | |||
334 | /** |
||
335 | * Calculate digest value for given node |
||
336 | * @param DOMNode $node |
||
337 | * @param string $algorithm |
||
338 | * @param array $canonical |
||
339 | * @return string |
||
340 | */ |
||
341 | 4 | private static function makeDigest( |
|
351 | |||
352 | /** |
||
353 | * Reduced to the canonical form |
||
354 | * @param DOMNode $node |
||
355 | * @param array $canonical |
||
356 | * @return string |
||
357 | */ |
||
358 | 4 | private static function canonize( |
|
369 | } |
||
370 |
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.