Completed
Push — master ( a7e5b2...5514ed )
by Thijs
03:16
created

Message::toUnsignedXML()   B

Complexity

Conditions 6
Paths 16

Size

Total Lines 32
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 32
rs 8.439
cc 6
eloc 18
nc 16
nop 0
1
<?php
2
3
namespace SAML2;
4
5
use RobRichards\XMLSecLibs\XMLSecurityKey;
6
use SAML2\Utilities\Temporal;
7
use SAML2\XML\samlp\Extensions;
8
9
/**
10
 * Base class for all SAML 2 messages.
11
 *
12
 * Implements what is common between the samlp:RequestAbstractType and
13
 * samlp:StatusResponseType element types.
14
 *
15
 * @package SimpleSAMLphp
16
 *
17
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
18
 */
19
abstract class Message implements SignedElement
20
{
21
    /**
22
     * Request extensions.
23
     *
24
     * @var array
25
     */
26
    protected $extensions;
27
28
    /**
29
     * The name of the root element of the DOM tree for the message.
30
     *
31
     * Used when creating a DOM tree from the message.
32
     *
33
     * @var string
34
     */
35
    private $tagName;
36
37
    /**
38
     * The identifier of this message.
39
     *
40
     * @var string
41
     */
42
    private $id;
43
44
    /**
45
     * The issue timestamp of this message, as an UNIX timestamp.
46
     *
47
     * @var int
48
     */
49
    private $issueInstant;
50
51
    /**
52
     * The destination URL of this message if it is known.
53
     *
54
     * @var string|null
55
     */
56
    private $destination;
57
58
    /**
59
     * The destination URL of this message if it is known.
60
     *
61
     * @var string|null
62
     */
63
    private $consent = Constants::CONSENT_UNSPECIFIED;
64
65
    /**
66
     * The entity id of the issuer of this message, or null if unknown.
67
     *
68
     * @var string|null
69
     */
70
    private $issuer;
71
72
    /**
73
     * The RelayState associated with this message.
74
     *
75
     * @var string|null
76
     */
77
    private $relayState;
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
     * The private key we should use to sign the message.
91
     *
92
     * The private key can be null, in which case the message is sent unsigned.
93
     *
94
     * @var XMLSecurityKey|null
95
     */
96
    private $signatureKey;
97
98
    /**
99
     * @var bool
100
     */
101
    protected $messageContainedSignatureUponConstruction = false;
102
103
    /**
104
     * List of certificates that should be included in the message.
105
     *
106
     * @var array
107
     */
108
    private $certificates;
109
110
    /**
111
     * Available methods for validating this message.
112
     *
113
     * @var array
114
     */
115
    private $validators;
116
117
    /**
118
     * @var null|string
119
     */
120
    private $signatureMethod;
121
122
    /**
123
     * Initialize a message.
124
     *
125
     * This constructor takes an optional parameter with a \DOMElement. If this
126
     * parameter is given, the message will be initialized with data from that
127
     * XML element.
128
     *
129
     * If no XML element is given, the message is initialized with suitable
130
     * default values.
131
     *
132
     * @param string          $tagName The tag name of the root element.
133
     * @param \DOMElement|null $xml     The input message.
134
     * @throws \Exception
135
     */
136
    protected function __construct($tagName, \DOMElement $xml = null)
137
    {
138
        assert('is_string($tagName)');
139
        $this->tagName = $tagName;
140
141
        $this->id = Utils::getContainer()->generateId();
142
        $this->issueInstant = Temporal::getTime();
143
        $this->certificates = array();
144
        $this->validators = array();
145
146
        if ($xml === null) {
147
            return;
148
        }
149
150
        if (!$xml->hasAttribute('ID')) {
151
            throw new \Exception('Missing ID attribute on SAML message.');
152
        }
153
        $this->id = $xml->getAttribute('ID');
154
155
        if ($xml->getAttribute('Version') !== '2.0') {
156
            /* Currently a very strict check. */
157
            throw new \Exception('Unsupported version: ' . $xml->getAttribute('Version'));
158
        }
159
160
        $this->issueInstant = Utils::xsDateTimeToTimestamp($xml->getAttribute('IssueInstant'));
161
162
        if ($xml->hasAttribute('Destination')) {
163
            $this->destination = $xml->getAttribute('Destination');
164
        }
165
166
        if ($xml->hasAttribute('Consent')) {
167
            $this->consent = $xml->getAttribute('Consent');
168
        }
169
170
        $issuer = Utils::xpQuery($xml, './saml_assertion:Issuer');
171
        if (!empty($issuer)) {
172
            $this->issuer = trim($issuer[0]->textContent);
173
        }
174
175
        /* Validate the signature element of the message. */
176
        try {
177
            /** @var null|\DOMAttr $signatureMethod */
178
            $signatureMethod = Utils::xpQuery($xml, './ds:Signature/ds:SignedInfo/ds:SignatureMethod/@Algorithm');
179
180
            $sig = Utils::validateElement($xml);
181
182
            if ($sig !== false) {
183
                $this->messageContainedSignatureUponConstruction = true;
184
                $this->certificates = $sig['Certificates'];
185
                $this->validators[] = array(
186
                    'Function' => array('\SAML2\Utils', 'validateSignature'),
187
                    'Data' => $sig,
188
                    );
189
                $this->signatureMethod = $signatureMethod[0]->value;
190
            }
191
        } catch (\Exception $e) {
192
            /* Ignore signature validation errors. */
193
        }
194
195
        $this->extensions = Extensions::getList($xml);
196
    }
197
198
    /**
199
     * Add a method for validating this message.
200
     *
201
     * This function is used by the HTTP-Redirect binding, to make it possible to
202
     * check the signature against the one included in the query string.
203
     *
204
     * @param callback $function The function which should be called.
205
     * @param mixed    $data     The data that should be included as the first parameter to the function.
206
     */
207
    public function addValidator($function, $data)
208
    {
209
        assert('is_callable($function)');
210
211
        $this->validators[] = array(
212
            'Function' => $function,
213
            'Data' => $data,
214
            );
215
    }
216
217
    /**
218
     * Validate this message against a public key.
219
     *
220
     * true is returned on success, false is returned if we don't have any
221
     * signature we can validate. An exception is thrown if the signature
222
     * validation fails.
223
     *
224
     * @param  XMLSecurityKey $key The key we should check against.
225
     * @return boolean        true on success, false when we don't have a signature.
226
     * @throws \Exception
227
     */
228 View Code Duplication
    public function validate(XMLSecurityKey $key)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
229
    {
230
        if (count($this->validators) === 0) {
231
            return false;
232
        }
233
234
        $exceptions = array();
235
236
        foreach ($this->validators as $validator) {
237
            $function = $validator['Function'];
238
            $data = $validator['Data'];
239
240
            try {
241
                call_user_func($function, $data, $key);
242
                /* We were able to validate the message with this validator. */
243
244
                return true;
245
            } catch (\Exception $e) {
246
                $exceptions[] = $e;
247
            }
248
        }
249
250
        /* No validators were able to validate the message. */
251
        throw $exceptions[0];
252
    }
253
254
    /**
255
     * Retrieve the identifier of this message.
256
     *
257
     * @return string The identifier of this message.
258
     */
259
    public function getId()
260
    {
261
        return $this->id;
262
    }
263
264
    /**
265
     * Set the identifier of this message.
266
     *
267
     * @param string $id The new identifier of this message.
268
     */
269
    public function setId($id)
270
    {
271
        assert('is_string($id)');
272
273
        $this->id = $id;
274
    }
275
276
    /**
277
     * Retrieve the issue timestamp of this message.
278
     *
279
     * @return int The issue timestamp of this message, as an UNIX timestamp.
280
     */
281
    public function getIssueInstant()
282
    {
283
        return $this->issueInstant;
284
    }
285
286
    /**
287
     * Set the issue timestamp of this message.
288
     *
289
     * @param int $issueInstant The new issue timestamp of this message, as an UNIX timestamp.
290
     */
291
    public function setIssueInstant($issueInstant)
292
    {
293
        assert('is_int($issueInstant)');
294
295
        $this->issueInstant = $issueInstant;
296
    }
297
298
    /**
299
     * Retrieve the destination of this message.
300
     *
301
     * @return string|null The destination of this message, or NULL if no destination is given.
302
     */
303
    public function getDestination()
304
    {
305
        return $this->destination;
306
    }
307
308
    /**
309
     * Set the destination of this message.
310
     *
311
     * @param string|null $destination The new destination of this message.
312
     */
313
    public function setDestination($destination)
314
    {
315
        assert('is_string($destination) || is_null($destination)');
316
317
        $this->destination = $destination;
318
    }
319
320
    /**
321
     * Set the given consent for this message.
322
     *
323
     * Most likely (though not required) a value of rn:oasis:names:tc:SAML:2.0:consent.
324
     * @see \SAML2\Constants
325
     *
326
     * @param string $consent
327
     */
328
    public function setConsent($consent)
329
    {
330
        assert('is_string($consent)');
331
332
        $this->consent = $consent;
333
    }
334
335
    /**
336
     * Set the given consent for this message.
337
     *
338
     * Most likely (though not required) a value of rn:oasis:names:tc:SAML:2.0:consent.
339
     * @see \SAML2\Constants
340
     *
341
     * @return string Consent
342
     */
343
    public function getConsent()
344
    {
345
        return $this->consent;
346
    }
347
348
    /**
349
     * Retrieve the issuer if this message.
350
     *
351
     * @return string|null The issuer of this message, or NULL if no issuer is given.
352
     */
353
    public function getIssuer()
354
    {
355
        return $this->issuer;
356
    }
357
358
    /**
359
     * Set the issuer of this message.
360
     *
361
     * @param string|null $issuer The new issuer of this message.
362
     */
363
    public function setIssuer($issuer)
364
    {
365
        assert('is_string($issuer) || is_null($issuer)');
366
367
        $this->issuer = $issuer;
368
    }
369
370
    /**
371
     * Query whether or not the message contained a signature at the root level when the object was constructed.
372
     *
373
     * @return bool
374
     */
375
    public function isMessageConstructedWithSignature()
376
    {
377
        return $this->messageContainedSignatureUponConstruction;
378
    }
379
380
    /**
381
     * Retrieve the RelayState associated with this message.
382
     *
383
     * @return string|null The RelayState, or NULL if no RelayState is given.
384
     */
385
    public function getRelayState()
386
    {
387
        return $this->relayState;
388
    }
389
390
    /**
391
     * Set the RelayState associated with this message.
392
     *
393
     * @param string|null $relayState The new RelayState.
394
     */
395
    public function setRelayState($relayState)
396
    {
397
        assert('is_string($relayState) || is_null($relayState)');
398
399
        $this->relayState = $relayState;
400
    }
401
402
    /**
403
     * Convert this message to an unsigned XML document.
404
     *
405
     * This method does not sign the resulting XML document.
406
     *
407
     * @return \DOMElement The root element of the DOM tree.
408
     */
409
    public function toUnsignedXML()
410
    {
411
        $this->document = DOMDocumentFactory::create();
412
413
        $root = $this->document->createElementNS(Constants::NS_SAMLP, 'samlp:' . $this->tagName);
414
        $this->document->appendChild($root);
415
416
        /* Ugly hack to add another namespace declaration to the root element. */
417
        $root->setAttributeNS(Constants::NS_SAML, 'saml:tmp', 'tmp');
418
        $root->removeAttributeNS(Constants::NS_SAML, 'tmp');
419
420
        $root->setAttribute('ID', $this->id);
421
        $root->setAttribute('Version', '2.0');
422
        $root->setAttribute('IssueInstant', gmdate('Y-m-d\TH:i:s\Z', $this->issueInstant));
423
424
        if ($this->destination !== null) {
425
            $root->setAttribute('Destination', $this->destination);
426
        }
427
        if ($this->consent !== null && $this->consent !== Constants::CONSENT_UNSPECIFIED) {
428
            $root->setAttribute('Consent', $this->consent);
429
        }
430
431
        if ($this->issuer !== null) {
432
            Utils::addString($root, Constants::NS_SAML, 'saml:Issuer', $this->issuer);
433
        }
434
435
        if (!empty($this->extensions)) {
436
            Extensions::addList($root, $this->extensions);
437
        }
438
439
        return $root;
440
    }
441
442
    /**
443
     * Convert this message to a signed XML document.
444
     *
445
     * This method sign the resulting XML document if the private key for
446
     * the signature is set.
447
     *
448
     * @return \DOMElement The root element of the DOM tree.
449
     */
450
    public function toSignedXML()
451
    {
452
        $root = $this->toUnsignedXML();
453
454
        if ($this->signatureKey === null) {
455
            /* We don't have a key to sign it with. */
456
457
            return $root;
458
        }
459
460
461
        /* Find the position we should insert the signature node at. */
462
        if ($this->issuer !== null) {
463
            /*
464
             * We have an issuer node. The signature node should come
465
             * after the issuer node.
466
             */
467
            $issuerNode = $root->firstChild;
468
            $insertBefore = $issuerNode->nextSibling;
469
        } else {
470
            /* No issuer node - the signature element should be the first element. */
471
            $insertBefore = $root->firstChild;
472
        }
473
474
475
        Utils::insertSignature($this->signatureKey, $this->certificates, $root, $insertBefore);
476
477
        return $root;
478
    }
479
480
481
    /**
482
     * Retrieve the private key we should use to sign the message.
483
     *
484
     * @return XMLSecurityKey|null The key, or NULL if no key is specified.
485
     */
486
    public function getSignatureKey()
487
    {
488
        return $this->signatureKey;
489
    }
490
491
492
    /**
493
     * Set the private key we should use to sign the message.
494
     *
495
     * If the key is null, the message will be sent unsigned.
496
     *
497
     * @param XMLSecurityKey|null $signatureKey
498
     */
499
    public function setSignatureKey(XMLsecurityKey $signatureKey = null)
500
    {
501
        $this->signatureKey = $signatureKey;
502
    }
503
504
505
    /**
506
     * Set the certificates that should be included in the message.
507
     *
508
     * The certificates should be strings with the PEM encoded data.
509
     *
510
     * @param array $certificates An array of certificates.
511
     */
512
    public function setCertificates(array $certificates)
513
    {
514
        $this->certificates = $certificates;
515
    }
516
517
518
    /**
519
     * Retrieve the certificates that are included in the message.
520
     *
521
     * @return array An array of certificates.
522
     */
523
    public function getCertificates()
524
    {
525
        return $this->certificates;
526
    }
527
528
529
    /**
530
     * Convert an XML element into a message.
531
     *
532
     * @param  \DOMElement    $xml The root XML element.
533
     * @return \SAML2\Message The message.
534
     * @throws \Exception
535
     */
536
    public static function fromXML(\DOMElement $xml)
537
    {
538 View Code Duplication
        if ($xml->namespaceURI !== Constants::NS_SAMLP) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
539
            throw new \Exception('Unknown namespace of SAML message: ' . var_export($xml->namespaceURI, true));
540
        }
541
542
        switch ($xml->localName) {
543
            case 'AttributeQuery':
544
                return new AttributeQuery($xml);
545
            case 'AuthnRequest':
546
                return new AuthnRequest($xml);
547
            case 'LogoutResponse':
548
                return new LogoutResponse($xml);
549
            case 'LogoutRequest':
550
                return new LogoutRequest($xml);
551
            case 'Response':
552
                return new Response($xml);
553
            case 'ArtifactResponse':
554
                return new ArtifactResponse($xml);
555
            case 'ArtifactResolve':
556
                return new ArtifactResolve($xml);
557
            default:
558
                throw new \Exception('Unknown SAML message: ' . var_export($xml->localName, true));
559
        }
560
    }
