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

Signature::setBackend()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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

118
            $this->sigNode->appendChild(/** @scrutinizer ignore-type */ $this->objectNode);
Loading history...
119
        }
120
121
        if (is_string($mimetype) && !empty($mimetype)) {
122
            $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

122
            $this->objectNode->/** @scrutinizer ignore-call */ 
123
                               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...
123
        }
124
125
        if (is_string($encoding) && !empty($encoding)) {
126
            $this->objectNode->setAttribute('Encoding', $encoding);
127
        }
128
129
        if ($data instanceof DOMElement) {
130
            $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

130
            $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...
131
        } else {
132
            $this->objectNode->appendChild($this->sigNode->ownerDocument->createTextNode($data));
133
        }
134
135
        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...
136
    }
137
138
139
    /**
140
     * Add a reference to a given node (an element or a document).
141
     *
142
     * @param \DOMNode $node A DOMElement that we want to sign, or a DOMDocument if we want to sign the entire document.
143
     * @param string $alg The identifier of a supported digest algorithm to use when processing this reference.
144
     * @param array $transforms An array containing a list of transforms that must be applied to the reference.
145
     * Optional.
146
     * @param array $options An array containing a set of options for this reference. Optional. Supported options are:
147
     *   - prefix (string): the XML prefix used in the element being referenced. Defaults to none (no prefix used).
148
     *
149
     *   - prefix_ns (string): the namespace associated with the given prefix. Defaults to none (no prefix used).
150
     *
151
     *   - id_name (string): the name of the "id" attribute in the referenced element. Defaults to "Id".
152
     *
153
     *   - force_uri (boolean): Whether to explicitly add a URI attribute to the reference when referencing a
154
     *     DOMDocument or not. Defaults to true. If force_uri is false and $node is a DOMDocument, the URI attribute
155
     *     will be completely omitted.
156
     *
157
     *   - overwrite (boolean): Whether to overwrite the identifier existing in the element referenced with a new,
158
     *     random one, or not. Defaults to true.
159
     *
160
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If $node is not
161
     *   an instance of DOMDocument or DOMElement.
162
     */
163
    public function addReference(DOMNode $node, string $alg, array $transforms = [], array $options = []): void
164
    {
165
        if (!in_array(get_class($node), ['DOMDocument', 'DOMElement'])) {
166
            throw new InvalidArgumentException('Only references to the DOM document or elements are allowed.');
167
        }
168
169
        $prefix = @$options['prefix'] ?: null;
170
        $prefixNS = @$options['prefix_ns'] ?: null;
171
        $idName = @$options['id_name'] ?: 'Id';
172
        $attrName = $prefix ? $prefix . ':' . $idName : $idName;
173
        $forceURI = true;
174
        if (isset($options['force_uri'])) {
175
            $forceURI = $options['force_uri'];
176
        }
177
        $overwrite = true;
178
        if (isset($options['overwrite'])) {
179
            $overwrite = $options['overwrite'];
180
        }
181
182
        $reference = $this->createElement('Reference');
183
        $this->sigInfoNode->appendChild($reference);
184
185
        // register reference
186
        $includeCommentNodes = false;
187
        if ($node instanceof DOMElement) {
188
            $uri = null;
189
            if (!$overwrite) {
190
                $uri = $prefixNS ? $node->getAttributeNS($prefixNS, $idName) : $node->getAttribute($idName);
191
            }
192
            if (empty($uri)) {
193
                $uri = Utils\Random::generateGUID();
194
                $node->setAttributeNS($prefixNS, $attrName, $uri);
195
            }
196
197
            if (
198
                in_array(C::C14N_EXCLUSIVE_WITH_COMMENTS, $transforms)
199
                || in_array(C::C14N_INCLUSIVE_WITH_COMMENTS, $transforms)
200
            ) {
201
                $includeCommentNodes = true;
202
                $reference->setAttribute('URI', "#xpointer($attrName('$uri'))");
203
            } else {
204
                $reference->setAttribute('URI', '#' . $uri);
205
            }
206
        } elseif ($forceURI) {
207
            // $node is a \DOMDocument, should add a reference to the root element (enveloped signature)
208
            if (in_array($this->c14nMethod, [C::C14N_INCLUSIVE_WITH_COMMENTS, C::C14N_EXCLUSIVE_WITH_COMMENTS])) {
209
                // if we want to use a C14N method that includes comments, the URI must be an xpointer
210
                $reference->setAttribute('URI', '#xpointer(/)');
211
            } else {
212
                // C14N without comments, we can set an empty URI
213
                $reference->setAttribute('URI', '');
214
            }
215
        }
216
217
        // apply and register transforms
218
        $transformList = $this->createElement('Transforms');
219
        $reference->appendChild($transformList);
220
221
        if (!empty($transforms)) {
222
            foreach ($transforms as $transform) {
223
                $transformNode = $this->createElement('Transform');
224
                $transformList->appendChild($transformNode);
225
226
                if (is_array($transform) && !empty($transform[C::XPATH_URI]['query'])) {
227
                    $transformNode->setAttribute('Algorithm', C::XPATH_URI);
228
                    $xpNode = $this->createElement('XPath', $transform[C::XPATH_URI]['query']);
229
                    $transformNode->appendChild($xpNode);
230
                } else {
231
                    $transformNode->setAttribute('Algorithm', $transform);
232
                }
233
            }
234
        } elseif (!empty($this->c14nMethod)) {
235
            $transformNode = $this->createElement('Transform');
236
            $transformList->appendChild($transformNode);
237
            $transformNode->setAttribute('Algorithm', $this->c14nMethod);
238
        }
239
240
        $canonicalData = $this->processTransforms($reference, $node, $includeCommentNodes);
241
        $digest = $this->hash($alg, $canonicalData);
242
243
        $digestMethod = $this->createElement('DigestMethod');
244
        $reference->appendChild($digestMethod);
245
        $digestMethod->setAttribute('Algorithm', $alg);
246
247
        $digestValue = $this->createElement('DigestValue', $digest);
248
        $reference->appendChild($digestValue);
249
250
        if (!in_array($node, $this->references)) {
251
            $this->references[] = $node;
252
        }
253
    }
