Completed
Push — master ( 751340...d701a1 )
by Thijs
11:10 queued 06:53
created

SAML2_Utils::parseNameId()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 12
rs 9.4286
cc 3
eloc 6
nc 3
nop 1
1
<?php
2
3
/**
4
 * Helper functions for the SAML2 library.
5
 *
6
 * @package SimpleSAMLphp
7
 */
8
class SAML2_Utils
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
9
{
10
    /**
11
     * Check the Signature in a XML element.
12
     *
13
     * This function expects the XML element to contain a Signature-element
14
     * which contains a reference to the XML-element. This is common for both
15
     * messages and assertions.
16
     *
17
     * Note that this function only validates the element itself. It does not
18
     * check this against any local keys.
19
     *
20
     * If no Signature-element is located, this function will return FALSE. All
21
     * other validation errors result in an exception. On successful validation
22
     * an array will be returned. This array contains the information required to
23
     * check the signature against a public key.
24
     *
25
     * @param  DOMElement  $root The element which should be validated.
26
     * @return array|bool An array with information about the Signature-element.
27
     * @throws Exception
28
     */
29
    public static function validateElement(DOMElement $root)
30
    {
31
        /* Create an XML security object. */
32
        $objXMLSecDSig = new XMLSecurityDSig();
33
34
        /* Both SAML messages and SAML assertions use the 'ID' attribute. */
35
        $objXMLSecDSig->idKeys[] = 'ID';
36
37
        /* Locate the XMLDSig Signature element to be used. */
38
        $signatureElement = self::xpQuery($root, './ds:Signature');
39
        if (count($signatureElement) === 0) {
40
            /* We don't have a signature element ot validate. */
41
42
            return FALSE;
43
        } elseif (count($signatureElement) > 1) {
44
            throw new Exception('XMLSec: more than one signature element in root.');
45
        }
46
        $signatureElement = $signatureElement[0];
47
        $objXMLSecDSig->sigNode = $signatureElement;
48
49
        /* Canonicalize the XMLDSig SignedInfo element in the message. */
50
        $objXMLSecDSig->canonicalizeSignedInfo();
51
52
        /* Validate referenced xml nodes. */
53
        if (!$objXMLSecDSig->validateReference()) {
54
            throw new Exception('XMLsec: digest validation failed');
55
        }
56
57
        /* Check that $root is one of the signed nodes. */
58
        $rootSigned = FALSE;
59
        /** @var DOMNode $signedNode */
60
        foreach ($objXMLSecDSig->getValidatedNodes() as $signedNode) {
0 ignored issues
show
Bug introduced by
The expression $objXMLSecDSig->getValidatedNodes() of type array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
61
            if ($signedNode->isSameNode($root)) {
62
                $rootSigned = TRUE;
63
                break;
64
            } elseif ($root->parentNode instanceof DOMDocument && $signedNode->isSameNode($root->ownerDocument)) {
65
                /* $root is the root element of a signed document. */
66
                $rootSigned = TRUE;
67
                break;
68
            }
69
        }
70
        if (!$rootSigned) {
71
            throw new Exception('XMLSec: The root element is not signed.');
72
        }
73
74
        /* Now we extract all available X509 certificates in the signature element. */
75
        $certificates = array();
76
        foreach (self::xpQuery($signatureElement, './ds:KeyInfo/ds:X509Data/ds:X509Certificate') as $certNode) {
77
            $certData = trim($certNode->textContent);
78
            $certData = str_replace(array("\r", "\n", "\t", ' '), '', $certData);
79
            $certificates[] = $certData;
80
        }
81
82
        $ret = array(
83
            'Signature' => $objXMLSecDSig,
84
            'Certificates' => $certificates,
85
            );
86
87
        return $ret;
88
    }
89
90
91
    /**
92
     * Helper function to convert a XMLSecurityKey to the correct algorithm.
93
     *
94
     * @param  XMLSecurityKey $key       The key.
95
     * @param  string         $algorithm The desired algorithm.
96
     * @param  string         $type      Public or private key, defaults to public.
97
     * @return XMLSecurityKey The new key.
98
     * @throws Exception
99
     */
100
    public static function castKey(XMLSecurityKey $key, $algorithm, $type = 'public')
101
    {
102
        assert('is_string($algorithm)');
103
        assert('$type === "public" || $type === "private"');
104
105
        // do nothing if algorithm is already the type of the key
106
        if ($key->type === $algorithm) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of $key->type (integer) and $algorithm (string) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
107
            return $key;
108
        }
109
110
        $keyInfo = openssl_pkey_get_details($key->key);
111
        if ($keyInfo === FALSE) {
112
            throw new Exception('Unable to get key details from XMLSecurityKey.');
113
        }
114
        if (!isset($keyInfo['key'])) {
115
            throw new Exception('Missing key in public key details.');
116
        }
117
118
        $newKey = new XMLSecurityKey($algorithm, array('type'=>$type));
119
        $newKey->loadKey($keyInfo['key']);
120
121
        return $newKey;
122
    }
