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

AbstractMessage::toUnsignedXML()   B

Complexity

Conditions 7
Paths 16

Size

Total Lines 29
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 15
nc 16
nop 1
dl 0
loc 29
rs 8.8333
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\SAML2\XML\samlp;
6
7
use DOMDocument;
8
use DOMElement;
9
use Exception;
10
use SimpleSAML\Assert\Assert;
11
use SimpleSAML\SAML2\Constants;
12
use SimpleSAML\SAML2\Utilities\Temporal;
13
use SimpleSAML\SAML2\Utils;
14
use SimpleSAML\SAML2\Utils\XPath;
15
use SimpleSAML\SAML2\XML\ExtendableElementTrait;
16
use SimpleSAML\SAML2\XML\saml\Issuer;
17
use SimpleSAML\XMLSecurity\XML\SignableElementInterface;
18
use SimpleSAML\XMLSecurity\XML\SignableElementTrait;
19
use SimpleSAML\XMLSecurity\XML\SignedElementInterface;
20
use SimpleSAML\XMLSecurity\XML\SignedElementTrait;
21
use SimpleSAML\XMLSecurity\XMLSecurityKey;
22
23
use function array_pop;
24
use function call_user_func;
25
use function count;
26
use function gmdate;
27
28
/**
29
 * Base class for all SAML 2 messages.
30
 *
31
 * Implements what is common between the samlp:RequestAbstractType and
32
 * samlp:StatusResponseType element types.
33
 *
34
 * @package simplesamlphp/saml2
35
 */
