Passed
Branch gettersetter (72890a)
by Tim
05:11 queued 03:09
created

Utils::extractLocalizedStrings()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 12
nc 4
nop 3
dl 0
loc 20
rs 9.5555
c 0
b 0
f 0
1
<?php
2
3
namespace SAML2;
4
5
use RobRichards\XMLSecLibs\XMLSecEnc;
6
use RobRichards\XMLSecLibs\XMLSecurityDSig;
7
use RobRichards\XMLSecLibs\XMLSecurityKey;
8
use SAML2\Compat\ContainerSingleton;
9
use SAML2\Exception\RuntimeException;
10
use SAML2\XML\ds\KeyInfo;
11
use SAML2\XML\ds\X509Certificate;
12
use SAML2\XML\ds\X509Data;
13
use SAML2\XML\md\KeyDescriptor;
14
15
/**
16
 * Helper functions for the SAML2 library.
17
 *
18
 * @package SimpleSAMLphp
19
 */
20
class Utils
21
{
22
    /**
23
     * Check the Signature in a XML element.
24
     *
25
     * This function expects the XML element to contain a Signature-element
26
     * which contains a reference to the XML-element. This is common for both
27
     * messages and assertions.
28
     *
29
     * Note that this function only validates the element itself. It does not
30
     * check this against any local keys.
31
     *
32
     * If no Signature-element is located, this function will return false. All
33
     * other validation errors result in an exception. On successful validation
34
     * an array will be returned. This array contains the information required to
35
     * check the signature against a public key.
36
     *
37
     * @param  \DOMElement  $root The element which should be validated.
38
     * @return array|bool An array with information about the Signature-element.
39
     * @throws \Exception
40
     */
41
    public static function validateElement(\DOMElement $root)
42
    {
43
        /* Create an XML security object. */
44
        $objXMLSecDSig = new XMLSecurityDSig();
45
46
        /* Both SAML messages and SAML assertions use the 'ID' attribute. */
47
        $objXMLSecDSig->idKeys[] = 'ID';
48
49
        /* Locate the XMLDSig Signature element to be used. */
50
        $signatureElement = self::xpQuery($root, './ds:Signature');
51
        if (count($signatureElement) === 0) {
52
            /* We don't have a signature element ot validate. */
53
54
            return false;
55
        } elseif (count($signatureElement) > 1) {
56
            throw new \Exception('XMLSec: more than one signature element in root.');
57
        }
58
        $signatureElement = $signatureElement[0];
59
        $objXMLSecDSig->sigNode = $signatureElement;
60
61
        /* Canonicalize the XMLDSig SignedInfo element in the message. */
62
        $objXMLSecDSig->canonicalizeSignedInfo();
63
64
        /* Validate referenced xml nodes. */
65
        if (!$objXMLSecDSig->validateReference()) {
66
            throw new \Exception('XMLsec: digest validation failed');
67
        }
68
69
        /* Check that $root is one of the signed nodes. */
70
        $rootSigned = false;
71
        /** @var \DOMNode $signedNode */
72
        foreach ($objXMLSecDSig->getValidatedNodes() as $signedNode) {
73
            if ($signedNode->isSameNode($root)) {
74
                $rootSigned = true;
75
                break;
76
            } elseif ($root->parentNode instanceof \DOMDocument && $signedNode->isSameNode($root->ownerDocument)) {
77
                /* $root is the root element of a signed document. */
78
                $rootSigned = true;
79
                break;
80
            }
81
        }
82
        if (!$rootSigned) {
83
            throw new \Exception('XMLSec: The root element is not signed.');
84
        }
85
86
        /* Now we extract all available X509 certificates in the signature element. */
87
        $certificates = [];
88
        foreach (self::xpQuery($signatureElement, './ds:KeyInfo/ds:X509Data/ds:X509Certificate') as $certNode) {
89
            $certData = trim($certNode->textContent);
90
            $certData = str_replace(["\r", "\n", "\t", ' '], '', $certData);
91
            $certificates[] = $certData;
92
        }
93
94
        $ret = [
95
            'Signature' => $objXMLSecDSig,
96
            'Certificates' => $certificates,
97
        ];
98
99
        return $ret;
100
    }
101
102
103
    /**
104
     * Helper function to convert a XMLSecurityKey to the correct algorithm.
105
     *
106
     * @param  XMLSecurityKey $key       The key.
107
     * @param  string         $algorithm The desired algorithm.
108
     * @param  string         $type      Public or private key, defaults to public.
109
     * @return XMLSecurityKey The new key.
110
     * @throws \Exception
111
     */
112
    public static function castKey(XMLSecurityKey $key, $algorithm, $type = 'public')
113
    {
114
        assert(is_string($algorithm));
115
        assert($type === "public" || $type === "private");
116
117
        // do nothing if algorithm is already the type of the key
118
        if ($key->type === $algorithm) {
119
            return $key;
120
        }
121
122
        if (!in_array($algorithm, [
123
            XMLSecurityKey::RSA_1_5,
124
            XMLSecurityKey::RSA_SHA1,
125
            XMLSecurityKey::RSA_SHA256,
126
            XMLSecurityKey::RSA_SHA384,
127
            XMLSecurityKey::RSA_SHA512
128
        ], true)) {
129
            throw new \Exception('Unsupported signing algorithm.');
130
        }
131
132
        $keyInfo = openssl_pkey_get_details($key->key);
133
        if ($keyInfo === false) {
134
            throw new \Exception('Unable to get key details from XMLSecurityKey.');
135
        }
136
        if (!isset($keyInfo['key'])) {
137
            throw new \Exception('Missing key in public key details.');
138
        }
139
140
        $newKey = new XMLSecurityKey($algorithm, ['type' => $type]);
141
        $newKey->loadKey($keyInfo['key']);
142
143
        return $newKey;
144
    }
145
146
147
    /**
148
     * Check a signature against a key.
149
     *
150
     * An exception is thrown if we are unable to validate the signature.
151
     *
152
     * @param array          $info The information returned by the validateElement()-function.
153
     * @param XMLSecurityKey $key  The publickey that should validate the Signature object.
154
     * @throws \Exception
155
     */
156
    public static function validateSignature(array $info, XMLSecurityKey $key)
157
    {
158
        assert(array_key_exists("Signature", $info));
159
160
        /** @var XMLSecurityDSig $objXMLSecDSig */
161
        $objXMLSecDSig = $info['Signature'];
162
163
        $sigMethod = self::xpQuery($objXMLSecDSig->sigNode, './ds:SignedInfo/ds:SignatureMethod');
164
        if (empty($sigMethod)) {
165
            throw new \Exception('Missing SignatureMethod element.');
166
        }
167
        $sigMethod = $sigMethod[0];
168
        if (!$sigMethod->hasAttribute('Algorithm')) {
169
            throw new \Exception('Missing Algorithm-attribute on SignatureMethod element.');
170
        }
171
        $algo = $sigMethod->getAttribute('Algorithm');
172
173
        if ($key->type === XMLSecurityKey::RSA_SHA256 && $algo !== $key->type) {
174
            $key = self::castKey($key, $algo);
175
        }
176
177
        /* Check the signature. */
178
        if ($objXMLSecDSig->verify($key) !== 1) {
179
            throw new \Exception("Unable to validate Signature");
180
        }
181
    }
182
183
184
    /**
185
     * Do an XPath query on an XML node.
186
     *
187
     * @param  \DOMNode $node  The XML node.
188
     * @param  string  $query The query.
189
     * @return \DOMElement[]    Array with matching DOM nodes.
190
     */
191
    public static function xpQuery(\DOMNode $node, $query)
192
    {
193
        assert(is_string($query));
194
        static $xpCache = null;
195
196
        if ($node instanceof \DOMDocument) {
197
            $doc = $node;
198
        } else {
199
            $doc = $node->ownerDocument;
200
        }
201
202
        if ($xpCache === null || !$xpCache->document->isSameNode($doc)) {
203
            $xpCache = new \DOMXPath($doc);
204
            $xpCache->registerNamespace('soap-env', Constants::NS_SOAP);
205
            $xpCache->registerNamespace('saml_protocol', Constants::NS_SAMLP);
206
            $xpCache->registerNamespace('saml_assertion', Constants::NS_SAML);
207
            $xpCache->registerNamespace('saml_metadata', Constants::NS_MD);
208
            $xpCache->registerNamespace('ds', XMLSecurityDSig::XMLDSIGNS);
209
            $xpCache->registerNamespace('xenc', XMLSecEnc::XMLENCNS);
210
        }
211
212
        $results = $xpCache->query($query, $node);
213
        $ret = [];
214
        for ($i = 0; $i < $results->length; $i++) {
215
            $ret[$i] = $results->item($i);
216
        }
217
218
        return $ret;
219
    }
220
221
222
    /**
223
     * Make an exact copy the specific \DOMElement.
224
     *
225
     * @param  \DOMElement      $element The element we should copy.
226
     * @param  \DOMElement|null $parent  The target parent element.
227
     * @return \DOMElement      The copied element.
228
     */
229
    public static function copyElement(\DOMElement $element, \DOMElement $parent = null)
230
    {
231
        if ($parent === null) {
232
            $document = DOMDocumentFactory::create();
233
        } else {
234
            $document = $parent->ownerDocument;
235
        }
236
237
        $namespaces = [];
238
        for ($e = $element; $e !== null; $e = $e->parentNode) {
239
            foreach (Utils::xpQuery($e, './namespace::*') as $ns) {
240
                $prefix = $ns->localName;
241
                if ($prefix === 'xml' || $prefix === 'xmlns') {
242
                    continue;
243
                }
244
                $uri = $ns->nodeValue;
245
                if (!isset($namespaces[$prefix])) {
246
                    $namespaces[$prefix] = $uri;
247
                }
248
            }
249
        }
250
251
        /** @var \DOMElement $newElement */
252
        $newElement = $document->importNode($element, true);
253
        if ($parent !== null) {
254
            /* We need to append the child to the parent before we add the namespaces. */
255
            $parent->appendChild($newElement);
256
        }
257
258
        foreach ($namespaces as $prefix => $uri) {
259
            $newElement->setAttributeNS($uri, $prefix.':__ns_workaround__', 'tmp');
260
            $newElement->removeAttributeNS($uri, '__ns_workaround__');
261
        }
262
263
        return $newElement;
264
    }
265
266
267
    /**
268
     * Parse a boolean attribute.
269
     *
270
     * @param  \DOMElement $node          The element we should fetch the attribute from.
271
     * @param  string     $attributeName The name of the attribute.
272
     * @param  mixed      $default       The value that should be returned if the attribute doesn't exist.
273
     * @return bool|mixed The value of the attribute, or $default if the attribute doesn't exist.
274
     * @throws \Exception
275
     */
276
    public static function parseBoolean(\DOMElement $node, $attributeName, $default = null)
277
    {
278
        assert(is_string($attributeName));
279
280
        if (!$node->hasAttribute($attributeName)) {
281
            return $default;
282
        }
283
        $value = $node->getAttribute($attributeName);
284
        switch (strtolower($value)) {
285
            case '0':
286
            case 'false':
287
                return false;
288
            case '1':
289
            case 'true':
290
                return true;
291
            default:
292
                throw new \Exception('Invalid value of boolean attribute '.var_export($attributeName, true).': '.var_export($value, true));
293
        }
294
    }
295
296
297
    /**
298
     * Create a NameID element.
299
     *
300
     * The NameId array can have the following elements: 'Value', 'Format',
301
     *   'NameQualifier, 'SPNameQualifier' and 'SPProviderID'.
302
     *
303
     * Only the 'Value'-element is required.
304
     *
305
     * @param \DOMElement $node   The DOM node we should append the NameId to.
306
     * @param array      $nameId The name identifier.
307
     *
308
     * @deprecated Please use \SAML2\XML\saml\NameID objects instead:
309
     *   $nameId = new \SAML2\XML\saml\NameID();
310
     *   $nameId->value = $value;
311
     *   ...
312
     *   $nameId->toXML($node);
313
     */
314
    public static function addNameId(\DOMElement $node, array $nameId)
315
    {
316
        assert(array_key_exists("Value", $nameId));
317
318
        $nid = new XML\saml\NameID();
319
320
        $nid->value = $nameId['Value'];
321
322
        if (array_key_exists('NameQualifier', $nameId) && $nameId['NameQualifier'] !== null) {
323
            $nid->setNameQualifier($nameId['NameQualifier']);
324
        }
325
        if (array_key_exists('SPNameQualifier', $nameId) && $nameId['SPNameQualifier'] !== null) {
326
            $nid->setSPNameQualifier($nameId['SPNameQualifier']);
327
        }
328
        if (array_key_exists('Format', $nameId) && $nameId['Format'] !== null) {
329
            $nid->setFormat($nameId['Format']);
330
        }
331
332
        $nid->toXML($node);
333
    }
334
335
    /**
336
     * Parse a NameID element.
337
     *
338
     * @param  \DOMElement $xml The DOM element we should parse.
339
     * @return array      The parsed name identifier.
340
     * @deprecated Please use \SAML2\XML\saml\NameID objects instead:
341
     *   $nameId = new \SAML2\XML\saml\NameID($xml);
342
     */
343
    public static function parseNameId(\DOMElement $xml)
344
    {
345
        $ret = ['Value' => trim($xml->textContent)];
346
347
        foreach (['NameQualifier', 'SPNameQualifier', 'SPProvidedID', 'Format'] as $attr) {
348
            if ($xml->hasAttribute($attr)) {
349
                $ret[$attr] = $xml->getAttribute($attr);
350
            }
351
        }
352
353
        return $ret;
354
    }
355
356
    /**
357
     * Insert a Signature-node.
358
     *
359
     * @param XMLSecurityKey $key           The key we should use to sign the message.
360
     * @param array          $certificates  The certificates we should add to the signature node.
361
     * @param \DOMElement     $root          The XML node we should sign.
362
     * @param \DOMNode        $insertBefore  The XML element we should insert the signature element before.
363
     */
364
    public static function insertSignature(
365
        XMLSecurityKey $key,
366
        array $certificates,
367
        \DOMElement $root,
368
        \DOMNode $insertBefore = null
369
    ) {
370
        $objXMLSecDSig = new XMLSecurityDSig();
371
        $objXMLSecDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N);
372
373
        switch ($key->type) {
374
            case XMLSecurityKey::RSA_SHA256:
375
                $type = XMLSecurityDSig::SHA256;
376
                break;
377
            case XMLSecurityKey::RSA_SHA384:
378
                $type = XMLSecurityDSig::SHA384;
379
                break;
380
            case XMLSecurityKey::RSA_SHA512:
381
                $type = XMLSecurityDSig::SHA512;
382
                break;
383
            default:
384
                $type = XMLSecurityDSig::SHA1;
385
        }
386
387
        $objXMLSecDSig->addReferenceList(
388
            [$root],
389
            $type,
390
            ['http://www.w3.org/2000/09/xmldsig#enveloped-signature', XMLSecurityDSig::EXC_C14N],
391
            ['id_name' => 'ID', 'overwrite' => false]
392
        );
393
394
        $objXMLSecDSig->sign($key);
395
396
        foreach ($certificates as $certificate) {
397
            $objXMLSecDSig->add509Cert($certificate, true);
398
        }
399
400
        $objXMLSecDSig->insertSignature($root, $insertBefore);
401
    }