561
562
    /**
563
     * Retrieve the Extensions.
564
     *
565
     * @return \SAML2\XML\samlp\Extensions.
566
     */
567
    public function getExtensions()
568
    {
569
        return $this->extensions;
570
    }
571
572
    /**
573
     * Set the Extensions.
574
     *
575
     * @param array|null $extensions The Extensions.
576
     */
577
    public function setExtensions($extensions)
578
    {
579
        assert('is_array($extensions) || is_null($extensions)');
580
581
        $this->extensions = $extensions;
0 ignored issues
show
Documentation Bug introduced by
It seems like $extensions can be null. However, the property $extensions is declared as array. Maybe change the type of the property to array|null or add a type check?

Our type inference engine has found an assignment of a scalar value (like a string, an integer or null) to a property which is an array.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property.

To type hint that a parameter can be either an array or null, you can set a type hint of array and a default value of null. The PHP interpreter will then accept both an array or null for that parameter.

function aContainsB(array $needle = null, array  $haystack) {
    if (!$needle) {
        return false;
    }

    return array_intersect($haystack, $needle) == $haystack;
}

The function can be called with either null or an array for the parameter $needle but will only accept an array as $haystack.

Loading history...
582
    }
583
584
    /**
585
     * @return null|string
586
     */
587
    public function getSignatureMethod()
588
    {
589
        return $this->signatureMethod;
590
    }
591
}
592