Passed
Pull Request — master (#3)
by Tim
18:01 queued 08:20
created

Signature::validateReferences()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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

129
            $this->sigNode->appendChild(/** @scrutinizer ignore-type */ $this->objectNode);
Loading history...
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

129
            $this->sigNode->/** @scrutinizer ignore-call */ 
130
                            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...
130
        }
131
132
        if (is_string($mimetype) && !empty($mimetype)) {
133
            $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

133
            $this->objectNode->/** @scrutinizer ignore-call */ 
134
                               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...
134
        }
135
136
        if (is_string($encoding) && !empty($encoding)) {
137
            $this->objectNode->setAttribute('Encoding', $encoding);
138
        }
139
140
        if ($data instanceof DOMElement) {
141
            $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

141
            $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...
142
        } else {
143
            $this->objectNode->appendChild($this->sigNode->ownerDocument->createTextNode($data));
144
        }
145
146
        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...
147
    }
148
149
150
    /**
151
     * Add a reference to a given node (an element or a document).
152
     *
153
     * @param \DOMNode $node A DOMElement that we want to sign, or a DOMDocument if we want to sign the entire document.
154
     * @param string $alg The identifier of a supported digest algorithm to use when processing this reference.
155
     * @param array $transforms An array containing a list of transforms that must be applied to the reference.
156
     * Optional.
157
     * @param array $options An array containing a set of options for this reference. Optional. Supported options are:
158
     *   - prefix (string): the XML prefix used in the element being referenced. Defaults to none (no prefix used).
159
     *
160
     *   - prefix_ns (string): the namespace associated with the given prefix. Defaults to none (no prefix used).
161
     *
162
     *   - id_name (string): the name of the "id" attribute in the referenced element. Defaults to "Id".
163
     *
164
     *   - force_uri (boolean): Whether to explicitly add a URI attribute to the reference when referencing a
165
     *     DOMDocument or not. Defaults to true. If force_uri is false and $node is a DOMDocument, the URI attribute
166
     *     will be completely omitted.
167
     *
168
     *   - overwrite (boolean): Whether to overwrite the identifier existing in the element referenced with a new,
169
     *     random one, or not. Defaults to true.
170
     *
171
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If $node is not
172
     *   an instance of DOMDocument or DOMElement.
173
     */
174
    public function addReference(DOMNode $node, string $alg, array $transforms = [], array $options = []): void
175
    {
176
        Assert::isInstanceOfAny(
177
            $node,
178
            [DOMDocument::class, DOMElement::class],
179
            'Only references to the DOM document or elements are allowed.'
180
        );
181
182
        $prefix = @$options['prefix'] ?: null;
183
        $prefixNS = @$options['prefix_ns'] ?: null;
184
        $idName = @$options['id_name'] ?: 'Id';
185
        $attrName = $prefix ? $prefix . ':' . $idName : $idName;
186
        $forceURI = true;
187
        if (isset($options['force_uri'])) {
188
            $forceURI = $options['force_uri'];
189
        }
190
        $overwrite = true;
191
        if (isset($options['overwrite'])) {
192
            $overwrite = $options['overwrite'];
193
        }
194
195
        $reference = $this->createElement('Reference');
196
        $this->sigInfoNode->appendChild($reference);
197
198
        // register reference
199
        $includeCommentNodes = false;
200
        if ($node instanceof DOMElement) {
201
            $uri = null;
202
            if (!$overwrite) {
203
                $uri = $prefixNS ? $node->getAttributeNS($prefixNS, $idName) : $node->getAttribute($idName);
204
            }
205
            if (empty($uri)) {
206
                $uri = Utils\Random::generateGUID();
207
                $node->setAttributeNS($prefixNS, $attrName, $uri);
208
            }
209
210
            if (
211
                in_array(C::C14N_EXCLUSIVE_WITH_COMMENTS, $transforms)
212
                || in_array(C::C14N_INCLUSIVE_WITH_COMMENTS, $transforms)
213
            ) {
214
                $includeCommentNodes = true;
215
                $reference->setAttribute('URI', "#xpointer($attrName('$uri'))");
216
            } else {
217
                $reference->setAttribute('URI', '#' . $uri);
218
            }
219
        } elseif ($forceURI) {
220
            // $node is a \DOMDocument, should add a reference to the root element (enveloped signature)
221
            if (in_array($this->c14nMethod, [C::C14N_INCLUSIVE_WITH_COMMENTS, C::C14N_EXCLUSIVE_WITH_COMMENTS])) {
222
                // if we want to use a C14N method that includes comments, the URI must be an xpointer
223
                $reference->setAttribute('URI', '#xpointer(/)');
224
            } else {
225
                // C14N without comments, we can set an empty URI
226
                $reference->setAttribute('URI', '');
227
            }
228
        }
229
230
        // apply and register transforms
231
        $transformList = $this->createElement('Transforms');
232
        $reference->appendChild($transformList);
233
234
        if (!empty($transforms)) {
235
            foreach ($transforms as $transform) {
236
                if (is_array($transform) && !empty($transform[C::XPATH_URI]['query'])) {
237
                    $t = new Transform(C::XPATH_URI, [new Chunk($transform[C::XPATH_URI]['query'])]);
238
                } else {
239
                    $t = new Transform($transform);
240
                }
241
                $t->toXML($transformList);
242
            }
243
        } elseif (!empty($this->c14nMethod)) {
244
            $t = new Transform($this->c14nMethod);
245
            $t->toXML($transformList);
246
        }
247
248
        $canonicalData = $this->processTransforms($reference, $node, $includeCommentNodes);
249
        $digest = $this->hash($alg, $canonicalData);
250
251
        $digestMethod = new DigestMethod($alg);
252
        $digestMethod->toXML($reference);
253
254
        $digestValue = new DigestValue($digest);
255
        $digestValue->toXML($reference);
256
257
        if (!in_array($node, $this->references)) {
258
            $this->references[] = $node;
259
        }
260
    }
