Completed
Push — master ( 4bf886...bfa5d8 )
by Jaime Pérez
03:50
created

Message::validateSignature()   A

Complexity

Conditions 3
Paths 7

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 12
nc 7
nop 1
dl 0
loc 21
rs 9.3142
c 0
b 0
f 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
 *
16
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
17
 */
18
abstract class Message implements SignedElement
19
{
20
    /**
21
     * Request extensions.
22
     *
23
     * @var array
24
     */
25
    protected $extensions;
26
27
    /**
28
     * The name of the root element of the DOM tree for the message.
29
     *
30
     * Used when creating a DOM tree from the message.
31
     *
32
     * @var string
33
     */
34
    private $tagName;
35
36
    /**
37
     * The identifier of this message.
38
     *
39
     * @var string
40
     */
41
    private $id;
42
43
    /**
44
     * The issue timestamp of this message, as an UNIX timestamp.
45
     *
46
     * @var int
47
     */
48
    private $issueInstant;
49
50
    /**
51
     * The destination URL of this message if it is known.
52
     *
53
     * @var string|null
54
     */
55
    private $destination;
56
57
    /**
58
     * The destination URL of this message if it is known.
59
     *
60
     * @var string|null
61
     */
62
    private $consent = Constants::CONSENT_UNSPECIFIED;
63
64
    /**
65
     * The entity id of the issuer of this message, or null if unknown.
66
     *
67
     * @var string|\SAML2\XML\saml\Issuer|null
68
     */
69
    private $issuer;
70
71
    /**
72
     * The RelayState associated with this message.
73
     *
74
     * @var string|null
75
     */
76
    private $relayState;
77
78
    /**
79
     * The \DOMDocument we are currently building.
80
     *
81
     * This variable is used while generating XML from this message. It holds the
82
     * \DOMDocument of the XML we are generating.
83
     *
84
     * @var \DOMDocument
85
     */
86
    protected $document;
87
88
    /**
89
     * The private key we should use to sign the message.
90
     *
91
     * The private key can be null, in which case the message is sent unsigned.
92
     *
93
     * @var XMLSecurityKey|null
94
     */
95
    private $signatureKey;
96
97
    /**
98
     * @var bool
99
     */
100
    protected $messageContainedSignatureUponConstruction = false;
101
102
    /**
103
     * List of certificates that should be included in the message.
104
     *
105
     * @var array
106
     */
107
    private $certificates;
108
109
    /**
110
     * Available methods for validating this message.
111
     *
112
     * @var array
113
     */
114
    private $validators;
115
116
    /**
117
     * @var null|string
118
     */
119
    private $signatureMethod;
120
121
    /**
122
     * Initialize a message.
123
     *
124
     * This constructor takes an optional parameter with a \DOMElement. If this
125
     * parameter is given, the message will be initialized with data from that
126
     * XML element.
127
     *
128
     * If no XML element is given, the message is initialized with suitable
129
     * default values.
130
     *
131
     * @param string           $tagName The tag name of the root element
132
     * @param \DOMElement|null $xml     The input message
133
     *
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 = new XML\saml\Issuer($issuer[0]);
173
            if ($this->issuer->Format === Constants::NAMEID_ENTITY) {
174
                $this->issuer = $this->issuer->value;
175
            }
176
        }
177
178
        $this->validateSignature($xml);
179
180
        $this->extensions = Extensions::getList($xml);
181
    }
182
183
184
    /**
185
     * Validate the signature element of a SAML message, and configure this object appropriately to perform the
186
     * signature verification afterwards.
187
     *
188
     * Please note this method does NOT verify the signature, it just validates the signature construction and prepares
189
     * this object to do the verification.
190
     *
191
     * @param \DOMElement $xml The SAML message whose signature we want to validate.
192
     */
193
    private function validateSignature(\DOMElement $xml)
194
    {
195
        try {
196
            /** @var null|\DOMAttr $signatureMethod */
197
            $signatureMethod = Utils::xpQuery($xml, './ds:Signature/ds:SignedInfo/ds:SignatureMethod/@Algorithm');
198
199
            $sig = Utils::validateElement($xml);
200
201
            if ($sig !== false) {
202
                $this->messageContainedSignatureUponConstruction = true;
203
                $this->certificates = $sig['Certificates'];
204
                $this->validators[] = array(
205
                    'Function' => array('\SAML2\Utils', 'validateSignature'),
206
                    'Data' => $sig,
207
                );
208
                $this->signatureMethod = $signatureMethod[0]->value;
209
            }
210
        } catch (\Exception $e) {
211
            // ignore signature validation errors
212
        }
213
    }
214
215
216
    /**
217
     * Add a method for validating this message.
218
     *
219
     * This function is used by the HTTP-Redirect binding, to make it possible to
220
     * check the signature against the one included in the query string.
221
     *
222
     * @param callback $function The function which should be called
223
     * @param mixed    $data     The data that should be included as the first parameter to the function
224
     */
225
    public function addValidator($function, $data)
226
    {
227
        assert('is_callable($function)');
228
229
        $this->validators[] = array(
230
            'Function' => $function,
231
            'Data' => $data,
232
        );
233
    }
234
235
    /**
236
     * Validate this message against a public key.
237
     *
238
     * true is returned on success, false is returned if we don't have any
239
     * signature we can validate. An exception is thrown if the signature
240
     * validation fails.
241
     *
242
     * @param XMLSecurityKey $key The key we should check against
243
     *
244
     * @return bool true on success, false when we don't have a signature
245
     *
246
     * @throws \Exception
247
     */
248 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...
249
    {
250
        if (count($this->validators) === 0) {
251
            return false;
252
        }
253
254
        $exceptions = array();
255
256
        foreach ($this->validators as $validator) {
257
            $function = $validator['Function'];
258
            $data = $validator['Data'];
259
260
            try {
261
                call_user_func($function, $data, $key);
262
                /* We were able to validate the message with this validator. */
263
264
                return true;
265
            } catch (\Exception $e) {
266
                $exceptions[] = $e;
267
            }
268
        }
269
270
        /* No validators were able to validate the message. */
271
        throw $exceptions[0];
272
    }
273
274
    /**
275
     * Retrieve the identifier of this message.
276
     *
277
     * @return string The identifier of this message
278
     */
279
    public function getId()
280
    {
281
        return $this->id;
282
    }
283
284
    /**
285
     * Set the identifier of this message.
286
     *
287
     * @param string $id The new identifier of this message
288
     */
289
    public function setId($id)
290
    {
291
        assert('is_string($id)');
292
293
        $this->id = $id;
294
    }
295
296
    /**
297
     * Retrieve the issue timestamp of this message.
298
     *
299
     * @return int The issue timestamp of this message, as an UNIX timestamp
300
     */
301
    public function getIssueInstant()
302
    {
303
        return $this->issueInstant;
304
    }
305
306
    /**
307
     * Set the issue timestamp of this message.
308
     *
309
     * @param int $issueInstant The new issue timestamp of this message, as an UNIX timestamp
310
     */
311
    public function setIssueInstant($issueInstant)
312
    {
313
        assert('is_int($issueInstant)');
314
315
        $this->issueInstant = $issueInstant;
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()
324
    {
325
        return $this->destination;
326
    }
327
328
    /**
329
     * Set the destination of this message.
330
     *
331
     * @param string|null $destination The new destination of this message
332
     */
333
    public function setDestination($destination)
334
    {
335
        assert('is_string($destination) || is_null($destination)');
336
337
        $this->destination = $destination;
338
    }
339
340
    /**
341
     * Set the given consent for this message.
342
     *
343
     * Most likely (though not required) a value of rn:oasis:names:tc:SAML:2.0:consent.
344
     *
345
     * @see \SAML2\Constants
346
     *
347
     * @param string $consent
348
     */
349
    public function setConsent($consent)
350
    {
351
        assert('is_string($consent)');
352
353
        $this->consent = $consent;
354
    }
355
356
    /**
357
     * Set the given consent for this message.
358
     *
359
     * Most likely (though not required) a value of rn:oasis:names:tc:SAML:2.0:consent.
360
     *
361
     * @see \SAML2\Constants
362
     *
363
     * @return string Consent
364
     */
365
    public function getConsent()
366
    {
367
        return $this->consent;
368
    }
369
370
    /**
371
     * Retrieve the issuer if this message.
372
     *
373
     * @return string|\SAML2\XML\saml\Issuer|null The issuer of this message, or NULL if no issuer is given
374
     */
375
    public function getIssuer()
376
    {
377
        if (is_string($this->issuer) || $this->issuer instanceof XML\saml\Issuer) {
378
            return $this->issuer;
379
        }
380
381
        return null;
382
    }
383
384
    /**
385
     * Set the issuer of this message.
386
     *
387
     * @param string|\SAML2\XML\saml\Issuer|null $issuer The new issuer of this message
388
     */
389
    public function setIssuer($issuer)
390
    {
391
        assert('is_string($issuer) || $issuer instanceof \SAML2\XML\saml\Issuer || is_null($issuer)');
392
393
        $this->issuer = $issuer;
394
    }
395
396
    /**
397
     * Query whether or not the message contained a signature at the root level when the object was constructed.
398
     *
399
     * @return bool
400
     */
401
    public function isMessageConstructedWithSignature()
402
    {
403
        return $this->messageContainedSignatureUponConstruction;
404
    }
405
406
    /**
407
     * Retrieve the RelayState associated with this message.
408
     *
409
     * @return string|null The RelayState, or NULL if no RelayState is given
410
     */
411
    public function getRelayState()
412
    {
413
        return $this->relayState;
414
    }
415
416
    /**
417
     * Set the RelayState associated with this message.
418
     *
419
     * @param string|null $relayState The new RelayState
420
     */
421
    public function setRelayState($relayState)
422
    {
423
        assert('is_string($relayState) || is_null($relayState)');
424
425
        $this->relayState = $relayState;
426
    }
427
428
    /**
429
     * Convert this message to an unsigned XML document.
430
     *
431
     * This method does not sign the resulting XML document.
432
     *
433
     * @return \DOMElement The root element of the DOM tree
434
     */
435
    public function toUnsignedXML()
436
    {
437
        $this->document = DOMDocumentFactory::create();
438
439
        $root = $this->document->createElementNS(Constants::NS_SAMLP, 'samlp:'.$this->tagName);
440
        $this->document->appendChild($root);
441
442
        /* Ugly hack to add another namespace declaration to the root element. */
443
        $root->setAttributeNS(Constants::NS_SAML, 'saml:tmp', 'tmp');
444
        $root->removeAttributeNS(Constants::NS_SAML, 'tmp');
445
446
        $root->setAttribute('ID', $this->id);
447
        $root->setAttribute('Version', '2.0');
448
        $root->setAttribute('IssueInstant', gmdate('Y-m-d\TH:i:s\Z', $this->issueInstant));
449
450
        if ($this->destination !== null) {
451
            $root->setAttribute('Destination', $this->destination);
452
        }
453
        if ($this->consent !== null && $this->consent !== Constants::CONSENT_UNSPECIFIED) {
454
            $root->setAttribute('Consent', $this->consent);
455
        }
456
457
        if ($this->issuer !== null) {
458 View Code Duplication
            if (is_string($this->issuer)) {
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...
459
                Utils::addString($root, \SAML2_Const::NS_SAML, 'saml:Issuer', $this->issuer);
460
            } elseif ($this->issuer instanceof XML\saml\Issuer) {
461
                $this->issuer->toXML($root);
462
            }
463
        }
464
465
        if (!empty($this->extensions)) {
466
            Extensions::addList($root, $this->extensions);
467
        }
468
469
        return $root;
470
    }
471
472
473
    /**
474
     * Convert this message to a signed XML document.
475
     *
476
     * This method sign the resulting XML document if the private key for
477
     * the signature is set.
478
     *
479
     * @return \DOMElement The root element of the DOM tree
480
     */
481
    public function toSignedXML()
482
    {
483
        $root = $this->toUnsignedXML();
484
485
        if ($this->signatureKey === null) {
486
            /* We don't have a key to sign it with. */
487
488
            return $root;
489
        }
490
491
        /* Find the position we should insert the signature node at. */
492
        if ($this->issuer !== null) {
493
            /*
494
             * We have an issuer node. The signature node should come
495
             * after the issuer node.
496
             */
497
            $issuerNode = $root->firstChild;
498
            $insertBefore = $issuerNode->nextSibling;
499
        } else {
500
            /* No issuer node - the signature element should be the first element. */
501
            $insertBefore = $root->firstChild;
502
        }
503
504
        Utils::insertSignature($this->signatureKey, $this->certificates, $root, $insertBefore);
505
506
        return $root;
507
    }
508
509
    /**
510
     * Retrieve the private key we should use to sign the message.
511
     *
512
     * @return XMLSecurityKey|null The key, or NULL if no key is specified
513
     */
514
    public function getSignatureKey()
515
    {
516
        return $this->signatureKey;
517
    }
518
519
    /**
520
     * Set the private key we should use to sign the message.
521
     *
522
     * If the key is null, the message will be sent unsigned.
523
     *
524
     * @param XMLSecurityKey|null $signatureKey
525
     */
526
    public function setSignatureKey(XMLsecurityKey $signatureKey = null)
527
    {
528
        $this->signatureKey = $signatureKey;
529
    }
530
531
    /**
532
     * Set the certificates that should be included in the message.
533
     *
534
     * The certificates should be strings with the PEM encoded data.
535
     *
536
     * @param array $certificates An array of certificates
537
     */
538
    public function setCertificates(array $certificates)
539
    {
540
        $this->certificates = $certificates;
541
    }
542
543
    /**
544
     * Retrieve the certificates that are included in the message.
545
     *
546
     * @return array An array of certificates
547
     */
548
    public function getCertificates()
549
    {
550
        return $this->certificates;
551
    }
552
553
    /**
554
     * Convert an XML element into a message.
555
     *
556
     * @param \DOMElement $xml The root XML element
557
     *
558
     * @return \SAML2\Message The message
559
     *
560
     * @throws \Exception
561
     */
562
    public static function fromXML(\DOMElement $xml)
563
    {
564 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...
565
            throw new \Exception('Unknown namespace of SAML message: '.var_export($xml->namespaceURI, true));
566
        }
567
568
        switch ($xml->localName) {
569
            case 'AttributeQuery':
570
                return new AttributeQuery($xml);
571
            case 'AuthnRequest':
572
                return new AuthnRequest($xml);
573
            case 'LogoutResponse':
574
                return new LogoutResponse($xml);
575
            case 'LogoutRequest':
576
                return new LogoutRequest($xml);
577
            case 'Response':
578
                return new Response($xml);
579
            case 'ArtifactResponse':
580
                return new ArtifactResponse($xml);
581
            case 'ArtifactResolve':
582
                return new ArtifactResolve($xml);
583
            default:
584
                throw new \Exception('Unknown SAML message: '.var_export($xml->localName, true));
585
        }
586
    }
587
588
    /**
589
     * Retrieve the Extensions.
590
     *
591
     * @return \SAML2\XML\samlp\Extensions
592
     */
593
    public function getExtensions()
594
    {
595
        return $this->extensions;
596
    }
597
598
    /**
599
     * Set the Extensions.
600
     *
601
     * @param array|null $extensions The Extensions
602
     */
603
    public function setExtensions($extensions)
604
    {
605
        assert('is_array($extensions) || is_null($extensions)');
606
607
        $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...
608
    }
609
610
    /**
611
     * @return null|string
612
     */
613
    public function getSignatureMethod()
614
    {
615
        return $this->signatureMethod;
616
    }
617
}
618