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

Signature::addX509Certificates()   D

Complexity

Conditions 19
Paths 81

Size

Total Lines 88
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
eloc 55
c 0
b 0
f 0
nc 81
nop 4
dl 0
loc 88
rs 4.5166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\DOMDocumentFactory;
10
use SimpleSAML\XML\Exception\InvalidDOMElementException;
11
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory;
12
use SimpleSAML\XMLSecurity\Backend\SignatureBackend;
13
use SimpleSAML\XMLSecurity\Constants as C;
14
use SimpleSAML\XMLSecurity\Exception\InvalidArgumentException;
15
use SimpleSAML\XMLSecurity\Exception\NoSignatureFound;
16
use SimpleSAML\XMLSecurity\Exception\RuntimeException;
17
use SimpleSAML\XMLSecurity\Key\AbstractKey;
18
use SimpleSAML\XMLSecurity\Key\X509Certificate;
19
use SimpleSAML\XMLSecurity\Utils\Security as Sec;
20
use SimpleSAML\XMLSecurity\Utils\XPath as XP;
21
use SimpleSAML\XMLSecurity\XML\ds\Signature as Sig;
22
use SimpleSAML\XMLSecurity\XML\ds\X509Certificate;
0 ignored issues
show
Bug introduced by
A parse error occurred: Cannot use SimpleSAML\XMLSecurity\XML\ds\X509Certificate as X509Certificate because the name is already in use
Loading history...
23
use SimpleSAML\XMLSecurity\XML\ds\X509Data;
24
use SimpleSAML\XMLSecurity\XML\ds\X509Digest;
25
use SimpleSAML\XMLSecurity\XML\ds\X509IssuerSerial;
26
use SimpleSAML\XMLSecurity\XML\ds\X509SubjectName;
27
28
/**
29
 * Class implementing XML digital signatures.
30
 *
31
 * @package SimpleSAML\XMLSecurity
32
 */