254
255
256
    /**
257
     * Add a set of references to the signature.
258
     *
259
     * @param \DOMNode[] $nodes An array of DOMNode objects to be referred in the signature.
260
     * @param string $alg The identifier of the digest algorithm to use.
261
     * @param array $transforms An array of transforms to apply to each reference.
262
     * @param array $options An array of options.
263
     *
264
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If any of the nodes in the $nodes
265
     *   array is not an instance of DOMDocument or DOMElement.
266
     *
267
     * @see addReference()
268
     */
269
    public function addReferences(array $nodes, string $alg, array $transforms = [], $options = []): void
270
    {
271
        foreach ($nodes as $node) {
272
            $this->addReference($node, $alg, $transforms, $options);
273
        }
274
    }
275
276
277
    /**
278
     * Attach one or more X509 certificates to the signature.
279
     *
280
     * @param \SimpleSAML\XMLSecurity\Key\X509Certificate|\SimpleSAML\XMLSecurity\Key\X509Certificate[] $certs
281
     *   An X509Certificate object or an array of them.
282
     * @param boolean $addSubject Whether to add the subject of the certificate or not.
283
     * @param string|false $digest A digest algorithm identifier if the digest of the certificate should be added. False
284
     * otherwise.
285
     * @param boolean $addIssuerSerial Whether to add the serial number of the issuer or not.
286
     *
287
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If $certs is not a
288
     *   X509Certificate object or an array of them.
289
     */