123
124
125
    /**
126
     * Check a signature against a key.
127
     *
128
     * An exception is thrown if we are unable to validate the signature.
129
     *
130
     * @param array          $info The information returned by the validateElement()-function.
131
     * @param XMLSecurityKey $key  The publickey that should validate the Signature object.
132
     * @throws Exception
133
     */
134
    public static function validateSignature(array $info, XMLSecurityKey $key)
135
    {
136
        assert('array_key_exists("Signature", $info)');
137
138
        /** @var XMLSecurityDSig $objXMLSecDSig */
139
        $objXMLSecDSig = $info['Signature'];
140
141
        $sigMethod = self::xpQuery($objXMLSecDSig->sigNode, './ds:SignedInfo/ds:SignatureMethod');
142
        if (empty($sigMethod)) {
143
            throw new Exception('Missing SignatureMethod element.');
144
        }
145
        $sigMethod = $sigMethod[0];
146
        if (!$sigMethod->hasAttribute('Algorithm')) {
147
            throw new Exception('Missing Algorithm-attribute on SignatureMethod element.');
148
        }
149
        $algo = $sigMethod->getAttribute('Algorithm');
150
151
        if ($key->type === XMLSecurityKey::RSA_SHA1 && $algo !== $key->type) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of $algo (string) and $key->type (integer) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
152
            $key = self::castKey($key, $algo);
153
        }
154
155
        /* Check the signature. */
156
        if (! $objXMLSecDSig->verify($key)) {
157
            throw new Exception("Unable to validate Signature");
158
        }
159
    }
160
161
162
    /**
163
     * Do an XPath query on an XML node.
164
     *
165
     * @param  DOMNode $node  The XML node.
166
     * @param  string  $query The query.
167
     * @return DOMElement[]    Array with matching DOM nodes.
168
     */
169
    public static function xpQuery(DOMNode $node, $query)
170
    {
171
        assert('is_string($query)');
172
        static $xpCache = NULL;
173
174
        if ($node instanceof DOMDocument) {
175
            $doc = $node;
176
        } else {
177
            $doc = $node->ownerDocument;
178
        }
179
180
        if ($xpCache === NULL || !$xpCache->document->isSameNode($doc)) {
181
            $xpCache = new DOMXPath($doc);
182
            $xpCache->registerNamespace('soap-env', SAML2_Const::NS_SOAP);
183
            $xpCache->registerNamespace('saml_protocol', SAML2_Const::NS_SAMLP);
184
            $xpCache->registerNamespace('saml_assertion', SAML2_Const::NS_SAML);
185
            $xpCache->registerNamespace('saml_metadata', SAML2_Const::NS_MD);
186
            $xpCache->registerNamespace('ds', XMLSecurityDSig::XMLDSIGNS);
187
            $xpCache->registerNamespace('xenc', XMLSecEnc::XMLENCNS);
188
        }
189
190
        $results = $xpCache->query($query, $node);
191
        $ret = array();
192
        for ($i = 0; $i < $results->length; $i++) {
193
            $ret[$i] = $results->item($i);
194
        }
195
196
        return $ret;
197
    }
198
199
200
    /**
201
     * Make an exact copy the specific DOMElement.
202
     *
203
     * @param  DOMElement      $element The element we should copy.
204
     * @param  DOMElement|NULL $parent  The target parent element.
205
     * @return DOMElement      The copied element.
206
     */
207
    public static function copyElement(DOMElement $element, DOMElement $parent = NULL)
