Test Failed
Pull Request — master (#245)
by Roberto
02:32
created

Signer::canonize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

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