290
    public function addX509Certificates(
291
        $certs,
292
        bool $addSubject = false,
293
        $digest = false,
294
        bool $addIssuerSerial = false
295
    ): void {
296
        if (is_array($certs) && !($certs instanceof Key\X509Certificate)) {
297
            throw new InvalidArgumentException(
298
                'Passed certificates must be either an X509Certificate or a list of them'
299
            );
300
        }
301
        if ($certs instanceof Key\X509Certificate) {
0 ignored issues
show
introduced by
$certs is always a sub-type of SimpleSAML\XMLSecurity\Key\X509Certificate.
Loading history...
302
            $certs = [$certs];
303
        }
304
305
        $keyInfoNode = $this->createElement('KeyInfo');
306
        $certDataNode = $this->createElement('X509Data');
307
        $keyInfoNode->appendChild($certDataNode);
308
309
        if ($this->objectNode === null) {
310
            $this->sigNode->appendChild($keyInfoNode);
311
        } else {
312
            $this->sigNode->insertBefore($keyInfoNode, $this->objectNode);
313
        }
314
315
        foreach ($certs as $cert) {
316
            if (!$cert instanceof Key\X509Certificate) {
317
                throw new InvalidArgumentException(
318
                    'The $certs array can only contain X509Certificate objects'
319
                );
320
            }
321
            $details = $cert->getCertificateDetails();
322
323
            if ($addSubject && isset($details['subject'])) {
324
                // add subject
325
                $subjectNameValue = $details['issuer'];
326
                if (is_array($details['subject'])) {
327
                    $parts = [];
328
                    foreach ($details['subject'] as $key => $value) {
329
                        if (is_array($value)) {
330
                            foreach ($value as $valueElement) {
331
                                array_unshift($parts, $key . '=' . $valueElement);
332
                            }
333
                        } else {
334
                            array_unshift($parts, $key . '=' . $value);
335
                        }
336
                    }
337
                    $subjectNameValue = implode(',', $parts);
338
                }
339
                $x509SubjectNode = new X509SubjectName($subjectNameValue);
340
                $x509SubjectNode->toXML($certDataNode);
341
            }
342
343
            if ($digest !== false) {
344
                // add certificate digest
345
                $fingerprint = base64_encode(hex2bin($cert->getRawThumbprint($digest)));
346
                $x509DigestNode = new X509Digest($fingerprint, $digest);
347
                $x509DigestNode->toXML($certDataNode);
348
            }
349
350
            if ($addIssuerSerial && isset($details['issuer']) && isset($details['serialNumber'])) {
351
                if (is_array($details['issuer'])) {
352
                    $parts = [];
353
                    foreach ($details['issuer'] as $key => $value) {
354
                        array_unshift($parts, $key . '=' . $value);
355
                    }
356
                    $issuerName = implode(',', $parts);
357
                } else {
358
                    $issuerName = $details['issuer'];
359
                }
360
361
                $x509IssuerNode = $this->createElement('X509IssuerSerial');
362
                $certDataNode->appendChild($x509IssuerNode);
363
364
                $x509IssuerNameNode = $this->createElement('X509IssuerName', $issuerName);
365
                $x509IssuerNode->appendChild($x509IssuerNameNode);
366
367
                $x509SerialNumberNode = $this->createElement('X509SerialNumber', $details['serialNumber']);
368
                $x509IssuerNode->appendChild($x509SerialNumberNode);
369
            }
370
371
            $pem_lines = explode("\n", trim($cert->getCertificate()));
372
            array_shift($pem_lines);
373
            array_pop($pem_lines);
374
            $pem = join($pem_lines);
0 ignored issues
show
Bug introduced by
The call to join() has too few arguments starting with array. ( Ignorable by Annotation )

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

374
            $pem = /** @scrutinizer ignore-call */ join($pem_lines);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
375
376
            $x509CertNode = new X509Certificate($pem);
377
            $x509CertNode->toXML($certDataNode);
378
        }
379
    }
380
381
382
    /**
383
     * Append a signature as the last child of the signed element.
384
     *
385
     * @return \DOMNode The appended signature.
386
     */
387
    public function append(): DOMNode
388
    {
389
        return $this->insert();
390
    }
391
392
393
    /**
394
     * Use this signature as an enveloping signature, effectively adding the signed data to a ds:Object element.
395
     *
396
     * @param string|null $mimetype The mime type corresponding to the signed data.
397
     * @param string|null $encoding The encoding corresponding to the signed data.
398
     */
399
    public function envelop(string $mimetype = null, string $encoding = null): void
400
    {
401
        $this->root = $this->addObject($this->root, $mimetype, $encoding);
402
    }
403
404
405
    /**
406
     * Build a new XML digital signature from a given document or node.
407
     *
408
     * @param \DOMNode $node The DOMDocument or DOMElement that contains the signature.
409
     *
410
     * @return Signature A Signature object corresponding to the signature present in the given DOM document or element.
411
     *
412
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If $node is not
413
     *   an instance of DOMDocument or DOMElement.
414
     * @throws \SimpleSAML\XMLSecurity\Exception\NoSignatureFound If there is no signature in the $node.
415
     */
