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

Signature::setPrefix()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 13
rs 10
1
<?php
2
3
namespace SimpleSAML\XMLSecurity;
4
5
use DOMDocument;
6
use DOMElement;
7
use DOMNode;
8
use SimpleSAML\Assert\Assert;
9
use SimpleSAML\XML\Chunk;
10
use SimpleSAML\XML\DOMDocumentFactory;
11
use SimpleSAML\XML\Exception\InvalidDOMElementException;
12
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory;
13
use SimpleSAML\XMLSecurity\Backend\SignatureBackend;
14
use SimpleSAML\XMLSecurity\Constants as C;
15
use SimpleSAML\XMLSecurity\Exception\InvalidArgumentException;
16
use SimpleSAML\XMLSecurity\Exception\NoSignatureFound;
17
use SimpleSAML\XMLSecurity\Exception\RuntimeException;
18
use SimpleSAML\XMLSecurity\Key;
19
use SimpleSAML\XMLSecurity\Utils\Certificate as CertificateUtils;
20
use SimpleSAML\XMLSecurity\Utils\Security as Sec;
21
use SimpleSAML\XMLSecurity\Utils\XPath as XP;
22
use SimpleSAML\XMLSecurity\XML\ds\DigestMethod;
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
use function array_key_exists;
36
use function array_pop;
37
use function array_shift;
38
use function array_unshift;
39
use function base64_decode;
40
use function base64_encode;
41
use function count;
42
use function explode;
43
use function get_class;
44
use function hash;
45
use function implode;
46
use function in_array;
47
use function is_array;
48
use function is_null;
49
use function is_string;
50
use function join;
51
use function parse_url;
52
use function rtrim;
53
use function trim;
54
55
/**
56
 * Class implementing XML digital signatures.
57
 *
58
 * @package SimpleSAML\XMLSecurity
59
 */