402
403
    /**
404
     * Decrypt an encrypted element.
405
     *
406
     * This is an internal helper function.
407
     *
408
     * @param  \DOMElement     $encryptedData The encrypted data.
409
     * @param  XMLSecurityKey $inputKey      The decryption key.
410
     * @param  array          &$blacklist    Blacklisted decryption algorithms.
411
     * @return \DOMElement     The decrypted element.
412
     * @throws \Exception
413
     */
414
    private static function doDecryptElement(\DOMElement $encryptedData, XMLSecurityKey $inputKey, array &$blacklist)
415
    {
416
        $enc = new XMLSecEnc();
417
418
        $enc->setNode($encryptedData);
419
        $enc->type = $encryptedData->getAttribute("Type");
420
421
        $symmetricKey = $enc->locateKey($encryptedData);
422
        if (!$symmetricKey) {
423
            throw new \Exception('Could not locate key algorithm in encrypted data.');
424
        }
425
426
        $symmetricKeyInfo = $enc->locateKeyInfo($symmetricKey);
427
        if (!$symmetricKeyInfo) {
428
            throw new \Exception('Could not locate <dsig:KeyInfo> for the encrypted key.');
429
        }
430
431
        $inputKeyAlgo = $inputKey->getAlgorithm();
432
        if ($symmetricKeyInfo->isEncrypted) {
433
            $symKeyInfoAlgo = $symmetricKeyInfo->getAlgorithm();
434
435
            if (in_array($symKeyInfoAlgo, $blacklist, true)) {
436
                throw new \Exception('Algorithm disabled: '.var_export($symKeyInfoAlgo, true));
437
            }
438
439
            if ($symKeyInfoAlgo === XMLSecurityKey::RSA_OAEP_MGF1P && $inputKeyAlgo === XMLSecurityKey::RSA_1_5) {
440
                /*
441
                 * The RSA key formats are equal, so loading an RSA_1_5 key
442
                 * into an RSA_OAEP_MGF1P key can be done without problems.
443
                 * We therefore pretend that the input key is an
444
                 * RSA_OAEP_MGF1P key.
445
                 */
446
                $inputKeyAlgo = XMLSecurityKey::RSA_OAEP_MGF1P;
447
            }
448
449
            /* Make sure that the input key format is the same as the one used to encrypt the key. */
450
            if ($inputKeyAlgo !== $symKeyInfoAlgo) {
451
                throw new \Exception(
452
                    'Algorithm mismatch between input key and key used to encrypt '.
453
                    ' the symmetric key for the message. Key was: '.
454
                    var_export($inputKeyAlgo, true).'; message was: '.
455
                    var_export($symKeyInfoAlgo, true)
456
                );
457
            }
458
459
            /** @var XMLSecEnc $encKey */
460
            $encKey = $symmetricKeyInfo->encryptedCtx;
461
            $symmetricKeyInfo->key = $inputKey->key;
462
463
            $keySize = $symmetricKey->getSymmetricKeySize();
464
            if ($keySize === null) {
465
                /* To protect against "key oracle" attacks, we need to be able to create a
466
                 * symmetric key, and for that we need to know the key size.
467
                 */
468
                throw new \Exception('Unknown key size for encryption algorithm: '.var_export($symmetricKey->type, true));
469
            }
470
471
            try {
472
                $key = $encKey->decryptKey($symmetricKeyInfo);
473
                if (strlen($key) != $keySize) {
474
                    throw new \Exception(
475
                        'Unexpected key size ('.strlen($key)*8.'bits) for encryption algorithm: '.
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected T_CONSTANT_ENCAPSED_STRING, expecting ',' or ')' on line 475 at column 63
Loading history...
476
                        var_export($symmetricKey->type, true)
477
                    );
478
                }
479
            } catch (\Exception $e) {
480
                /* We failed to decrypt this key. Log it, and substitute a "random" key. */
481
                Utils::getContainer()->getLogger()->error('Failed to decrypt symmetric key: '.$e->getMessage());
482
                /* Create a replacement key, so that it looks like we fail in the same way as if the key was correctly padded. */
483
484
                /* We base the symmetric key on the encrypted key and private key, so that we always behave the
485
                 * same way for a given input key.
486
                 */
487
                $encryptedKey = $encKey->getCipherValue();
488
                $pkey = openssl_pkey_get_details($symmetricKeyInfo->key);
489
                $pkey = sha1(serialize($pkey), true);
490
                $key = sha1($encryptedKey.$pkey, true);
491
492
                /* Make sure that the key has the correct length. */
493
                if (strlen($key) > $keySize) {
494
                    $key = substr($key, 0, $keySize);
495
                } elseif (strlen($key) < $keySize) {
496
                    $key = str_pad($key, $keySize);
497
                }
498
            }
499
            $symmetricKey->loadkey($key);
500
        } else {
501
            $symKeyAlgo = $symmetricKey->getAlgorithm();
502
            /* Make sure that the input key has the correct format. */
503
            if ($inputKeyAlgo !== $symKeyAlgo) {
504
                throw new \Exception(
505
                    'Algorithm mismatch between input key and key in message. '.
506
                    'Key was: '.var_export($inputKeyAlgo, true).'; message was: '.
507
                    var_export($symKeyAlgo, true)
508
                );
509
            }
510
            $symmetricKey = $inputKey;
511
        }
512
513
        $algorithm = $symmetricKey->getAlgorithm();
514
        if (in_array($algorithm, $blacklist, true)) {
515
            throw new \Exception('Algorithm disabled: '.var_export($algorithm, true));
516
        }
517
518
        /** @var string $decrypted */
519
        $decrypted = $enc->decryptNode($symmetricKey, false);
520
521
        /*
522
         * This is a workaround for the case where only a subset of the XML
523
         * tree was serialized for encryption. In that case, we may miss the
524
         * namespaces needed to parse the XML.
525
         */
526
        $xml = '<root xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" '.
527
                        'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' .
528
            $decrypted .
529
            '</root>';
530
531
        try {
532
            $newDoc = DOMDocumentFactory::fromString($xml);
533
        } catch (RuntimeException $e) {
534
            throw new \Exception('Failed to parse decrypted XML. Maybe the wrong sharedkey was used?', 0, $e);
535
        }
536
537
        $decryptedElement = $newDoc->firstChild->firstChild;
538
        if ($decryptedElement === null) {
539
            throw new \Exception('Missing encrypted element.');
540
        }
541
542
        if (!($decryptedElement instanceof \DOMElement)) {
543
            throw new \Exception('Decrypted element was not actually a \DOMElement.');
544
        }
545
546
        return $decryptedElement;
547
    }