261
262
263
    /**
264
     * Add a set of references to the signature.
265
     *
266
     * @param \DOMNode[] $nodes An array of DOMNode objects to be referred in the signature.
267
     * @param string $alg The identifier of the digest algorithm to use.
268
     * @param array $transforms An array of transforms to apply to each reference.
269
     * @param array $options An array of options.
270
     *
271
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If any of the nodes in the $nodes
272
     *   array is not an instance of DOMDocument or DOMElement.
273
     *
274
     * @see addReference()
275
     */
276
    public function addReferences(array $nodes, string $alg, array $transforms = [], $options = []): void
277
    {
278
        foreach ($nodes as $node) {
279
            $this->addReference($node, $alg, $transforms, $options);
280
        }
281
    }
282
283
284
    /**
285
     * Attach one or more X509 certificates to the signature.
286
     *
287
     * @param \SimpleSAML\XMLSecurity\Key\X509Certificate[] $certs
288
     *   An X509Certificate object or an array of them.
289
     * @param boolean $addSubject Whether to add the subject of the certificate or not.
290
     * @param string|false $digest A digest algorithm identifier if the digest of the certificate should be added. False
291
     * otherwise.
292
     * @param boolean $addIssuerSerial Whether to add the serial number of the issuer or not.
293
     *
294
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If $certs is not a
295
     *   X509Certificate object or an array of them.
296
     */
297
    public function addX509Certificates(
298
        array $certs,
299
        bool $addSubject = false,
300
        $digest = false,
301
        bool $addIssuerSerial = false
302
    ): void {
303
        Assert::allIsInstanceOf($certs, Key\X509Certificate::class);
304
305
        $certData = [];
306
307
        foreach ($certs as $cert) {
308
            $details = $cert->getCertificateDetails();
309
310
            if ($addSubject && isset($details['subject'])) {
311
                // add subject
312
                $subjectNameValue = $details['issuer'];
313
                if (is_array($details['subject'])) {
314
                    $parts = [];
315
                    foreach ($details['subject'] as $key => $value) {
316
                        if (is_array($value)) {
317
                            foreach ($value as $valueElement) {
318
                                array_unshift($parts, $key . '=' . $valueElement);
319
                            }
320
                        } else {
321
                            array_unshift($parts, $key . '=' . $value);
322
                        }
323
                    }
324
                    $subjectNameValue = implode(',', $parts);
325
                }
326
                $certData[] = new X509SubjectName($subjectNameValue);
327
            }
328
329
            if ($digest !== false) {
330
                // add certificate digest
331
                $fingerprint = base64_encode(hex2bin($cert->getRawThumbprint($digest)));
332
                $certData[] = new X509Digest($fingerprint, $digest);
333
            }
334
335
            if ($addIssuerSerial && isset($details['issuer']) && isset($details['serialNumber'])) {
336
                $issuerName = CertificateUtils::parseIssuer($details['issuer']);
337
338
                $certData[] = new X509IssuerSerial(
339
                    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...
340
                    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...
341
                );
342
            }
343
344
            $certData[] = new X509Certificate(CertificateUtils::stripHeaders($cert->getCertificate()));
345
        }
346
347
        $keyInfo = new KeyInfo($certData);
348
        $keyInfoNode = $keyInfo->toXML();
349
350
        if ($this->objectNode === null) {
351
            $this->sigNode->appendChild($keyInfoNode);
352
        } else {
353
            $this->sigNode->insertBefore($keyInfoNode, $this->objectNode);
354
        }
355
    }
