Completed
Pull Request — master (#110)
by Roberto
02:29
created

Signer   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 273
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 93.66%

Importance

Changes 0
Metric Value
wmc 26
lcom 1
cbo 3
dl 0
loc 273
ccs 133
cts 142
cp 0.9366
rs 10
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
B sign() 0 40 6
A removeSignature() 0 10 2
A isSigned() 0 10 4
A createSignature() 0 71 3
A existsSignature() 0 8 2
A signatureCheck() 0 16 2
B digestCheck() 0 35 6
A makeDigest() 0 13 1
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\Strings;
28
use RuntimeException;
29
use DOMDocument;
30
use DOMElement;
31
32
class Signer
33
{
34
    private static $canonical = [false,false,null,null];
35
    
36
    /**
37
     * Make Signature tag
38
     * @param string $content
39
     * @param string $tagname
40
     * @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...
41
     * @param string $algorithm
42
     * @param array $canonical parameters to format node for signature
43
     * @param string $rootname name of tag to insert signature block
44
     * @return string
45
     * @throws \NFePHP\Common\Exception\SignnerException
46
     */
47 2
    public static function sign(
48
        Certificate $certificate,
49
        $content,
50
        $tagname = '',
51
        $mark = 'Id',
52
        $algorithm = OPENSSL_ALGO_SHA1,
53
        $canonical = [false,false,null,null],
54
        $rootname = ''
55
    ) {
56 2
        if (!empty($canonical)) {
57 2
            self::$canonical = $canonical;
58
        }
59 2
        $dom = new DOMDocument('1.0', 'UTF-8');
60 2
        $dom->loadXML($content);
61 2
        $dom->preserveWhiteSpace = false;
62 2
        $dom->formatOutput = false;
63 2
        $root = $dom->documentElement;
64 2
        if (!empty($rootname)) {
65
            $root = $dom->getElementsByTagName($rootname)->item(0);
66
        }
67 2
        $node = $dom->getElementsByTagName($tagname)->item(0);
68 2
        if (empty($node) || empty($root)) {
69
            throw new \RuntimeException(
70
                'Tag not found ' . $tagname . ' ' . $rootname
71
            );
72
        }
73 2
        if (! self::existsSignature($dom)) {
74 2
            $dom = self::createSignature(
75
                $certificate,
76
                $dom,
77
                $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...
78
                $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...
79
                $mark,
80
                $algorithm,
81
                $canonical
82
            );
83
        };
84
        return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
85 2
            . $dom->saveXML($dom->documentElement, LIBXML_NOXMLDECL);
86
    }
87
    
88
    /**
89
     * Remove old signature from document to replace it
90
     * @param DOMDocument $dom
91
     * @return DOMDocument
92
     */
93 1
    public static function removeSignature(DOMDocument $dom)
94
    {
95 1
        $node = $dom->documentElement;
96 1
        $signature = $node->getElementsByTagName('Signature')->item(0);
97 1
        if (!empty($signature)) {
98 1
            $parent = $signature->parentNode;
99 1
            $oldsignature = $parent->removeChild($signature);
0 ignored issues
show
Unused Code introduced by
$oldsignature is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

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

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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 4
            $clone = $signature->cloneNode(true);
0 ignored issues
show
Unused Code introduced by
$clone is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
258
        } else {
259
            $signature = $dom->getElementsByTagName('Signature')->item(0);
260
        }
261 4
        $sigMethAlgo = $signature->getElementsByTagName('SignatureMethod')
262 4
            ->item(0)
263 4
            ->getAttribute('Algorithm');
264 4
        $algorithm = 'sha256';
265 4
        if ($sigMethAlgo == 'http://www.w3.org/2000/09/xmldsig#rsa-sha1') {
266 4
            $algorithm = 'sha1';
267
        }
268 4
        $sigURI = $signature->getElementsByTagName('Reference')
269 4
            ->item(0)
270 4
            ->getAttribute('URI');
271 4
        if ($sigURI == '') {
272
            $node->removeChild($signature);
273
        }
274 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...
275 4
        $informedDigest = $signature->getElementsByTagName('DigestValue')
276 4
            ->item(0)
277 4
            ->nodeValue;
278 4
        if ($calculatedDigest != $informedDigest) {
279 2
            throw \NFePHP\Common\Exception\SignerException::digestComparisonFailed();
280
        }
281 2
        return true;
282
    }
283
    
284
    /**
285
     * Calculate digest value for given node
286
     * @param \DOMElement $node
287
     * @param string $algorithm
288
     * @param array $canonical
289
     * @return string
290
     */
291 5
    private static function makeDigest(DOMElement $node, $algorithm, $canonical = [false,false,null,null])
292
    {
293 5
        $dados = $node->C14N(true, false, null, null);
0 ignored issues
show
Unused Code introduced by
$dados is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
294
        //calcular o hash dos dados
295 5
        $c14n = $node->C14N(
296 5
            $canonical[0],
297 5
            $canonical[1],
298 5
            $canonical[2],
299 5
            $canonical[3]
300
        );
301 5
        $hashValue = hash($algorithm, $c14n, true);
302 5
        return base64_encode($hashValue);
303
    }
304
}
305