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

AbstractMessage::setDestination()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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