416
    public static function fromXML(DOMNode $node): Signature
417
    {
418
        if (!in_array(get_class($node), ['DOMElement', 'DOMDocument'])) {
419
            throw new InvalidArgumentException('Signatures can only be created from DOM documents or elements');
420
        }
421
422
        $signature = self::findSignature($node);
423
        if ($node instanceof DOMDocument) {
424
            $node = $node->documentElement;
425
        }
426
        $dsig = new self($node);
427
        $dsig->setSignatureElement($signature);
428
        return $dsig;
429
    }
430
431
432
    /**
433
     * Obtain the list of currently blacklisted algorithms.
434
     *
435
     * Signatures using blacklisted algorithms cannot be created or verified.
436
     *
437
     * @return array An array containing the identifiers of the algorithms blacklisted currently.
438
     */
439
    public function getBlacklistedAlgorithms(): array
440
    {
441
        return $this->algBlacklist;
442
    }
443
444
445
    /**
446
     * Get the list of namespaces to designate ID attributes.
447
     *
448
     * @return array An array of strings with the namespaces used in ID attributes.
449
     */
450
    public function getIdNamespaces(): array
451
    {
452
        return $this->idNS;
453
    }
454
455
456
    /**
457
     * Get a list of attributes used as an ID.
458
     *
459
     * @return array An array of strings with the attributes used as an ID.
460
     */
461
    public function getIdAttributes(): array
462
    {
463
        return $this->idKeys;
464
    }
465
466
467
    /**
468
     * Get the root configured for this signature.
469
     *
470
     * This will be the signed element, whether that's a user-provided XML element or a ds:Object element containing
471
     * the actual data (which can in turn be either XML or not).
472
     *
473
     * @return \DOMElement The root element for this signature.
474
     */
475
    public function getRoot(): DOMElement
476
    {
477
        return $this->root;
478
    }
479
480
481
    /**
482
     * Get the identifier of the algorithm used in this signature.
483
     *
484
     * @return string The identifier of the algorithm used in this signature.
485
     */
486
    public function getSignatureMethod(): string
487
    {
488
        return $this->sigAlg;
489
    }
490
491
492
    /**
493
     * Get a list of elements verified by this signature.
494
     *
495
     * The elements in this list are referenced by the signature and the references verified to be correct. However,
496
     * this doesn't mean the signature is valid, only that the references were so.
497
     *
498
     * Note that the list returned will be empty unless verify() has been called before.
499
     *
500
     * @return \DOMElement[] A list of elements correctly referenced by this signature. An empty list of verify() has
501
     * not been called yet, or if the references couldn't be verified.
502
     */
503
    public function getVerifiedElements(): array
504
    {
505
        return $this->verifiedElements;
506
    }
507
508
509
    /**
510
     * Insert a signature as a child of the signed element, optionally before a given element.
511
     *
512
     * @param \DOMElement|false $before An optional DOM element the signature should be prepended to.
513
     *
514
     * @return \DOMNode The inserted signature.
515
     *
516
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If this signature is in enveloping mode.
517
     */
518
    public function insert($before = false): DOMNode
519
    {
520
        if ($this->enveloping) {
521
            throw new RuntimeException('Cannot insert the signature in the object it is enveloping.');
522
        }
523
524
        $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

524
        $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

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

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

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

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

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

780
        $xp = XP::getXPath(/** @scrutinizer ignore-type */ $this->sigNode->ownerDocument);
Loading history...
781
        $sigval = $xp->evaluate('string(./ds:SignatureValue)', $this->sigNode);
782
        if (empty($sigval)) {
783
            throw new RuntimeException('Unable to locate SignatureValue');
784
        }
785
786
        $siginfo = $this->canonicalizeData($this->sigInfoNode, $this->c14nMethod);
787
        if (!$this->validateReferences()) {
788
            throw new RuntimeException('Unable to verify all references');
789
        }
790
791
        $factory = new SignatureAlgorithmFactory($this->algBlacklist);
792
        $alg = $factory->getAlgorithm($this->sigAlg, $key);
793
        if ($this->backend !== null) {
794
            $alg->setBackend($this->backend);
795
        }
796
        return $alg->verify($siginfo, base64_decode($sigval));
797
    }