208
    {
209
        if ($parent === NULL) {
210
            $document = SAML2_DOMDocumentFactory::create();
211
        } else {
212
            $document = $parent->ownerDocument;
213
        }
214
215
        $namespaces = array();
216
        for ($e = $element; $e !== NULL; $e = $e->parentNode) {
217
            foreach (SAML2_Utils::xpQuery($e, './namespace::*') as $ns) {
218
                $prefix = $ns->localName;
219
                if ($prefix === 'xml' || $prefix === 'xmlns') {
220
                    continue;
221
                }
222
                $uri = $ns->nodeValue;
223
                if (!isset($namespaces[$prefix])) {
224
                    $namespaces[$prefix] = $uri;
225
                }
226
            }
227
        }
228
229
        /** @var DOMElement $newElement */
230
        $newElement = $document->importNode($element, TRUE);
231
        if ($parent !== NULL) {
232
            /* We need to append the child to the parent before we add the namespaces. */
233
            $parent->appendChild($newElement);
234
        }
235
236
        foreach ($namespaces as $prefix => $uri) {
237
            $newElement->setAttributeNS($uri, $prefix . ':__ns_workaround__', 'tmp');
238
            $newElement->removeAttributeNS($uri, '__ns_workaround__');
239
        }
240
241
        return $newElement;
242
    }
243
244
245
    /**
246
     * Parse a boolean attribute.
247
     *
248
     * @param  DOMElement $node          The element we should fetch the attribute from.
249
     * @param  string     $attributeName The name of the attribute.
250
     * @param  mixed      $default       The value that should be returned if the attribute doesn't exist.
251
     * @return bool|mixed The value of the attribute, or $default if the attribute doesn't exist.
252
     * @throws Exception
253
     */
254
    public static function parseBoolean(DOMElement $node, $attributeName, $default = NULL)
255
    {
256
        assert('is_string($attributeName)');
257
258
        if (!$node->hasAttribute($attributeName)) {
259
            return $default;
260
        }
261
        $value = $node->getAttribute($attributeName);
262
        switch (strtolower($value)) {
263
            case '0':
264
            case 'false':
265
                return FALSE;
266
            case '1':
267
            case 'true':
268
                return TRUE;
269
            default:
270
                throw new Exception('Invalid value of boolean attribute ' . var_export($attributeName, TRUE) . ': ' . var_export($value, TRUE));
271
        }
272
    }
273
274
275
    /**
276
     * Create a NameID element.
277
     *
278
     * The NameId array can have the following elements: 'Value', 'Format',
279
     *   'NameQualifier, 'SPNameQualifier'
280
     *
281
     * Only the 'Value'-element is required.
282
     *
283
     * @param DOMElement $node   The DOM node we should append the NameId to.
284
     * @param array      $nameId The name identifier.
285
     */
286
    public static function addNameId(DOMElement $node, array $nameId)
287
    {
288
        assert('array_key_exists("Value", $nameId)');
289
290
        $xml = SAML2_Utils::addString($node, SAML2_Const::NS_SAML, 'saml:NameID', $nameId['Value']);
291
292
        if (array_key_exists('NameQualifier', $nameId) && $nameId['NameQualifier'] !== NULL) {
293
            $xml->setAttribute('NameQualifier', $nameId['NameQualifier']);
294
        }
295 View Code Duplication
        if (array_key_exists('SPNameQualifier', $nameId) && $nameId['SPNameQualifier'] !== NULL) {
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...
296
            $xml->setAttribute('SPNameQualifier', $nameId['SPNameQualifier']);
297
        }
298 View Code Duplication
        if (array_key_exists('Format', $nameId) && $nameId['Format'] !== NULL) {
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...
299
            $xml->setAttribute('Format', $nameId['Format']);
300
        }
301
    }
302
303
    /**
304
     * Parse a NameID element.
305
     *
306
     * @param  DOMElement $xml The DOM element we should parse.
307
     * @return array      The parsed name identifier.
308
     */
309
    public static function parseNameId(DOMElement $xml)
310
    {
311
        $ret = array('Value' => trim($xml->textContent));
312
313
        foreach (array('NameQualifier', 'SPNameQualifier', 'Format') as $attr) {
314
            if ($xml->hasAttribute($attr)) {
315
                $ret[$attr] = $xml->getAttribute($attr);
316
            }
317
        }
318
319
        return $ret;
320
    }