548
549
    /**
550
     * Decrypt an encrypted element.
551
     *
552
     * @param  \DOMElement     $encryptedData The encrypted data.
553
     * @param  XMLSecurityKey $inputKey      The decryption key.
554
     * @param  array          $blacklist     Blacklisted decryption algorithms.
555
     * @return \DOMElement     The decrypted element.
556
     * @throws \Exception
557
     */
558
    public static function decryptElement(\DOMElement $encryptedData, XMLSecurityKey $inputKey, array $blacklist = [])
559
    {
560
        try {
561
            return self::doDecryptElement($encryptedData, $inputKey, $blacklist);
562
        } catch (\Exception $e) {
563
            /*
564
             * Something went wrong during decryption, but for security
565
             * reasons we cannot tell the user what failed.
566
             */
567
            Utils::getContainer()->getLogger()->error('Decryption failed: '.$e->getMessage());
568
            throw new \Exception('Failed to decrypt XML element.', 0, $e);
569
        }
570
    }
571
572
    /**
573
     * Extract localized strings from a set of nodes.
574
     *
575
     * @param  \DOMElement $parent       The element that contains the localized strings.
576
     * @param  string     $namespaceURI The namespace URI the localized strings should have.
577
     * @param  string     $localName    The localName of the localized strings.
578
     * @return array      Localized strings.
579
     */