798
799
800
    /**
801
     * Canonicalize any given node.
802
     *
803
     * @param \DOMNode $node The DOM node that needs canonicalization.
804
     * @param string $c14nMethod The identifier of the canonicalization algorithm to use.
805
     * See \SimpleSAML\XMLSecurity\Constants.
806
     * @param array|null $xpaths An array of xpaths to filter the nodes by. Defaults to null (no filters).
807
     * @param array|null $prefixes An array of namespace prefixes to filter the nodes by. Defaults to null (no filters).
808
     *
809
     * @return string The canonical representation of the given DOM node, according to the algorithm requested.
810
     */
811
    protected function canonicalizeData(
812
        DOMNode $node,
813
        string $c14nMethod,
814
        array $xpaths = null,
815
        array $prefixes = null
816
    ): string {
817
        $exclusive = false;
818
        $withComments = false;
819
        switch ($c14nMethod) {
820
            case C::C14N_EXCLUSIVE_WITH_COMMENTS:
821
            case C::C14N_INCLUSIVE_WITH_COMMENTS:
822
                $withComments = true;
823
        }
824
        switch ($c14nMethod) {
825
            case C::C14N_EXCLUSIVE_WITH_COMMENTS:
826
            case C::C14N_EXCLUSIVE_WITHOUT_COMMENTS:
827
                $exclusive = true;
828
        }
829
830
        if (
831
            is_null($xpaths)
832
            && ($node->ownerDocument !== null)
833
            && $node->isSameNode($node->ownerDocument->documentElement)
834
        ) {
835
            // check for any PI or comments as they would have been excluded
836
            $element = $node;
837
            while ($refNode = $element->previousSibling) {
838
                if (
839
                    (($refNode->nodeType === XML_COMMENT_NODE) && $withComments)
840
                    || $refNode->nodeType === XML_PI_NODE
841
                ) {
842
                    break;
843
                }
844
                $element = $refNode;
845
            }
846
            if ($refNode == null) {
847
                $node = $node->ownerDocument;
848
            }
849
        }
850
851
        return $node->C14N($exclusive, $withComments, $xpaths, $prefixes);
852
    }
853
854
855
    /**
856
     * Create a new element in this signature.
857
     *
858
     * @param string $name The name of this element.
859
     * @param string|null $content The text contents of the element, or null if it is not supposed to have any text
860
     * contents. Defaults to null.
861
     * @param string $ns The namespace the new element must be created under. Defaults to the standard XMLDSIG
862
     * namespace.
863
     *
864
     * @return \DOMElement A new DOM element with the given name.
865
     */
866
    protected function createElement(
867
        string $name,
868
        string $content = null,
869
        string $ns = C::XMLDSIGNS
870
    ): DOMElement {
871
        if ($this->sigNode === null) {
872
            // initialize signature
873
            $doc = DOMDocumentFactory::create();
874
        } else {
875
            $doc = $this->sigNode->ownerDocument;
876
        }
877
878
        $nsPrefix = $this->nsPrefix;
879
880
        if ($content !== null) {
881
            return $doc->createElementNS($ns, $nsPrefix . $name, $content);
882
        }
883
884
        return $doc->createElementNS($ns, $nsPrefix . $name);
885
    }
886
887
888
    /**
889
     * Find a signature from a given node.
890
     *
891
     * @param \DOMNode $node A DOMElement node where a signature is expected as a child (enveloped) or a DOMDocument
892
     * node to search for document signatures (one single reference with an empty URI).
893
     *
894
     * @return \DOMElement The signature element.
895
     *
896
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If there is no DOMDocument element available.
897
     * @throws \SimpleSAML\XMLSecurity\Exception\NoSignatureFound If no signature is found.
898
     */
899
    protected static function findSignature(DOMNode $node): DOMElement
900
    {
901
        $doc = $node instanceof DOMDocument ? $node : $node->ownerDocument;
902
903
        if ($doc === null) {
904
            throw new RuntimeException('Cannot search for signatures, no DOM document available');
905
        }
906
907
        $xp = XP::getXPath($doc);
908
        $nodeset = $xp->query('./ds:Signature', $node);
909
910
        if ($nodeset->length === 0) {
911
            throw new NoSignatureFound();
912
        }
913
        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...
914
    }