60
class Signature
61
{
62
    /** @var array */
63
    public array $idNS = [];
64
65
    /** @var array */
66
    public array $idKeys = [];
67
68
    /** @var \SimpleSAML\XMLSecurity\Backend\SignatureBackend|null */
69
    protected ?SignatureBackend $backend = null;
70
71
    /** @var \DOMElement */
72
    protected DOMElement $root;
73
74
    /** @var \DOMElement|null */
75
    protected ?DOMElement $sigNode = null;
76
77
    /** @var \DOMElement */
78
    protected DOMElement $sigMethodNode;
79
80
    /** @var \DOMElement */
81
    protected DOMElement $c14nMethodNode;
82
83
    /** @var \DOMElement */
84
    protected DOMElement $sigInfoNode;
85
86
    /** @var \DOMElement|null */
87
    protected ?DOMElement $objectNode = null;
88
89
    /** @var string */
90
    protected string $signfo;
91
92
    /** @var string */
93
    protected string $sigAlg;
94
95
    /** @var \DOMElement[] */
96
    protected array $verifiedElements = [];
97
98
    /** @var string */
99
    protected string $c14nMethod = C::C14N_EXCLUSIVE_WITHOUT_COMMENTS;
100
101
    /** @var string */
102
    protected string $nsPrefix = 'ds:';
103
104
    /** @var array */
105
    protected array $algBlacklist = [
106
        C::SIG_RSA_SHA1,
107
        C::SIG_HMAC_SHA1,
108
    ];
109
110
    /** @var array */
111
    protected array $references = [];
112
113
    /** @var bool */
114
    protected bool $enveloping = false;
115
116
117
    /**
118
     * Signature constructor.
119
     *
120
     * @param \DOMElement|string $root The DOM element or a string of data we want to sign.
121
     * @param \SimpleSAML\XMLSecurity\Backend\SignatureBackend|null $backend The backend to use to
122
     *   generate or verify signatures. See individual algorithms for defaults.
123
     */
124
    public function __construct($root, SignatureBackend $backend = null)
125
    {
126
        $this->backend = $backend;
127
        $this->initSignature();
128
129
        if (is_string($root)) {
130
            $this->root = $this->addObject($root);
131
            $this->enveloping = true;
132
        } else {
133
            $this->root = $root;
134
        }
135
    }
136
137
138
    /**
139
     * Add an object element to the signature containing the given data.
140
     *
141
     * @param \DOMElement|string $data The data we want to envelope inside the signature.
142
     * @param string|null $mimetype An optional mime type to specify.
143
     * @param string|null $encoding An optional encoding to specify.
144
     *
145
     * @return \DOMElement The resulting object element added to the signature.
146
     */
147
    public function addObject($data, ?string $mimetype = null, ?string $encoding = null): DOMElement
148
    {
149
        if ($this->objectNode === null) {
150
            $this->objectNode = $this->createElement('Object');
151
            $this->sigNode->appendChild($this->objectNode);
0 ignored issues
show
Bug introduced by
$this->objectNode of type null is incompatible with the type DOMNode expected by parameter $node of DOMNode::appendChild(). ( Ignorable by Annotation )

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

151
            $this->sigNode->appendChild(/** @scrutinizer ignore-type */ $this->objectNode);
Loading history...
Bug introduced by
The method appendChild() does not exist on null. ( Ignorable by Annotation )

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

151
            $this->sigNode->/** @scrutinizer ignore-call */ 
152
                            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...
152
        }
153
154
        if (is_string($mimetype) && !empty($mimetype)) {
155
            $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

155
            $this->objectNode->/** @scrutinizer ignore-call */ 
156
                               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...
156
        }
157
158
        if (is_string($encoding) && !empty($encoding)) {
159
            $this->objectNode->setAttribute('Encoding', $encoding);
160
        }
161
162
        if ($data instanceof DOMElement) {
163
            $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

163
            $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...
164
        } else {
165
            $this->objectNode->appendChild($this->sigNode->ownerDocument->createTextNode($data));
166
        }
167
168
        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...
169
    }
170
171
172
    /**
173
     * Add a reference to a given node (an element or a document).
174
     *
175
     * @param \DOMNode $node A DOMElement that we want to sign, or a DOMDocument if we want to sign the entire document.
176
     * @param string $alg The identifier of a supported digest algorithm to use when processing this reference.
177
     * @param array $transforms An array containing a list of transforms that must be applied to the reference.
178
     * Optional.
179
     * @param array $options An array containing a set of options for this reference. Optional. Supported options are:
180
     *   - prefix (string): the XML prefix used in the element being referenced. Defaults to none (no prefix used).
181
     *
182
     *   - prefix_ns (string): the namespace associated with the given prefix. Defaults to none (no prefix used).
183
     *
184
     *   - id_name (string): the name of the "id" attribute in the referenced element. Defaults to "Id".
185
     *
186
     *   - force_uri (boolean): Whether to explicitly add a URI attribute to the reference when referencing a
187
     *     DOMDocument or not. Defaults to true. If force_uri is false and $node is a DOMDocument, the URI attribute
188
     *     will be completely omitted.
189
     *
190
     *   - overwrite (boolean): Whether to overwrite the identifier existing in the element referenced with a new,
191
     *     random one, or not. Defaults to true.
192
     *
193
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If $node is not
194
     *   an instance of DOMDocument or DOMElement.
195
     */
196
    public function addReference(DOMNode $node, string $alg, array $transforms = [], array $options = []): void
197
    {
198
        Assert::isInstanceOfAny(
199
            $node,
200
            [DOMDocument::class, DOMElement::class],
201
            'Only references to the DOM document or elements are allowed.'
202
        );
203
204
        $prefix = @$options['prefix'] ?: null;
205
        $prefixNS = @$options['prefix_ns'] ?: null;
206
        $idName = @$options['id_name'] ?: 'Id';
207
        $attrName = $prefix ? $prefix . ':' . $idName : $idName;
208
        $forceURI = true;
209
        if (isset($options['force_uri'])) {
210
            $forceURI = $options['force_uri'];
211
        }
212
        $overwrite = true;
213
        if (isset($options['overwrite'])) {
214
            $overwrite = $options['overwrite'];
215
        }
216
217
        $reference = $this->createElement('Reference');
218
        $this->sigInfoNode->appendChild($reference);
219
220
        // register reference
221
        $includeCommentNodes = false;
222
        if ($node instanceof DOMElement) {
223
            $uri = null;
224
            if (!$overwrite) {
225
                $uri = $prefixNS ? $node->getAttributeNS($prefixNS, $idName) : $node->getAttribute($idName);
226
            }
227
            if (empty($uri)) {
228
                $uri = Utils\Random::generateGUID();
229
                $node->setAttributeNS($prefixNS, $attrName, $uri);
230
            }
231
232
            if (
233
                in_array(C::C14N_EXCLUSIVE_WITH_COMMENTS, $transforms)
234
                || in_array(C::C14N_INCLUSIVE_WITH_COMMENTS, $transforms)
235
            ) {
236
                $includeCommentNodes = true;
237
                $reference->setAttribute('URI', "#xpointer($attrName('$uri'))");
238
            } else {
239
                $reference->setAttribute('URI', '#' . $uri);
240
            }
241
        } elseif ($forceURI) {
242
            // $node is a \DOMDocument, should add a reference to the root element (enveloped signature)
243
            if (in_array($this->c14nMethod, [C::C14N_INCLUSIVE_WITH_COMMENTS, C::C14N_EXCLUSIVE_WITH_COMMENTS])) {
244
                // if we want to use a C14N method that includes comments, the URI must be an xpointer
245
                $reference->setAttribute('URI', '#xpointer(/)');
246
            } else {
247
                // C14N without comments, we can set an empty URI
248
                $reference->setAttribute('URI', '');
249
            }
250
        }
251
252
        // apply and register transforms
253
        $transformList = $this->createElement('Transforms');
254
        $reference->appendChild($transformList);
255
256
        if (!empty($transforms)) {
257
            foreach ($transforms as $transform) {
258
                if (is_array($transform) && !empty($transform[C::XPATH_URI]['query'])) {
259
                    $t = new Transform(C::XPATH_URI, [new Chunk($transform[C::XPATH_URI]['query'])]);
0 ignored issues
show
Bug introduced by
array(new SimpleSAML\XML...::XPATH_URI]['query'])) of type array<integer,SimpleSAML\XML\Chunk> is incompatible with the type SimpleSAML\XMLSecurity\XML\ds\XPath|null expected by parameter $xpath of SimpleSAML\XMLSecurity\X...ransform::__construct(). ( Ignorable by Annotation )

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

259
                    $t = new Transform(C::XPATH_URI, /** @scrutinizer ignore-type */ [new Chunk($transform[C::XPATH_URI]['query'])]);
Loading history...
260
                } else {
261
                    $t = new Transform($transform);
262
                }
263
                $t->toXML($transformList);
264
            }
265
        } elseif (!empty($this->c14nMethod)) {
266
            $t = new Transform($this->c14nMethod);
267
            $t->toXML($transformList);
268
        }
