Passed
Pull Request — master (#178)
by
unknown
02:33
created

Signer::signatureCheck()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 23
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 3

Importance

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