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

131
            $this->sigNode->/** @scrutinizer ignore-call */ 
132
                            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

131
            $this->sigNode->appendChild(/** @scrutinizer ignore-type */ $this->objectNode);
Loading history...
132
        }
133
134
        if (is_string($mimetype) && !empty($mimetype)) {
135
            $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

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

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

506
        $signature = $this->root->ownerDocument->importNode(/** @scrutinizer ignore-type */ $this->sigNode, true);
Loading history...
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

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

662
        $xp = XP::getXPath(/** @scrutinizer ignore-type */ $this->sigNode->ownerDocument);
Loading history...
663
664
        $signedInfoNodes = $xp->query('./ds:SignedInfo', $this->sigNode);
665
666
        Assert::minCount(
667
            $signedInfoNodes,
668
            1,
669
            'There is no SignedInfo element in the signature',
670
            RuntimeException::class
671
        );
672
        $this->sigInfoNode = $signedInfoNodes->item(0);
673
674
675
        $this->sigAlg = $xp->evaluate('string(./ds:SignedInfo/ds:SignatureMethod/@Algorithm)', $this->sigNode);
676
        Assert::stringNotEmpty($this->sigAlg, 'Unable to determine SignatureMethod', RuntimeException::class);
677
678
        $c14nMethodNodes = $xp->query('./ds:CanonicalizationMethod', $this->sigInfoNode);
679
        Assert::minCount(
680
            $c14nMethodNodes,
681
            1,
682
            'There is no CanonicalizationMethod in the signature',
683
            RuntimeException::class
684
        );
685
686
        $this->c14nMethodNode = $c14nMethodNodes->item(0);
687
        if (!$this->c14nMethodNode->hasAttribute('Algorithm')) {
688
            throw new RuntimeException('CanonicalizationMethod missing required Algorithm attribute');
689
        }
690
        $this->c14nMethod = $this->c14nMethodNode->getAttribute('Algorithm');
691
    }
692
693
694
    /**
695
     * Sign the document or element.
696
     *
697
     * This method will finish the signature process. It will create an XML signature valid for document or elements
698
     * specified previously with addReference() or addReferences(). If none of those methods have been called previous
699
     * to calling sign() (there are no references in the signature), the $root passed during construction of the
700
     * Signature object will be referenced automatically.
701
     *
702
     * @param \SimpleSAML\XMLSecurity\Key\AbstractKey $key The key to use for signing. Bear in mind that the type of
703
     *   this key must be compatible with the types of key accepted by the algorithm specified in $alg.
704
     * @param string $alg The identifier of the signature algorithm to use. See \SimpleSAML\XMLSecurity\Constants.
705
     * @param bool $appendToNode Whether to append the signature as the last child of the root element or not.
706
     *
707
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If $appendToNode is true and
708
     *   this is an enveloping signature.
709
     */
710
    public function sign(Key\AbstractKey $key, string $alg, bool $appendToNode = false): void
711
    {
712
        Assert::false(
713
            ($this->enveloping && $appendToNode),
714
            'Cannot append the signature, we are in enveloping mode.',
715
            InvalidArgumentException::class
716
        );
717
718
        $this->sigMethodNode->setAttribute('Algorithm', $alg);
719
        $factory = new SignatureAlgorithmFactory($this->algBlacklist);
720
        $signer = $factory->getAlgorithm($alg, $key);
721
        if ($this->backend !== null) {
722
            $signer->setBackend($this->backend);
723
        }
724
725
        if (empty($this->references)) {
726
            // no references have been added, ref root
727
            $transforms = [];
728
            if (!$this->enveloping) {
729
                $transforms[] = C::XMLDSIG_ENVELOPED;
730
            }
731
            $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

731
            $this->addReference(/** @scrutinizer ignore-type */ $this->root->ownerDocument, $signer->getDigest(), $transforms, []);
Loading history...
732
        }
733
734
        if ($appendToNode) {
735
            $this->sigNode = $this->append();
736
        } elseif (in_array($this->c14nMethod, [C::C14N_INCLUSIVE_WITHOUT_COMMENTS, C::C14N_INCLUSIVE_WITH_COMMENTS])) {
737
            // append Signature to root node for inclusive canonicalization
738
            $restoreSigNode = $this->sigNode;
739
            $this->sigNode = $this->prepend();
740
        }
741
742
        $sigValue = base64_encode($signer->sign($this->canonicalizeData($this->sigInfoNode, $this->c14nMethod)));
743
744
        // remove Signature from node if we added it for c14n
745
        if (
746
            !$appendToNode &&
747
            in_array($this->c14nMethod, [C::C14N_INCLUSIVE_WITHOUT_COMMENTS, C::C14N_INCLUSIVE_WITH_COMMENTS])
748
        ) { // remove from root in case we added it for inclusive canonicalization
749
            $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

749
            $this->root->removeChild(/** @scrutinizer ignore-type */ $this->root->lastChild);
Loading history...
750
            /** @var \DOMElement $restoreSigNode */
751
            $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...
752
        }
753
754
        $sigValueNode = $this->createElement('SignatureValue', $sigValue);
755
        if ($this->sigInfoNode->nextSibling) {
756
            $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

756
            $this->sigInfoNode->nextSibling->parentNode->/** @scrutinizer ignore-call */ 
757
                                                         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...
757
        } else {
758
            $this->sigNode->appendChild($sigValueNode);
759
        }
760
    }