580
    public static function extractLocalizedStrings(\DOMElement $parent, $namespaceURI, $localName)
581
    {
582
        assert(is_string($namespaceURI));
583
        assert(is_string($localName));
584
585
        $ret = [];
586
        for ($node = $parent->firstChild; $node !== null; $node = $node->nextSibling) {
587
            if ($node->namespaceURI !== $namespaceURI || $node->localName !== $localName) {
588
                continue;
589
            }
590
591
            if ($node->hasAttribute('xml:lang')) {
592
                $language = $node->getAttribute('xml:lang');
593
            } else {
594
                $language = 'en';
595
            }
596
            $ret[$language] = trim($node->textContent);
597
        }
598
599
        return $ret;
600
    }
601
602
    /**
603
     * Extract strings from a set of nodes.
604
     *
605
     * @param  \DOMElement $parent       The element that contains the localized strings.
606
     * @param  string     $namespaceURI The namespace URI the string elements should have.
607
     * @param  string     $localName    The localName of the string elements.
608
     * @return array      The string values of the various nodes.
609
     */
610
    public static function extractStrings(\DOMElement $parent, $namespaceURI, $localName)
611
    {
612
        assert(is_string($namespaceURI));
613
        assert(is_string($localName));
614
615
        $ret = [];
616
        for ($node = $parent->firstChild; $node !== null; $node = $node->nextSibling) {
617
            if ($node->namespaceURI !== $namespaceURI || $node->localName !== $localName) {
618
                continue;
619
            }
620
            $ret[] = trim($node->textContent);
621
        }
622
623
        return $ret;
624
    }