269
270
        $canonicalData = $this->processTransforms($reference, $node, $includeCommentNodes);
271
        $digest = $this->hash($alg, $canonicalData);
272
273
        $digestMethod = new DigestMethod($alg);
274
        $digestMethod->toXML($reference);
275
276
        $digestValue = new DigestValue($digest);
277
        $digestValue->toXML($reference);
278
279
        if (!in_array($node, $this->references)) {
280
            $this->references[] = $node;
281
        }
282
    }
283
284
285
    /**
286
     * Add a set of references to the signature.
287
     *
288
     * @param \DOMNode[] $nodes An array of DOMNode objects to be referred in the signature.
289
     * @param string $alg The identifier of the digest algorithm to use.
290
     * @param array $transforms An array of transforms to apply to each reference.
291
     * @param array $options An array of options.
292
     *
293
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If any of the nodes in the $nodes
294
     *   array is not an instance of DOMDocument or DOMElement.
295
     *
296
     * @see addReference()
297
     */
298
    public function addReferences(array $nodes, string $alg, array $transforms = [], $options = []): void
299
    {
300
        foreach ($nodes as $node) {
301
            $this->addReference($node, $alg, $transforms, $options);
302
        }
303
    }
304
305
306
    /**
307
     * Attach one or more X509 certificates to the signature.
308
     *
309
     * @param \SimpleSAML\XMLSecurity\Key\X509Certificate[] $certs
310
     *   An X509Certificate object or an array of them.
311
     * @param boolean $addSubject Whether to add the subject of the certificate or not.
312
     * @param string|false $digest A digest algorithm identifier if the digest of the certificate should be added. False
313
     * otherwise.
314
     * @param boolean $addIssuerSerial Whether to add the serial number of the issuer or not.
315
     *
316
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If $certs is not a
317
     *   X509Certificate object or an array of them.
318
     */
