Passed
Push — master ( 7623c7...fc4617 )
by Roberto
41s
created

Signer::digestCheck()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 33
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 6.0184

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 33
ccs 23
cts 25
cp 0.92
rs 8.439
cc 6
eloc 25
nc 10
nop 2
crap 6.0184
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 DOMDocument;
30
use DOMNode;
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 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 SignerException::tagNotFound($tagname);
71
        }
72 2
        if (! self::existsSignature($dom)) {
73 2
            $dom = self::createSignature(
74
                $certificate,
75
                $dom,
76
                $root,
77
                $node,
78
                $mark,
79
                $algorithm,
80
                $canonical
81
            );
82
        };
83
        return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
84 2
            . $dom->saveXML($dom->documentElement, LIBXML_NOXMLDECL);
85
    }
86
    
87
    /**
88
     * Remove old signature from document to replace it
89
     * @param DOMDocument $dom
90
     * @return DOMDocument
91
     */
92 1
    public static function removeSignature(DOMDocument $dom)
93
    {
94 1
        $node = $dom->documentElement;
95 1
        $signature = $node->getElementsByTagName('Signature')->item(0);
96 1
        if (!empty($signature)) {
97 1
            $parent = $signature->parentNode;
98 1
            $parent->removeChild($signature);
99
        }
100 1
        return $dom;
101
    }
102
    
103
    /**
104
     * Verify if xml signature is valid
105
     * @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...
106
     * @param string $tagname tag for sign
107
     * @return boolean
108
     */
109 6
    public static function isSigned(DOMDocument $dom, $tagname)
110
    {
111 6
        if (self::existsSignature($dom)
112 6
            && self::digestCheck($dom, $tagname)
113 3
            && self::signatureCheck($dom)
114
        ) {
115 2
            return true;
116
        }
117 1
        return false;
118
    }
119
    
120
    /**
121
     * Method that provides the signature of xml as standard SEFAZ
122
     * @param Certificate $certificate
123
     * @param DOMDocument $dom
124
     * @param DOMNode $root xml root
125
     * @param DOMNode $node node to be signed
126
     * @param string $mark Marker signed attribute
127
     * @param int $algorithm cryptographic algorithm
128
     * @param array $canonical parameters to format node for signature
129
     * @return \DOMDocument
130
     */
131 2
    private static function createSignature(
132
        Certificate $certificate,
133
        DOMDocument $dom,
134
        DOMNode $root,
135
        DOMNode $node,
136
        $mark,
137
        $algorithm = OPENSSL_ALGO_SHA1,
138
        $canonical = [false,false,null,null]
139
    ) {
140 2
        $nsDSIG = 'http://www.w3.org/2000/09/xmldsig#';
141 2
        $nsCannonMethod = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
142 2
        $nsSignatureMethod = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
143 2
        $nsDigestMethod = 'http://www.w3.org/2000/09/xmldsig#sha1';
144 2
        $digestAlgorithm = 'sha1';
145 2
        if ($algorithm == OPENSSL_ALGO_SHA256) {
146
            $digestAlgorithm = 'sha256';
147
            $nsSignatureMethod = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256';
148
            $nsDigestMethod = 'http://www.w3.org/2001/04/xmlenc#sha256';
149
        }
150 2
        $nsTransformMethod1 ='http://www.w3.org/2000/09/xmldsig#enveloped-signature';
151 2
        $nsTransformMethod2 = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
152 2
        $idSigned = trim($node->getAttribute($mark));
153 2
        $digestValue = self::makeDigest($node, $digestAlgorithm, $canonical);
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...
154 2
        $signatureNode = $dom->createElementNS($nsDSIG, 'Signature');
155 2
        $root->appendChild($signatureNode);
156 2
        $signedInfoNode = $dom->createElement('SignedInfo');
157 2
        $signatureNode->appendChild($signedInfoNode);
158 2
        $canonicalNode = $dom->createElement('CanonicalizationMethod');
159 2
        $signedInfoNode->appendChild($canonicalNode);
160 2
        $canonicalNode->setAttribute('Algorithm', $nsCannonMethod);
161 2
        $signatureMethodNode = $dom->createElement('SignatureMethod');
162 2
        $signedInfoNode->appendChild($signatureMethodNode);
163 2
        $signatureMethodNode->setAttribute('Algorithm', $nsSignatureMethod);
164 2
        $referenceNode = $dom->createElement('Reference');
165 2
        $signedInfoNode->appendChild($referenceNode);
166 2
        if (!empty($idSigned)) {
167 2
            $idSigned = "#$idSigned";
168
        }
169 2
        $referenceNode->setAttribute('URI', $idSigned);
170 2
        $transformsNode = $dom->createElement('Transforms');
171 2
        $referenceNode->appendChild($transformsNode);
172 2
        $transfNode1 = $dom->createElement('Transform');
173 2
        $transformsNode->appendChild($transfNode1);
174 2
        $transfNode1->setAttribute('Algorithm', $nsTransformMethod1);
175 2
        $transfNode2 = $dom->createElement('Transform');
176 2
        $transformsNode->appendChild($transfNode2);
177 2
        $transfNode2->setAttribute('Algorithm', $nsTransformMethod2);
178 2
        $digestMethodNode = $dom->createElement('DigestMethod');
179 2
        $referenceNode->appendChild($digestMethodNode);
180 2
        $digestMethodNode->setAttribute('Algorithm', $nsDigestMethod);
181 2
        $digestValueNode = $dom->createElement('DigestValue', $digestValue);
182 2
        $referenceNode->appendChild($digestValueNode);
183 2
        $c14n = $signedInfoNode->C14N(
184 2
            $canonical[0],
185 2
            $canonical[1],
186 2
            $canonical[2],
187 2
            $canonical[3]
188
        );
189 2
        $signature = $certificate->sign($c14n, $algorithm);
190 2
        $signatureValue = base64_encode($signature);
191 2
        $signatureValueNode = $dom->createElement('SignatureValue', $signatureValue);
192 2
        $signatureNode->appendChild($signatureValueNode);
193 2
        $keyInfoNode = $dom->createElement('KeyInfo');
194 2
        $signatureNode->appendChild($keyInfoNode);
195 2
        $x509DataNode = $dom->createElement('X509Data');
196 2
        $keyInfoNode->appendChild($x509DataNode);
197 2
        $pubKeyClean = $certificate->publicKey->unFormated();
198 2
        $x509CertificateNode = $dom->createElement('X509Certificate', $pubKeyClean);
199 2
        $x509DataNode->appendChild($x509CertificateNode);
200 2
        return $dom;
201
    }
