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

AbstractMessage::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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