319
    public function addX509Certificates(
320
        array $certs,
321
        bool $addSubject = false,
322
        $digest = false,
323
        bool $addIssuerSerial = false
324
    ): void {
325
        Assert::allIsInstanceOf($certs, Key\X509Certificate::class);
326
327
        $certData = [];
328
329
        foreach ($certs as $cert) {
330
            $details = $cert->getCertificateDetails();
331
332
            if ($addSubject && isset($details['subject'])) {
333
                // add subject
334
                $subjectNameValue = $details['subject'];
335
                if (is_array($subjectNameValue)) {
336
                    $parts = [];
337
                    foreach ($details['subject'] as $key => $value) {
338
                        if (is_array($value)) {
339
                            foreach ($value as $valueElement) {
340
                                array_unshift($parts, $key . '=' . $valueElement);
341
                            }
342
                        } else {
343
                            array_unshift($parts, $key . '=' . $value);
344
                        }
345
                    }
346
                    $subjectNameValue = implode(',', $parts);
347
                }
348
                $certData[] = new X509SubjectName($subjectNameValue);
349
            }
350
351
            if ($digest !== false) {
352
                // add certificate digest
353
                $fingerprint = base64_encode(hex2bin($cert->getRawThumbprint($digest)));
354
                $certData[] = new X509Digest($fingerprint, $digest);
355
            }
356
357
            if ($addIssuerSerial && isset($details['issuer']) && isset($details['serialNumber'])) {
358
                $issuerName = CertificateUtils::parseIssuer($details['issuer']);
359
360
                $certData[] = new X509IssuerSerial(
361
                    new X509IssuerName($issuerName),
362
                    new X509SerialNumber($details['serialNumber'])
363
                );
364
            }
365
366
            $certData[] = new X509Certificate(CertificateUtils::stripHeaders($cert->getCertificate()));
367
        }
368
        $keyInfo = new KeyInfo([new X509Data($certData)]);
369
        $keyInfoNode = $keyInfo->toXML();
370
371
        if ($this->objectNode === null) {
372
            $this->sigNode->appendChild($this->sigNode->ownerDocument->importNode($keyInfoNode, true));
373
        } else {
374
            $this->sigNode->insertBefore($keyInfoNode, $this->objectNode);
375
        }
376
    }
377
378
379
    /**
380
     * Append a signature as the last child of the signed element.
381
     *
382
     * @return \DOMNode The appended signature.
383
     */
384
    public function append(): DOMNode
385
    {
386
        return $this->insert();
387
    }
388
389
390
    /**
391
     * Use this signature as an enveloping signature, effectively adding the signed data to a ds:Object element.
392
     *
393
     * @param string|null $mimetype The mime type corresponding to the signed data.
394
     * @param string|null $encoding The encoding corresponding to the signed data.
395
     */
396
    public function envelop(string $mimetype = null, string $encoding = null): void