625
626
    /**
627
     * Append string element.
628
     *
629
     * @param  \DOMElement $parent    The parent element we should append the new nodes to.
630
     * @param  string     $namespace The namespace of the created element.
631
     * @param  string     $name      The name of the created element.
632
     * @param  string     $value     The value of the element.
633
     * @return \DOMElement The generated element.
634
     */
635
    public static function addString(\DOMElement $parent, $namespace, $name, $value)
636
    {
637
        assert(is_string($namespace));
638
        assert(is_string($name));
639
        assert(is_string($value));
640
641
        $doc = $parent->ownerDocument;
642
643
        $n = $doc->createElementNS($namespace, $name);
644
        $n->appendChild($doc->createTextNode($value));
645
        $parent->appendChild($n);
646
647
        return $n;
648
    }
649
650
    /**
651
     * Append string elements.
652
     *
653
     * @param \DOMElement $parent    The parent element we should append the new nodes to.
654
     * @param string     $namespace The namespace of the created elements
655
     * @param string     $name      The name of the created elements
656
     * @param bool       $localized Whether the strings are localized, and should include the xml:lang attribute.
657
     * @param array      $values    The values we should create the elements from.
658
     */
659
    public static function addStrings(\DOMElement $parent, $namespace, $name, $localized, array $values)
