Passed
Pull Request — master (#3)
by Tim
01:55
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\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\Certificate as CertificateUtils;
17
use SimpleSAML\XMLSecurity\Utils\Security as Sec;
18
use SimpleSAML\XMLSecurity\Utils\XPath as XP;
19
use SimpleSAML\XMLSecurity\XML\ds\X509Certificate;
20
use SimpleSAML\XMLSecurity\XML\ds\X509Digest;
0 ignored issues
show
Bug introduced by
The type SimpleSAML\XMLSecurity\XML\ds\X509Digest was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

120
            $this->sigNode->/** @scrutinizer ignore-call */ 
121
                            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...
121
        }
122
123
        if (is_string($mimetype) && !empty($mimetype)) {
124
            $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

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

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

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

512
        /** @scrutinizer ignore-call */ 
513
        $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

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

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

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

742
            $this->root->removeChild(/** @scrutinizer ignore-type */ $this->root->lastChild);
Loading history...
743
            /** @var \DOMElement $restoreSigNode */
744
            $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...
745
        }
746
747
        $sigValueNode = $this->createElement('SignatureValue', $sigValue);
748
        if ($this->sigInfoNode->nextSibling) {
749
            $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

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

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

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

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

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

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

1158
            $this->sigNode->parentNode->removeChild(/** @scrutinizer ignore-type */ $this->sigNode);
Loading history...
1159
        }
1160
1161
        $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

1161
        $xp = XP::getXPath(/** @scrutinizer ignore-type */ $doc);
Loading history...
1162
        $refNodes = $xp->query('./ds:SignedInfo/ds:Reference', $this->sigNode);
1163
        if ($refNodes->length < 1) {
1164
            throw new RuntimeException('There are no Reference nodes');
1165
        }
1166
1167
        $verified = true;
1168
        foreach ($refNodes as $refNode) {
1169
            $verified = $this->processReference($refNode) && $verified;
1170
        }
1171
1172
        return $verified;
1173
    }
1174
}
1175