397
    {
398
        $this->root = $this->addObject($this->root, $mimetype, $encoding);
399
    }
400
401
402
    /**
403
     * Build a new XML digital signature from a given document or node.
404
     *
405
     * @param \DOMNode $node The DOMDocument or DOMElement that contains the signature.
406
     *
407
     * @return Signature A Signature object corresponding to the signature present in the given DOM document or element.
408
     *
409
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If $node is not
410
     *   an instance of DOMDocument or DOMElement.
411
     * @throws \SimpleSAML\XMLSecurity\Exception\NoSignatureFound If there is no signature in the $node.
412
     */
413
    public static function fromXML(DOMNode $node): Signature
414
    {
415
        Assert::isInstanceOfAny(
416
            $node,
417
            [DOMDocument::class, DOMElement::class],
418
            'Signatures can only be created from DOM documents or elements'
419
        );
420
421
        $signature = self::findSignature($node);
422
        if ($node instanceof DOMDocument) {
423
            $node = $node->documentElement;
424
        }
425
        $dsig = new self($node);
426
        $dsig->setSignatureElement($signature);
427
        return $dsig;
428
    }
429
430
431
    /**
432
     * Obtain the list of currently blacklisted algorithms.
433
     *
434
     * Signatures using blacklisted algorithms cannot be created or verified.
435
     *
436
     * @return array An array containing the identifiers of the algorithms blacklisted currently.
437
     */
438
    public function getBlacklistedAlgorithms(): array
439
    {
440
        return $this->algBlacklist;
441
    }
442
443
444
    /**
445
     * Get the list of namespaces to designate ID attributes.
446
     *
447
     * @return array An array of strings with the namespaces used in ID attributes.
448
     */
449
    public function getIdNamespaces(): array
450
    {
451
        return $this->idNS;
452
    }
453
454
455
    /**
456
     * Get a list of attributes used as an ID.
457
     *
458
     * @return array An array of strings with the attributes used as an ID.
459
     */
460
    public function getIdAttributes(): array
461
    {
462
        return $this->idKeys;
463
    }
464
465
466
    /**
467
     * Get the root configured for this signature.
468
     *
469
     * This will be the signed element, whether that's a user-provided XML element or a ds:Object element containing
470
     * the actual data (which can in turn be either XML or not).
471
     *
472
     * @return \DOMElement The root element for this signature.
473
     */
474
    public function getRoot(): DOMElement
475
    {
476
        return $this->root;
477
    }
478
479
480
    /**
481
     * Get the identifier of the algorithm used in this signature.
482
     *
483
     * @return string The identifier of the algorithm used in this signature.
484
     */
485
    public function getSignatureMethod(): string
486
    {
487
        return $this->sigAlg;
488
    }
489
490
491
    /**
492
     * Get a list of elements verified by this signature.
493
     *
494
     * The elements in this list are referenced by the signature and the references verified to be correct. However,
495
     * this doesn't mean the signature is valid, only that the references were so.
496
     *
497
     * Note that the list returned will be empty unless verify() has been called before.
498
     *
499
     * @return \DOMElement[] A list of elements correctly referenced by this signature. An empty list of verify() has
500
     * not been called yet, or if the references couldn't be verified.
501
     */
502
    public function getVerifiedElements(): array
503
    {
504
        return $this->verifiedElements;
505
    }
506
507
508
    /**
509
     * Insert a signature as a child of the signed element, optionally before a given element.
510
     *
511
     * @param \DOMElement|false $before An optional DOM element the signature should be prepended to.
512
     *
513
     * @return \DOMNode The inserted signature.
514
     *
515
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If this signature is in enveloping mode.
516
     */
517
    public function insert($before = false): DOMNode