761
762
763
    /**
764
     * Verify this signature with a given key.
765
     *
766
     * @param \SimpleSAML\XMLSecurity\Key\AbstractKey $key The key to use to verify this signature.
767
     *
768
     * @return bool True if the signature can be verified with $key, false otherwise.
769
     *
770
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If there is no SignatureValue in
771
     *   the signature, or we couldn't verify all the references.
772
     */
773
    public function verify(Key\AbstractKey $key): bool
774
    {
775
        $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

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

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

993
                    $xp = XP::getXPath(/** @scrutinizer ignore-type */ $ref->ownerDocument);
Loading history...
994
                    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...
995
                        foreach ($this->idNS as $nspf => $ns) {
996
                            $xp->registerNamespace($nspf, $ns);
997
                        }
998
                    }
999
                    $iDlist = '@Id="' . $identifier . '"';
1000
                    if (is_array($this->idKeys)) {
0 ignored issues
show
introduced by
The condition is_array($this->idKeys) is always true.
Loading history...
1001
                        foreach ($this->idKeys as $idKey) {
1002
                            $iDlist .= " or @$idKey='$identifier'";
1003
                        }
1004
                    }
1005
                    $query = '//*[' . $iDlist . ']';
1006
                    $dataObject = $xp->query($query)->item(0);
1007
                    if ($dataObject === null) {
1008
                        throw new RuntimeException('Reference not found');
1009
                    }
1010
                }
1011
            } else {
1012
                throw new RuntimeException('Processing of external documents is not supported');
1013
            }
1014
        } else {
1015
            // this reference identifies the root node with an empty URI, it should not include comments
1016
            $includeCommentNodes = false;
1017
        }
1018
1019
        $data = $this->processTransforms($ref, $dataObject, $includeCommentNodes);
1020
        if (!$this->validateDigest($ref, $data)) {
1021
            return false;
1022
        }
1023
1024
        // parse the canonicalized reference...
1025
        $doc = DOMDocumentFactory::create();
1026
        $doc->loadXML($data);
1027
        $dataObject = $doc->documentElement;
1028
1029
        // ... and add it to the list of verified elements
1030
        if (!empty($identifier)) {
1031
            $this->verifiedElements[$identifier] = $dataObject;
1032
        } else {
1033
            $this->verifiedElements[] = $dataObject;
1034
        }
1035
1036
        return true;
1037
    }
1038
1039
1040
    /**
1041
     * Process all transforms specified by a given Reference element.
1042
     *
1043
     * @param \DOMElement $ref The Reference element.
1044
     * @param mixed $data The data referenced.
1045
     * @param bool $includeCommentNodes Whether to allow canonicalization with comments or not.
1046
     *
1047
     * @return string The canonicalized data after applying all transforms specified by $ref.
1048
     *
1049
     * @see http://www.w3.org/TR/xmldsig-core/#sec-ReferenceProcessingModel
1050
     */
1051
    protected function processTransforms(DOMElement $ref, $data, bool $includeCommentNodes = false): string