33
class Signature
34
{
35
    /** @var array */
36
    public array $idNS = [];
37
38
    /** @var array */
39
    public array $idKeys = [];
40
41
    /** @var \SimpleSAML\XMLSecurity\Backend\SignatureBackend|null */
42
    protected ?SignatureBackend $backend = null;
43
44
    /** @var \DOMElement */
45
    protected DOMElement $root;
46
47
    /** @var \DOMElement|null */
48
    protected ?DOMElement $sigNode = null;
49
50
    /** @var \DOMElement */
51
    protected DOMElement $sigMethodNode;
52
53
    /** @var \DOMElement */
54
    protected DOMElement $c14nMethodNode;
55
56
    /** @var \DOMElement */
57
    protected DOMElement $sigInfoNode;
58
59
    /** @var \DOMElement|null */
60
    protected ?DOMElement $objectNode = null;
61
62
    /** @var string */
63
    protected string $signfo;
64
65
    /** @var string */
66
    protected string $sigAlg;
67
68
    /** @var \DOMElement[] */
69
    protected array $verifiedElements = [];
70
71
    /** @var string */
72
    protected string $c14nMethod = C::C14N_EXCLUSIVE_WITHOUT_COMMENTS;
73
74
    /** @var string */
75
    protected string $nsPrefix = 'ds:';
76
77
    /** @var array */
78
    protected array $algBlacklist = [
79
        C::SIG_RSA_SHA1,
80
        C::SIG_HMAC_SHA1,
81
    ];
82
83
    /** @var array */
84
    protected array $references = [];
85
86
    /** @var bool */
87
    protected bool $enveloping = false;
88
89
90
    /**
91
     * Signature constructor.
92
     *
93
     * @param \DOMElement|string $root The DOM element or a string of data we want to sign.
94
     * @param \SimpleSAML\XMLSecurity\Backend\SignatureBackend|null $backend The backend to use to
95
     *   generate or verify signatures. See individual algorithms for defaults.
96
     */
97
    public function __construct($root, SignatureBackend $backend = null)
98
    {
99
        $this->backend = $backend;
100
        $this->initSignature();
101
102
        if (is_string($root)) {
103
            $this->root = $this->addObject($root);
104
            $this->enveloping = true;
105
        } else {
106
            $this->root = $root;
107
        }
108
    }
109
110
111
    /**
112
     * Add an object element to the signature containing the given data.
113
     *
114
     * @param \DOMElement|string $data The data we want to envelope inside the signature.
115
     * @param string|null $mimetype An optional mime type to specify.
116
     * @param string|null $encoding An optional encoding to specify.
117
     *
118
     * @return \DOMElement The resulting object element added to the signature.
119
     */
120
    public function addObject($data, ?string $mimetype = null, ?string $encoding = null): DOMElement
121
    {
122
        if ($this->objectNode === null) {
123
            $this->objectNode = $this->createElement('Object');
124
            $this->sigNode->appendChild($this->objectNode);
125
        }
126
127
        if (is_string($mimetype) && !empty($mimetype)) {
128
            $this->objectNode->setAttribute('MimeType', $mimetype);
129
        }
130
131
        if (is_string($encoding) && !empty($encoding)) {
132
            $this->objectNode->setAttribute('Encoding', $encoding);
133
        }
134
135
        if ($data instanceof DOMElement) {
136
            $this->objectNode->appendChild($this->sigNode->ownerDocument->importNode($data, true));
137
        } else {
138
            $this->objectNode->appendChild($this->sigNode->ownerDocument->createTextNode($data));
139
        }
140
141
        return $this->objectNode;
142
    }
143
144
145
    /**
146
     * Add a reference to a given node (an element or a document).
147
     *
148
     * @param \DOMNode $node A DOMElement that we want to sign, or a DOMDocument if we want to sign the entire document.
149
     * @param string $alg The identifier of a supported digest algorithm to use when processing this reference.
150
     * @param array $transforms An array containing a list of transforms that must be applied to the reference.
151
     * Optional.
152
     * @param array $options An array containing a set of options for this reference. Optional. Supported options are:
153
     *   - prefix (string): the XML prefix used in the element being referenced. Defaults to none (no prefix used).
154
     *
155
     *   - prefix_ns (string): the namespace associated with the given prefix. Defaults to none (no prefix used).
156
     *
157
     *   - id_name (string): the name of the "id" attribute in the referenced element. Defaults to "Id".
158
     *
159
     *   - force_uri (boolean): Whether to explicitly add a URI attribute to the reference when referencing a
160
     *     DOMDocument or not. Defaults to true. If force_uri is false and $node is a DOMDocument, the URI attribute
161
     *     will be completely omitted.
162
     *
163
     *   - overwrite (boolean): Whether to overwrite the identifier existing in the element referenced with a new,
164
     *     random one, or not. Defaults to true.
165
     *
166
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If $node is not
167
     *   an instance of DOMDocument or DOMElement.
168
     */
169
    public function addReference(DOMNode $node, string $alg, array $transforms = [], array $options = []): void
170
    {
171
        Assert::isInstanceOfAny(
172
            $node,
173
            [DOMDocument::class, DOMElement::class],
174
            'Only references to the DOM document or elements are allowed.'
175
        );
176
177
        $prefix = @$options['prefix'] ?: null;
178
        $prefixNS = @$options['prefix_ns'] ?: null;
179
        $idName = @$options['id_name'] ?: 'Id';
180
        $attrName = $prefix ? $prefix . ':' . $idName : $idName;
181
        $forceURI = true;
182
        if (isset($options['force_uri'])) {
183
            $forceURI = $options['force_uri'];
184
        }
185
        $overwrite = true;
186
        if (isset($options['overwrite'])) {
187
            $overwrite = $options['overwrite'];
188
        }
189
190
        $reference = $this->createElement('Reference');
191
        $this->sigInfoNode->appendChild($reference);
192
193
        // register reference
194
        $includeCommentNodes = false;
195
        if ($node instanceof DOMElement) {
196
            $uri = null;
197
            if (!$overwrite) {
198
                $uri = $prefixNS ? $node->getAttributeNS($prefixNS, $idName) : $node->getAttribute($idName);
199
            }
200
            if (empty($uri)) {
201
                $uri = Utils\Random::generateGUID();
202
                $node->setAttributeNS($prefixNS, $attrName, $uri);
203
            }
204
205
            if (
206
                in_array(C::C14N_EXCLUSIVE_WITH_COMMENTS, $transforms)
207
                || in_array(C::C14N_INCLUSIVE_WITH_COMMENTS, $transforms)
208
            ) {
209
                $includeCommentNodes = true;
210
                $reference->setAttribute('URI', "#xpointer($attrName('$uri'))");
211
            } else {
212
                $reference->setAttribute('URI', '#' . $uri);
213
            }
214
        } elseif ($forceURI) {
215
            // $node is a \DOMDocument, should add a reference to the root element (enveloped signature)
216
            if (in_array($this->c14nMethod, [C::C14N_INCLUSIVE_WITH_COMMENTS, C::C14N_EXCLUSIVE_WITH_COMMENTS])) {
217
                // if we want to use a C14N method that includes comments, the URI must be an xpointer
218
                $reference->setAttribute('URI', '#xpointer(/)');
219
            } else {
220
                // C14N without comments, we can set an empty URI
221
                $reference->setAttribute('URI', '');
222
            }
223
        }
224
225
        // apply and register transforms
226
        $transformList = $this->createElement('Transforms');
227
        $reference->appendChild($transformList);
228
229
        if (!empty($transforms)) {
230
            foreach ($transforms as $transform) {
231
                $transformNode = $this->createElement('Transform');
232
                $transformList->appendChild($transformNode);
233
234
                if (is_array($transform) && !empty($transform[C::XPATH_URI]['query'])) {
235
                    $transformNode->setAttribute('Algorithm', C::XPATH_URI);
236
                    $xpNode = $this->createElement('XPath', $transform[C::XPATH_URI]['query']);
237
                    $transformNode->appendChild($xpNode);
238
                } else {
239
                    $transformNode->setAttribute('Algorithm', $transform);
240
                }
241
            }
242
        } elseif (!empty($this->c14nMethod)) {
243
            $transformNode = $this->createElement('Transform');
244
            $transformList->appendChild($transformNode);
245
            $transformNode->setAttribute('Algorithm', $this->c14nMethod);
246
        }
247
248
        $canonicalData = $this->processTransforms($reference, $node, $includeCommentNodes);
249
        $digest = $this->hash($alg, $canonicalData);
250
251
        $digestMethod = $this->createElement('DigestMethod');
252
        $reference->appendChild($digestMethod);
253
        $digestMethod->setAttribute('Algorithm', $alg);
254
255
        $digestValue = $this->createElement('DigestValue', $digest);
256
        $reference->appendChild($digestValue);
257
258
        if (!in_array($node, $this->references)) {
259
            $this->references[] = $node;
260
        }
261
    }
262
263
264
    /**
265
     * Add a set of references to the signature.
266
     *
267
     * @param \DOMNode[] $nodes An array of DOMNode objects to be referred in the signature.
268
     * @param string $alg The identifier of the digest algorithm to use.
269
     * @param array $transforms An array of transforms to apply to each reference.
270
     * @param array $options An array of options.
271
     *
272
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If any of the nodes in the $nodes
273
     *   array is not an instance of DOMDocument or DOMElement.
274
     *
275
     * @see addReference()
276
     */
277
    public function addReferences(array $nodes, string $alg, array $transforms = [], $options = []): void
278
    {
279
        foreach ($nodes as $node) {
280
            $this->addReference($node, $alg, $transforms, $options);
281
        }
282
    }
283
284
285
    /**
286
     * Attach one or more X509 certificates to the signature.
287
     *
288
     * @param \SimpleSAML\XMLSecurity\Key\X509Certificate[] $certs
289
     *   An X509Certificate object or an array of them.
290
     * @param boolean $addSubject Whether to add the subject of the certificate or not.
291
     * @param string|false $digest A digest algorithm identifier if the digest of the certificate should be added. False
292
     * otherwise.
293
     * @param boolean $addIssuerSerial Whether to add the serial number of the issuer or not.
294
     *
295
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If $certs is not a
296
     *   X509Certificate object or an array of them.
297
     */
298
    public function addX509Certificates(
299
        array $certs,
300
        bool $addSubject = false,
301
        $digest = false,
302
        bool $addIssuerSerial = false
303
    ): void {
304
        Assert::allIsInstanceOf($certs, Key\X509Certificate::class);
305
306
        $certData = [];
307
308
        foreach ($certs as $cert) {
309
            $details = $cert->getCertificateDetails();
310
311
            if ($addSubject && isset($details['subject'])) {
312
                // add subject
313
                $subjectNameValue = $details['issuer'];
314
                if (is_array($details['subject'])) {
315
                    $parts = [];
316
                    foreach ($details['subject'] as $key => $value) {
317
                        if (is_array($value)) {
318
                            foreach ($value as $valueElement) {
319
                                array_unshift($parts, $key . '=' . $valueElement);
320
                            }
321
                        } else {
322
                            array_unshift($parts, $key . '=' . $value);
323
                        }
324
                    }
325
                    $subjectNameValue = implode(',', $parts);
326
                }
327
                $certData[] = new X509SubjectName($subjectNameValue);
328
            }
329
330
            if ($digest !== false) {
331
                // add certificate digest
332
                $fingerprint = base64_encode(hex2bin($cert->getRawThumbprint($digest)));
333
                $certData[] = new X509Digest($fingerprint, $digest);
334
            }
335
336
            if ($addIssuerSerial && isset($details['issuer']) && isset($details['serialNumber'])) {
337
                if (is_array($details['issuer'])) {
338
                    $parts = [];
339
                    foreach ($details['issuer'] as $key => $value) {
340
                        array_unshift($parts, $key . '=' . $value);
341
                    }
342
                    $issuerName = implode(',', $parts);
343
                } else {
344
                    $issuerName = $details['issuer'];
345
                }
346
347
                $certData[] = new X509IssuerSerial($issuerName, $details['serialNumber']);
348
            }
349
350
            $certData[] = new X509Certificate(CertificateUtils::stripHeaders($cert->getCertificate()));
351
        }
352
353
        $keyInfoNode = $this->createElement('KeyInfo');
354
355
        $certDataNode = new X509Data($certData);
356
        $certDataNode->toXML($keyInfoNode);
357
358
        if ($this->objectNode === null) {
359
            $this->sigNode->appendChild($keyInfoNode);
360
        } else {
361
            $this->sigNode->insertBefore($keyInfoNode, $this->objectNode);
362
        }
363
    }
364
365
366
    /**
367
     * Append a signature as the last child of the signed element.
368
     *
369
     * @return \DOMNode The appended signature.
370
     */
371
    public function append(): DOMNode
372
    {
373
        return $this->insert();
374
    }
375
376
377
    /**
378
     * Use this signature as an enveloping signature, effectively adding the signed data to a ds:Object element.
379
     *
380
     * @param string|null $mimetype The mime type corresponding to the signed data.
381
     * @param string|null $encoding The encoding corresponding to the signed data.
382
     */
383
    public function envelop(string $mimetype = null, string $encoding = null): void
384
    {
385
        $this->root = $this->addObject($this->root, $mimetype, $encoding);
386
    }
387
388
389
    /**
390
     * Build a new XML digital signature from a given document or node.
391
     *
392
     * @param \DOMNode $node The DOMDocument or DOMElement that contains the signature.
393
     *
394
     * @return Signature A Signature object corresponding to the signature present in the given DOM document or element.
395
     *
396
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If $node is not
397
     *   an instance of DOMDocument or DOMElement.
398
     * @throws \SimpleSAML\XMLSecurity\Exception\NoSignatureFound If there is no signature in the $node.
399
     */
400
    public static function fromXML(DOMNode $node): Signature
401
    {
402
        Assert::isInstanceOfAny(
403
            $node,
404
            [DOMDocument::class, DOMElement::class],
405
            'Signatures can only be created from DOM documents or elements'
406
        );
407
408
        $signature = self::findSignature($node);
409
        if ($node instanceof DOMDocument) {
410
            $node = $node->documentElement;
411
        }
412
        $dsig = new self($node);
413
        $dsig->setSignatureElement($signature);
414
        return $dsig;
415
    }
416
417
418
    /**
419
     * Obtain the list of currently blacklisted algorithms.
420
     *
421
     * Signatures using blacklisted algorithms cannot be created or verified.
422
     *
423
     * @return array An array containing the identifiers of the algorithms blacklisted currently.
424
     */
425
    public function getBlacklistedAlgorithms(): array
426
    {
427
        return $this->algBlacklist;
428
    }
429
430
431
    /**
432
     * Get the list of namespaces to designate ID attributes.
433
     *
434
     * @return array An array of strings with the namespaces used in ID attributes.
435
     */
436
    public function getIdNamespaces(): array
437
    {
438
        return $this->idNS;
439
    }
440
441
442
    /**
443
     * Get the prefix to designate the XML digital signature namespace currently configured.
444
     *
445
     * @return string The prefix used in this signature.
446
     */
447
    public function getPrefix(): string
448
    {
449
        return rtrim($this->nsPrefix, ':');
450
    }
451
452
453
    /**
454
     * Get a list of attributes used as an ID.
455
     *
456
     * @return array An array of strings with the attributes used as an ID.
457
     */
458
    public function getIdAttributes(): array
459
    {
460
        return $this->idKeys;
461
    }
462
463
464
    /**
465
     * Get the root configured for this signature.
466
     *
467
     * This will be the signed element, whether that's a user-provided XML element or a ds:Object element containing
468
     * the actual data (which can in turn be either XML or not).
469
     *
470
     * @return \DOMElement The root element for this signature.
471
     */
472
    public function getRoot(): DOMElement
473
    {
474
        return $this->root;
475
    }
476
477
478
    /**
479
     * Get the identifier of the algorithm used in this signature.
480
     *
481
     * @return string The identifier of the algorithm used in this signature.
482
     */
483
    public function getSignatureMethod(): string
484
    {
485
        return $this->sigAlg;
486
    }
487
488
489
    /**
490
     * Get a list of elements verified by this signature.
491
     *
492
     * The elements in this list are referenced by the signature and the references verified to be correct. However,
493
     * this doesn't mean the signature is valid, only that the references were so.
494
     *
495
     * Note that the list returned will be empty unless verify() has been called before.
496
     *
497
     * @return \DOMElement[] A list of elements correctly referenced by this signature. An empty list of verify() has
498
     * not been called yet, or if the references couldn't be verified.
499
     */
500
    public function getVerifiedElements(): array
501
    {
502
        return $this->verifiedElements;
503
    }
504
505
506
    /**
507
     * Insert a signature as a child of the signed element, optionally before a given element.
508
     *
509
     * @param \DOMElement|false $before An optional DOM element the signature should be prepended to.
510
     *
511
     * @return \DOMNode The inserted signature.
512
     *
513
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If this signature is in enveloping mode.
514
     */
515
    public function insert($before = false): DOMNode
516
    {
517
        Assert::false(
518
            $this->enveloping,
519
            'Cannot insert the signature in the object it is enveloping.',
520
            RuntimeException::class
521
        );
522
523
        $signature = $this->root->ownerDocument->importNode($this->sigNode, true);
524
525
        if ($before instanceof DOMElement) {
526
            return $this->root->insertBefore($signature, $before);
527
        }
528
        return $this->root->insertBefore($signature);
529
    }
530
531
532
    /**
533
     * Prepend a signature as the first child of the signed element.
534
     *
535
     * @return \DOMNode The prepended signature.
536
     */
537
    public function prepend(): DOMNode
538
    {
539
        foreach ($this->root->childNodes as $child) {
540
            // look for the first child element, if any
541
            if ($child instanceof \DOMElement) {
542
                return $this->insert($child);
543
            }
544
        }
545
        return $this->append();
546
    }
547
548
549
    /**
550
     * Set the backend to create or verify signatures.
551
     *
552
     * @param SignatureBackend $backend The SignatureBackend implementation to use. See individual algorithms for
553
     * details about the default backends used.
554
     */
555
    public function setBackend(SignatureBackend $backend): void
556
    {
557
        $this->backend = $backend;
558
    }
559
560
561
    /**
562
     * Set the list of currently blacklisted algorithms.
563
     *
564
     * Signatures using blacklisted algorithms cannot be created or verified.
565
     *
566
     * @param array $algs An array containing the identifiers of the algorithms to blacklist.
567
     */
568
    public function setBlacklistedAlgorithms(array $algs): void
569
    {
570
        $this->algBlacklist = $algs;
571
    }
572
573
574
    /**
575
     * Set the canonicalization method used in this signature.
576
     *
577
     * Note that exclusive canonicalization without comments is used by default, so it's not necessary to call
578
     * setCanonicalizationMethod() if that canonicalization method is desired.
579
     *
580
     * @param string $method The identifier of the canonicalization method to use.
581
     *
582
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If $method is not a valid
583
     *   identifier of a supported canonicalization method.
584
     */
585
    public function setCanonicalizationMethod(string $method): void
586
    {
587
        Assert::oneOf(
588
            $method,
589
            [
590
                C::C14N_EXCLUSIVE_WITH_COMMENTS,
591
                C::C14N_EXCLUSIVE_WITHOUT_COMMENTS,
592
                C::C14N_INCLUSIVE_WITH_COMMENTS,
593
                C::C14N_INCLUSIVE_WITHOUT_COMMENTS
594
            ],
595
            'Invalid canonicalization method',
596
            InvalidArgumentException::class
597
        );
598
599
        $this->c14nMethod = $method;
600
        $this->c14nMethodNode->setAttribute('Algorithm', $method);
601
    }
602
603
604
    /**
605
     * Set the encoding for the signed contents in an enveloping signature.
606
     *
607
     * @param string $encoding The encoding used in the signed contents.
608
     *
609
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If this is not an enveloping signature.
610
     */
611
    public function setEncoding(string $encoding): void
612
    {
613
        Assert::true(
614
            $this->enveloping,
615
            'Cannot set the encoding for non-enveloping signatures.',
616
            RuntimeException::class
617
        );
618
619
        $this->root->setAttribute('Encoding', $encoding);
620
    }
621
622
623
    /**
624
     * Set a list of attributes used as an ID.
625
     *
626
     * @param array $keys An array of strings with the attributes used as an ID.
627
     */
628
    public function setIdAttributes(array $keys): void
629
    {
630
        $this->idKeys = $keys;
631
    }
632
633
634
    /**
635
     * Set the list of namespaces to designate ID attributes.
636
     *
637
     * @param array $namespaces An array of strings with the namespaces used in ID attributes.
638
     */
639
    public function setIdNamespaces(array $namespaces): void
640
    {
641
        $this->idNS = $namespaces;
642
    }
643
644
645
    /**
646
     * Set the mime type for the signed contents in an enveloping signature.
647
     *
648
     * @param string $mimetype The mime type of the signed contents.
649
     *
650
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If this is not an enveloping signature.
651
     */
652
    public function setMimeType(string $mimetype): void
653
    {
654
        Assert::true(
655
            $this->enveloping,
656
            'Cannot set the mime type for non-enveloping signatures.',
657
            RuntimeException::class
658
        );
659
660
        $this->root->setAttribute('MimeType', $mimetype);
661
    }
662
663
664
    /**
665
     * Set the prefix to designate the XML digital signature namespace currently configured.
666
     *
667
     * @param string|false $prefix The prefix to use in this signature, or false to not use any prefix.
668
     */
669
    public function setPrefix($prefix): void
670
    {
671
        if (!is_string($prefix) || empty($prefix)) {
672
            $this->nsPrefix = '';
673
        } else {
674
            if ($this->nsPrefix === $prefix . ':') {
675
                return;
676
            }
677
            $this->nsPrefix = $prefix . ':';
678
        }
679
680
        $this->sigNode->ownerDocument->removeChild($this->sigNode);
681
        $this->initSignature();
682
    }
683
684
685
    /**
686
     * Set the signature element to a given one, and initialize the signature from there.
687
     *
688
     * @param \DOMElement $element A DOM element containing an XML signature.
689
     *
690
     * @throws \SimpleSAML\XML\Exception\InvalidDOMElementException If the element does not correspond to an XML
691
     *   signature or it is malformed (e.g. there are missing mandatory elements or attributes).
692
     */
693
    public function setSignatureElement(DOMElement $element): void
694
    {
695
        Assert::same($element->localName, 'Signature', InvalidDOMElementException::class);
696
        Assert::same($element->namespaceURI, Sig::NS, InvalidDOMElementException::class);
697
698
        $this->sigNode = $element;
699
700
        $xp = XP::getXPath($this->sigNode->ownerDocument);
701
702
        $signedInfoNodes = $xp->query('./ds:SignedInfo', $this->sigNode);
703
704
        Assert::minCount(
705
            $signedInfoNodes,
706
            1,
707
            'There is no SignedInfo element in the signature',
708
            RuntimeException::class
709
        );
710
        $this->sigInfoNode = $signedInfoNodes->item(0);
711
712
713
        $this->sigAlg = $xp->evaluate('string(./ds:SignedInfo/ds:SignatureMethod/@Algorithm)', $this->sigNode);
714
        Assert::stringNotEmpty($this->sigAlg, 'Unable to determine SignatureMethod', RuntimeException::class);
715
716
        $c14nMethodNodes = $xp->query('./ds:CanonicalizationMethod', $this->sigInfoNode);
717
        Assert::minCount(
718
            $c14nMethodNodes,
719
            1,
720
            'There is no CanonicalizationMethod in the signature',
721
            RuntimeException::class
722
        );
723
724
        $this->c14nMethodNode = $c14nMethodNodes->item(0);
725
        if (!$this->c14nMethodNode->hasAttribute('Algorithm')) {
726
            throw new RuntimeException('CanonicalizationMethod missing required Algorithm attribute');
727
        }
728
        $this->c14nMethod = $this->c14nMethodNode->getAttribute('Algorithm');
729
    }
730
731
732
    /**
733
     * Sign the document or element.
734
     *
735
     * This method will finish the signature process. It will create an XML signature valid for document or elements
736
     * specified previously with addReference() or addReferences(). If none of those methods have been called previous
737
     * to calling sign() (there are no references in the signature), the $root passed during construction of the
738
     * Signature object will be referenced automatically.
739
     *
740
     * @param \SimpleSAML\XMLSecurity\Key\AbstractKey $key The key to use for signing. Bear in mind that the type of
741
     *   this key must be compatible with the types of key accepted by the algorithm specified in $alg.
742
     * @param string $alg The identifier of the signature algorithm to use. See \SimpleSAML\XMLSecurity\Constants.
743
     * @param bool $appendToNode Whether to append the signature as the last child of the root element or not.
744
     *
745
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If $appendToNode is true and
746
     *   this is an enveloping signature.
747
     */
748
    public function sign(AbstractKey $key, string $alg, bool $appendToNode = false): void
749
    {
750
        Assert::false(
751
            ($this->enveloping && $appendToNode),
752
            'Cannot append the signature, we are in enveloping mode.',
753
            InvalidArgumentException::class
754
        );
755
756
        $this->sigMethodNode->setAttribute('Algorithm', $alg);
757
        $factory = new SignatureAlgorithmFactory($this->algBlacklist);
758
        $signer = $factory->getAlgorithm($alg, $key);
759
        if ($this->backend !== null) {
760
            $signer->setBackend($this->backend);
761
        }
762
763
        if (empty($this->references)) {
764
            // no references have been added, ref root
765
            $transforms = [];
766
            if (!$this->enveloping) {
767
                $transforms[] = C::XMLDSIG_ENVELOPED;
768
            }
769
            $this->addReference($this->root->ownerDocument, $signer->getDigest(), $transforms, []);
770
        }
771
772
        if ($appendToNode) {
773
            $this->sigNode = $this->append();
774
        } elseif (in_array($this->c14nMethod, [C::C14N_INCLUSIVE_WITHOUT_COMMENTS, C::C14N_INCLUSIVE_WITH_COMMENTS])) {
775
            // append Signature to root node for inclusive canonicalization
776
            $restoreSigNode = $this->sigNode;
777
            $this->sigNode = $this->prepend();
778
        }
779
780
        $sigValue = base64_encode($signer->sign($this->canonicalizeData($this->sigInfoNode, $this->c14nMethod)));
781
782
        // remove Signature from node if we added it for c14n
783
        if (
784
            !$appendToNode &&
785
            in_array($this->c14nMethod, [C::C14N_INCLUSIVE_WITHOUT_COMMENTS, C::C14N_INCLUSIVE_WITH_COMMENTS])
786
        ) { // remove from root in case we added it for inclusive canonicalization
787
            $this->root->removeChild($this->root->lastChild);
788
            /** @var \DOMElement $restoreSigNode */
789
            $this->sigNode = $restoreSigNode;
790
        }
791
792
        $sigValueNode = $this->createElement('SignatureValue', $sigValue);
793
        if ($this->sigInfoNode->nextSibling) {
794
            $this->sigInfoNode->nextSibling->parentNode->insertBefore($sigValueNode, $this->sigInfoNode->nextSibling);
795
        } else {
796
            $this->sigNode->appendChild($sigValueNode);
797
        }
798
    }
799
800
801
    /**
802
     * Verify this signature with a given key.
803
     *
804
     * @param \SimpleSAML\XMLSecurity\Key\AbstractKey $key The key to use to verify this signature.
805
     *
806
     * @return bool True if the signature can be verified with $key, false otherwise.
807
     *
808
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If there is no SignatureValue in
809
     *   the signature, or we couldn't verify all the references.
810
     */
811
    public function verify(AbstractKey $key): bool
812
    {
813
        $xp = XP::getXPath($this->sigNode->ownerDocument);
814
        $sigval = $xp->evaluate('string(./ds:SignatureValue)', $this->sigNode);
815
        if (empty($sigval)) {
816
            throw new RuntimeException('Unable to locate SignatureValue');
817
        }
818
819
        $siginfo = $this->canonicalizeData($this->sigInfoNode, $this->c14nMethod);
820
        if (!$this->validateReferences()) {
821
            throw new RuntimeException('Unable to verify all references');
822
        }
823
824
        $factory = new SignatureAlgorithmFactory($this->algBlacklist);
825
        $alg = $factory->getAlgorithm($this->sigAlg, $key);
826
        if ($this->backend !== null) {
827
            $alg->setBackend($this->backend);
828
        }
829
        return $alg->verify($siginfo, base64_decode($sigval));
830
    }
831
832
833
    /**
834
     * Canonicalize any given node.
835
     *
836
     * @param \DOMNode $node The DOM node that needs canonicalization.
837
     * @param string $c14nMethod The identifier of the canonicalization algorithm to use.
838
     * See \SimpleSAML\XMLSecurity\Constants.
839
     * @param array|null $xpaths An array of xpaths to filter the nodes by. Defaults to null (no filters).
840
     * @param array|null $prefixes An array of namespace prefixes to filter the nodes by. Defaults to null (no filters).
841
     *
842
     * @return string The canonical representation of the given DOM node, according to the algorithm requested.
843
     */
844
    protected function canonicalizeData(
845
        DOMNode $node,
846
        string $c14nMethod,
847
        array $xpaths = null,
848
        array $prefixes = null
849
    ): string {
850
        $exclusive = false;
851
        $withComments = false;
852
        switch ($c14nMethod) {
853
            case C::C14N_EXCLUSIVE_WITH_COMMENTS:
854
            case C::C14N_INCLUSIVE_WITH_COMMENTS:
855
                $withComments = true;
856
        }
857
        switch ($c14nMethod) {
858
            case C::C14N_EXCLUSIVE_WITH_COMMENTS:
859
            case C::C14N_EXCLUSIVE_WITHOUT_COMMENTS:
860
                $exclusive = true;
861
        }
862
863
        if (
864
            is_null($xpaths)
865
            && ($node->ownerDocument !== null)
866
            && $node->isSameNode($node->ownerDocument->documentElement)
867
        ) {
868
            // check for any PI or comments as they would have been excluded
869
            $element = $node;
870
            while ($refNode = $element->previousSibling) {
871
                if (
872
                    (($refNode->nodeType === XML_COMMENT_NODE) && $withComments)
873
                    || $refNode->nodeType === XML_PI_NODE
874
                ) {
875
                    break;
876
                }
877
                $element = $refNode;
878
            }
879
            if ($refNode == null) {
880
                $node = $node->ownerDocument;
881
            }
882
        }
883
884
        return $node->C14N($exclusive, $withComments, $xpaths, $prefixes);
885
    }
886
887
888
    /**
889
     * Create a new element in this signature.
890
     *
891
     * @param string $name The name of this element.
892
     * @param string|null $content The text contents of the element, or null if it is not supposed to have any text
893
     * contents. Defaults to null.
894
     * @param string $ns The namespace the new element must be created under. Defaults to the standard XMLDSIG
895
     * namespace.
896
     * @param string|null $nsPrefix The prefix to use for the elements's name.
897
     *
898
     * @return \DOMElement A new DOM element with the given name.
899
     */
900
    protected function createElement(
901
        string $name,
902
        string $content = null,
903
        string $ns = C::XMLDSIGNS,
904
        string $nsPrefix = null
905
    ): DOMElement {
906
        if ($this->sigNode === null) {
907
            // initialize signature
908
            $doc = DOMDocumentFactory::create();
909
        } else {
910
            $doc = $this->sigNode->ownerDocument;
911
        }
912
913
        if ($nsPrefix === null) {
914
            $nsPrefix = $this->nsPrefix;
915
        }
916
917
        if ($content !== null) {
918
            return $doc->createElementNS($ns, $nsPrefix . $name, $content);
919
        }
920
921
        return $doc->createElementNS($ns, $nsPrefix . $name);
922
    }
923
924
925
    /**
926
     * Find a signature from a given node.
927
     *
928
     * @param \DOMNode $node A DOMElement node where a signature is expected as a child (enveloped) or a DOMDocument
929
     * node to search for document signatures (one single reference with an empty URI).
930
     *
931
     * @return \DOMElement The signature element.
932
     *
933
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If there is no DOMDocument element available.
934
     * @throws \SimpleSAML\XMLSecurity\Exception\NoSignatureFound If no signature is found.
935
     */
936
    protected static function findSignature(DOMNode $node): DOMElement
937
    {
938
        $doc = $node instanceof DOMDocument ? $node : $node->ownerDocument;
939
940
        Assert::notNull($doc, 'Cannot search for signatures, no DOM document available', RuntimeException::class);
941
942
        $xp = XP::getXPath($doc);
943
        $nodeset = $xp->query('./ds:Signature', $node);
944
945
        if ($nodeset->length === 0) {
946
            throw new NoSignatureFound();
947
        }
948
        return $nodeset->item(0);
949
    }
950
951
952
    /**
953
     * Compute the hash for some data with a given algorithm.
954
     *
955
     * @param string $alg The identifier of the algorithm to use.
956
     * @param string $data The data to digest.
957
     * @param bool $encode Whether to bas64-encode the result or not. Defaults to true.
958
     *
959
     * @return string The (binary or base64-encoded) digest corresponding to the given data.
960
     *
961
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If $alg is not a valid
962
     *   identifier of a supported digest algorithm.
963
     */
964
    protected function hash(string $alg, string $data, bool $encode = true): string
965
    {
966
        Assert::keyExists(
967
            C::$DIGEST_ALGORITHMS,
968
            $alg,
969
            'Unsupported digest method "%s"',
970
            InvalidArgumentException::class
971
        );
972
973
        $digest = hash(C::$DIGEST_ALGORITHMS[$alg], $data, true);
974
        return $encode ? base64_encode($digest) : $digest;
975
    }
976
977
978
    /**
979
     * Initialize the basic structure of a signature from scratch.
980
     *
981
     */
982
    protected function initSignature(): void
983
    {
984
        $this->sigNode = $this->createElement('Signature');
985
        $this->sigInfoNode = $this->createElement('SignedInfo');
986
        $this->c14nMethodNode = $this->createElement('CanonicalizationMethod');
987
        $this->c14nMethodNode->setAttribute('Algorithm', $this->c14nMethod);
988
        $this->sigMethodNode = $this->createElement('SignatureMethod');
989
990
        $this->sigInfoNode->appendChild($this->c14nMethodNode);
991
        $this->sigInfoNode->appendChild($this->sigMethodNode);
992
        $this->sigNode->appendChild($this->sigInfoNode);
993
        $this->sigNode->ownerDocument->appendChild($this->sigNode);
994
    }
995
996
997
    /**
998
     * Process a given reference, by looking for it, processing the specified transforms, canonicalizing the result
999
     * and comparing its corresponding digest.
1000
     *
1001
     * Verified references will be stored in the "verifiedElements" property.
1002
     *
1003
     * @param \DOMElement $ref The ds:Reference element to process.
1004
     *
1005
     * @return bool True if the digest of the processed reference matches the one given, false otherwise.
1006
     *
1007
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If the referenced element is missing or
1008
     *   the reference points to an external document.
1009
     *
1010
     * @see http://www.w3.org/TR/xmldsig-core/#sec-ReferenceProcessingModel
1011
     */
1012
    protected function processReference(DOMElement $ref): bool
1013
    {
1014
        /*
1015
         * Depending on the URI, we may need to remove comments during canonicalization.
1016
         * See: http://www.w3.org/TR/xmldsig-core/#sec-ReferenceProcessingModel
1017
         */
1018
        $includeCommentNodes = true;
1019
        $dataObject = $ref->ownerDocument;
1020
        if ($ref->hasAttribute("URI")) {
1021
            $uri = $ref->getAttribute('URI');
1022
            if (empty($uri)) {
1023
                // this reference identifies the enclosing XML, it should not include comments
1024
                $includeCommentNodes = false;
1025
            }
1026
            $arUrl = parse_url($uri);
1027
            if (empty($arUrl['path'])) {
1028
                if ($identifier = @$arUrl['fragment']) {
1029
                    /*
1030
                     * This reference identifies a node with the given ID by using a URI on the form '#identifier'.
1031
                     * This should not include comments.
1032
                     */
1033
                    $includeCommentNodes = false;
1034
1035
                    $xp = XP::getXPath($ref->ownerDocument);
1036
                    if ($this->idNS && is_array($this->idNS)) {
1037
                        foreach ($this->idNS as $nspf => $ns) {
1038
                            $xp->registerNamespace($nspf, $ns);
1039
                        }
1040
                    }
1041
                    $iDlist = '@Id="' . $identifier . '"';
1042
                    if (is_array($this->idKeys)) {
1043
                        foreach ($this->idKeys as $idKey) {
1044
                            $iDlist .= " or @$idKey='$identifier'";
1045
                        }
1046
                    }
1047
                    $query = '//*[' . $iDlist . ']';
1048
                    $dataObject = $xp->query($query)->item(0);
1049
                    if ($dataObject === null) {
1050
                        throw new RuntimeException('Reference not found');
1051
                    }
1052
                }
1053
            } else {
1054
                throw new RuntimeException('Processing of external documents is not supported');
1055
            }
1056
        } else {
1057
            // this reference identifies the root node with an empty URI, it should not include comments
1058
            $includeCommentNodes = false;
1059
        }
1060
1061
        $data = $this->processTransforms($ref, $dataObject, $includeCommentNodes);
1062
        if (!$this->validateDigest($ref, $data)) {
1063
            return false;
1064
        }
1065
1066
        // parse the canonicalized reference...
1067
        $doc = DOMDocumentFactory::create();
1068
        $doc->loadXML($data);
1069
        $dataObject = $doc->documentElement;
1070
1071
        // ... and add it to the list of verified elements
1072
        if (!empty($identifier)) {
1073
            $this->verifiedElements[$identifier] = $dataObject;
1074
        } else {
1075
            $this->verifiedElements[] = $dataObject;
1076
        }
1077
1078
        return true;
1079
    }
1080
1081
1082
    /**
1083
     * Process all transforms specified by a given Reference element.
1084
     *
1085
     * @param \DOMElement $ref The Reference element.
1086
     * @param mixed $data The data referenced.
1087
     * @param bool $includeCommentNodes Whether to allow canonicalization with comments or not.
1088
     *
1089
     * @return string The canonicalized data after applying all transforms specified by $ref.
1090
     *
1091
     * @see http://www.w3.org/TR/xmldsig-core/#sec-ReferenceProcessingModel
1092
     */
1093
    protected function processTransforms(DOMElement $ref, $data, bool $includeCommentNodes = false): string
1094
    {
1095
        if (!($data instanceof DOMNode)) {
1096
            return $data;
1097
        }
1098
1099
        $xp = XP::getXPath($ref->ownerDocument);
1100
        $transforms = $xp->query('./ds:Transforms/ds:Transform', $ref);
1101
        $canonicalMethod = C::C14N_EXCLUSIVE_WITHOUT_COMMENTS;
1102
        $arXPath = null;
1103
        $prefixList = null;
1104
        foreach ($transforms as $transform) {
1105
            /** @var \DOMElement $transform */
1106
            $algorithm = $transform->getAttribute("Algorithm");
1107
            switch ($algorithm) {
1108
                case C::C14N_EXCLUSIVE_WITHOUT_COMMENTS:
1109
                case C::C14N_EXCLUSIVE_WITH_COMMENTS:
1110
                    if (!$includeCommentNodes) {
1111
                        // remove comment nodes by forcing it to use a canonicalization without comments
1112
                        $canonicalMethod = C::C14N_EXCLUSIVE_WITHOUT_COMMENTS;
1113
                    } else {
1114
                        $canonicalMethod = $algorithm;
1115
                    }
1116
1117
                    $node = $transform->firstChild;
1118
                    while ($node) {
1119
                        if ($node->localName === 'InclusiveNamespaces') {
1120
                            if ($pfx = $node->getAttribute('PrefixList')) {
1121
                                $arpfx = [];
1122
                                $pfxlist = explode(" ", $pfx);
1123
                                foreach ($pfxlist as $pfx) {
1124
                                    $val = trim($pfx);
1125
                                    if (! empty($val)) {
1126
                                        $arpfx[] = $val;
1127
                                    }
1128
                                }
1129
                                if (count($arpfx) > 0) {
1130
                                    $prefixList = $arpfx;
1131
                                }
1132
                            }
1133
                            break;
1134
                        }
1135
                        $node = $node->nextSibling;
1136
                    }
1137
                    break;
1138
                case C::C14N_INCLUSIVE_WITHOUT_COMMENTS:
1139
                case C::C14N_INCLUSIVE_WITH_COMMENTS:
1140
                    if (!$includeCommentNodes) {
1141
                        // remove comment nodes by forcing it to use a canonicalization without comments
1142
                        $canonicalMethod = C::C14N_INCLUSIVE_WITHOUT_COMMENTS;
1143
                    } else {
1144
                        $canonicalMethod = $algorithm;
1145
                    }
1146
1147
                    break;
1148
                case C::XPATH_URI:
1149
                    $node = $transform->firstChild;
1150
                    while ($node) {
1151
                        if ($node->localName == 'XPath') {
1152
                            $arXPath = [];
1153
                            $arXPath['query'] = '(.//. | .//@* | .//namespace::*)[' . $node->nodeValue . ']';
1154
                            $arXpath['namespaces'] = [];
1155
                            $nslist = $xp->query('./namespace::*', $node);
1156
                            foreach ($nslist as $nsnode) {
1157
                                if ($nsnode->localName != "xml") {
1158
                                    $arXPath['namespaces'][$nsnode->localName] = $nsnode->nodeValue;
1159
                                }
1160
                            }
1161
                            break;
1162
                        }
1163
                        $node = $node->nextSibling;
1164
                    }
1165
                    break;
1166
            }
1167
        }
1168
1169
        return $this->canonicalizeData($data, $canonicalMethod, $arXPath, $prefixList);
1170
    }
1171
1172
1173
    /**
1174
     * Compute and compare the digest corresponding to some data given to the one specified by a reference.
1175
     *
1176
     * @param \DOMElement $ref The ds:Reference element containing the digest.
1177
     * @param string $data The referenced element, canonicalized, to digest and compare.
1178
     *
1179
     * @return bool True if the resulting digest matches the one in the reference, false otherwise.
1180
     */
1181
    protected function validateDigest(DOMElement $ref, string $data): bool
1182
    {
1183
        $xp = XP::getXPath($ref->ownerDocument);
1184
        $alg = $xp->evaluate('string(./ds:DigestMethod/@Algorithm)', $ref);
1185
        $computed = $this->hash($alg, $data, false);
1186
        $evaluated = base64_decode($xp->evaluate('string(./ds:DigestValue)', $ref));
1187
        return Sec::compareStrings($computed, $evaluated);
1188
    }
1189
1190
1191
    /**
1192
     * Iterate over the references specified by the signature, apply their transforms, and validate their digests
1193
     * against the referenced elements.
1194
     *
1195
     * @return boolean True if all references could be verified, false otherwise.
1196
     *
1197
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If there are no references.
1198
     */
1199
    protected function validateReferences(): bool
1200
    {
1201
        $doc = $this->sigNode->ownerDocument;
1202
1203
        if (!$doc->documentElement->isSameNode($this->sigNode) && $this->sigNode->parentNode !== null) {
1204
            // enveloped signature, remove it
1205
            $this->sigNode->parentNode->removeChild($this->sigNode);
1206
        }
1207
1208
        $xp = XP::getXPath($doc);
1209
        $refNodes = $xp->query('./ds:SignedInfo/ds:Reference', $this->sigNode);
1210
        Assert::minCount($refNodes, 1, 'There are no Reference nodes', RuntimeException::class);
1211
1212
        $verified = true;
1213
        foreach ($refNodes as $refNode) {
1214
            $verified = $this->processReference($refNode) && $verified;
1215
        }
1216
1217
        return $verified;
1218
    }
1219
}
1220