Passed
Pull Request — master (#226)
by Jaime Pérez
02:38
created

AbstractMessage::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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