1052
    {
1053
        if (!($data instanceof DOMNode)) {
1054
            return $data;
1055
        }
1056
1057
        $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

1057
        $xp = XP::getXPath(/** @scrutinizer ignore-type */ $ref->ownerDocument);
Loading history...
1058
        $transforms = $xp->query('./ds:Transforms/ds:Transform', $ref);
1059
        $canonicalMethod = C::C14N_EXCLUSIVE_WITHOUT_COMMENTS;
1060
        $arXPath = null;
1061
        $prefixList = null;
1062
        foreach ($transforms as $transform) {
1063
            /** @var \DOMElement $transform */
1064
            $algorithm = $transform->getAttribute("Algorithm");
1065
            switch ($algorithm) {
1066
                case C::C14N_EXCLUSIVE_WITHOUT_COMMENTS:
1067
                case C::C14N_EXCLUSIVE_WITH_COMMENTS:
1068
                    if (!$includeCommentNodes) {
1069
                        // remove comment nodes by forcing it to use a canonicalization without comments
1070
                        $canonicalMethod = C::C14N_EXCLUSIVE_WITHOUT_COMMENTS;
1071
                    } else {
1072
                        $canonicalMethod = $algorithm;
1073
                    }
1074
1075
                    $node = $transform->firstChild;
1076
                    while ($node) {
1077
                        if ($node->localName === 'InclusiveNamespaces') {
1078
                            if ($pfx = $node->getAttribute('PrefixList')) {
1079
                                $arpfx = [];
1080
                                $pfxlist = explode(" ", $pfx);
1081
                                foreach ($pfxlist as $pfx) {
1082
                                    $val = trim($pfx);
1083
                                    if (! empty($val)) {
1084
                                        $arpfx[] = $val;
1085
                                    }
1086
                                }
1087
                                if (count($arpfx) > 0) {
1088
                                    $prefixList = $arpfx;
1089
                                }
1090
                            }
1091
                            break;
1092
                        }
1093
                        $node = $node->nextSibling;
1094
                    }
1095
                    break;
1096
                case C::C14N_INCLUSIVE_WITHOUT_COMMENTS:
1097
                case C::C14N_INCLUSIVE_WITH_COMMENTS:
1098
                    if (!$includeCommentNodes) {
1099
                        // remove comment nodes by forcing it to use a canonicalization without comments
1100
                        $canonicalMethod = C::C14N_INCLUSIVE_WITHOUT_COMMENTS;
1101
                    } else {
1102
                        $canonicalMethod = $algorithm;
1103
                    }
1104
1105
                    break;
1106
                case C::XPATH_URI:
1107
                    $node = $transform->firstChild;
1108
                    while ($node) {
1109
                        if ($node->localName == 'XPath') {
1110
                            $arXPath = [];
1111
                            $arXPath['query'] = '(.//. | .//@* | .//namespace::*)[' . $node->nodeValue . ']';
1112
                            $arXpath['namespaces'] = [];
1113
                            $nslist = $xp->query('./namespace::*', $node);
1114
                            foreach ($nslist as $nsnode) {
1115
                                if ($nsnode->localName != "xml") {
1116
                                    $arXPath['namespaces'][$nsnode->localName] = $nsnode->nodeValue;
1117
                                }
1118
                            }
1119
                            break;
1120
                        }
1121
                        $node = $node->nextSibling;
1122
                    }
1123
                    break;
1124
            }
1125
        }
1126
1127
        return $this->canonicalizeData($data, $canonicalMethod, $arXPath, $prefixList);
1128
    }
1129
1130
1131
    /**
1132
     * Compute and compare the digest corresponding to some data given to the one specified by a reference.
1133
     *
1134
     * @param \DOMElement $ref The ds:Reference element containing the digest.
1135
     * @param string $data The referenced element, canonicalized, to digest and compare.
1136
     *
1137
     * @return bool True if the resulting digest matches the one in the reference, false otherwise.
1138
     */
1139
    protected function validateDigest(DOMElement $ref, string $data): bool
1140
    {
1141
        $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

1141
        $xp = XP::getXPath(/** @scrutinizer ignore-type */ $ref->ownerDocument);
Loading history...
1142
        $alg = $xp->evaluate('string(./ds:DigestMethod/@Algorithm)', $ref);
1143
        $computed = $this->hash($alg, $data, false);
1144
        $evaluated = base64_decode($xp->evaluate('string(./ds:DigestValue)', $ref));
1145
        return Sec::compareStrings($computed, $evaluated);
1146
    }
1147
1148
1149
    /**
1150
     * Iterate over the references specified by the signature, apply their transforms, and validate their digests
1151
     * against the referenced elements.
1152
     *
1153
     * @return boolean True if all references could be verified, false otherwise.
1154
     *
1155
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If there are no references.
1156
     */
1157
    protected function validateReferences(): bool
1158
    {
1159
        $doc = $this->sigNode->ownerDocument;
1160
1161
        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

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

1163
            $this->sigNode->parentNode->removeChild(/** @scrutinizer ignore-type */ $this->sigNode);
Loading history...
1164
        }
1165
1166
        $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

1166
        $xp = XP::getXPath(/** @scrutinizer ignore-type */ $doc);
Loading history...
1167
        $refNodes = $xp->query('./ds:SignedInfo/ds:Reference', $this->sigNode);
1168
        Assert::minCount($refNodes, 1, 'There are no Reference nodes', RuntimeException::class);
1169
1170
        $verified = true;
1171
        foreach ($refNodes as $refNode) {
1172
            $verified = $this->processReference($refNode) && $verified;
1173
        }
1174
1175
        return $verified;
1176
    }
1177
}
1178