660
    {
661
        assert(is_string($namespace));
662
        assert(is_string($name));
663
        assert(is_bool($localized));
664
665
        $doc = $parent->ownerDocument;
666
667
        foreach ($values as $index => $value) {
668
            $n = $doc->createElementNS($namespace, $name);
669
            $n->appendChild($doc->createTextNode($value));
670
            if ($localized) {
671
                $n->setAttribute('xml:lang', $index);
672
            }
673
            $parent->appendChild($n);
674
        }
675
    }
676
677
    /**
678
     * Create a KeyDescriptor with the given certificate.
679
     *
680
     * @param  string                     $x509Data The certificate, as a base64-encoded DER data.
681
     * @return \SAML2\XML\md\KeyDescriptor The keydescriptor.
682
     */
683
    public static function createKeyDescriptor($x509Data)
684
    {
685
        assert(is_string($x509Data));
686
687
        $x509Certificate = new X509Certificate();
688
        $x509Certificate->setCertificate($x509Data);
689
690
        $x509Data = new X509Data();
691
        $x509Data->addData($x509Certificate);
692
693
        $keyInfo = new KeyInfo();
694
        $keyInfo->addInfo($x509Data);
695
696
        $keyDescriptor = new KeyDescriptor();
697
        $keyDescriptor->KeyInfo = $keyInfo;
698
699
        return $keyDescriptor;
700
    }
