Passed
Pull Request — master (#3)
by Tim
02:00
created

Signature::getIdAttributes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
3
namespace SimpleSAML\XMLSecurity;
4
5
use DOMDocument;
6
use DOMElement;
7
use DOMNode;
8
use SimpleSAML\Assert\Assert;
9
use SimpleSAML\XML\Chunk;
10
use SimpleSAML\XML\DOMDocumentFactory;
11
use SimpleSAML\XML\Exception\InvalidDOMElementException;
12
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory;
13
use SimpleSAML\XMLSecurity\Backend\SignatureBackend;
14
use SimpleSAML\XMLSecurity\Constants as C;
15
use SimpleSAML\XMLSecurity\Exception\InvalidArgumentException;
16
use SimpleSAML\XMLSecurity\Exception\NoSignatureFound;
17
use SimpleSAML\XMLSecurity\Exception\RuntimeException;
18
use SimpleSAML\XMLSecurity\Key;
19
use SimpleSAML\XMLSecurity\Utils\Certificate as CertificateUtils;
20
use SimpleSAML\XMLSecurity\Utils\Security as Sec;
21
use SimpleSAML\XMLSecurity\Utils\XPath as XP;
22
use SimpleSAML\XMLSecurity\XML\ds\DigestMethod;
0 ignored issues
show
Bug introduced by
The type SimpleSAML\XMLSecurity\XML\ds\DigestMethod was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
23
use SimpleSAML\XMLSecurity\XML\ds\DigestValue;
0 ignored issues
show
Bug introduced by
The type SimpleSAML\XMLSecurity\XML\ds\DigestValue was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
24
use SimpleSAML\XMLSecurity\XML\ds\Signature as Sig;
25
use SimpleSAML\XMLSecurity\XML\ds\Transform;
26
use SimpleSAML\XMLSecurity\XML\ds\X509Certificate;
27
use SimpleSAML\XMLSecurity\XML\ds\X509Data;
28
use SimpleSAML\XMLSecurity\XML\ds\X509Digest;
29
use SimpleSAML\XMLSecurity\XML\ds\X509IssuerSerial;
0 ignored issues
show
Bug introduced by
The type SimpleSAML\XMLSecurity\XML\ds\X509IssuerSerial was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
30
use SimpleSAML\XMLSecurity\XML\ds\X509SubjectName;
31
32
/**
33
 * Class implementing XML digital signatures.
34
 *
35
 * @package SimpleSAML\XMLSecurity
36
 */
37
class Signature
38
{
39
    /** @var array */
40
    public array $idNS = [];
41
42
    /** @var array */
43
    public array $idKeys = [];
44
45
    /** @var \SimpleSAML\XMLSecurity\Backend\SignatureBackend|null */
46
    protected ?SignatureBackend $backend = null;
47
48
    /** @var \DOMElement */
49
    protected DOMElement $root;
50
51
    /** @var \DOMElement|null */
52
    protected ?DOMElement $sigNode = null;
53
54
    /** @var \DOMElement */
55
    protected DOMElement $sigMethodNode;
56
57
    /** @var \DOMElement */
58
    protected DOMElement $c14nMethodNode;
59
60
    /** @var \DOMElement */
61
    protected DOMElement $sigInfoNode;
62
63
    /** @var \DOMElement|null */
64
    protected ?DOMElement $objectNode = null;
65
66
    /** @var string */
67
    protected string $signfo;
68
69
    /** @var string */
70
    protected string $sigAlg;
71
72
    /** @var \DOMElement[] */
73
    protected array $verifiedElements = [];
74
75
    /** @var string */
76
    protected string $c14nMethod = C::C14N_EXCLUSIVE_WITHOUT_COMMENTS;
77
78
    /** @var string */
79
    protected string $nsPrefix = 'ds:';
80
81
    /** @var array */
82
    protected array $algBlacklist = [
83
        C::SIG_RSA_SHA1,
84
        C::SIG_HMAC_SHA1,
85
    ];
86
87
    /** @var array */
88
    protected array $references = [];
89
90
    /** @var bool */
91
    protected bool $enveloping = false;
92
93
94
    /**
95
     * Signature constructor.
96
     *
97
     * @param \DOMElement|string $root The DOM element or a string of data we want to sign.
98
     * @param \SimpleSAML\XMLSecurity\Backend\SignatureBackend|null $backend The backend to use to
99
     *   generate or verify signatures. See individual algorithms for defaults.
100
     */
101
    public function __construct($root, SignatureBackend $backend = null)
102
    {
103
        $this->backend = $backend;
104
        $this->initSignature();
105
106
        if (is_string($root)) {
107
            $this->root = $this->addObject($root);
108
            $this->enveloping = true;
109
        } else {
110
            $this->root = $root;
111
        }
112
    }
113
114
115
    /**
116
     * Add an object element to the signature containing the given data.
117
     *
118
     * @param \DOMElement|string $data The data we want to envelope inside the signature.
119
     * @param string|null $mimetype An optional mime type to specify.
120
     * @param string|null $encoding An optional encoding to specify.
121
     *
122
     * @return \DOMElement The resulting object element added to the signature.
123
     */
124
    public function addObject($data, ?string $mimetype = null, ?string $encoding = null): DOMElement
125
    {
126
        if ($this->objectNode === null) {
127
            $this->objectNode = $this->createElement('Object');
128
            $this->sigNode->appendChild($this->objectNode);
0 ignored issues
show
Bug introduced by
The method appendChild() does not exist on null. ( Ignorable by Annotation )

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

128
            $this->sigNode->/** @scrutinizer ignore-call */ 
129
                            appendChild($this->objectNode);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
$this->objectNode of type null is incompatible with the type DOMNode expected by parameter $node of DOMNode::appendChild(). ( Ignorable by Annotation )

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

128
            $this->sigNode->appendChild(/** @scrutinizer ignore-type */ $this->objectNode);
Loading history...
129
        }
130
131
        if (is_string($mimetype) && !empty($mimetype)) {
132
            $this->objectNode->setAttribute('MimeType', $mimetype);
0 ignored issues
show
Bug introduced by
The method setAttribute() does not exist on null. ( Ignorable by Annotation )

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

132
            $this->objectNode->/** @scrutinizer ignore-call */ 
133
                               setAttribute('MimeType', $mimetype);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
133
        }
134
135
        if (is_string($encoding) && !empty($encoding)) {
136
            $this->objectNode->setAttribute('Encoding', $encoding);
137
        }
138
139
        if ($data instanceof DOMElement) {
140
            $this->objectNode->appendChild($this->sigNode->ownerDocument->importNode($data, true));
0 ignored issues
show
Bug introduced by
The method importNode() does not exist on null. ( Ignorable by Annotation )

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

140
            $this->objectNode->appendChild($this->sigNode->ownerDocument->/** @scrutinizer ignore-call */ importNode($data, true));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
141
        } else {
142
            $this->objectNode->appendChild($this->sigNode->ownerDocument->createTextNode($data));
143
        }
144
145
        return $this->objectNode;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->objectNode could return the type null which is incompatible with the type-hinted return DOMElement. Consider adding an additional type-check to rule them out.
Loading history...
146
    }
147
148
149
    /**
150
     * Add a reference to a given node (an element or a document).
151
     *
152
     * @param \DOMNode $node A DOMElement that we want to sign, or a DOMDocument if we want to sign the entire document.
153
     * @param string $alg The identifier of a supported digest algorithm to use when processing this reference.
154
     * @param array $transforms An array containing a list of transforms that must be applied to the reference.
155
     * Optional.
156
     * @param array $options An array containing a set of options for this reference. Optional. Supported options are:
157
     *   - prefix (string): the XML prefix used in the element being referenced. Defaults to none (no prefix used).
158
     *
159
     *   - prefix_ns (string): the namespace associated with the given prefix. Defaults to none (no prefix used).
160
     *
161
     *   - id_name (string): the name of the "id" attribute in the referenced element. Defaults to "Id".
162
     *
163
     *   - force_uri (boolean): Whether to explicitly add a URI attribute to the reference when referencing a
164
     *     DOMDocument or not. Defaults to true. If force_uri is false and $node is a DOMDocument, the URI attribute
165
     *     will be completely omitted.
166
     *
167
     *   - overwrite (boolean): Whether to overwrite the identifier existing in the element referenced with a new,
168
     *     random one, or not. Defaults to true.
169
     *
170
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If $node is not
171
     *   an instance of DOMDocument or DOMElement.
172
     */
173
    public function addReference(DOMNode $node, string $alg, array $transforms = [], array $options = []): void
174
    {
175
        Assert::isInstanceOfAny(
176
            $node,
177
            [DOMDocument::class, DOMElement::class],
178
            'Only references to the DOM document or elements are allowed.'
179
        );
180
181
        $prefix = @$options['prefix'] ?: null;
182
        $prefixNS = @$options['prefix_ns'] ?: null;
183
        $idName = @$options['id_name'] ?: 'Id';
184
        $attrName = $prefix ? $prefix . ':' . $idName : $idName;
185
        $forceURI = true;
186
        if (isset($options['force_uri'])) {
187
            $forceURI = $options['force_uri'];
188
        }
189
        $overwrite = true;
190
        if (isset($options['overwrite'])) {
191
            $overwrite = $options['overwrite'];
192
        }
193
194
        $reference = $this->createElement('Reference');
195
        $this->sigInfoNode->appendChild($reference);
196
197
        // register reference
198
        $includeCommentNodes = false;
199
        if ($node instanceof DOMElement) {
200
            $uri = null;
201
            if (!$overwrite) {
202
                $uri = $prefixNS ? $node->getAttributeNS($prefixNS, $idName) : $node->getAttribute($idName);
203
            }
204
            if (empty($uri)) {
205
                $uri = Utils\Random::generateGUID();
206
                $node->setAttributeNS($prefixNS, $attrName, $uri);
207
            }
208
209
            if (
210
                in_array(C::C14N_EXCLUSIVE_WITH_COMMENTS, $transforms)
211
                || in_array(C::C14N_INCLUSIVE_WITH_COMMENTS, $transforms)
212
            ) {
213
                $includeCommentNodes = true;
214
                $reference->setAttribute('URI', "#xpointer($attrName('$uri'))");
215
            } else {
216
                $reference->setAttribute('URI', '#' . $uri);
217
            }
218
        } elseif ($forceURI) {
219
            // $node is a \DOMDocument, should add a reference to the root element (enveloped signature)
220
            if (in_array($this->c14nMethod, [C::C14N_INCLUSIVE_WITH_COMMENTS, C::C14N_EXCLUSIVE_WITH_COMMENTS])) {
221
                // if we want to use a C14N method that includes comments, the URI must be an xpointer
222
                $reference->setAttribute('URI', '#xpointer(/)');
223
            } else {
224
                // C14N without comments, we can set an empty URI
225
                $reference->setAttribute('URI', '');
226
            }
227
        }
228
229
        // apply and register transforms
230
        $transformList = $this->createElement('Transforms');
231
        $reference->appendChild($transformList);
232
233
        if (!empty($transforms)) {
234
            foreach ($transforms as $transform) {
235
                if (is_array($transform) && !empty($transform[C::XPATH_URI]['query'])) {
236
                    $t = new Transform(C::XPATH_URI, [new Chunk($transform[C::XPATH_URI]['query'])]);
237
                } else {
238
                    $t = new Transform($transform);
239
                }
240
                $t->toXML($transformList);
241
            }
242
        } elseif (!empty($this->c14nMethod)) {
243
            $t = new Transform($this->c14nMethod);
244
            $t->toXML($transformList);
245
        }
246
247
        $canonicalData = $this->processTransforms($reference, $node, $includeCommentNodes);
248
        $digest = $this->hash($alg, $canonicalData);
249
250
        $digestMethod = new DigestMethod($alg);
251
        $digestMethod->toXML($reference);
252
253
        $digestValue = new DigestValue($digest);
254
        $digestValue->toXML($reference);
255
256
        if (!in_array($node, $this->references)) {
257
            $this->references[] = $node;
258
        }
259
    }
260
261
262
    /**
263
     * Add a set of references to the signature.
264
     *
265
     * @param \DOMNode[] $nodes An array of DOMNode objects to be referred in the signature.
266
     * @param string $alg The identifier of the digest algorithm to use.
267
     * @param array $transforms An array of transforms to apply to each reference.
268
     * @param array $options An array of options.
269
     *
270
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If any of the nodes in the $nodes
271
     *   array is not an instance of DOMDocument or DOMElement.
272
     *
273
     * @see addReference()
274
     */
275
    public function addReferences(array $nodes, string $alg, array $transforms = [], $options = []): void
276
    {
277
        foreach ($nodes as $node) {
278
            $this->addReference($node, $alg, $transforms, $options);
279
        }
280
    }
281
282
283
    /**
284
     * Attach one or more X509 certificates to the signature.
285
     *
286
     * @param \SimpleSAML\XMLSecurity\Key\X509Certificate[] $certs
287
     *   An X509Certificate object or an array of them.
288
     * @param boolean $addSubject Whether to add the subject of the certificate or not.
289
     * @param string|false $digest A digest algorithm identifier if the digest of the certificate should be added. False
290
     * otherwise.
291
     * @param boolean $addIssuerSerial Whether to add the serial number of the issuer or not.
292
     *
293
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If $certs is not a
294
     *   X509Certificate object or an array of them.
295
     */
296
    public function addX509Certificates(
297
        array $certs,
298
        bool $addSubject = false,
299
        $digest = false,
300
        bool $addIssuerSerial = false
301
    ): void {
302
        Assert::allIsInstanceOf($certs, Key\X509Certificate::class);
303
304
        $certData = [];
305
306
        foreach ($certs as $cert) {
307
            $details = $cert->getCertificateDetails();
308
309
            if ($addSubject && isset($details['subject'])) {
310
                // add subject
311
                $subjectNameValue = $details['issuer'];
312
                if (is_array($details['subject'])) {
313
                    $parts = [];
314
                    foreach ($details['subject'] as $key => $value) {
315
                        if (is_array($value)) {
316
                            foreach ($value as $valueElement) {
317
                                array_unshift($parts, $key . '=' . $valueElement);
318
                            }
319
                        } else {
320
                            array_unshift($parts, $key . '=' . $value);
321
                        }
322
                    }
323
                    $subjectNameValue = implode(',', $parts);
324
                }
325
                $certData[] = new X509SubjectName($subjectNameValue);
326
            }
327
328
            if ($digest !== false) {
329
                // add certificate digest
330
                $fingerprint = base64_encode(hex2bin($cert->getRawThumbprint($digest)));
331
                $certData[] = new X509Digest($fingerprint, $digest);
332
            }
333
334
            if ($addIssuerSerial && isset($details['issuer']) && isset($details['serialNumber'])) {
335
                $issuerName = CertificateUtils::parseIssuer($details['issuer']);
0 ignored issues
show
Bug introduced by
The method parseIssuer() does not exist on SimpleSAML\XMLSecurity\Utils\Certificate. ( Ignorable by Annotation )

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

335
                /** @scrutinizer ignore-call */ 
336
                $issuerName = CertificateUtils::parseIssuer($details['issuer']);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
336
337
                $certData[] = new X509IssuerSerial(
338
                    new X509IssuerName($issuerName),
0 ignored issues
show
Bug introduced by
The type SimpleSAML\XMLSecurity\X509IssuerName was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
339
                    new X509SerialNumber($details['serialNumber'])
0 ignored issues
show
Bug introduced by
The type SimpleSAML\XMLSecurity\X509SerialNumber was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
340
                );
341
            }
342
343
            $certData[] = new X509Certificate(CertificateUtils::stripHeaders($cert->getCertificate()));
344
        }
345
346
        $keyInfo = new KeyInfo($certData);
0 ignored issues
show
Bug introduced by
The type SimpleSAML\XMLSecurity\KeyInfo was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
347
        $keyInfoNode = $keyInfo->toXML();
348
349
        if ($this->objectNode === null) {
350
            $this->sigNode->appendChild($keyInfoNode);
351
        } else {
352
            $this->sigNode->insertBefore($keyInfoNode, $this->objectNode);
353
        }
354
    }
355
356
357
    /**
358
     * Append a signature as the last child of the signed element.
359
     *
360
     * @return \DOMNode The appended signature.
361
     */
362
    public function append(): DOMNode
363
    {
364
        return $this->insert();
365
    }
366
367
368
    /**
369
     * Use this signature as an enveloping signature, effectively adding the signed data to a ds:Object element.
370
     *
371
     * @param string|null $mimetype The mime type corresponding to the signed data.
372
     * @param string|null $encoding The encoding corresponding to the signed data.
373
     */
374
    public function envelop(string $mimetype = null, string $encoding = null): void
375
    {
376
        $this->root = $this->addObject($this->root, $mimetype, $encoding);
377
    }
378
379
380
    /**
381
     * Build a new XML digital signature from a given document or node.
382
     *
383
     * @param \DOMNode $node The DOMDocument or DOMElement that contains the signature.
384
     *
385
     * @return Signature A Signature object corresponding to the signature present in the given DOM document or element.
386
     *
387
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If $node is not
388
     *   an instance of DOMDocument or DOMElement.
389
     * @throws \SimpleSAML\XMLSecurity\Exception\NoSignatureFound If there is no signature in the $node.
390
     */
391
    public static function fromXML(DOMNode $node): Signature
392
    {
393
        Assert::isInstanceOfAny(
394
            $node,
395
            [DOMDocument::class, DOMElement::class],
396
            'Signatures can only be created from DOM documents or elements'
397
        );
398
399
        $signature = self::findSignature($node);
400
        if ($node instanceof DOMDocument) {
401
            $node = $node->documentElement;
402
        }
403
        $dsig = new self($node);
404
        $dsig->setSignatureElement($signature);
405
        return $dsig;
406
    }
407
408
409
    /**
410
     * Obtain the list of currently blacklisted algorithms.
411
     *
412
     * Signatures using blacklisted algorithms cannot be created or verified.
413
     *
414
     * @return array An array containing the identifiers of the algorithms blacklisted currently.
415
     */
416
    public function getBlacklistedAlgorithms(): array
417
    {
418
        return $this->algBlacklist;
419
    }
420
421
422
    /**
423
     * Get the list of namespaces to designate ID attributes.
424
     *
425
     * @return array An array of strings with the namespaces used in ID attributes.
426
     */
427
    public function getIdNamespaces(): array
428
    {
429
        return $this->idNS;
430
    }
431
432
433
    /**
434
     * Get a list of attributes used as an ID.
435
     *
436
     * @return array An array of strings with the attributes used as an ID.
437
     */
438
    public function getIdAttributes(): array
439
    {
440
        return $this->idKeys;
441
    }
442
443
444
    /**
445
     * Get the root configured for this signature.
446
     *
447
     * This will be the signed element, whether that's a user-provided XML element or a ds:Object element containing
448
     * the actual data (which can in turn be either XML or not).
449
     *
450
     * @return \DOMElement The root element for this signature.
451
     */
452
    public function getRoot(): DOMElement
453
    {
454
        return $this->root;
455
    }
456
457
458
    /**
459
     * Get the identifier of the algorithm used in this signature.
460
     *
461
     * @return string The identifier of the algorithm used in this signature.
462
     */
463
    public function getSignatureMethod(): string
464
    {
465
        return $this->sigAlg;
466
    }
467
468
469
    /**
470
     * Get a list of elements verified by this signature.
471
     *
472
     * The elements in this list are referenced by the signature and the references verified to be correct. However,
473
     * this doesn't mean the signature is valid, only that the references were so.
474
     *
475
     * Note that the list returned will be empty unless verify() has been called before.
476
     *
477
     * @return \DOMElement[] A list of elements correctly referenced by this signature. An empty list of verify() has
478
     * not been called yet, or if the references couldn't be verified.
479
     */
480
    public function getVerifiedElements(): array
481
    {
482
        return $this->verifiedElements;
483
    }
484
485
486
    /**
487
     * Insert a signature as a child of the signed element, optionally before a given element.
488
     *
489
     * @param \DOMElement|false $before An optional DOM element the signature should be prepended to.
490
     *
491
     * @return \DOMNode The inserted signature.
492
     *
493
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If this signature is in enveloping mode.
494
     */
495
    public function insert($before = false): DOMNode
496
    {
497
        Assert::false(
498
            $this->enveloping,
499
            'Cannot insert the signature in the object it is enveloping.',
500
            RuntimeException::class
501
        );
502
503
        $signature = $this->root->ownerDocument->importNode($this->sigNode, true);
0 ignored issues
show
Bug introduced by
The method importNode() does not exist on null. ( Ignorable by Annotation )

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

503
        /** @scrutinizer ignore-call */ 
504
        $signature = $this->root->ownerDocument->importNode($this->sigNode, true);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
It seems like $this->sigNode can also be of type null; however, parameter $node of DOMDocument::importNode() does only seem to accept DOMNode, 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

503
        $signature = $this->root->ownerDocument->importNode(/** @scrutinizer ignore-type */ $this->sigNode, true);
Loading history...
504
505
        if ($before instanceof DOMElement) {
506
            return $this->root->insertBefore($signature, $before);
507
        }
508
        return $this->root->insertBefore($signature);
509
    }
510
511
512
    /**
513
     * Prepend a signature as the first child of the signed element.
514
     *
515
     * @return \DOMNode The prepended signature.
516
     */
517
    public function prepend(): DOMNode
518
    {
519
        foreach ($this->root->childNodes as $child) {
520
            // look for the first child element, if any
521
            if ($child instanceof \DOMElement) {
522
                return $this->insert($child);
523
            }
524
        }
525
        return $this->append();
526
    }
527
528
529
    /**
530
     * Set the backend to create or verify signatures.
531
     *
532
     * @param SignatureBackend $backend The SignatureBackend implementation to use. See individual algorithms for
533
     * details about the default backends used.
534
     */
535
    public function setBackend(SignatureBackend $backend): void
536
    {
537
        $this->backend = $backend;
538
    }
539
540
541
    /**
542
     * Set the list of currently blacklisted algorithms.
543
     *
544
     * Signatures using blacklisted algorithms cannot be created or verified.
545
     *
546
     * @param array $algs An array containing the identifiers of the algorithms to blacklist.
547
     */
548
    public function setBlacklistedAlgorithms(array $algs): void
549
    {
550
        $this->algBlacklist = $algs;
551
    }
552
553
554
    /**
555
     * Set the encoding for the signed contents in an enveloping signature.
556
     *
557
     * @param string $encoding The encoding used in the signed contents.
558
     *
559
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If this is not an enveloping signature.
560
     */
561
    public function setEncoding(string $encoding): void
562
    {
563
        Assert::true(
564
            $this->enveloping,
565
            'Cannot set the encoding for non-enveloping signatures.',
566
            RuntimeException::class
567
        );
568
569
        $this->root->setAttribute('Encoding', $encoding);
570
    }
571
572
573
    /**
574
     * Set a list of attributes used as an ID.
575
     *
576
     * @param array $keys An array of strings with the attributes used as an ID.
577
     */
578
    public function setIdAttributes(array $keys): void
579
    {
580
        $this->idKeys = $keys;
581
    }
582
583
584
    /**
585
     * Set the list of namespaces to designate ID attributes.
586
     *
587
     * @param array $namespaces An array of strings with the namespaces used in ID attributes.
588
     */
589
    public function setIdNamespaces(array $namespaces): void
590
    {
591
        $this->idNS = $namespaces;
592
    }
593
594
595
    /**
596
     * Set the mime type for the signed contents in an enveloping signature.
597
     *
598
     * @param string $mimetype The mime type of the signed contents.
599
     *
600
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If this is not an enveloping signature.
601
     */
602
    public function setMimeType(string $mimetype): void
603
    {
604
        Assert::true(
605
            $this->enveloping,
606
            'Cannot set the mime type for non-enveloping signatures.',
607
            RuntimeException::class
608
        );
609
610
        $this->root->setAttribute('MimeType', $mimetype);
611
    }
612
613
614
    /**
615
     * Set the signature element to a given one, and initialize the signature from there.
616
     *
617
     * @param \DOMElement $element A DOM element containing an XML signature.
618
     *
619
     * @throws \SimpleSAML\XML\Exception\InvalidDOMElementException If the element does not correspond to an XML
620
     *   signature or it is malformed (e.g. there are missing mandatory elements or attributes).
621
     */
622
    public function setSignatureElement(DOMElement $element): void
623
    {
624
        Assert::same($element->localName, 'Signature', InvalidDOMElementException::class);
625
        Assert::same($element->namespaceURI, Sig::NS, InvalidDOMElementException::class);
626
627
        $this->sigNode = $element;
628
629
        $xp = XP::getXPath($this->sigNode->ownerDocument);
0 ignored issues
show
Bug introduced by
It seems like $this->sigNode->ownerDocument can also be of type null; however, parameter $doc of SimpleSAML\XMLSecurity\Utils\XPath::getXPath() does only seem to accept DOMDocument, 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

629
        $xp = XP::getXPath(/** @scrutinizer ignore-type */ $this->sigNode->ownerDocument);
Loading history...
630
631
        $signedInfoNodes = $xp->query('./ds:SignedInfo', $this->sigNode);
632
633
        Assert::minCount(
634
            $signedInfoNodes,
635
            1,
636
            'There is no SignedInfo element in the signature',
637
            RuntimeException::class
638
        );
639
        $this->sigInfoNode = $signedInfoNodes->item(0);
640
641
642
        $this->sigAlg = $xp->evaluate('string(./ds:SignedInfo/ds:SignatureMethod/@Algorithm)', $this->sigNode);
643
        Assert::stringNotEmpty($this->sigAlg, 'Unable to determine SignatureMethod', RuntimeException::class);
644
645
        $c14nMethodNodes = $xp->query('./ds:CanonicalizationMethod', $this->sigInfoNode);
646
        Assert::minCount(
647
            $c14nMethodNodes,
648
            1,
649
            'There is no CanonicalizationMethod in the signature',
650
            RuntimeException::class
651
        );
652
653
        $this->c14nMethodNode = $c14nMethodNodes->item(0);
654
        if (!$this->c14nMethodNode->hasAttribute('Algorithm')) {
655
            throw new RuntimeException('CanonicalizationMethod missing required Algorithm attribute');
656
        }
657
        $this->c14nMethod = $this->c14nMethodNode->getAttribute('Algorithm');
658
    }
659
660
661
    /**
662
     * Sign the document or element.
663
     *
664
     * This method will finish the signature process. It will create an XML signature valid for document or elements
665
     * specified previously with addReference() or addReferences(). If none of those methods have been called previous
666
     * to calling sign() (there are no references in the signature), the $root passed during construction of the
667
     * Signature object will be referenced automatically.
668
     *
669
     * @param \SimpleSAML\XMLSecurity\Key\AbstractKey $key The key to use for signing. Bear in mind that the type of
670
     *   this key must be compatible with the types of key accepted by the algorithm specified in $alg.
671
     * @param string $alg The identifier of the signature algorithm to use. See \SimpleSAML\XMLSecurity\Constants.
672
     * @param bool $appendToNode Whether to append the signature as the last child of the root element or not.
673
     *
674
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If $appendToNode is true and
675
     *   this is an enveloping signature.
676
     */
677
    public function sign(Key\AbstractKey $key, string $alg, bool $appendToNode = false): void
678
    {
679
        Assert::false(
680
            ($this->enveloping && $appendToNode),
681
            'Cannot append the signature, we are in enveloping mode.',
682
            InvalidArgumentException::class
683
        );
684
685
        $this->sigMethodNode->setAttribute('Algorithm', $alg);
686
        $factory = new SignatureAlgorithmFactory($this->algBlacklist);
687
        $signer = $factory->getAlgorithm($alg, $key);
688
        if ($this->backend !== null) {
689
            $signer->setBackend($this->backend);
690
        }
691
692
        if (empty($this->references)) {
693
            // no references have been added, ref root
694
            $transforms = [];
695
            if (!$this->enveloping) {
696
                $transforms[] = C::XMLDSIG_ENVELOPED;
697
            }
698
            $this->addReference($this->root->ownerDocument, $signer->getDigest(), $transforms, []);
0 ignored issues
show
Bug introduced by
It seems like $this->root->ownerDocument can also be of type null; however, parameter $node of SimpleSAML\XMLSecurity\Signature::addReference() does only seem to accept DOMNode, 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

698
            $this->addReference(/** @scrutinizer ignore-type */ $this->root->ownerDocument, $signer->getDigest(), $transforms, []);
Loading history...
699
        }
700
701
        if ($appendToNode) {
702
            $this->sigNode = $this->append();
703
        } elseif (in_array($this->c14nMethod, [C::C14N_INCLUSIVE_WITHOUT_COMMENTS, C::C14N_INCLUSIVE_WITH_COMMENTS])) {
704
            // append Signature to root node for inclusive canonicalization
705
            $restoreSigNode = $this->sigNode;
706
            $this->sigNode = $this->prepend();
707
        }
708
709
        $sigValue = base64_encode($signer->sign($this->canonicalizeData($this->sigInfoNode, $this->c14nMethod)));
710
711
        // remove Signature from node if we added it for c14n
712
        if (
713
            !$appendToNode &&
714
            in_array($this->c14nMethod, [C::C14N_INCLUSIVE_WITHOUT_COMMENTS, C::C14N_INCLUSIVE_WITH_COMMENTS])
715
        ) { // remove from root in case we added it for inclusive canonicalization
716
            $this->root->removeChild($this->root->lastChild);
0 ignored issues
show
Bug introduced by
It seems like $this->root->lastChild can also be of type null; however, parameter $child of DOMNode::removeChild() does only seem to accept DOMNode, 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

716
            $this->root->removeChild(/** @scrutinizer ignore-type */ $this->root->lastChild);
Loading history...
717
            /** @var \DOMElement $restoreSigNode */
718
            $this->sigNode = $restoreSigNode;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $restoreSigNode does not seem to be defined for all execution paths leading up to this point.
Loading history...
719
        }
720
721
        $sigValueNode = $this->createElement('SignatureValue', $sigValue);
722
        if ($this->sigInfoNode->nextSibling) {
723
            $this->sigInfoNode->nextSibling->parentNode->insertBefore($sigValueNode, $this->sigInfoNode->nextSibling);
0 ignored issues
show
Bug introduced by
The method insertBefore() does not exist on null. ( Ignorable by Annotation )

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

723
            $this->sigInfoNode->nextSibling->parentNode->/** @scrutinizer ignore-call */ 
724
                                                         insertBefore($sigValueNode, $this->sigInfoNode->nextSibling);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
724
        } else {
725
            $this->sigNode->appendChild($sigValueNode);
726
        }
727
    }
728
729
730
    /**
731
     * Verify this signature with a given key.
732
     *
733
     * @param \SimpleSAML\XMLSecurity\Key\AbstractKey $key The key to use to verify this signature.
734
     *
735
     * @return bool True if the signature can be verified with $key, false otherwise.
736
     *
737
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If there is no SignatureValue in
738
     *   the signature, or we couldn't verify all the references.
739
     */
740
    public function verify(Key\AbstractKey $key): bool
741
    {
742
        $xp = XP::getXPath($this->sigNode->ownerDocument);
0 ignored issues
show
Bug introduced by
It seems like $this->sigNode->ownerDocument can also be of type null; however, parameter $doc of SimpleSAML\XMLSecurity\Utils\XPath::getXPath() does only seem to accept DOMDocument, 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

742
        $xp = XP::getXPath(/** @scrutinizer ignore-type */ $this->sigNode->ownerDocument);
Loading history...
743
        $sigval = $xp->evaluate('string(./ds:SignatureValue)', $this->sigNode);
744
        if (empty($sigval)) {
745
            throw new RuntimeException('Unable to locate SignatureValue');
746
        }
747
748
        $siginfo = $this->canonicalizeData($this->sigInfoNode, $this->c14nMethod);
749
        if (!$this->validateReferences()) {
750
            throw new RuntimeException('Unable to verify all references');
751
        }
752
753
        $factory = new SignatureAlgorithmFactory($this->algBlacklist);
754
        $alg = $factory->getAlgorithm($this->sigAlg, $key);
755
        if ($this->backend !== null) {
756
            $alg->setBackend($this->backend);
757
        }
758
        return $alg->verify($siginfo, base64_decode($sigval));
759
    }
760
761
762
    /**
763
     * Canonicalize any given node.
764
     *
765
     * @param \DOMNode $node The DOM node that needs canonicalization.
766
     * @param string $c14nMethod The identifier of the canonicalization algorithm to use.
767
     * See \SimpleSAML\XMLSecurity\Constants.
768
     * @param array|null $xpaths An array of xpaths to filter the nodes by. Defaults to null (no filters).
769
     * @param array|null $prefixes An array of namespace prefixes to filter the nodes by. Defaults to null (no filters).
770
     *
771
     * @return string The canonical representation of the given DOM node, according to the algorithm requested.
772
     */
773
    protected function canonicalizeData(
774
        DOMNode $node,
775
        string $c14nMethod,
776
        array $xpaths = null,
777
        array $prefixes = null
778
    ): string {
779
        $exclusive = false;
780
        $withComments = false;
781
        switch ($c14nMethod) {
782
            case C::C14N_EXCLUSIVE_WITH_COMMENTS:
783
            case C::C14N_INCLUSIVE_WITH_COMMENTS:
784
                $withComments = true;
785
        }
786
        switch ($c14nMethod) {
787
            case C::C14N_EXCLUSIVE_WITH_COMMENTS:
788
            case C::C14N_EXCLUSIVE_WITHOUT_COMMENTS:
789
                $exclusive = true;
790
        }
791
792
        if (
793
            is_null($xpaths)
794
            && ($node->ownerDocument !== null)
795
            && $node->isSameNode($node->ownerDocument->documentElement)
796
        ) {
797
            // check for any PI or comments as they would have been excluded
798
            $element = $node;
799
            while ($refNode = $element->previousSibling) {
800
                if (
801
                    (($refNode->nodeType === XML_COMMENT_NODE) && $withComments)
802
                    || $refNode->nodeType === XML_PI_NODE
803
                ) {
804
                    break;
805
                }
806
                $element = $refNode;
807
            }
808
            if ($refNode == null) {
809
                $node = $node->ownerDocument;
810
            }
811
        }
812
813
        return $node->C14N($exclusive, $withComments, $xpaths, $prefixes);
814
    }
815
816
817
    /**
818
     * Create a new element in this signature.
819
     *
820
     * @param string $name The name of this element.
821
     * @param string|null $content The text contents of the element, or null if it is not supposed to have any text
822
     * contents. Defaults to null.
823
     * @param string $ns The namespace the new element must be created under. Defaults to the standard XMLDSIG
824
     * namespace.
825
     *
826
     * @return \DOMElement A new DOM element with the given name.
827
     */
828
    protected function createElement(
829
        string $name,
830
        string $content = null,
831
        string $ns = C::XMLDSIGNS
832
    ): DOMElement {
833
        if ($this->sigNode === null) {
834
            // initialize signature
835
            $doc = DOMDocumentFactory::create();
836
        } else {
837
            $doc = $this->sigNode->ownerDocument;
838
        }
839
840
        $nsPrefix = $this->nsPrefix;
841
842
        if ($content !== null) {
843
            return $doc->createElementNS($ns, $nsPrefix . $name, $content);
844
        }
845
846
        return $doc->createElementNS($ns, $nsPrefix . $name);
847
    }
848
849
850
    /**
851
     * Find a signature from a given node.
852
     *
853
     * @param \DOMNode $node A DOMElement node where a signature is expected as a child (enveloped) or a DOMDocument
854
     * node to search for document signatures (one single reference with an empty URI).
855
     *
856
     * @return \DOMElement The signature element.
857
     *
858
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If there is no DOMDocument element available.
859
     * @throws \SimpleSAML\XMLSecurity\Exception\NoSignatureFound If no signature is found.
860
     */
861
    protected static function findSignature(DOMNode $node): DOMElement
862
    {
863
        $doc = $node instanceof DOMDocument ? $node : $node->ownerDocument;
864
865
        Assert::notNull($doc, 'Cannot search for signatures, no DOM document available', RuntimeException::class);
866
867
        $xp = XP::getXPath($doc);
0 ignored issues
show
Bug introduced by
It seems like $doc can also be of type null; however, parameter $doc of SimpleSAML\XMLSecurity\Utils\XPath::getXPath() does only seem to accept DOMDocument, 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

867
        $xp = XP::getXPath(/** @scrutinizer ignore-type */ $doc);
Loading history...
868
        $nodeset = $xp->query('./ds:Signature', $node);
869
870
        if ($nodeset->length === 0) {
871
            throw new NoSignatureFound();
872
        }
873
        return $nodeset->item(0);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $nodeset->item(0) returns the type null which is incompatible with the type-hinted return DOMElement.
Loading history...
874
    }
875
876
877
    /**
878
     * Compute the hash for some data with a given algorithm.
879
     *
880
     * @param string $alg The identifier of the algorithm to use.
881
     * @param string $data The data to digest.
882
     * @param bool $encode Whether to bas64-encode the result or not. Defaults to true.
883
     *
884
     * @return string The (binary or base64-encoded) digest corresponding to the given data.
885
     *
886
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If $alg is not a valid
887
     *   identifier of a supported digest algorithm.
888
     */
889
    protected function hash(string $alg, string $data, bool $encode = true): string
890
    {
891
        Assert::keyExists(
892
            C::$DIGEST_ALGORITHMS,
893
            $alg,
894
            'Unsupported digest method "%s"',
895
            InvalidArgumentException::class
896
        );
897
898
        $digest = hash(C::$DIGEST_ALGORITHMS[$alg], $data, true);
899
        return $encode ? base64_encode($digest) : $digest;
900
    }
901
902
903
    /**
904
     * Initialize the basic structure of a signature from scratch.
905
     *
906
     */
907
    protected function initSignature(): void
908
    {
909
        $this->sigNode = $this->createElement('Signature');
910
        $this->sigInfoNode = $this->createElement('SignedInfo');
911
        $this->c14nMethodNode = $this->createElement('CanonicalizationMethod');
912
        $this->c14nMethodNode->setAttribute('Algorithm', $this->c14nMethod);
913
        $this->sigMethodNode = $this->createElement('SignatureMethod');
914
915
        $this->sigInfoNode->appendChild($this->c14nMethodNode);
916
        $this->sigInfoNode->appendChild($this->sigMethodNode);
917
        $this->sigNode->appendChild($this->sigInfoNode);
918
        $this->sigNode->ownerDocument->appendChild($this->sigNode);
919
    }
920
921
922
    /**
923
     * Process a given reference, by looking for it, processing the specified transforms, canonicalizing the result
924
     * and comparing its corresponding digest.
925
     *
926
     * Verified references will be stored in the "verifiedElements" property.
927
     *
928
     * @param \DOMElement $ref The ds:Reference element to process.
929
     *
930
     * @return bool True if the digest of the processed reference matches the one given, false otherwise.
931
     *
932
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If the referenced element is missing or
933
     *   the reference points to an external document.
934
     *
935
     * @see http://www.w3.org/TR/xmldsig-core/#sec-ReferenceProcessingModel
936
     */
937
    protected function processReference(DOMElement $ref): bool
938
    {
939
        /*
940
         * Depending on the URI, we may need to remove comments during canonicalization.
941
         * See: http://www.w3.org/TR/xmldsig-core/#sec-ReferenceProcessingModel
942
         */
943
        $includeCommentNodes = true;
944
        $dataObject = $ref->ownerDocument;
945
        if ($ref->hasAttribute("URI")) {
946
            $uri = $ref->getAttribute('URI');
947
            if (empty($uri)) {
948
                // this reference identifies the enclosing XML, it should not include comments
949
                $includeCommentNodes = false;
950
            }
951
            $arUrl = parse_url($uri);
952
            if (empty($arUrl['path'])) {
953
                if ($identifier = @$arUrl['fragment']) {
954
                    /*
955
                     * This reference identifies a node with the given ID by using a URI on the form '#identifier'.
956
                     * This should not include comments.
957
                     */
958
                    $includeCommentNodes = false;
959
960
                    $xp = XP::getXPath($ref->ownerDocument);
0 ignored issues
show
Bug introduced by
It seems like $ref->ownerDocument can also be of type null; however, parameter $doc of SimpleSAML\XMLSecurity\Utils\XPath::getXPath() does only seem to accept DOMDocument, 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

960
                    $xp = XP::getXPath(/** @scrutinizer ignore-type */ $ref->ownerDocument);
Loading history...
961
                    if ($this->idNS && is_array($this->idNS)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->idNS of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
962
                        foreach ($this->idNS as $nspf => $ns) {
963
                            $xp->registerNamespace($nspf, $ns);
964
                        }
965
                    }
966
                    $iDlist = '@Id="' . $identifier . '"';
967
                    if (is_array($this->idKeys)) {
0 ignored issues
show
introduced by
The condition is_array($this->idKeys) is always true.
Loading history...
968
                        foreach ($this->idKeys as $idKey) {
969
                            $iDlist .= " or @$idKey='$identifier'";
970
                        }
971
                    }
972
                    $query = '//*[' . $iDlist . ']';
973
                    $dataObject = $xp->query($query)->item(0);
974
                    if ($dataObject === null) {
975
                        throw new RuntimeException('Reference not found');
976
                    }
977
                }
978
            } else {
979
                throw new RuntimeException('Processing of external documents is not supported');
980
            }
981
        } else {
982
            // this reference identifies the root node with an empty URI, it should not include comments
983
            $includeCommentNodes = false;
984
        }
985
986
        $data = $this->processTransforms($ref, $dataObject, $includeCommentNodes);
987
        if (!$this->validateDigest($ref, $data)) {
988
            return false;
989
        }
990
991
        // parse the canonicalized reference...
992
        $doc = DOMDocumentFactory::create();
993
        $doc->loadXML($data);
994
        $dataObject = $doc->documentElement;
995
996
        // ... and add it to the list of verified elements
997
        if (!empty($identifier)) {
998
            $this->verifiedElements[$identifier] = $dataObject;
999
        } else {
1000
            $this->verifiedElements[] = $dataObject;
1001
        }
1002
1003
        return true;
1004
    }
1005
1006
1007
    /**
1008
     * Process all transforms specified by a given Reference element.
1009
     *
1010
     * @param \DOMElement $ref The Reference element.
1011
     * @param mixed $data The data referenced.
1012
     * @param bool $includeCommentNodes Whether to allow canonicalization with comments or not.
1013
     *
1014
     * @return string The canonicalized data after applying all transforms specified by $ref.
1015
     *
1016
     * @see http://www.w3.org/TR/xmldsig-core/#sec-ReferenceProcessingModel
1017
     */
1018
    protected function processTransforms(DOMElement $ref, $data, bool $includeCommentNodes = false): string
1019
    {
1020
        if (!($data instanceof DOMNode)) {
1021
            return $data;
1022
        }
1023
1024
        $xp = XP::getXPath($ref->ownerDocument);
0 ignored issues
show
Bug introduced by
It seems like $ref->ownerDocument can also be of type null; however, parameter $doc of SimpleSAML\XMLSecurity\Utils\XPath::getXPath() does only seem to accept DOMDocument, 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

1024
        $xp = XP::getXPath(/** @scrutinizer ignore-type */ $ref->ownerDocument);
Loading history...
1025
        $transforms = $xp->query('./ds:Transforms/ds:Transform', $ref);
1026
        $canonicalMethod = C::C14N_EXCLUSIVE_WITHOUT_COMMENTS;
1027
        $arXPath = null;
1028
        $prefixList = null;
1029
        foreach ($transforms as $transform) {
1030
            /** @var \DOMElement $transform */
1031
            $algorithm = $transform->getAttribute("Algorithm");
1032
            switch ($algorithm) {
1033
                case C::C14N_EXCLUSIVE_WITHOUT_COMMENTS:
1034
                case C::C14N_EXCLUSIVE_WITH_COMMENTS:
1035
                    if (!$includeCommentNodes) {
1036
                        // remove comment nodes by forcing it to use a canonicalization without comments
1037
                        $canonicalMethod = C::C14N_EXCLUSIVE_WITHOUT_COMMENTS;
1038
                    } else {
1039
                        $canonicalMethod = $algorithm;
1040
                    }
1041
1042
                    $node = $transform->firstChild;
1043
                    while ($node) {
1044
                        if ($node->localName === 'InclusiveNamespaces') {
1045
                            if ($pfx = $node->getAttribute('PrefixList')) {
1046
                                $arpfx = [];
1047
                                $pfxlist = explode(" ", $pfx);
1048
                                foreach ($pfxlist as $pfx) {
1049
                                    $val = trim($pfx);
1050
                                    if (! empty($val)) {
1051
                                        $arpfx[] = $val;
1052
                                    }
1053
                                }
1054
                                if (count($arpfx) > 0) {
1055
                                    $prefixList = $arpfx;
1056
                                }
1057
                            }
1058
                            break;
1059
                        }
1060
                        $node = $node->nextSibling;
1061
                    }
1062
                    break;
1063
                case C::C14N_INCLUSIVE_WITHOUT_COMMENTS:
1064
                case C::C14N_INCLUSIVE_WITH_COMMENTS:
1065
                    if (!$includeCommentNodes) {
1066
                        // remove comment nodes by forcing it to use a canonicalization without comments
1067
                        $canonicalMethod = C::C14N_INCLUSIVE_WITHOUT_COMMENTS;
1068
                    } else {
1069
                        $canonicalMethod = $algorithm;
1070
                    }
1071
1072
                    break;
1073
                case C::XPATH_URI:
1074
                    $node = $transform->firstChild;
1075
                    while ($node) {
1076
                        if ($node->localName == 'XPath') {
1077
                            $arXPath = [];
1078
                            $arXPath['query'] = '(.//. | .//@* | .//namespace::*)[' . $node->nodeValue . ']';
1079
                            $arXpath['namespaces'] = [];
1080
                            $nslist = $xp->query('./namespace::*', $node);
1081
                            foreach ($nslist as $nsnode) {
1082
                                if ($nsnode->localName != "xml") {
1083
                                    $arXPath['namespaces'][$nsnode->localName] = $nsnode->nodeValue;
1084
                                }
1085
                            }
1086
                            break;
1087
                        }
1088
                        $node = $node->nextSibling;
1089
                    }
1090
                    break;
1091
            }
1092
        }
1093
1094
        return $this->canonicalizeData($data, $canonicalMethod, $arXPath, $prefixList);
1095
    }
1096
1097
1098
    /**
1099
     * Compute and compare the digest corresponding to some data given to the one specified by a reference.
1100
     *
1101
     * @param \DOMElement $ref The ds:Reference element containing the digest.
1102
     * @param string $data The referenced element, canonicalized, to digest and compare.
1103
     *
1104
     * @return bool True if the resulting digest matches the one in the reference, false otherwise.
1105
     */
1106
    protected function validateDigest(DOMElement $ref, string $data): bool
1107
    {
1108
        $xp = XP::getXPath($ref->ownerDocument);
0 ignored issues
show
Bug introduced by
It seems like $ref->ownerDocument can also be of type null; however, parameter $doc of SimpleSAML\XMLSecurity\Utils\XPath::getXPath() does only seem to accept DOMDocument, 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

1108
        $xp = XP::getXPath(/** @scrutinizer ignore-type */ $ref->ownerDocument);
Loading history...
1109
        $alg = $xp->evaluate('string(./ds:DigestMethod/@Algorithm)', $ref);
1110
        $computed = $this->hash($alg, $data, false);
1111
        $evaluated = base64_decode($xp->evaluate('string(./ds:DigestValue)', $ref));
1112
        return Sec::compareStrings($computed, $evaluated);
1113
    }
1114
1115
1116
    /**
1117
     * Iterate over the references specified by the signature, apply their transforms, and validate their digests
1118
     * against the referenced elements.
1119
     *
1120
     * @return boolean True if all references could be verified, false otherwise.
1121
     *
1122
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If there are no references.
1123
     */
1124
    protected function validateReferences(): bool
1125
    {
1126
        $doc = $this->sigNode->ownerDocument;
1127
1128
        if (!$doc->documentElement->isSameNode($this->sigNode) && $this->sigNode->parentNode !== null) {
0 ignored issues
show
Bug introduced by
It seems like $this->sigNode can also be of type null; however, parameter $otherNode of DOMNode::isSameNode() does only seem to accept DOMNode, 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

1128
        if (!$doc->documentElement->isSameNode(/** @scrutinizer ignore-type */ $this->sigNode) && $this->sigNode->parentNode !== null) {
Loading history...
1129
            // enveloped signature, remove it
1130
            $this->sigNode->parentNode->removeChild($this->sigNode);
0 ignored issues
show
Bug introduced by
It seems like $this->sigNode can also be of type null; however, parameter $child of DOMNode::removeChild() does only seem to accept DOMNode, 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

1130
            $this->sigNode->parentNode->removeChild(/** @scrutinizer ignore-type */ $this->sigNode);
Loading history...
1131
        }
1132
1133
        $xp = XP::getXPath($doc);
0 ignored issues
show
Bug introduced by
It seems like $doc can also be of type null; however, parameter $doc of SimpleSAML\XMLSecurity\Utils\XPath::getXPath() does only seem to accept DOMDocument, 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

1133
        $xp = XP::getXPath(/** @scrutinizer ignore-type */ $doc);
Loading history...
1134
        $refNodes = $xp->query('./ds:SignedInfo/ds:Reference', $this->sigNode);
1135
        Assert::minCount($refNodes, 1, 'There are no Reference nodes', RuntimeException::class);
1136
1137
        $verified = true;
1138
        foreach ($refNodes as $refNode) {
1139
            $verified = $this->processReference($refNode) && $verified;
1140
        }
1141
1142
        return $verified;
1143
    }
1144
}
1145