Passed
Pull Request — master (#140)
by
unknown
02:08
created

Signer::createSignature()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 63
Code Lines 60

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 50
CRAP Score 3.0016

Importance

Changes 0
Metric Value
dl 0
loc 63
ccs 50
cts 53
cp 0.9434
rs 9.4347
c 0
b 0
f 0
cc 3
eloc 60
nc 2
nop 7
crap 3.0016

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