915
916
917
    /**
918
     * Compute the hash for some data with a given algorithm.
919
     *
920
     * @param string $alg The identifier of the algorithm to use.
921
     * @param string $data The data to digest.
922
     * @param bool $encode Whether to bas64-encode the result or not. Defaults to true.
923
     *
924
     * @return string The (binary or base64-encoded) digest corresponding to the given data.
925
     *
926
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If $alg is not a valid
927
     *   identifier of a supported digest algorithm.
928
     */
929
    protected function hash(string $alg, string $data, bool $encode = true): string
930
    {
931
        if (!array_key_exists($alg, C::$DIGEST_ALGORITHMS)) {
932
            throw new InvalidArgumentException('Unsupported digest method "' . $alg . '"');
933
        }
934
935
        $digest = hash(C::$DIGEST_ALGORITHMS[$alg], $data, true);
936
        if ($encode) {
937
            $digest = base64_encode($digest);
938
        }
939
        return $digest;
940
    }
941
942
943
    /**
944
     * Initialize the basic structure of a signature from scratch.
945
     *
946
     */
947
    protected function initSignature(): void
948
    {
949
        $this->sigNode = $this->createElement('Signature');
950
        $this->sigInfoNode = $this->createElement('SignedInfo');
951
        $this->c14nMethodNode = $this->createElement('CanonicalizationMethod');
952
        $this->c14nMethodNode->setAttribute('Algorithm', $this->c14nMethod);
953
        $this->sigMethodNode = $this->createElement('SignatureMethod');
954
955
        $this->sigInfoNode->appendChild($this->c14nMethodNode);
956
        $this->sigInfoNode->appendChild($this->sigMethodNode);
957
        $this->sigNode->appendChild($this->sigInfoNode);
958
        $this->sigNode->ownerDocument->appendChild($this->sigNode);
959
    }
960
961
962
    /**
963
     * Process a given reference, by looking for it, processing the specified transforms, canonicalizing the result
964
     * and comparing its corresponding digest.
965
     *
966
     * Verified references will be stored in the "verifiedElements" property.
967
     *
968
     * @param \DOMElement $ref The ds:Reference element to process.
969
     *
970
     * @return bool True if the digest of the processed reference matches the one given, false otherwise.
971
     *
972
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If the referenced element is missing or
973
     *   the reference points to an external document.
974
     *
975
     * @see http://www.w3.org/TR/xmldsig-core/#sec-ReferenceProcessingModel
976
     */
977
    protected function processReference(DOMElement $ref): bool
978
    {
979
        /*
980
         * Depending on the URI, we may need to remove comments during canonicalization.
981
         * See: http://www.w3.org/TR/xmldsig-core/#sec-ReferenceProcessingModel
982
         */
983
        $includeCommentNodes = true;
984
        $dataObject = $ref->ownerDocument;
985
        if ($ref->hasAttribute("URI")) {
986
            $uri = $ref->getAttribute('URI');
987
            if (empty($uri)) {
988
                // this reference identifies the enclosing XML, it should not include comments
989
                $includeCommentNodes = false;
990
            }
991
            $arUrl = parse_url($uri);
992
            if (empty($arUrl['path'])) {
993
                if ($identifier = @$arUrl['fragment']) {
994
                    /*
995
                     * This reference identifies a node with the given ID by using a URI on the form '#identifier'.
996
                     * This should not include comments.
997
                     */
998
                    $includeCommentNodes = false;
999
1000
                    $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

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

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

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

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

1170
            $this->sigNode->parentNode->removeChild(/** @scrutinizer ignore-type */ $this->sigNode);
Loading history...
1171
        }
1172
1173
        $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

1173
        $xp = XP::getXPath(/** @scrutinizer ignore-type */ $doc);
Loading history...
1174
        $refNodes = $xp->query('./ds:SignedInfo/ds:Reference', $this->sigNode);
1175
        if ($refNodes->length < 1) {
1176
            throw new RuntimeException('There are no Reference nodes');
1177
        }
1178
1179
        $verified = true;
1180
        foreach ($refNodes as $refNode) {
1181
            $verified = $this->processReference($refNode) && $verified;
1182
        }
1183
1184
        return $verified;
1185
    }
1186
}
1187