36
abstract class AbstractMessage extends AbstractSamlpElement implements SignableElementInterface, SignedElementInterface
37
{
38
    use ExtendableElementTrait;
39
    use SignableElementTrait;
1 ignored issue
show
introduced by
The trait SimpleSAML\XMLSecurity\XML\SignableElementTrait requires some properties which are not provided by SimpleSAML\SAML2\XML\samlp\AbstractMessage: $ownerDocument, $documentElement
Loading history...
40
    use SignedElementTrait;
1 ignored issue
show
introduced by
The trait SimpleSAML\XMLSecurity\XML\SignedElementTrait requires some properties which are not provided by SimpleSAML\SAML2\XML\samlp\AbstractMessage: $ownerDocument, $documentElement
Loading history...
41
42
    /**
43
     * The identifier of this message.
44
     *
45
     * @var string
46
     */
47
    protected string $id;
48
49
    /** @var \DOMElement $xml */
50
    protected DOMElement $xml;
51
52
    /**
53
     * The version of this message.
54
     *
55
     * @var string
56
     */
57
    protected string $version = '2.0';
58
59
    /**
60
     * The issue timestamp of this message, as an UNIX timestamp.
61
     *
62
     * @var int
63
     */
64
    protected int $issueInstant;
65
66
    /**
67
     * The destination URL of this message if it is known.
68
     *
69
     * @var string|null
70
     */
71
    protected ?string $destination = null;
72
73
    /**
74
     * The destination URL of this message if it is known.
75
     *
76
     * @var string|null
77
     */
78
    protected ?string $consent;
79
80
    /**
81
     * The entity id of the issuer of this message, or null if unknown.
82
     *
83
     * @var \SimpleSAML\SAML2\XML\saml\Issuer|null
84
     */
85
    protected ?Issuer $issuer = null;
86
87
    /**
88
     * The RelayState associated with this message.
89
     *
90
     * @var string|null
91
     */
92
    protected ?string $relayState = null;
93
94
    /**
95
     * The \DOMDocument we are currently building.
96
     *
97
     * This variable is used while generating XML from this message. It holds the
98
     * \DOMDocument of the XML we are generating.
99
     *
100
     * @var \DOMDocument|null
101
     */
102
    protected ?DOMDocument $document = null;
103
104
    /** @var bool */
105
    protected bool $messageContainedSignatureUponConstruction = false;
106
107
    /**
108
     * Available methods for validating this message.
109
     *
110
     * @var array
111
     */
112
    private array $validators = [];
113
114
115
    /**
116
     * Initialize a message.
117
     *
118
     * @param \SimpleSAML\SAML2\XML\saml\Issuer|null $issuer
119
     * @param string|null $id
120
     * @param int|null $issueInstant
121
     * @param string|null $destination
122
     * @param string|null $consent
123
     * @param \SimpleSAML\SAML2\XML\samlp\Extensions $extensions
124
     * @param string|null $relayState
125
     *
126
     * @throws \Exception
127
     */
128
    protected function __construct(
129
        ?Issuer $issuer = null,
130
        ?string $id = null,
131
        ?int $issueInstant = null,
132
        ?string $destination = null,
133
        ?string $consent = null,
134
        ?Extensions $extensions = null,
135
        ?string $relayState = null
136
    ) {
137
        $this->setIssuer($issuer);
138
        $this->setId($id);
139
        $this->setIssueInstant($issueInstant);
140
        $this->setDestination($destination);
141
        $this->setConsent($consent);
142
        $this->setExtensions($extensions);
143
        $this->setRelayState($relayState);
144
        $this->addValidator([$this, 'xmlSignatureValidatorWrapper'], []);
145
    }
146
147
148
    /**
149
     * Add a method for validating this message.
150
     *
151
     * This function is used by the HTTP-Redirect binding, to make it possible to
152
     * check the signature against the one included in the query string.
153
     *
154
     * @param callable $function The function which should be called
155
     * @param mixed $data The data that should be included as the first parameter to the function
156
     */
157
    public function addValidator(callable $function, $data): void
158
    {
159
        $this->validators[] = [
160
            'Function' => $function,
161
            'Data' => $data,
162
        ];
163
    }
164
165
166
    /**
167
     * Validate this message against a public key.
168
     *
169
     * true is returned on success, false is returned if we don't have any
170
     * signature we can validate. An exception is thrown if the signature
171
     * validation fails.
172
     *
173
     * @param \SimpleSAML\XMLSecurity\XMLSecurityKey $key The key we should check against
174
     * @throws \Exception
175
     * @return bool true on success, false when we don't have a signature
176
     */
177
    public function validate(XMLSecurityKey $key): bool
178
    {
179
        if (count($this->validators) === 0) {
180
            return false;
181
        }
182
183
        $exceptions = [];
184
185
        foreach ($this->validators as $validator) {
186
            $function = $validator['Function'];
187
            $data = $validator['Data'];
188
189
            try {
190
                call_user_func($function, $data, $key);
191
                /* We were able to validate the message with this validator. */
192
193
                return true;
194
            } catch (Exception $e) {
195
                $exceptions[] = $e;
196
            }
197
        }
198
199
        Assert::notEmpty($exceptions);
200
201
        /**
202
         * No validators were able to validate the message.
203
         * @psalm-suppress InvalidThrow
204
         */
205
        throw array_pop($exceptions);
206
    }
207
208
209
    /**
210
     * Retrieve the identifier of this message.
211
     *
212
     * @return string The identifier of this message
213
     */
214
    public function getId(): string
215
    {
216
        return $this->id;
217
    }
218
219
220
    /**
221
     * Set the identifier of this message.
222
     *
223
     * @param string|null $id The new identifier of this message
224
     */
225
    private function setId(?string $id): void
226
    {
227
        Assert::nullOrNotWhitespaceOnly($id);
228
229
        if ($id === null) {
230
            $id = Utils::getContainer()->generateId();
231
        }
232
233
        $this->id = $id;
234
    }
235
236
237
    /**
238
     * Retrieve the version of this message.
239
     *
240
     * @return string The version of this message
241
     */
242
    public function getVersion(): string
243
    {
244
        return $this->version;
245
    }
246
247
248
    /**
249
     * Retrieve the issue timestamp of this message.
250
     *
251
     * @return int The issue timestamp of this message, as an UNIX timestamp
252
     */
253
    public function getIssueInstant(): int
254
    {
255
        return $this->issueInstant;
256
    }
257
258
259
    /**
260
     * Set the issue timestamp of this message.
261
     *
262
     * @param int|null $issueInstant The new issue timestamp of this message, as an UNIX timestamp
263
     */
264
    private function setIssueInstant(?int $issueInstant): void
265
    {
266
        if ($issueInstant === null) {
267
            $issueInstant = Temporal::getTime();
268
        }
269
270
        $this->issueInstant = $issueInstant;
271
    }
272
273
274
    /**
275
     * Retrieve the destination of this message.
276
     *
277
     * @return string|null The destination of this message, or NULL if no destination is given
278
     */
279
    public function getDestination(): ?string
280
    {
281
        return $this->destination;
282
    }
283
284
285
    /**
286
     * Set the destination of this message.
287
     *
288
     * @param string|null $destination The new destination of this message
289
     */
290
    private function setDestination(string $destination = null): void
291
    {
292
        Assert::nullOrNotWhitespaceOnly($destination);
293
294
        $this->destination = $destination;
295
    }
296
297
298
    /**
299
     * Get the given consent for this message.
300
     * Most likely (though not required) a value of urn:oasis:names:tc:SAML:2.0:consent.
301
     *
302
     * @see \SimpleSAML\SAML2\Constants
303
     * @return string|null Consent
304
     */
305
    public function getConsent(): ?string
306
    {
307
        return $this->consent;
308
    }
309
310
311
    /**
312
     * Set the given consent for this message.
313
     * Most likely (though not required) a value of urn:oasis:names:tc:SAML:2.0:consent.
314
     *
315
     * @see \SimpleSAML\SAML2\Constants
316
     * @param string|null $consent
317
     */
318
    private function setConsent(?string $consent): void
319
    {
320
        Assert::nullOrNotWhitespaceOnly($consent);
321
322
        $this->consent = $consent;
323
    }
324
325
326
    /**
327
     * Retrieve the issuer if this message.
328
     *
329
     * @return \SimpleSAML\SAML2\XML\saml\Issuer|null The issuer of this message, or NULL if no issuer is given
330
     */
331
    public function getIssuer(): ?Issuer
332
    {
333
        return $this->issuer;
334
    }
335
336
337
    /**
338
     * Set the issuer of this message.
339
     *
340
     * @param \SimpleSAML\SAML2\XML\saml\Issuer|null $issuer The new issuer of this message
341
     */
342
    private function setIssuer(Issuer $issuer = null): void
343
    {
344
        $this->issuer = $issuer;
345
    }
346
347
348
    /**
349
     * Query whether or not the message contained a signature at the root level when the object was constructed.
350
     *
351
     * @return bool
352
     */
353
    public function isMessageConstructedWithSignature(): bool
354
    {
355
        return $this->messageContainedSignatureUponConstruction;
356
    }
357
358
359
    /**
360
     * Retrieve the RelayState associated with this message.
361
     *
362
     * @return string|null The RelayState, or NULL if no RelayState is given
363
     */
364
    public function getRelayState(): ?string
365
    {
366
        return $this->relayState;
367
    }
368
369
370
    /**
371
     * Set the RelayState associated with this message.
372
     *
373
     * @param string|null $relayState The new RelayState
374
     */
375
    public function setRelayState(string $relayState = null): void
376
    {
377
        Assert::nullOrNotWhitespaceOnly($relayState);
378
379
        $this->relayState = $relayState;
380
    }
381
382
383
    /**
384
     * Wrapper method over SignedElementTrait to use as a validator for enveloped XML signatures.
385
     *
386
     * @param array $_
387
     * @param \SimpleSAML\XMLSecurity\XMLSecurityKey $key The key to use to verify the enveloped signature.
388
     *
389
     * @throws \Exception If there's no enveloped signature, or it fails to validate.
390
     */
391
    protected function xmlSignatureValidatorWrapper(array $_, XMLSecurityKey $key): void
392
    {
393
        if ($this->validateEnvelopedXmlSignature($key) === false) {
0 ignored issues
show
Bug introduced by
The method validateEnvelopedXmlSignature() does not exist on SimpleSAML\SAML2\XML\samlp\AbstractMessage. Did you maybe mean validate()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

393
        if ($this->/** @scrutinizer ignore-call */ validateEnvelopedXmlSignature($key) === false) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
394
            throw new Exception('No enveloped signature found');
395
        }
396
    }
397
398
399
    /**
400
     * Get the XML element.
401
     *
402
     * @return \DOMElement
403
     */
404
    public function getXML(): DOMElement
405
    {
406
        return $this->xml;
407
    }
408
409
410
    /**
411
     * Set the XML element.
412
     *
413
     * @param \DOMElement $xml
414
     */
415
    protected function setXML(DOMElement $xml): void
416
    {
417
        $this->xml = $xml;
418
    }
419
420
421
    /**
422
     * Create XML from this class
423
     *
424
     * @param \DOMElement|null $parent
425
     * @return \DOMElement
426
     */
427
    public function toXML(?DOMElement $parent = null): DOMElement
428
    {
429
        $e = $this->toUnsignedXML($parent);
430
431
        if ($this->signer !== null) {
432
            $signedXML = $this->doSign($e);
433
434
            // Test for an Issuer
435
            $messageElements = XPath::xpQuery($signedXML, './saml_assertion:Issuer', XPath::getXPath($signedXML));
436
            $issuer = array_pop($messageElements);
437
438
            $signedXML->insertBefore(
439
                $this->signature->toXML($signedXML),
440
                $issuer ? $issuer->nextSibling : $signedXML->firstChild
441
            );
442
            return $signedXML;
443
        }
444
445
        return $e;
446
    }
447
448
449
    /**
450
     * @return \DOMElement
451
     */
452
    protected function getOriginalXML(): DOMElement
453
    {
454
        return $this->xml ?? $this->toUnsignedXML();
455
    }
456
457
458
    /**
459
     * Convert this message to an unsigned XML document.
460
     * This method does not sign the resulting XML document.
461
     *
462
     * @param \DOMElement|null $parent
463
     * @return \DOMElement The root element of the DOM tree
464
     */
465
    protected function toUnsignedXML(?DOMElement $parent = null): DOMElement
466
    {
467
        $root = $this->instantiateParentElement($parent);
468
469
        /* Ugly hack to add another namespace declaration to the root element. */
470
        $root->setAttributeNS(Constants::NS_SAML, 'saml:tmp', 'tmp');
471
        $root->removeAttributeNS(Constants::NS_SAML, 'tmp');
472
473
        $root->setAttribute('Version', $this->version);
474
        $root->setAttribute('ID', $this->id);
475
        $root->setAttribute('IssueInstant', gmdate('Y-m-d\TH:i:s\Z', $this->issueInstant));
476
477
        if ($this->destination !== null) {
478
            $root->setAttribute('Destination', $this->destination);
479
        }
480
481
        if ($this->consent !== null && $this->consent !== Constants::CONSENT_UNSPECIFIED) {
482
            $root->setAttribute('Consent', $this->consent);
483
        }
484
485
        if ($this->issuer !== null) {
486
            $this->issuer->toXML($root);
487
        }
488
489
        if ($this->Extensions !== null && !$this->Extensions->isEmptyElement()) {
490
            $this->Extensions->toXML($root);
491
        }
492
493
        return $root;
494
    }
495
}
496