Passed
Push — master ( 6db269...70aff8 )
by Jaime Pérez
02:49
created

Message::setExtensions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 2
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SAML2;
6
7
use DOMElement;
8
use RobRichards\XMLSecLibs\XMLSecurityKey;
9
use SAML2\Utilities\Temporal;
10
use SAML2\XML\ExtendableElementTrait;
11
use SAML2\XML\saml\Issuer;
12
use SAML2\XML\samlp\Extensions;
13
use Webmozart\Assert\Assert;
14
15
/**
16
 * Base class for all SAML 2 messages.
17
 *
18
 * Implements what is common between the samlp:RequestAbstractType and
19
 * samlp:StatusResponseType element types.
20
 */
21
abstract class Message extends SignedElement
22
{
23
    use ExtendableElementTrait;
24
25
    /**
26
     * The name of the root element of the DOM tree for the message.
27
     *
28
     * Used when creating a DOM tree from the message.
29
     *
30
     * @var string
31
     */
32
    private $tagName;
33
34
    /**
35
     * The identifier of this message.
36
     *
37
     * @var string
38
     */
39
    private $id;
40
41
    /**
42
     * The issue timestamp of this message, as an UNIX timestamp.
43
     *
44
     * @var int
45
     */
46
    private $issueInstant;
47
48
    /**
49
     * The destination URL of this message if it is known.
50
     *
51
     * @var string|null
52
     */
53
    private $destination = null;
54
55
    /**
56
     * The destination URL of this message if it is known.
57
     *
58
     * @var string
59
     */
60
    private $consent = Constants::CONSENT_UNSPECIFIED;
61
62
    /**
63
     * The entity id of the issuer of this message, or null if unknown.
64
     *
65
     * @var \SAML2\XML\saml\Issuer|null
66
     */
67
    private $issuer = null;
68
69
    /**
70
     * The RelayState associated with this message.
71
     *
72
     * @var string|null
73
     */
74
    private $relayState = null;
75
76
    /**
77
     * The \DOMDocument we are currently building.
78
     *
79
     * This variable is used while generating XML from this message. It holds the
80
     * \DOMDocument of the XML we are generating.
81
     *
82
     * @var \DOMDocument
83
     */
84
    protected $document;
85
86
    /**
87
     * The private key we should use to sign the message.
88
     *
89
     * The private key can be null, in which case the message is sent unsigned.
90
     *
91
     * @var XMLSecurityKey|null
92
     */
93
    protected $signatureKey;
94
95
    /**
96
     * @var bool
97
     */
98
    protected $messageContainedSignatureUponConstruction = false;
99
100
    /**
101
     * List of certificates that should be included in the message.
102
     *
103
     * @var array
104
     */
105
    protected $certificates = [];
106
107
    /**
108
     * Available methods for validating this message.
109
     *
110
     * @var array
111
     */
112
    private $validators = [];
113
114
    /**
115
     * @var null|string
116
     */
117
    private $signatureMethod = null;
118
119
120
    /**
121
     * Initialize a message.
122
     *
123
     * This constructor takes an optional parameter with a \DOMElement. If this
124
     * parameter is given, the message will be initialized with data from that
125
     * XML element.
126
     *
127
     * If no XML element is given, the message is initialized with suitable
128
     * default values.
129
     *
130
     * @param string $tagName The tag name of the root element
131
     * @param \DOMElement|null $xml The input message
132
     *
133
     * @throws \Exception
134
     */
135
    protected function __construct(string $tagName, DOMElement $xml = null)
136
    {
137
        $this->tagName = $tagName;
138
139
        $this->id = Utils::getContainer()->generateId();
140
        $this->issueInstant = Temporal::getTime();
141
142
        if ($xml === null) {
143
            return;
144
        }
145
146
        if (!$xml->hasAttribute('ID')) {
147
            throw new \Exception('Missing ID attribute on SAML message.');
148
        }
149
        $this->id = $xml->getAttribute('ID');
150
151
        if ($xml->getAttribute('Version') !== '2.0') {
152
            /* Currently a very strict check. */
153
            throw new \Exception('Unsupported version: ' . $xml->getAttribute('Version'));
154
        }
155
156
        $this->issueInstant = Utils::xsDateTimeToTimestamp($xml->getAttribute('IssueInstant'));
157
158
        if ($xml->hasAttribute('Destination')) {
159
            $this->destination = $xml->getAttribute('Destination');
160
        }
161
162
        if ($xml->hasAttribute('Consent')) {
163
            $this->consent = $xml->getAttribute('Consent');
164
        }
165
166
        /** @var \DOMElement[] $issuer */
167
        $issuer = Utils::xpQuery($xml, './saml_assertion:Issuer');
168
        if (!empty($issuer)) {
169
            $this->issuer = new Issuer($issuer[0]);
170
        }
171
172
        $this->validateSignature($xml);
173
174
        $extensions = Extensions::getChildrenOfClass($xml);
175
        Assert::maxCount($extensions, 1, 'Only one saml:Extensions element is allowed.');
176
        if (!empty($extensions)) {
177
            $this->Extensions = $extensions[0];
178
        }
179
    }
180
181
182
    /**
183
     * Validate the signature element of a SAML message, and configure this object appropriately to perform the
184
     * signature verification afterwards.
185
     *
186
     * Please note this method does NOT verify the signature, it just validates the signature construction and prepares
187
     * this object to do the verification.
188
     *
189
     * @param \DOMElement $xml The SAML message whose signature we want to validate.
190
     * @return void
191
     */
192
    private function validateSignature(DOMElement $xml): void
193
    {
194
        try {
195
            /** @var \DOMAttr[] $signatureMethod */
196
            $signatureMethod = Utils::xpQuery($xml, './ds:Signature/ds:SignedInfo/ds:SignatureMethod/@Algorithm');
197
            if (empty($signatureMethod)) {
198
                throw new \Exception('No Algorithm specified in signature.');
199
            }
200
201
            $sig = Utils::validateElement($xml);
202
203
            if ($sig !== false) {
204
                $this->messageContainedSignatureUponConstruction = true;
205
                $this->certificates = $sig['Certificates'];
206
                $this->validators[] = [
207
                    'Function' => ['\SAML2\Utils', 'validateSignature'],
208
                    'Data' => $sig,
209
                ];
210
                $this->signatureMethod = $signatureMethod[0]->value;
211
            }
212
        } catch (\Exception $e) {
213
            // ignore signature validation errors
214
        }
215
    }
216
217
218
    /**
219
     * Add a method for validating this message.
220
     *
221
     * This function is used by the HTTP-Redirect binding, to make it possible to
222
     * check the signature against the one included in the query string.
223
     *
224
     * @param callable $function The function which should be called
225
     * @param mixed $data The data that should be included as the first parameter to the function
226
     * @return void
227
     */
228
    public function addValidator(callable $function, $data): void
229
    {
230
        $this->validators[] = [
231
            'Function' => $function,
232
            'Data' => $data,
233
        ];
234
    }
235
236
237
    /**
238
     * Validate this message against a public key.
239
     *
240
     * true is returned on success, false is returned if we don't have any
241
     * signature we can validate. An exception is thrown if the signature
242
     * validation fails.
243
     *
244
     * @param XMLSecurityKey $key The key we should check against
245
     * @throws \Exception
246
     * @return bool true on success, false when we don't have a signature
247
     */
248
    public function validate(XMLSecurityKey $key): bool
249
    {
250
        if (count($this->validators) === 0) {
251
            return false;
252
        }
253
254
        $exceptions = [];
255
256
        foreach ($this->validators as $validator) {
257
            $function = $validator['Function'];
258
            $data = $validator['Data'];
259
260
            try {
261
                call_user_func($function, $data, $key);
262
                /* We were able to validate the message with this validator. */
263
264
                return true;
265
            } catch (\Exception $e) {
266
                $exceptions[] = $e;
267
            }
268
        }
269
270
        /* No validators were able to validate the message. */
271
        throw $exceptions[0];
272
    }
273
274
275
    /**
276
     * Retrieve the identifier of this message.
277
     *
278
     * @return string The identifier of this message
279
     */
280
    public function getId(): string
281
    {
282
        return $this->id;
283
    }
284
285
286
    /**
287
     * Set the identifier of this message.
288
     *
289
     * @param string $id The new identifier of this message
290
     * @return void
291
     */
292
    public function setId(string $id): void
293
    {
294
        $this->id = $id;
295
    }
296
297
298
    /**
299
     * Retrieve the issue timestamp of this message.
300
     *
301
     * @return int The issue timestamp of this message, as an UNIX timestamp
302
     */
303
    public function getIssueInstant(): int
304
    {
305
        return $this->issueInstant;
306
    }
307
308
309
    /**
310
     * Set the issue timestamp of this message.
311
     *
312
     * @param int $issueInstant The new issue timestamp of this message, as an UNIX timestamp
313
     * @return void
314
     */
315
    public function setIssueInstant(int $issueInstant): void
316
    {
317
        $this->issueInstant = $issueInstant;
318
    }
319
320
321
    /**
322
     * Retrieve the destination of this message.
323
     *
324
     * @return string|null The destination of this message, or NULL if no destination is given
325
     */
326
    public function getDestination(): ?string
327
    {
328
        return $this->destination;
329
    }
330
331
332
    /**
333
     * Set the destination of this message.
334
     *
335
     * @param string|null $destination The new destination of this message
336
     * @return void
337
     */
338
    public function setDestination(string $destination = null): void
339
    {
340
        $this->destination = $destination;
341
    }
342
343
344
    /**
345
     * Set the given consent for this message.
346
     * Most likely (though not required) a value of urn:oasis:names:tc:SAML:2.0:consent.
347
     *
348
     * @see \SAML2\Constants
349
     * @param string $consent
350
     * @return void
351
     */
352
    public function setConsent(string $consent): void
353
    {
354
        $this->consent = $consent;
355
    }
356
357
358
    /**
359
     * Get the given consent for this message.
360
     * Most likely (though not required) a value of urn:oasis:names:tc:SAML:2.0:consent.
361
     *
362
     * @see \SAML2\Constants
363
     * @return string|null Consent
364
     */
365
    public function getConsent(): ?string
366
    {
367
        return $this->consent;
368
    }
369
370
371
    /**
372
     * Retrieve the issuer if this message.
373
     *
374
     * @return \SAML2\XML\saml\Issuer|null The issuer of this message, or NULL if no issuer is given
375
     */
376
    public function getIssuer(): ?Issuer
377
    {
378
        return $this->issuer;
379
    }
380
381
382
    /**
383
     * Set the issuer of this message.
384
     *
385
     * @param \SAML2\XML\saml\Issuer|null $issuer The new issuer of this message
386
     * @return void
387
     */
388
    public function setIssuer(Issuer $issuer = null): void
389
    {
390
        $this->issuer = $issuer;
391
    }
392
393
394
    /**
395
     * Query whether or not the message contained a signature at the root level when the object was constructed.
396
     *
397
     * @return bool
398
     */
399
    public function isMessageConstructedWithSignature(): bool
400
    {
401
        return $this->messageContainedSignatureUponConstruction;
402
    }
403
404
405
    /**
406
     * Retrieve the RelayState associated with this message.
407
     *
408
     * @return string|null The RelayState, or NULL if no RelayState is given
409
     */
410
    public function getRelayState(): ?string
411
    {
412
        return $this->relayState;
413
    }
414
415
416
    /**
417
     * Set the RelayState associated with this message.
418
     *
419
     * @param string|null $relayState The new RelayState
420
     * @return void
421
     */
422
    public function setRelayState(string $relayState = null): void
423
    {
424
        $this->relayState = $relayState;
425
    }
426
427
428
    /**
429
     * Convert this message to an unsigned XML document.
430
     * This method does not sign the resulting XML document.
431
     *
432
     * @return \DOMElement The root element of the DOM tree
433
     */
434
    public function toXML(): DOMElement
435
    {
436
        $this->document = DOMDocumentFactory::create();
437
438
        $root = $this->document->createElementNS(Constants::NS_SAMLP, 'samlp:' . $this->tagName);
439
        $this->document->appendChild($root);
440
441
        /* Ugly hack to add another namespace declaration to the root element. */
442
        $root->setAttributeNS(Constants::NS_SAML, 'saml:tmp', 'tmp');
443
        $root->removeAttributeNS(Constants::NS_SAML, 'tmp');
444
445
        $root->setAttribute('ID', $this->id);
446
        $root->setAttribute('Version', '2.0');
447
        $root->setAttribute('IssueInstant', gmdate('Y-m-d\TH:i:s\Z', $this->issueInstant));
448
449
        if ($this->destination !== null) {
450
            $root->setAttribute('Destination', $this->destination);
451
        }
452
        if ($this->consent !== Constants::CONSENT_UNSPECIFIED) {
453
            $root->setAttribute('Consent', $this->consent);
454
        }
455
456
        if ($this->issuer !== null) {
457
            $this->issuer->toXML($root);
458
        }
459
460
        if ($this->Extensions !== null && !$this->Extensions->isEmptyElement()) {
461
            $this->Extensions->toXML($root);
462
        }
463
464
        return $root;
465
    }
466
467
468
    /**
469
     * Convert this message to a signed XML document.
470
     * This method sign the resulting XML document if the private key for
471
     * the signature is set.
472
     *
473
     * @return \DOMElement The root element of the DOM tree
474
     */
475
    public function toSignedXML(): DOMElement
476
    {
477
        $root = $this->toXML();
478
479
        if ($this->signatureKey === null) {
480
            /* We don't have a key to sign it with. */
481
482
            return $root;
483
        }
484
485
        /* Find the position we should insert the signature node at. */
486
        if ($this->issuer !== null) {
487
            /*
488
             * We have an issuer node. The signature node should come
489
             * after the issuer node.
490
             */
491
            $issuerNode = $root->firstChild;
492
            /** @psalm-suppress PossiblyNullPropertyFetch */
493
            $insertBefore = $issuerNode->nextSibling;
494
        } else {
495
            /* No issuer node - the signature element should be the first element. */
496
            $insertBefore = $root->firstChild;
497
        }
498
499
        Utils::insertSignature($this->signatureKey, $this->certificates, $root, $insertBefore);
500
501
        return $root;
502
    }
503
504
505
    /**
506
     * Retrieve the private key we should use to sign the message.
507
     *
508
     * @return XMLSecurityKey|null The key, or NULL if no key is specified
509
     */
510
    public function getSignatureKey(): ?XMLSecurityKey
511
    {
512
        return $this->signatureKey;
513
    }
514
515
516
    /**
517
     * Set the private key we should use to sign the message.
518
     * If the key is null, the message will be sent unsigned.
519
     *
520
     * @param XMLSecurityKey|null $signatureKey
521
     * @return void
522
     */
523
    public function setSignatureKey(XMLSecurityKey $signatureKey = null): void
524
    {
525
        $this->signatureKey = $signatureKey;
526
    }
527
528
529
    /**
530
     * Set the certificates that should be included in the message.
531
     * The certificates should be strings with the PEM encoded data.
532
     *
533
     * @param string[] $certificates An array of certificates
534
     * @return void
535
     */
536
    public function setCertificates(array $certificates): void
537
    {
538
        $this->certificates = $certificates;
539
    }
540
541
542
    /**
543
     * Retrieve the certificates that are included in the message.
544
     *
545
     * @return string[] An array of certificates
546
     */
547
    public function getCertificates(): array
548
    {
549
        return $this->certificates;
550
    }
551
552
553
    /**
554
     * Convert an XML element into a message.
555
     *
556
     * @param \DOMElement $xml The root XML element
557
     * @throws \Exception
558
     * @return \SAML2\Message The message
559
     *
560
     * @throws \InvalidArgumentException if assertions are false
561
     */
562
    public static function fromXML(\DOMElement $xml): Message
563
    {
564
        Assert::same(
565
            $xml->namespaceURI,
566
            Constants::NS_SAMLP,
567
            'Unknown namespace of SAML message: ' . var_export($xml->namespaceURI, true)
568
        );
569
570
        switch ($xml->localName) {
571
            case 'AttributeQuery':
572
                return new AttributeQuery($xml);
573
            case 'AuthnRequest':
574
                return new AuthnRequest($xml);
575
            case 'LogoutResponse':
576
                return new LogoutResponse($xml);
577
            case 'LogoutRequest':
578
                return new LogoutRequest($xml);
579
            case 'Response':
580
                return new Response($xml);
581
            case 'ArtifactResponse':
582
                return new ArtifactResponse($xml);
583
            case 'ArtifactResolve':
584
                return new ArtifactResolve($xml);
585
            default:
586
                throw new \Exception('Unknown SAML message: ' . var_export($xml->localName, true));
587
        }
588
    }
589
590
591
    /**
592
     * @return null|string
593
     */
594
    public function getSignatureMethod(): ?string
595
    {
596
        return $this->signatureMethod;
597
    }
598
}
599