701
702
    /**
703
     * This function converts a SAML2 timestamp on the form
704
     * yyyy-mm-ddThh:mm:ss(\.s+)?Z to a UNIX timestamp. The sub-second
705
     * part is ignored.
706
     *
707
     * Andreas comments:
708
     *  I got this timestamp from Shibboleth 1.3 IdP: 2008-01-17T11:28:03.577Z
709
     *  Therefore I added to possibility to have microseconds to the format.
710
     * Added: (\.\\d{1,3})? to the regex.
711
     *
712
     * Note that we always require a 'Z' timezone for the dateTime to be valid.
713
     * This is not in the SAML spec but that's considered to be a bug in the
714
     * spec. See https://github.com/simplesamlphp/saml2/pull/36 for some
715
     * background.
716
     *
717
     * @param string $time The time we should convert.
718
     * @return int Converted to a unix timestamp.
719
     * @throws \Exception
720
     */
721
    public static function xsDateTimeToTimestamp($time)
722
    {
723
        $matches = [];
724
725
        // We use a very strict regex to parse the timestamp.
726
        $regex = '/^(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d)T(\\d\\d):(\\d\\d):(\\d\\d)(?:\\.\\d{1,9})?Z$/D';
727
        if (preg_match($regex, $time, $matches) == 0) {
728
            throw new \Exception(
729
                'Invalid SAML2 timestamp passed to xsDateTimeToTimestamp: '.$time
730
            );
731
        }
732
733
        // Extract the different components of the time from the  matches in the regex.
734
        // intval will ignore leading zeroes in the string.
735
        $year   = intval($matches[1]);
736
        $month  = intval($matches[2]);
737
        $day    = intval($matches[3]);
738
        $hour   = intval($matches[4]);
739
        $minute = intval($matches[5]);
740
        $second = intval($matches[6]);
741
742
        // We use gmmktime because the timestamp will always be given
743
        //in UTC.
744
        $ts = gmmktime($hour, $minute, $second, $month, $day, $year);
745
746
        return $ts;
747
    }
748
749
    /**
750
     * @return \SAML2\Compat\Ssp\Container
751
     */
752
    public static function getContainer()
753
    {
754
        return ContainerSingleton::getInstance();
755
    }
756
}
757