356
357
358
    /**
359
     * Append a signature as the last child of the signed element.
360
     *
361
     * @return \DOMNode The appended signature.
362
     */
363
    public function append(): DOMNode
364
    {
365
        return $this->insert();
366
    }
367
368
369
    /**
370
     * Use this signature as an enveloping signature, effectively adding the signed data to a ds:Object element.
371
     *
372
     * @param string|null $mimetype The mime type corresponding to the signed data.
373
     * @param string|null $encoding The encoding corresponding to the signed data.
374
     */
375
    public function envelop(string $mimetype = null, string $encoding = null): void
376
    {
377
        $this->root = $this->addObject($this->root, $mimetype, $encoding);
378
    }
379
380
381
    /**
382
     * Build a new XML digital signature from a given document or node.
383
     *
384
     * @param \DOMNode $node The DOMDocument or DOMElement that contains the signature.
385
     *
386
     * @return Signature A Signature object corresponding to the signature present in the given DOM document or element.
387
     *
388
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If $node is not
389
     *   an instance of DOMDocument or DOMElement.
390
     * @throws \SimpleSAML\XMLSecurity\Exception\NoSignatureFound If there is no signature in the $node.
391
     */
392
    public static function fromXML(DOMNode $node): Signature
393
    {
394
        Assert::isInstanceOfAny(
395
            $node,
396
            [DOMDocument::class, DOMElement::class],
397
            'Signatures can only be created from DOM documents or elements'
398
        );
399
400
        $signature = self::findSignature($node);
401
        if ($node instanceof DOMDocument) {
402
            $node = $node->documentElement;
403
        }
404
        $dsig = new self($node);
405
        $dsig->setSignatureElement($signature);
406
        return $dsig;
407
    }
408
409
410
    /**
411
     * Obtain the list of currently blacklisted algorithms.
412
     *
413
     * Signatures using blacklisted algorithms cannot be created or verified.
414
     *
415
     * @return array An array containing the identifiers of the algorithms blacklisted currently.
416
     */
417
    public function getBlacklistedAlgorithms(): array
418
    {
419
        return $this->algBlacklist;
420
    }
421
422
423
    /**
424
     * Get the list of namespaces to designate ID attributes.
425
     *
426
     * @return array An array of strings with the namespaces used in ID attributes.
427
     */
428
    public function getIdNamespaces(): array
429
    {
430
        return $this->idNS;
431
    }
432
433
434
    /**
435
     * Get a list of attributes used as an ID.
436
     *
437
     * @return array An array of strings with the attributes used as an ID.
438
     */
439
    public function getIdAttributes(): array
440
    {
441
        return $this->idKeys;
442
    }
443
444
445
    /**
446
     * Get the root configured for this signature.
447
     *
448
     * This will be the signed element, whether that's a user-provided XML element or a ds:Object element containing
449
     * the actual data (which can in turn be either XML or not).
450
     *
451
     * @return \DOMElement The root element for this signature.
452
     */
453
    public function getRoot(): DOMElement
454
    {
455
        return $this->root;
456
    }
457
458
459
    /**
460
     * Get the identifier of the algorithm used in this signature.
461
     *
462
     * @return string The identifier of the algorithm used in this signature.
463
     */
464
    public function getSignatureMethod(): string
465
    {
466
        return $this->sigAlg;
467
    }
468
469
470
    /**
471
     * Get a list of elements verified by this signature.
472
     *
473
     * The elements in this list are referenced by the signature and the references verified to be correct. However,
474
     * this doesn't mean the signature is valid, only that the references were so.
475
     *
476
     * Note that the list returned will be empty unless verify() has been called before.
477
     *
478
     * @return \DOMElement[] A list of elements correctly referenced by this signature. An empty list of verify() has
479
     * not been called yet, or if the references couldn't be verified.
480
     */