202
203
    /**
204
     * Check if Signature tag already exists
205
     * @param \DOMDocument $dom
206
     * @return boolean
207
     */
208 7
    private static function existsSignature(DOMDocument $dom)
209
    {
210 7
        $signature = $dom->getElementsByTagName('Signature')->item(0);
211 7
        if (!isset($signature)) {
212 3
            return false;
213
        }
214 5
        return true;
215
    }
216
    
217
    /**
218
     * Verify signature value
219
     * @param \DOMDocument $dom
220
     * @return boolean
221
     */
222 2
    private static function signatureCheck(DOMDocument $dom)
223
    {
224 2
        $signature = $dom->getElementsByTagName('Signature')->item(0);
225 2
        $sigMethAlgo = $signature->getElementsByTagName('SignatureMethod')->item(0)->getAttribute('Algorithm');
226 2
        if ($sigMethAlgo == 'http://www.w3.org/2000/09/xmldsig#rsa-sha1') {
227 2
            $algorithm = OPENSSL_ALGO_SHA1;
228
        } else {
229
            $algorithm = OPENSSL_ALGO_SHA256;
230
        }
231 2
        $certificateContent = $signature->getElementsByTagName('X509Certificate')->item(0)->nodeValue;
232 2
        $publicKey = PublicKey::createFromContent($certificateContent);
233 2
        $signContent = $signature->getElementsByTagName('SignedInfo')->item(0)->C14N(true, false, null, null);
234 2
        $signatureValue = $signature->getElementsByTagName('SignatureValue')->item(0)->nodeValue;
235 2
        $decodedSignature = base64_decode(str_replace(array("\r", "\n"), '', $signatureValue));
236 2
        return $publicKey->verify($signContent, $decodedSignature, $algorithm);
237
    }
238
    
239
    /**
240
     * digestCheck
241
     * Verify digest value
242
     * @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...
243
     * @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...
244
     * @return boolean
245
     * @throws SignerException
246
     */
247 5
    private static function digestCheck(DOMDocument $dom, $tagname = '')
248
    {
249 5
        $node = $dom->getElementsByTagName($tagname)->item(0);
250 5
        if (empty($node)) {
251 1
            throw new \RuntimeException('Tag not found ' .$tagname);
252
        }
253 4
        $signature = $dom->getElementsByTagName('Signature')->item(0);
254 4
        if (empty($signature)) {
255
            //not sign document
256
            return false;
257
        }
258 4
        $sigMethAlgo = $signature->getElementsByTagName('SignatureMethod')
259 4
            ->item(0)
260 4
            ->getAttribute('Algorithm');
261 4
        $algorithm = 'sha256';
262 4
        if ($sigMethAlgo == 'http://www.w3.org/2000/09/xmldsig#rsa-sha1') {
263 4
            $algorithm = 'sha1';
264
        }
265 4
        $sigURI = $signature->getElementsByTagName('Reference')
266 4
            ->item(0)
267 4
            ->getAttribute('URI');
268 4
        if ($sigURI == '') {
269
            $node->removeChild($signature);
270
        }
271 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...
272 4
        $informedDigest = $signature->getElementsByTagName('DigestValue')
273 4
            ->item(0)
274 4
            ->nodeValue;
275 4
        if ($calculatedDigest != $informedDigest) {
276 2
            throw SignerException::digestComparisonFailed();
277
        }
278 2
        return true;
279
    }
280
    
281
    /**
282
     * Calculate digest value for given node
283
     * @param \DOMElement $node
284
     * @param string $algorithm
285
     * @param array $canonical
286
     * @return string
287
     */
288 5
    private static function makeDigest(DOMElement $node, $algorithm, $canonical = [false,false,null,null])
289
    {
290
        //calcular o hash dos dados
291 5
        $c14n = $node->C14N(
292 5
            $canonical[0],
293 5
            $canonical[1],
294 5
            $canonical[2],
295 5
            $canonical[3]
296
        );
297 5
        $hashValue = hash($algorithm, $c14n, true);
298 5
        return base64_encode($hashValue);
299
    }
300
}
301