321
322
    /**
323
     * Insert a Signature-node.
324
     *
325
     * @param XMLSecurityKey $key           The key we should use to sign the message.
326
     * @param array          $certificates  The certificates we should add to the signature node.
327
     * @param DOMElement     $root          The XML node we should sign.
328
     * @param DOMNode        $insertBefore  The XML element we should insert the signature element before.
329
     */
330
    public static function insertSignature(
331
        XMLSecurityKey $key,
332
        array $certificates,
333
        DOMElement $root,
334
        DOMNode $insertBefore = NULL
335
    ) {
336
        $objXMLSecDSig = new XMLSecurityDSig();
337
        $objXMLSecDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N);
338
339
        switch ($key->type) {
340
            case XMLSecurityKey::RSA_SHA256:
341
                $type = XMLSecurityDSig::SHA256;
342
                break;
343
            case XMLSecurityKey::RSA_SHA384:
344
                $type = XMLSecurityDSig::SHA384;
345
                break;
346
            case XMLSecurityKey::RSA_SHA512:
347
                $type = XMLSecurityDSig::SHA512;
348
                break;
349
            default:
350
                $type = XMLSecurityDSig::SHA1;
351
        }
352
353
        $objXMLSecDSig->addReferenceList(
354
            array($root),
355
            $type,
356
            array('http://www.w3.org/2000/09/xmldsig#enveloped-signature', XMLSecurityDSig::EXC_C14N),
357
            array('id_name' => 'ID', 'overwrite' => FALSE)
358
        );
359
360
        $objXMLSecDSig->sign($key);
361
362
        foreach ($certificates as $certificate) {
363
            $objXMLSecDSig->add509Cert($certificate, TRUE);
364
        }
365
366
        $objXMLSecDSig->insertSignature($root, $insertBefore);
367
368
    }
369
370
    /**
371
     * Decrypt an encrypted element.
372
     *
373
     * This is an internal helper function.
374
     *
375
     * @param  DOMElement     $encryptedData The encrypted data.
376
     * @param  XMLSecurityKey $inputKey      The decryption key.
377
     * @param  array          &$blacklist    Blacklisted decryption algorithms.
378
     * @return DOMElement     The decrypted element.
379
     * @throws Exception
380
     */
381
    private static function doDecryptElement(DOMElement $encryptedData, XMLSecurityKey $inputKey, array &$blacklist)