518
    {
519
        Assert::false(
520
            $this->enveloping,
521
            'Cannot insert the signature in the object it is enveloping.',
522
            RuntimeException::class
523
        );
524
525
        $signature = $this->root->ownerDocument->importNode($this->sigNode, true);
0 ignored issues
show
Bug introduced by
The method importNode() does not exist on null. ( Ignorable by Annotation )

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

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

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

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

Loading history...
Bug introduced by
It seems like $this->sigNode can also be of type null; however, parameter $node of DOMDocument::importNode() does only seem to accept DOMNode, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

750
            $this->addReference(/** @scrutinizer ignore-type */ $this->root->ownerDocument, $signer->getDigest(), $transforms, []);
Loading history...
751
        }
752
753
        if ($appendToNode) {
754
            $this->sigNode = $this->append();
755
        } elseif (in_array($this->c14nMethod, [C::C14N_INCLUSIVE_WITHOUT_COMMENTS, C::C14N_INCLUSIVE_WITH_COMMENTS])) {
756
            // append Signature to root node for inclusive canonicalization
757
            $restoreSigNode = $this->sigNode;
758
            $this->sigNode = $this->prepend();
759
        }
760
761
        $sigValue = base64_encode($signer->sign($this->canonicalizeData($this->sigInfoNode, $this->c14nMethod)));
0 ignored issues
show
Bug introduced by
It seems like $signer->sign($this->can...de, $this->c14nMethod)) can also be of type false; however, parameter $string of base64_encode() does only seem to accept string, 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

761
        $sigValue = base64_encode(/** @scrutinizer ignore-type */ $signer->sign($this->canonicalizeData($this->sigInfoNode, $this->c14nMethod)));
Loading history...
762
763
        // remove Signature from node if we added it for c14n
764
        if (
765
            !$appendToNode &&
766
            in_array($this->c14nMethod, [C::C14N_INCLUSIVE_WITHOUT_COMMENTS, C::C14N_INCLUSIVE_WITH_COMMENTS])
767
        ) { // remove from root in case we added it for inclusive canonicalization
768
            $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

768
            $this->root->removeChild(/** @scrutinizer ignore-type */ $this->root->lastChild);
Loading history...
769
            /** @var \DOMElement $restoreSigNode */
770
            $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...
771
        }
772
773
        $sigValueNode = $this->createElement('SignatureValue', $sigValue);
774
        if ($this->sigInfoNode->nextSibling) {
775
            $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

775
            $this->sigInfoNode->nextSibling->parentNode->/** @scrutinizer ignore-call */ 
776
                                                         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...
776
        } else {
777
            $this->sigNode->appendChild($sigValueNode);
778
        }
779
    }
780
781
782
    /**
783
     * Verify this signature with a given key.
784
     *
785
     * @param \SimpleSAML\XMLSecurity\Key\AbstractKey $key The key to use to verify this signature.
786
     *
787
     * @return bool True if the signature can be verified with $key, false otherwise.
788
     *
789
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If there is no SignatureValue in
790
     *   the signature, or we couldn't verify all the references.
791
     */
792
    public function verify(Key\AbstractKey $key): bool
793
    {
794
        $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 $node of SimpleSAML\XMLSecurity\Utils\XPath::getXPath() 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

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

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

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

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

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

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

1176
            $this->sigNode->parentNode->removeChild(/** @scrutinizer ignore-type */ $this->sigNode);
Loading history...
1177
        }
1178
1179
        $xp = XP::getXPath($doc);
0 ignored issues
show
Bug introduced by
It seems like $doc can also be of type null; however, parameter $node of SimpleSAML\XMLSecurity\Utils\XPath::getXPath() 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

1179
        $xp = XP::getXPath(/** @scrutinizer ignore-type */ $doc);
Loading history...
1180
        $refNodes = $xp->query('./ds:SignedInfo/ds:Reference', $this->sigNode);
1181
        Assert::minCount($refNodes, 1, 'There are no Reference nodes', RuntimeException::class);
1182
1183
        $verified = true;
1184
        foreach ($refNodes as $refNode) {
1185
            $verified = $this->processReference($refNode) && $verified;
1186
        }
1187
1188
        return $verified;
1189
    }
1190
}
1191