Passed
Pull Request — master (#145)
by Roberto
02:19
created

Signer::createSignature()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 66
Code Lines 62

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 52
CRAP Score 3.0014

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 66
ccs 52
cts 55
cp 0.9455
rs 9.3191
cc 3
eloc 62
nc 4
nop 7
crap 3.0014

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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;
28
use NFePHP\Common\Certificate\PublicKey;
29
use NFePHP\Common\Exception\SignerException;
30
use NFePHP\Common\Strings;
31
use NFePHP\Common\Validator;
32
use DOMDocument;
33
use DOMNode;
34
use DOMElement;
35
36
class Signer
37
{
38
    private static $canonical = [true,false,null,null];
39
    
40
    /**
41
     * Make Signature tag
42
     * @param Certificate $certificate
43
     * @param string $content xml for signed
44
     * @param string $tagname
45
     * @param string $mark for URI (opcional)
46
     * @param int $algorithm (opcional)
47
     * @param array $canonical parameters to format node for signature (opcional)
48
     * @param string $rootname name of tag to insert signature block (opcional)
49
     * @return string
50
     * @throws SignnerException
51
     */
52 2
    public static function sign(
53
        Certificate $certificate,
54
        $content,
55
        $tagname,
56
        $mark = 'Id',
57
        $algorithm = OPENSSL_ALGO_SHA1,
58
        $canonical = [true,false,null,null],
59
        $rootname = ''
60
    ) {
61 2
        if (!empty($canonical)) {
62 2
            self::$canonical = $canonical;
63
        }
64 2
        if (empty($content)) {
65
            throw SignerException::isNotXml();
66
        }
67 2
        if (! Validator::isXML($content)) {
68 1
            throw SignerException::isNotXml();
69
        }
70 1
        $dom = new DOMDocument('1.0', 'UTF-8');
71 1
        $dom->loadXML($content);
72 1
        $dom->preserveWhiteSpace = false;
73 1
        $dom->formatOutput = false;
74 1
        $root = $dom->documentElement;
75 1
        if (!empty($rootname)) {
76
            $root = $dom->getElementsByTagName($rootname)->item(0);
77
        }
78 1
        $node = $dom->getElementsByTagName($tagname)->item(0);
79 1
        if (empty($node) || empty($root)) {
80
            throw SignerException::tagNotFound($tagname);
81
        }
82 1
        if (! self::existsSignature($content)) {
83 1
            $dom = self::createSignature(
84
                $certificate,
85
                $dom,
86
                $root,
87
                $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...
88
                $mark,
89
                $algorithm,
90
                $canonical
91
            );
92
        };
93
        return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
94 1
            . $dom->saveXML($dom->documentElement, LIBXML_NOXMLDECL);
95
    }
96
    
97
    /**
98
     * Method that provides the signature of xml as standard SEFAZ
99
     * @param Certificate $certificate
100
     * @param \DOMDocument $dom
101
     * @param \DOMNode $root xml root
102
     * @param \DOMElement $node node to be signed
103
     * @param string $mark Marker signed attribute
104
     * @param int $algorithm cryptographic algorithm (opcional)
105
     * @param array $canonical parameters to format node for signature (opcional)
106
     * @return \DOMDocument
107
     */
108 1
    private static function createSignature(
109
        Certificate $certificate,
110
        DOMDocument $dom,
111
        DOMNode $root,
112
        DOMElement $node,
113
        $mark,
114
        $algorithm = OPENSSL_ALGO_SHA1,
115
        $canonical = [true,false,null,null]
116
    ) {
117 1
        $nsDSIG = 'http://www.w3.org/2000/09/xmldsig#';
118 1
        $nsCannonMethod = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
119 1
        $nsSignatureMethod = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
120 1
        $nsDigestMethod = 'http://www.w3.org/2000/09/xmldsig#sha1';
121 1
        $digestAlgorithm = 'sha1';
122 1
        if ($algorithm == OPENSSL_ALGO_SHA256) {
123
            $digestAlgorithm = 'sha256';
124
            $nsSignatureMethod = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256';
125
            $nsDigestMethod = 'http://www.w3.org/2001/04/xmlenc#sha256';
126
        }
127 1
        $nsTransformMethod1 ='http://www.w3.org/2000/09/xmldsig#enveloped-signature';
128 1
        $nsTransformMethod2 = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
129 1
        $idSigned = trim($node->getAttribute($mark));
130 1
        $digestValue = self::makeDigest($node, $digestAlgorithm, $canonical);
131 1
        $signatureNode = $dom->createElementNS($nsDSIG, 'Signature');
132 1
        $root->appendChild($signatureNode);
133 1
        $signedInfoNode = $dom->createElement('SignedInfo');
134 1
        $signatureNode->appendChild($signedInfoNode);
135 1
        $canonicalNode = $dom->createElement('CanonicalizationMethod');
136 1
        $signedInfoNode->appendChild($canonicalNode);
137 1
        $canonicalNode->setAttribute('Algorithm', $nsCannonMethod);
138 1
        $signatureMethodNode = $dom->createElement('SignatureMethod');
139 1
        $signedInfoNode->appendChild($signatureMethodNode);
140 1
        $signatureMethodNode->setAttribute('Algorithm', $nsSignatureMethod);
141 1
        $referenceNode = $dom->createElement('Reference');
142 1
        $signedInfoNode->appendChild($referenceNode);
143 1
        if (!empty($idSigned)) {
144 1
            $idSigned = "#$idSigned";
145
        }
146 1
        $referenceNode->setAttribute('URI', $idSigned);
147 1
        $transformsNode = $dom->createElement('Transforms');
148 1
        $referenceNode->appendChild($transformsNode);
149 1
        $transfNode1 = $dom->createElement('Transform');
150 1
        $transformsNode->appendChild($transfNode1);
151 1
        $transfNode1->setAttribute('Algorithm', $nsTransformMethod1);
152 1
        $transfNode2 = $dom->createElement('Transform');
153 1
        $transformsNode->appendChild($transfNode2);
154 1
        $transfNode2->setAttribute('Algorithm', $nsTransformMethod2);
155 1
        $digestMethodNode = $dom->createElement('DigestMethod');
156 1
        $referenceNode->appendChild($digestMethodNode);
157 1
        $digestMethodNode->setAttribute('Algorithm', $nsDigestMethod);
158 1
        $digestValueNode = $dom->createElement('DigestValue', $digestValue);
159 1
        $referenceNode->appendChild($digestValueNode);
160 1
        $c14n = self::canonize($signedInfoNode, $canonical);
161 1
        $signature = $certificate->sign($c14n, $algorithm);
162 1
        $signatureValue = base64_encode($signature);
163 1
        $signatureValueNode = $dom->createElement('SignatureValue', $signatureValue);
164 1
        $signatureNode->appendChild($signatureValueNode);
165 1
        $keyInfoNode = $dom->createElement('KeyInfo');
166 1
        $signatureNode->appendChild($keyInfoNode);
167 1
        $x509DataNode = $dom->createElement('X509Data');
168 1
        $keyInfoNode->appendChild($x509DataNode);
169 1
        $pubKeyClean = $certificate->publicKey->unFormated();
170 1
        $x509CertificateNode = $dom->createElement('X509Certificate', $pubKeyClean);
171 1
        $x509DataNode->appendChild($x509CertificateNode);
172 1
        return $dom;
173
    }
174
175
    /**
176
     * Remove old signature from document to replace it
177
     * @param string $content
178
     * @return string
179
     */
180 1
    public static function removeSignature($content)
181
    {
182 1
        if (! self::existsSignature($content)) {
183
            return $content;
184
        }
185 1
        $dom = new \DOMDocument('1.0', 'utf-8');
186 1
        $dom->formatOutput = false;
187 1
        $dom->preserveWhiteSpace = false;
188 1
        $dom->loadXML($content);
189 1
        $node = $dom->documentElement;
190 1
        $signature = $node->getElementsByTagName('Signature')->item(0);
191 1
        if (!empty($signature)) {
192 1
            $parent = $signature->parentNode;
193 1
            $parent->removeChild($signature);
194
        }
195 1
        return $dom->saveXML();
196
    }
197
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(
208
        $content,
209
        $tagname = '',
210
        $canonical = [true,false,null,null]
211
    ) {
212 6
        if (self::existsSignature($content)) {
213 5
            if (self::digestCheck($content, $tagname, $canonical)) {
214 3
                if (self::signatureCheck($content, $canonical)) {
215 2
                    return true;
216
                }
217
            }
218
        }
219 1
        return false;
220
    }
221
    
222
    /**
223
     * Check if Signature tag already exists
224
     * @param string $content
225
     * @return boolean
226
     */
227 6
    public static function existsSignature($content)
228
    {
229 6
        if (! Validator::isXML($content)) {
230
            throw SignerException::isNotXml();
231
        }
232 6
        $dom = new \DOMDocument('1.0', 'utf-8');
233 6
        $dom->formatOutput = false;
234 6
        $dom->preserveWhiteSpace = false;
235 6
        $dom->loadXML($content);
236 6
        $signature = $dom->getElementsByTagName('Signature')->item(0);
237 6
        if (empty($signature)) {
238 2
            return false;
239
        }
240 6
        return true;
241
    }
242
    
243
    /**
244
     * Verify signature value from SignatureInfo node and public key
245
     * @param string $xml
246
     * @param array $canonical
247
     * @return boolean
248
     */
249 3
    private static function signatureCheck(
250
        $xml,
251
        $canonical = [true,false,null,null]
252
    ) {
253 3
        $dom = new \DOMDocument('1.0', 'utf-8');
254 3
        $dom->formatOutput = false;
255 3
        $dom->preserveWhiteSpace = false;
256 3
        $dom->loadXML($xml);
257
        
258 3
        $signature = $dom->getElementsByTagName('Signature')->item(0);
259 3
        $sigMethAlgo = $signature->getElementsByTagName('SignatureMethod')
260 3
            ->item(0)->getAttribute('Algorithm');
261 3
        $algorithm = OPENSSL_ALGO_SHA256;
262 3
        if ($sigMethAlgo == 'http://www.w3.org/2000/09/xmldsig#rsa-sha1') {
263 3
            $algorithm = OPENSSL_ALGO_SHA1;
264
        }
265 3
        $certificateContent = $signature->getElementsByTagName('X509Certificate')
266 3
            ->item(0)->nodeValue;
267 3
        $publicKey = PublicKey::createFromContent($certificateContent);
268 3
        $signInfoNode = self::canonize(
269 3
            $signature->getElementsByTagName('SignedInfo')->item(0),
270
            $canonical
271
        );
272 3
        $signatureValue = $signature->getElementsByTagName('SignatureValue')
273 3
            ->item(0)->nodeValue;
274 3
        $decodedSignature = base64_decode(
275 3
            str_replace(array("\r", "\n"), '', $signatureValue)
276
        );
277 3
        if (! $publicKey->verify($signInfoNode, $decodedSignature, $algorithm)) {
278 1
            throw SignerException::signatureComparisonFailed();
279
        }
280 2
        return true;
281
    }
282
    
283
    /**
284
     * Verify digest value of data node
285
     * @param string $xml
286
     * @param string $tagname
287
     * @return boolean
288
     * @throws SignerException
289
     */
290 5
    private static function digestCheck(
291
        $xml,
292
        $tagname = '',
293
        $canonical = [true,false,null,null]
294
    ) {
295 5
        $dom = new \DOMDocument('1.0', 'utf-8');
296 5
        $dom->formatOutput = false;
297 5
        $dom->preserveWhiteSpace = false;
298 5
        $dom->loadXML($xml);
299 5
        $root = $dom->documentElement;
300 5
        $signature = $dom->getElementsByTagName('Signature')->item(0);
301 5
        $sigURI = $signature->getElementsByTagName('Reference')
302 5
            ->item(0)
303 5
            ->getAttribute('URI');
304 5
        if (empty($tagname)) {
305 4
            if (empty($sigURI)) {
306
                $tagname = $root->nodeName;
307
            } else {
308 4
                $xpath = new \DOMXPath($dom);
309 4
                $entries = $xpath->query('//@Id');
310 4
                foreach ($entries as $entry) {
311 4
                    $tagname = $entry->ownerElement->nodeName;
312
                }
313
            }
314
        }
315 5
        $node = $dom->getElementsByTagName($tagname)->item(0);
316 5
        if (empty($node)) {
317 1
            throw SignerException::tagNotFound($tagname);
318
        }
319 4
        $sigMethAlgo = $signature->getElementsByTagName('SignatureMethod')
320 4
            ->item(0)
321 4
            ->getAttribute('Algorithm');
322 4
        $algorithm = 'sha256';
323 4
        if ($sigMethAlgo == 'http://www.w3.org/2000/09/xmldsig#rsa-sha1') {
324 4
            $algorithm = 'sha1';
325
        }
326 4
        if ($sigURI == '') {
327
            $node->removeChild($signature);
328
        }
329 4
        $calculatedDigest = self::makeDigest($node, $algorithm, $canonical);
330 4
        $informedDigest = $signature->getElementsByTagName('DigestValue')
331 4
            ->item(0)
332 4
            ->nodeValue;
333 4
        if ($calculatedDigest != $informedDigest) {
334 1
            throw SignerException::digestComparisonFailed();
335
        }
336 3
        return true;
337
    }
338
    
339
    /**
340
     * Calculate digest value for given node
341
     * @param DOMNode $node
342
     * @param string $algorithm
343
     * @param array $canonical
344
     * @return string
345
     */
346 4
    private static function makeDigest(
347
        DOMNode $node,
348
        $algorithm,
349
        $canonical = [true,false,null,null]
350
    ) {
351
        //calcular o hash dos dados
352 4
        $c14n = self::canonize($node, $canonical);
353 4
        $hashValue = hash($algorithm, $c14n, true);
354 4
        return base64_encode($hashValue);
355
    }
356
    
357
    /**
358
     * Reduced to the canonical form
359
     * @param DOMNode $node
360
     * @param array $canonical
361
     * @return string
362
     */
363 4
    private static function canonize(
364
        DOMNode $node,
365
        $canonical = [true,false,null,null]
366
    ) {
367 4
        return $node->C14N(
368 4
            $canonical[0],
369 4
            $canonical[1],
370 4
            $canonical[2],
371 4
            $canonical[3]
372
        );
373
    }
374
}
375