382
    {
383
        $enc = new XMLSecEnc();
384
385
        $enc->setNode($encryptedData);
386
        $enc->type = $encryptedData->getAttribute("Type");
387
388
        $symmetricKey = $enc->locateKey($encryptedData);
389
        if (!$symmetricKey) {
390
            throw new Exception('Could not locate key algorithm in encrypted data.');
391
        }
392
393
        $symmetricKeyInfo = $enc->locateKeyInfo($symmetricKey);
394
        if (!$symmetricKeyInfo) {
395
            throw new Exception('Could not locate <dsig:KeyInfo> for the encrypted key.');
396
        }
397
398
        $inputKeyAlgo = $inputKey->getAlgorith();
399
        if ($symmetricKeyInfo->isEncrypted) {
400
            $symKeyInfoAlgo = $symmetricKeyInfo->getAlgorith();
401
402 View Code Duplication
            if (in_array($symKeyInfoAlgo, $blacklist, TRUE)) {
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...
403
                throw new Exception('Algorithm disabled: ' . var_export($symKeyInfoAlgo, TRUE));
404
            }
405
406
            if ($symKeyInfoAlgo === XMLSecurityKey::RSA_OAEP_MGF1P && $inputKeyAlgo === XMLSecurityKey::RSA_1_5) {
407
                /*
408
                 * The RSA key formats are equal, so loading an RSA_1_5 key
409
                 * into an RSA_OAEP_MGF1P key can be done without problems.
410
                 * We therefore pretend that the input key is an
411
                 * RSA_OAEP_MGF1P key.
412
                 */
413
                $inputKeyAlgo = XMLSecurityKey::RSA_OAEP_MGF1P;
414
            }
415
416
            /* Make sure that the input key format is the same as the one used to encrypt the key. */
417 View Code Duplication
            if ($inputKeyAlgo !== $symKeyInfoAlgo) {
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...
418
                throw new Exception(
419
                    'Algorithm mismatch between input key and key used to encrypt ' .
420
                    ' the symmetric key for the message. Key was: ' .
421
                    var_export($inputKeyAlgo, TRUE) . '; message was: ' .
422
                    var_export($symKeyInfoAlgo, TRUE)
423
                );
424
            }
425
426
            /** @var XMLSecEnc $encKey */
427
            $encKey = $symmetricKeyInfo->encryptedCtx;
428
            $symmetricKeyInfo->key = $inputKey->key;
429
430
            $keySize = $symmetricKey->getSymmetricKeySize();
431
            if ($keySize === NULL) {
432
                /* To protect against "key oracle" attacks, we need to be able to create a
433
                 * symmetric key, and for that we need to know the key size.
434
                 */
435
                throw new Exception('Unknown key size for encryption algorithm: ' . var_export($symmetricKey->type, TRUE));
436
            }
437
438
            try {
439
                $key = $encKey->decryptKey($symmetricKeyInfo);
440
                if (strlen($key) != $keySize) {
441
                    throw new Exception(
442
                        'Unexpected key size (' . strlen($key) * 8 . 'bits) for encryption algorithm: ' .
443
                        var_export($symmetricKey->type, TRUE)
444
                    );
445
                }
446
            } catch (Exception $e) {
447
                /* We failed to decrypt this key. Log it, and substitute a "random" key. */
448
                SAML2_Utils::getContainer()->getLogger()->error('Failed to decrypt symmetric key: ' . $e->getMessage());
449
                /* Create a replacement key, so that it looks like we fail in the same way as if the key was correctly padded. */
450
451
                /* We base the symmetric key on the encrypted key and private key, so that we always behave the
452
                 * same way for a given input key.
453
                 */
454
                $encryptedKey = $encKey->getCipherValue();
455
                $pkey = openssl_pkey_get_details($symmetricKeyInfo->key);
456
                $pkey = sha1(serialize($pkey), TRUE);
457
                $key = sha1($encryptedKey . $pkey, TRUE);
458
459
                /* Make sure that the key has the correct length. */
460
                if (strlen($key) > $keySize) {
461
                    $key = substr($key, 0, $keySize);
462
                } elseif (strlen($key) < $keySize) {
463
                    $key = str_pad($key, $keySize);
464
                }
465
            }
466
            $symmetricKey->loadkey($key);
467
468
        } else {
469
            $symKeyAlgo = $symmetricKey->getAlgorith();
470
            /* Make sure that the input key has the correct format. */
471 View Code Duplication
            if ($inputKeyAlgo !== $symKeyAlgo) {
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...
472
                throw new Exception(
473
                    'Algorithm mismatch between input key and key in message. ' .
474
                    'Key was: ' . var_export($inputKeyAlgo, TRUE) . '; message was: ' .
475
                    var_export($symKeyAlgo, TRUE)
476
                );
477
            }
478
            $symmetricKey = $inputKey;
479
        }
480
481
        $algorithm = $symmetricKey->getAlgorith();
482 View Code Duplication
        if (in_array($algorithm, $blacklist, TRUE)) {
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...
483
            throw new Exception('Algorithm disabled: ' . var_export($algorithm, TRUE));
484
        }
485
486
        /** @var string $decrypted */
487
        $decrypted = $enc->decryptNode($symmetricKey, FALSE);
488
489
        /*
490
         * This is a workaround for the case where only a subset of the XML
491
         * tree was serialized for encryption. In that case, we may miss the
492
         * namespaces needed to parse the XML.
493
         */
494
        $xml = '<root xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" '.
495
                     'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' .
496
            $decrypted .
497
            '</root>';
498
499
        try {
500
            $newDoc = SAML2_DOMDocumentFactory::fromString($xml);
501
        } catch (SAML2_Exception_RuntimeException $e) {
502
            throw new Exception('Failed to parse decrypted XML. Maybe the wrong sharedkey was used?', 0, $e);
503
        }
504
505
        $decryptedElement = $newDoc->firstChild->firstChild;
506
        if ($decryptedElement === NULL) {
507
            throw new Exception('Missing encrypted element.');
508
        }
509
510
        if (!($decryptedElement instanceof DOMElement)) {
511
            throw new Exception('Decrypted element was not actually a DOMElement.');
512
        }
513
514
        return $decryptedElement;
515
    }