481
    public function getVerifiedElements(): array
482
    {
483
        return $this->verifiedElements;
484
    }
485
486
487
    /**
488
     * Insert a signature as a child of the signed element, optionally before a given element.
489
     *
490
     * @param \DOMElement|false $before An optional DOM element the signature should be prepended to.
491
     *
492
     * @return \DOMNode The inserted signature.
493
     *
494
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If this signature is in enveloping mode.
495
     */
496
    public function insert($before = false): DOMNode
497
    {
498
        Assert::false(
499
            $this->enveloping,
500
            'Cannot insert the signature in the object it is enveloping.',
501
            RuntimeException::class
502
        );
503
504
        $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

504
        /** @scrutinizer ignore-call */ 
505
        $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

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

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

699
            $this->addReference(/** @scrutinizer ignore-type */ $this->root->ownerDocument, $signer->getDigest(), $transforms, []);
Loading history...
700
        }
701
702
        if ($appendToNode) {
703
            $this->sigNode = $this->append();
704
        } elseif (in_array($this->c14nMethod, [C::C14N_INCLUSIVE_WITHOUT_COMMENTS, C::C14N_INCLUSIVE_WITH_COMMENTS])) {
705
            // append Signature to root node for inclusive canonicalization
706
            $restoreSigNode = $this->sigNode;
707
            $this->sigNode = $this->prepend();
708
        }
709
710
        $sigValue = base64_encode($signer->sign($this->canonicalizeData($this->sigInfoNode, $this->c14nMethod)));
711
712
        // remove Signature from node if we added it for c14n
713
        if (
714
            !$appendToNode &&
715
            in_array($this->c14nMethod, [C::C14N_INCLUSIVE_WITHOUT_COMMENTS, C::C14N_INCLUSIVE_WITH_COMMENTS])
716
        ) { // remove from root in case we added it for inclusive canonicalization
717
            $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

717
            $this->root->removeChild(/** @scrutinizer ignore-type */ $this->root->lastChild);
Loading history...
718
            /** @var \DOMElement $restoreSigNode */
719
            $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...
720
        }
721
722
        $sigValueNode = $this->createElement('SignatureValue', $sigValue);
723
        if ($this->sigInfoNode->nextSibling) {
724
            $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

724
            $this->sigInfoNode->nextSibling->parentNode->/** @scrutinizer ignore-call */ 
725
                                                         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...
725
        } else {
726
            $this->sigNode->appendChild($sigValueNode);
727
        }
728
    }
729
730
731
    /**
732
     * Verify this signature with a given key.
733
     *
734
     * @param \SimpleSAML\XMLSecurity\Key\AbstractKey $key The key to use to verify this signature.
735
     *
736
     * @return bool True if the signature can be verified with $key, false otherwise.
737
     *
738
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If there is no SignatureValue in
739
     *   the signature, or we couldn't verify all the references.
740
     */
741
    public function verify(Key\AbstractKey $key): bool
742
    {
743
        $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

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

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

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

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

1109
        $xp = XP::getXPath(/** @scrutinizer ignore-type */ $ref->ownerDocument);
Loading history...
1110
        $alg = $xp->evaluate('string(./ds:DigestMethod/@Algorithm)', $ref);
1111
        $computed = $this->hash($alg, $data, false);
1112
        $evaluated = base64_decode($xp->evaluate('string(./ds:DigestValue)', $ref));
1113
        return Sec::compareStrings($computed, $evaluated);
1114
    }
1115
1116
1117
    /**
1118
     * Iterate over the references specified by the signature, apply their transforms, and validate their digests
1119
     * against the referenced elements.
1120
     *
1121
     * @return boolean True if all references could be verified, false otherwise.
1122
     *
1123
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If there are no references.
1124
     */
1125
    protected function validateReferences(): bool
1126
    {
1127
        $doc = $this->sigNode->ownerDocument;
1128
1129
        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

1129
        if (!$doc->documentElement->isSameNode(/** @scrutinizer ignore-type */ $this->sigNode) && $this->sigNode->parentNode !== null) {
Loading history...
1130
            // enveloped signature, remove it
1131
            $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

1131
            $this->sigNode->parentNode->removeChild(/** @scrutinizer ignore-type */ $this->sigNode);
Loading history...
1132
        }
1133
1134
        $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

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