Passed
Push — master ( c7dd0a...05e73b )
by Daniel
01:28
created

XmlSignatureValidator::loadPublicKey()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 6
ccs 3
cts 4
cp 0.75
rs 10
cc 2
nc 2
nop 1
crap 2.0625
1
<?php
2
3
namespace Selective\XmlDSig;
4
5
use DOMDocument;
6
use DOMElement;
7
use DOMXPath;
8
use Selective\XmlDSig\Exception\XmlSignatureValidatorException;
9
10
/**
11
 * Verify the Digital Signatures of XML Documents.
12
 */
13
final class XmlSignatureValidator
14
{
15
    //
16
    // RSA (PKCS#1 v1.5) Identifier
17
    // https://www.w3.org/TR/xmldsig-core/#sec-PKCS1
18
    //
19
    private const SHA1_URL = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
20
    private const SHA224_URL = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha224';
21
    private const SHA256_URL = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256';
22
    private const SHA384_URL = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384';
23
    private const SHA512_URL = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512';
24
25
    /**
26
     * @var resource|false
27
     */
28
    private $publicKeyId;
29
30
    /**
31
     * @var XmlReader
32
     */
33
    private $xmlReader;
34
35
    /**
36
     * The constructor.
37
     */
38 2
    public function __construct()
39
    {
40 2
        $this->xmlReader = new XmlReader();
41 2
    }
42
43
    /**
44
     * Read and load the pfx file.
45
     *
46
     * @param string $filename PFX filename
47
     * @param string $password PFX password
48
     *
49
     * @throws XmlSignatureValidatorException
50
     *
51
     * @return void
52
     */
53 1
    public function loadPfxFile(string $filename, string $password)
54
    {
55 1
        if (!file_exists($filename)) {
56
            throw new XmlSignatureValidatorException(sprintf('File not found: %s', $filename));
57
        }
58
59 1
        $pkcs12 = file_get_contents($filename);
60
61 1
        if (!$pkcs12) {
62
            throw new XmlSignatureValidatorException(sprintf('File could not be read: %s', $filename));
63
        }
64
65 1
        $this->loadPfx($pkcs12, $password);
66 1
    }
67
68
    /**
69
     * Read and load the pfx file.
70
     *
71
     * @param string $pkcs12 The certificate store data
72
     * @param string $password encryption password for unlocking the PKCS12 file
73
     *
74
     * @throws XmlSignatureValidatorException
75
     *
76
     * @return void
77
     */
78 1
    public function loadPfx(string $pkcs12, string $password): void
79
    {
80 1
        $status = openssl_pkcs12_read($pkcs12, $certificates, $password);
81
82 1
        if (!$status) {
83
            throw new XmlSignatureValidatorException('Invalid PFX password');
84
        }
85
86 1
        $this->publicKeyId = openssl_get_publickey($certificates['cert']);
0 ignored issues
show
Documentation Bug introduced by
It seems like openssl_get_publickey($certificates['cert']) of type OpenSSLAsymmetricKey is incompatible with the declared type false|resource of property $publicKeyId.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
87
88 1
        if (!$this->publicKeyId) {
89
            throw new XmlSignatureValidatorException('Invalid public key');
90
        }
91 1
    }
92
93
    /**
94
     * Read and load the public key file.
95
     *
96
     * @param string $filename The public key file
97
     *
98
     * @throws XmlSignatureValidatorException
99
     *
100
     * @return void
101
     */
102 1
    public function loadPublicKeyFile(string $filename): void
103
    {
104 1
        if (!file_exists($filename)) {
105
            throw new XmlSignatureValidatorException(sprintf('File not found: %s', $filename));
106
        }
107
108 1
        $publicKey = file_get_contents($filename);
109
110 1
        if (!$publicKey) {
111
            throw new XmlSignatureValidatorException(sprintf('File could not be read: %s', $filename));
112
        }
113
114 1
        $this->loadPublicKey($publicKey);
115 1
    }
116
117
    /**
118
     * Load the public key content.
119
     *
120
     * @param string $publicKey The public key data
121
     *
122
     * @throws XmlSignatureValidatorException
123
     *
124
     * @return void
125
     */
126 1
    public function loadPublicKey(string $publicKey): void
127
    {
128 1
        $this->publicKeyId = openssl_get_publickey($publicKey);
0 ignored issues
show
Documentation Bug introduced by
It seems like openssl_get_publickey($publicKey) of type OpenSSLAsymmetricKey is incompatible with the declared type false|resource of property $publicKeyId.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
129
130 1
        if (!$this->publicKeyId) {
131
            throw new XmlSignatureValidatorException('Invalid public key');
132
        }
133 1
    }
134
135
    /**
136
     * Sign an XML file and save the signature in a new file.
137
     * This method does not save the public key within the XML file.
138
     *
139
     * https://www.xml.com/pub/a/2001/08/08/xmldsig.html#verify
140
     *
141
     * @param string $filename Input file
142
     *
143
     * @throws XmlSignatureValidatorException
144
     *
145
     * @return bool Success
146
     */
147 2
    public function verifyXmlFile(string $filename): bool
148
    {
149 2
        if (!file_exists($filename)) {
150
            throw new XmlSignatureValidatorException(sprintf('File not found: %s', $filename));
151
        }
152
153 2
        if (!$this->publicKeyId) {
154
            throw new XmlSignatureValidatorException('No public key provided');
155
        }
156
157 2
        $xmlContent = file_get_contents($filename);
158
159 2
        if (!$xmlContent) {
160
            throw new XmlSignatureValidatorException(sprintf('File could not be read: %s', $filename));
161
        }
162
163 2
        return $this->verifyXml($xmlContent);
164
    }
165
166
    /**
167
     * Sign an XML string.
168
     *
169
     * https://www.xml.com/pub/a/2001/08/08/xmldsig.html#verify
170
     *
171
     * @param string $xmlContent The xml content
172
     *
173
     * @throws XmlSignatureValidatorException
174
     *
175
     * @return bool Success
176
     */
177 2
    public function verifyXml(string $xmlContent): bool
178
    {
179
        // Read the xml file content
180 2
        $xml = new DOMDocument();
181 2
        $xml->preserveWhiteSpace = true;
182 2
        $xml->formatOutput = false;
183 2
        $isValid = $xml->loadXML($xmlContent);
184
185 2
        if (!$isValid || !$xml->documentElement) {
186
            throw new XmlSignatureValidatorException('Invalid XML content');
187
        }
188
189 2
        $digestAlgorithm = $this->getDigestAlgorithm($xml);
190 2
        $signatureValue = $this->getSignatureValue($xml);
191 2
        $xpath = new DOMXPath($xml);
192 2
        $xpath->registerNamespace('xmlns', 'http://www.w3.org/2000/09/xmldsig#');
193
194
        /** @var DOMElement $signedInfoNode */
195 2
        foreach ($xpath->evaluate('//xmlns:Signature/xmlns:SignedInfo') as $signedInfoNode) {
196
            // Remove SignatureValue value
197 2
            $signatureValueElement = $this->xmlReader->queryDomNode($xpath, '//xmlns:SignatureValue', $signedInfoNode);
198 2
            $signatureValueElement->nodeValue = '';
199
200 2
            $canonicalData = $signedInfoNode->C14N(true, false);
201
202 2
            $xml2 = new DOMDocument();
203 2
            $xml2->preserveWhiteSpace = true;
204 2
            $xml2->formatOutput = true;
205 2
            $xml2->loadXML($canonicalData);
206 2
            $canonicalData = $xml2->C14N(true, false);
207
208 2
            $status = openssl_verify($canonicalData, $signatureValue, $this->publicKeyId, $digestAlgorithm);
209
210 2
            if ($status === 1) {
211
                // The XML signature is valid
212 2
                return true;
213
            }
214
215
            if ($status === 0) {
216
                // The XML signature is not valid
217
                return false;
218
            }
219
220
            throw new XmlSignatureValidatorException('Error checking signature');
221
        }
222
223
        // @todo check digest value
224
        //$digestValue = $this->getDigestValue($xml);
225
        //$signatureNodes = $xpath->query('//xmlns:Signature');
226
227
        // Canonicalize the content, exclusive and without comments
228
        //$canonicalData = $xml->documentElement->C14N(true, false);
229
230
        //foreach ($xpath->evaluate('//xmlns:Signature/xmlns:SignedInfo') as $signedInfoNode) {
231
        // $signedInfoNode->parentNode->removeChild($signedInfoNode);
232
        // }
233
234
        return false;
235
    }
236
237
    /**
238
     * Detect digest algorithm.
239
     *
240
     * @param DOMDocument $xml The xml document
241
     *
242
     * @throws XmlSignatureValidatorException
243
     *
244
     * @return int The algorithm code
245
     */
246 2
    private function getDigestAlgorithm(DOMDocument $xml): int
247
    {
248 2
        $xpath = new DOMXPath($xml);
249 2
        $xpath->registerNamespace('xmlns', 'http://www.w3.org/2000/09/xmldsig#');
250 2
        $xpath->registerNamespace('Algorithm', 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315');
251
252 2
        $signatureMethodNodes = $xpath->query('//xmlns:Signature/xmlns:SignedInfo/xmlns:SignatureMethod');
253
254
        // Throw an exception if no signature was found.
255 2
        if (!$signatureMethodNodes || $signatureMethodNodes->length < 1) {
0 ignored issues
show
introduced by
$signatureMethodNodes is of type DOMNodeList, thus it always evaluated to true.
Loading history...
256
            throw new XmlSignatureValidatorException('Verification failed: No Signature was found in the document.');
257
        }
258
259
        // We only support one signature for the entire XML document.
260
        // Throw an exception if more than one signature was found.
261 2
        if ($signatureMethodNodes->length > 1) {
262
            throw new XmlSignatureValidatorException(
263
                'Verification failed: More that one signature was found for the document.'
264
            );
265
        }
266
267
        /** @var DOMElement $element */
268 2
        $element = $signatureMethodNodes->item(0);
269 2
        if (!$element instanceof DOMElement) {
0 ignored issues
show
introduced by
$element is always a sub-type of DOMElement.
Loading history...
270
            throw new XmlSignatureValidatorException(
271
                'Verification failed: Signature algorithm was found for the document.'
272
            );
273
        }
274
275 2
        $algorithm = $element->getAttribute('Algorithm');
276
277
        switch ($algorithm) {
278 2
            case self::SHA1_URL:
279 2
                return OPENSSL_ALGO_SHA1;
280 2
            case self::SHA224_URL:
281 2
                return OPENSSL_ALGO_SHA224;
282 2
            case self::SHA256_URL:
283 2
                return OPENSSL_ALGO_SHA256;
284 2
            case self::SHA384_URL:
285 2
                return OPENSSL_ALGO_SHA384;
286 2
            case self::SHA512_URL:
287 2
                return OPENSSL_ALGO_SHA512;
288
            default:
289
                throw new XmlSignatureValidatorException("Cannot verify: Unsupported Algorithm <$algorithm>");
290
        }
291
    }
292
293
    /**
294
     * Get signature value.
295
     *
296
     * @param DOMDocument $xml The xml document
297
     *
298
     * @throws XmlSignatureValidatorException
299
     *
300
     * @return string The signature value
301
     */
302 2
    private function getSignatureValue(DOMDocument $xml): string
303
    {
304 2
        $xpath = new DOMXPath($xml);
305 2
        $xpath->registerNamespace('xmlns', 'http://www.w3.org/2000/09/xmldsig#');
306
307
        // Find the SignatureValue node
308 2
        $signatureNodes = $xpath->query('//xmlns:Signature/xmlns:SignatureValue');
309
310
        // Throw an exception if no signature was found.
311 2
        if (!$signatureNodes || $signatureNodes->length < 1) {
0 ignored issues
show
introduced by
$signatureNodes is of type DOMNodeList, thus it always evaluated to true.
Loading history...
312
            throw new XmlSignatureValidatorException('Verification failed: No Signature was found in the document.');
313
        }
314
315
        // We only support one signature for the entire XML document.
316
        // Throw an exception if more than one signature was found.
317 2
        if ($signatureNodes->length > 1) {
318
            throw new XmlSignatureValidatorException(
319
                'Verification failed: More that one signature was found for the document.'
320
            );
321
        }
322
323 2
        $domNode = $signatureNodes->item(0);
324 2
        if (!$domNode) {
325
            throw new XmlSignatureValidatorException(
326
                'Verification failed: No Signature item was found in the document.'
327
            );
328
        }
329
330 2
        $result = base64_decode($domNode->nodeValue, true);
331
332 2
        if ($result === false) {
333
            throw new XmlSignatureValidatorException('Verification failed: Invalid base64 data.');
334
        }
335
336 2
        return (string)$result;
337
    }
338
339
    /**
340
     * Destructor.
341
     */
342 2
    public function __destruct()
343
    {
344
        // Free the key from memory
345
        // PHP 8 deprecates openssl_free_key and automatically destroys the key instance when it goes out of scope.
346 2
        if ($this->publicKeyId && version_compare(PHP_VERSION, '8.0.0', '<')) {
347 2
            openssl_free_key($this->publicKeyId);
0 ignored issues
show
Bug introduced by
It seems like $this->publicKeyId can also be of type true; however, parameter $key of openssl_free_key() does only seem to accept OpenSSLAsymmetricKey|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

347
            openssl_free_key(/** @scrutinizer ignore-type */ $this->publicKeyId);
Loading history...
348
        }
349 2
    }
350
351
    /**
352
     * Get the digest value.
353
     *
354
     * @param DOMDocument $xml The xml document
355
     *
356
     * @throws XmlSignatureValidatorException
357
     *
358
     * @return string The signature value
359
     */
360
    private function getDigestValue(DOMDocument $xml): string
0 ignored issues
show
Unused Code introduced by
The method getDigestValue() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
361
    {
362
        $xpath = new DOMXPath($xml);
363
        $xpath->registerNamespace('xmlns', 'http://www.w3.org/2000/09/xmldsig#');
364
365
        // Find the DigestValue node
366
        $signatureNodes = $xpath->query('//xmlns:Signature/xmlns:SignedInfo/xmlns:Reference/xmlns:DigestValue');
367
368
        // Throw an exception if no signature was found.
369
        if (!$signatureNodes || $signatureNodes->length < 1) {
0 ignored issues
show
introduced by
$signatureNodes is of type DOMNodeList, thus it always evaluated to true.
Loading history...
370
            throw new XmlSignatureValidatorException('Verification failed: No Signature was found in the document.');
371
        }
372
373
        // We only support one signature for the entire XML document.
374
        // Throw an exception if more than one signature was found.
375
        if ($signatureNodes->length > 1) {
376
            throw new XmlSignatureValidatorException(
377
                'Verification failed: More that one signature was found for the document.'
378
            );
379
        }
380
381
        $domNode = $signatureNodes->item(0);
382
        if (!$domNode) {
383
            throw new XmlSignatureValidatorException(
384
                'Verification failed: No Signature item was found in the document.'
385
            );
386
        }
387
388
        $result = base64_decode($domNode->nodeValue, true);
389
390
        if ($result === false) {
391
            throw new XmlSignatureValidatorException('Verification failed: Invalid base64 data.');
392
        }
393
394
        return (string)$result;
395
    }
396
}
397