516
517
    /**
518
     * Decrypt an encrypted element.
519
     *
520
     * @param  DOMElement     $encryptedData The encrypted data.
521
     * @param  XMLSecurityKey $inputKey      The decryption key.
522
     * @param  array          $blacklist     Blacklisted decryption algorithms.
523
     * @return DOMElement     The decrypted element.
524
     * @throws Exception
525
     */
526
    public static function decryptElement(DOMElement $encryptedData, XMLSecurityKey $inputKey, array $blacklist = array())
527
    {
528
        try {
529
            return self::doDecryptElement($encryptedData, $inputKey, $blacklist);
530
        } catch (Exception $e) {
531
            /*
532
             * Something went wrong during decryption, but for security
533
             * reasons we cannot tell the user what failed.
534
             */
535
            SAML2_Utils::getContainer()->getLogger()->error('Decryption failed: ' . $e->getMessage());
536
            throw new Exception('Failed to decrypt XML element.', 0, $e);
537
        }
538
    }
539
540
    /**
541
     * Extract localized strings from a set of nodes.
542
     *
543
     * @param  DOMElement $parent       The element that contains the localized strings.
544
     * @param  string     $namespaceURI The namespace URI the localized strings should have.
545
     * @param  string     $localName    The localName of the localized strings.
546
     * @return array      Localized strings.
547
     */
548
    public static function extractLocalizedStrings(DOMElement $parent, $namespaceURI, $localName)
549
    {
550
        assert('is_string($namespaceURI)');
551
        assert('is_string($localName)');
552
553
        $ret = array();
554
        for ($node = $parent->firstChild; $node !== NULL; $node = $node->nextSibling) {
555
            if ($node->namespaceURI !== $namespaceURI || $node->localName !== $localName) {
556
                continue;
557
            }
558
559
            if ($node->hasAttribute('xml:lang')) {
0 ignored issues
show
Bug introduced by
The method hasAttribute() does not exist on DOMNode. Did you maybe mean hasAttributes()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
560
                $language = $node->getAttribute('xml:lang');
561
            } else {
562
                $language = 'en';
563
            }
564
            $ret[$language] = trim($node->textContent);
565
        }
566
567
        return $ret;
568
    }
569
570
    /**
571
     * Extract strings from a set of nodes.
572
     *
573
     * @param  DOMElement $parent       The element that contains the localized strings.
574
     * @param  string     $namespaceURI The namespace URI the string elements should have.
575
     * @param  string     $localName    The localName of the string elements.
576
     * @return array      The string values of the various nodes.
577
     */
578
    public static function extractStrings(DOMElement $parent, $namespaceURI, $localName)
579
    {
580
        assert('is_string($namespaceURI)');
581
        assert('is_string($localName)');
582
583
        $ret = array();
584
        for ($node = $parent->firstChild; $node !== NULL; $node = $node->nextSibling) {
585
            if ($node->namespaceURI !== $namespaceURI || $node->localName !== $localName) {
586
                continue;
587
            }
588
            $ret[] = trim($node->textContent);
589
        }
590
591
        return $ret;
592
    }
593
594
    /**
595
     * Append string element.
596
     *
597
     * @param  DOMElement $parent    The parent element we should append the new nodes to.
598
     * @param  string     $namespace The namespace of the created element.
599
     * @param  string     $name      The name of the created element.
600
     * @param  string     $value     The value of the element.
601
     * @return DOMElement The generated element.
602
     */
603
    public static function addString(DOMElement $parent, $namespace, $name, $value)
604
    {
605
        assert('is_string($namespace)');
606
        assert('is_string($name)');
607
        assert('is_string($value)');
608
609
        $doc = $parent->ownerDocument;
610
611
        $n = $doc->createElementNS($namespace, $name);
612
        $n->appendChild($doc->createTextNode($value));
613
        $parent->appendChild($n);
614
615
        return $n;
616
    }
