Passed
Pull Request — master (#119)
by Roberto
02:33
created

Signer::sign()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 40
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 6.1979

Importance

Changes 0
Metric Value
dl 0
loc 40
ccs 14
cts 17
cp 0.8235
rs 8.439
c 0
b 0
f 0
cc 6
eloc 32
nc 12
nop 7
crap 6.1979
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 RuntimeException;
30
use DOMDocument;
31
use DOMElement;
32
33
class Signer
34
{
35
    private static $canonical = [false,false,null,null];
36
    
37
    /**
38
     * Make Signature tag
39
     * @param string $content
40
     * @param string $tagname
41
     * @param string $marker for URI
0 ignored issues
show
Bug introduced by
There is no parameter named $marker. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
42
     * @param string $algorithm
43
     * @param array $canonical parameters to format node for signature
44
     * @param string $rootname name of tag to insert signature block
45
     * @return string
46
     * @throws \NFePHP\Common\Exception\SignnerException
47
     */
48 2
    public static function sign(
49
        Certificate $certificate,
50
        $content,
51
        $tagname = '',
52
        $mark = 'Id',
53
        $algorithm = OPENSSL_ALGO_SHA1,
54
        $canonical = [false,false,null,null],
55
        $rootname = ''
56
    ) {
57 2
        if (!empty($canonical)) {
58 2
            self::$canonical = $canonical;
59
        }
60 2
        $dom = new DOMDocument('1.0', 'UTF-8');
61 2
        $dom->loadXML($content);
62 2
        $dom->preserveWhiteSpace = false;
63 2
        $dom->formatOutput = false;
64 2
        $root = $dom->documentElement;
65 2
        if (!empty($rootname)) {
66
            $root = $dom->getElementsByTagName($rootname)->item(0);
67
        }
68 2
        $node = $dom->getElementsByTagName($tagname)->item(0);
69 2
        if (empty($node) || empty($root)) {
70
            throw new \RuntimeException(
71
                'Tag not found ' . $tagname . ' ' . $rootname
72
            );
73
        }
74 2
        if (! self::existsSignature($dom)) {
75 2
            $dom = self::createSignature(
76
                $certificate,
77
                $dom,
78
                $root,
0 ignored issues
show
Compatibility introduced by
$root 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...
79
                $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...
80
                $mark,
81
                $algorithm,
82
                $canonical
83
            );
84
        };
85
        return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
86 2
            . $dom->saveXML($dom->documentElement, LIBXML_NOXMLDECL);
87
    }
88
    
89
    /**
90
     * Remove old signature from document to replace it
91
     * @param DOMDocument $dom
92
     * @return DOMDocument
93
     */
94 1
    public static function removeSignature(DOMDocument $dom)
95
    {
96 1
        $node = $dom->documentElement;
97 1
        $signature = $node->getElementsByTagName('Signature')->item(0);
98 1
        if (!empty($signature)) {
99 1
            $parent = $signature->parentNode;
100 1
            $parent->removeChild($signature);
101
        }
102 1
        return $dom;
103
    }
104
    
105
    /**
106
     * Verify if xml signature is valid
107
     * @param string $content xml content
0 ignored issues
show
Bug introduced by
There is no parameter named $content. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
108
     * @param string $tagname tag for sign
109
     * @return boolean
110
     */
111 6
    public static function isSigned(DOMDocument $dom, $tagname)
112
    {
113 6
        if (self::existsSignature($dom)
114 6
            && self::digestCheck($dom, $tagname)
115 3
            && self::signatureCheck($dom)
116
        ) {
117 2
            return true;
118
        }
119 1
        return false;
120
    }
121
    
122
    /**
123
     * Method that provides the signature of xml as standard SEFAZ
124
     * @param Certificate $certificate
125
     * @param \DOMDocument $dom
126
     * @param \DOMElement $root xml root
127
     * @param \DOMElement $node node to be signed
128
     * @param string $mark Marker signed attribute
129
     * @param int $algorithm cryptographic algorithm
130
     * @param array $canonical parameters to format node for signature
131
     * @return \DOMDocument
132
     */
133 2
    private static function createSignature(
134
        Certificate $certificate,
135
        DOMDocument $dom,
136
        DOMElement $root,
137
        DOMElement $node,
138
        $mark,
139
        $algorithm = OPENSSL_ALGO_SHA1,
140
        $canonical = [false,false,null,null]
141
    ) {
142 2
        $nsDSIG = 'http://www.w3.org/2000/09/xmldsig#';
143 2
        $nsCannonMethod = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
144 2
        $nsSignatureMethod = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
145 2
        $nsDigestMethod = 'http://www.w3.org/2000/09/xmldsig#sha1';
146 2
        $digestAlgorithm = 'sha1';
147 2
        if ($algorithm == OPENSSL_ALGO_SHA256) {
148
            $digestAlgorithm = 'sha256';
149
            $nsSignatureMethod = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256';
150
            $nsDigestMethod = 'http://www.w3.org/2001/04/xmlenc#sha256';
151
        }
152 2
        $nsTransformMethod1 ='http://www.w3.org/2000/09/xmldsig#enveloped-signature';
153 2
        $nsTransformMethod2 = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
154 2
        $idSigned = trim($node->getAttribute($mark));
155 2
        $digestValue = self::makeDigest($node, $digestAlgorithm, $canonical);
156 2
        $signatureNode = $dom->createElementNS($nsDSIG, 'Signature');
157 2
        $root->appendChild($signatureNode);
158 2
        $signedInfoNode = $dom->createElement('SignedInfo');
159 2
        $signatureNode->appendChild($signedInfoNode);
160 2
        $canonicalNode = $dom->createElement('CanonicalizationMethod');
161 2
        $signedInfoNode->appendChild($canonicalNode);
162 2
        $canonicalNode->setAttribute('Algorithm', $nsCannonMethod);
163 2
        $signatureMethodNode = $dom->createElement('SignatureMethod');
164 2
        $signedInfoNode->appendChild($signatureMethodNode);
165 2
        $signatureMethodNode->setAttribute('Algorithm', $nsSignatureMethod);
166 2
        $referenceNode = $dom->createElement('Reference');
167 2
        $signedInfoNode->appendChild($referenceNode);
168 2
        if (!empty($idSigned)) {
169 2
            $idSigned = "#$idSigned";
170
        }
171 2
        $referenceNode->setAttribute('URI', $idSigned);
172 2
        $transformsNode = $dom->createElement('Transforms');
173 2
        $referenceNode->appendChild($transformsNode);
174 2
        $transfNode1 = $dom->createElement('Transform');
175 2
        $transformsNode->appendChild($transfNode1);
176 2
        $transfNode1->setAttribute('Algorithm', $nsTransformMethod1);
177 2
        $transfNode2 = $dom->createElement('Transform');
178 2
        $transformsNode->appendChild($transfNode2);
179 2
        $transfNode2->setAttribute('Algorithm', $nsTransformMethod2);
180 2
        $digestMethodNode = $dom->createElement('DigestMethod');
181 2
        $referenceNode->appendChild($digestMethodNode);
182 2
        $digestMethodNode->setAttribute('Algorithm', $nsDigestMethod);
183 2
        $digestValueNode = $dom->createElement('DigestValue', $digestValue);
184 2
        $referenceNode->appendChild($digestValueNode);
185 2
        $c14n = $signedInfoNode->C14N(
186 2
            $canonical[0],
187 2
            $canonical[1],
188 2
            $canonical[2],
189 2
            $canonical[3]
190
        );
191 2
        $signature = $certificate->sign($c14n, $algorithm);
192 2
        $signatureValue = base64_encode($signature);
193 2
        $signatureValueNode = $dom->createElement('SignatureValue', $signatureValue);
194 2
        $signatureNode->appendChild($signatureValueNode);
195 2
        $keyInfoNode = $dom->createElement('KeyInfo');
196 2
        $signatureNode->appendChild($keyInfoNode);
197 2
        $x509DataNode = $dom->createElement('X509Data');
198 2
        $keyInfoNode->appendChild($x509DataNode);
199 2
        $pubKeyClean = $certificate->publicKey->unFormated();
200 2
        $x509CertificateNode = $dom->createElement('X509Certificate', $pubKeyClean);
201 2
        $x509DataNode->appendChild($x509CertificateNode);
202 2
        return $dom;
203
    }
204
205
    /**
206
     * Check if Signature tag already exists
207
     * @param \DOMDocument $dom
208
     * @return boolean
209
     */
210 7
    private static function existsSignature(DOMDocument $dom)
211
    {
212 7
        $signature = $dom->getElementsByTagName('Signature')->item(0);
213 7
        if (!isset($signature)) {
214 3
            return false;
215
        }
216 5
        return true;
217
    }
218
    
219
    /**
220
     * Verify signature value
221
     * @param \DOMDocument $dom
222
     * @return boolean
223
     */
224 2
    private static function signatureCheck(DOMDocument $dom)
225
    {
226 2
        $signature = $dom->getElementsByTagName('Signature')->item(0);
227 2
        $sigMethAlgo = $signature->getElementsByTagName('SignatureMethod')->item(0)->getAttribute('Algorithm');
228 2
        if ($sigMethAlgo == 'http://www.w3.org/2000/09/xmldsig#rsa-sha1') {
229 2
            $algorithm = OPENSSL_ALGO_SHA1;
230
        } else {
231
            $algorithm = OPENSSL_ALGO_SHA256;
232
        }
233 2
        $certificateContent = $signature->getElementsByTagName('X509Certificate')->item(0)->nodeValue;
234 2
        $publicKey = PublicKey::createFromContent($certificateContent);
235 2
        $signContent = $signature->getElementsByTagName('SignedInfo')->item(0)->C14N(true, false, null, null);
236 2
        $signatureValue = $signature->getElementsByTagName('SignatureValue')->item(0)->nodeValue;
237 2
        $decodedSignature = base64_decode(str_replace(array("\r", "\n"), '', $signatureValue));
238 2
        return $publicKey->verify($signContent, $decodedSignature, $algorithm);
239
    }
240
    
241
    /**
242
     * digestCheck
243
     * Verify digest value
244
     * @param string $content
0 ignored issues
show
Bug introduced by
There is no parameter named $content. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
245
     * @param string $tagid
0 ignored issues
show
Bug introduced by
There is no parameter named $tagid. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
246
     * @return boolean
247
     * @throws SignerException
248
     */
249 5
    private static function digestCheck(DOMDocument $dom, $tagname = '')
250
    {
251 5
        $node = $dom->getElementsByTagName($tagname)->item(0);
252 5
        if (empty($node)) {
253 1
            throw new \RuntimeException('Tag not found ' .$tagname);
254
        }
255 4
        $signature = $dom->getElementsByTagName('Signature')->item(0);
256 4
        if (empty($signature)) {
257
            //not sign document
258
            return false;
259
        }
260 4
        $sigMethAlgo = $signature->getElementsByTagName('SignatureMethod')
261 4
            ->item(0)
262 4
            ->getAttribute('Algorithm');
263 4
        $algorithm = 'sha256';
264 4
        if ($sigMethAlgo == 'http://www.w3.org/2000/09/xmldsig#rsa-sha1') {
265 4
            $algorithm = 'sha1';
266
        }
267 4
        $sigURI = $signature->getElementsByTagName('Reference')
268 4
            ->item(0)
269 4
            ->getAttribute('URI');
270 4
        if ($sigURI == '') {
271
            $node->removeChild($signature);
272
        }
273 4
        $calculatedDigest = self::makeDigest($node, $algorithm);
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...
274 4
        $informedDigest = $signature->getElementsByTagName('DigestValue')
275 4
            ->item(0)
276 4
            ->nodeValue;
277 4
        if ($calculatedDigest != $informedDigest) {
278 2
            throw SignerException::digestComparisonFailed();
279
        }
280 2
        return true;
281
    }
282
    
283
    /**
284
     * Calculate digest value for given node
285
     * @param \DOMElement $node
286
     * @param string $algorithm
287
     * @param array $canonical
288
     * @return string
289
     */
290 5
    private static function makeDigest(DOMElement $node, $algorithm, $canonical = [false,false,null,null])
291
    {
292
        //calcular o hash dos dados
293 5
        $c14n = $node->C14N(
294 5
            $canonical[0],
295 5
            $canonical[1],
296 5
            $canonical[2],
297 5
            $canonical[3]
298
        );
299 5
        $hashValue = hash($algorithm, $c14n, true);
300 5
        return base64_encode($hashValue);
301
    }
302
}
303