Completed
Push — master ( b82e2f...5623fd )
by Roberto
07:03 queued 42s
created

Signer   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 337
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 94.19%

Importance

Changes 0
Metric Value
wmc 33
lcom 1
cbo 4
dl 0
loc 337
ccs 162
cts 172
cp 0.9419
rs 9.3999
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A makeDigest() 0 10 1
A canonize() 0 11 1
C sign() 0 41 7
A createSignature() 0 66 3
A removeSignature() 0 17 3
A isSigned() 0 14 4
A existsSignature() 0 15 3
B signatureCheck() 0 33 3
C digestCheck() 0 49 8
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(
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(
342
        DOMNode $node,
343
        $algorithm,
344
        $canonical = self::CANONICAL
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 = self::CANONICAL
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