617
618
    /**
619
     * Append string elements.
620
     *
621
     * @param DOMElement $parent    The parent element we should append the new nodes to.
622
     * @param string     $namespace The namespace of the created elements
623
     * @param string     $name      The name of the created elements
624
     * @param bool       $localized Whether the strings are localized, and should include the xml:lang attribute.
625
     * @param array      $values    The values we should create the elements from.
626
     */
627
    public static function addStrings(DOMElement $parent, $namespace, $name, $localized, array $values)
628
    {
629
        assert('is_string($namespace)');
630
        assert('is_string($name)');
631
        assert('is_bool($localized)');
632
633
        $doc = $parent->ownerDocument;
634
635
        foreach ($values as $index => $value) {
636
            $n = $doc->createElementNS($namespace, $name);
637
            $n->appendChild($doc->createTextNode($value));
638
            if ($localized) {
639
                $n->setAttribute('xml:lang', $index);
640
            }
641
            $parent->appendChild($n);
642
        }
643
    }
644
645
    /**
646
     * Create a KeyDescriptor with the given certificate.
647
     *
648
     * @param  string                     $x509Data The certificate, as a base64-encoded DER data.
649
     * @return SAML2_XML_md_KeyDescriptor The keydescriptor.
650
     */
651
    public static function createKeyDescriptor($x509Data)
652
    {
653
        assert('is_string($x509Data)');
654
655
        $x509Certificate = new SAML2_XML_ds_X509Certificate();
656
        $x509Certificate->certificate = $x509Data;
657
658
        $x509Data = new SAML2_XML_ds_X509Data();
659
        $x509Data->data[] = $x509Certificate;
660
661
        $keyInfo = new SAML2_XML_ds_KeyInfo();
662
        $keyInfo->info[] = $x509Data;
663
664
        $keyDescriptor = new SAML2_XML_md_KeyDescriptor();
665
        $keyDescriptor->KeyInfo = $keyInfo;
666
667
        return $keyDescriptor;
668
    }
669
670
    /**
671
     * This function converts a SAML2 timestamp on the form
672
     * yyyy-mm-ddThh:mm:ss(\.s+)?Z to a UNIX timestamp. The sub-second
673
     * part is ignored.
674
     *
675
     * Andreas comments:
676
     *  I got this timestamp from Shibboleth 1.3 IdP: 2008-01-17T11:28:03.577Z
677
     *  Therefore I added to possibility to have microseconds to the format.
678
     * Added: (\.\\d{1,3})? to the regex.
679
     *
680
     * Note that we always require a 'Z' timezone for the dateTime to be valid.
681
     * This is not in the SAML spec but that's considered to be a bug in the
682
     * spec. See https://github.com/simplesamlphp/saml2/pull/36 for some
683
     * background.
684
     *
685
     * @param string $time The time we should convert.
686
     * @return int Converted to a unix timestamp.
687
     * @throws Exception
688
     */
689
    public static function xsDateTimeToTimestamp($time)
690
    {
691
        $matches = array();
692
693
        // We use a very strict regex to parse the timestamp.
694
        $regex = '/^(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d)T(\\d\\d):(\\d\\d):(\\d\\d)(?:\\.\\d+)?Z$/D';
695
        if (preg_match($regex, $time, $matches) == 0) {
696
            throw new Exception(
697
                'Invalid SAML2 timestamp passed to xsDateTimeToTimestamp: ' . $time
698
            );
699
        }
700
701
        // Extract the different components of the time from the  matches in the regex.
702
        // intval will ignore leading zeroes in the string.
703
        $year   = intval($matches[1]);
704
        $month  = intval($matches[2]);
705
        $day    = intval($matches[3]);
706
        $hour   = intval($matches[4]);
707
        $minute = intval($matches[5]);
708
        $second = intval($matches[6]);
709
710
        // We use gmmktime because the timestamp will always be given
711
        //in UTC.
712
        $ts = gmmktime($hour, $minute, $second, $month, $day, $year);
713
714
        return $ts;
715
    }
716
717
    /**
718
     * @return SAML2_Compat_Ssp_Container
719
     */
720
    public static function getContainer()
721
    {
722
        return SAML2_Compat_ContainerSingleton::getInstance();
723
    }
724
}
725