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

388
        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...
389
            throw new Exception('No enveloped signature found');
390
        }
391
    }
392
393
394
    /**
395
     * Get the XML element.
396
     *
397
     * @return \DOMElement
398
     */
399
    public function getXML(): DOMElement
400
    {
401
        return $this->xml;
402
    }
403
404
405
    /**
406
     * Set the XML element.
407
     *
408
     * @param \DOMElement $xml
409
     */
410
    protected function setXML(DOMElement $xml): void
411
    {
412
        $this->xml = $xml;
0 ignored issues
show
Bug Best Practice introduced by
The property xml does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
413
    }
414
415
416
    /**
417
     * @return \DOMElement
418
     */
419
    protected function getOriginalXML(): DOMElement
420
    {
421
        return $this->xml ?? $this->toUnsignedXML();
422
    }
423
424
425
    /**
426
     * @return array|null
427
     */
428
    public function getBlacklistedAlgorithms(): ?array
429
    {
430
        $container = ContainerSingleton::getInstance();
0 ignored issues
show
Bug introduced by
The type SimpleSAML\SAML2\XML\samlp\ContainerSingleton was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
431
        return $container->getBlacklistedEncryptionAlgorithms();
432
    }
433
434
435
    /**
436
     * Convert this message to an unsigned XML document.
437
     * This method does not sign the resulting XML document.
438
     *
439
     * @return \DOMElement The root element of the DOM tree
440
     */
441
    protected function toUnsignedXML(?DOMElement $parent = null): DOMElement
442
    {
443
        $root = $this->instantiateParentElement($parent);
444
445
        /* Ugly hack to add another namespace declaration to the root element. */
446
        $root->setAttributeNS(C::NS_SAML, 'saml:tmp', 'tmp');
447
        $root->removeAttributeNS(C::NS_SAML, 'tmp');
448
449
        $root->setAttribute('Version', $this->getVersion());
450
        $root->setAttribute('ID', $this->getId());
451
        $root->setAttribute('IssueInstant', gmdate('Y-m-d\TH:i:s\Z', $this->getIssueInstant()));
452
453
        if ($this->getDestination() !== null) {
454
            $root->setAttribute('Destination', $this->getDestination());
455
        }
456
457
        if ($this->getConsent() !== null && $this->getConsent() !== C::CONSENT_UNSPECIFIED) {
458
            $root->setAttribute('Consent', $this->getConsent());
459
        }
460
461
        $this->getIssuer()?->toXML($root);
462
463
        if ($this->getExtensions() !== null && !$this->getExtensions()->isEmptyElement()) {
464
            $this->getExtensions()->toXML($root);
465
        }
466
467
        return $root;
468
    }
469
470
471
    /**
472
     * Create XML from this class
473
     *
474
     * @param \DOMElement|null $parent
475
     * @return \DOMElement
476
     */
477
    public function toXML(?DOMElement $parent = null): DOMElement
478
    {
479
        if ($this->isSigned() === true && $this->signer === null) {
480
            $e = $this->instantiateParentElement($parent);
481
482
            // We already have a signed document and no signer was set to re-sign it
483
            $node = $e->ownerDocument->importNode($this->xml, true);
0 ignored issues
show
Bug introduced by
The method importNode() does not exist on null. ( Ignorable by Annotation )

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

483
            /** @scrutinizer ignore-call */ 
484
            $node = $e->ownerDocument->importNode($this->xml, true);

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...
484
            return $e->appendChild($node);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $e->appendChild($node) returns the type DOMNode which includes types incompatible with the type-hinted return DOMElement.
Loading history...
485
        }
486
487
        $e = $this->toUnsignedXML($parent);
488
489
        if ($this->signer !== null) {
490
            $signedXML = $this->doSign($e);
491
492
            // Test for an Issuer
493
            $messageElements = XPath::xpQuery($signedXML, './saml_assertion:Issuer', XPath::getXPath($signedXML));
494
            $issuer = array_pop($messageElements);
495
496
            $signedXML->insertBefore($this->signature->toXML($signedXML), $issuer->nextSibling);
497
            return $signedXML;
498
        }
499
500
        return $e;
501
    }
502
}
503