Completed
Push — master ( 4e360d...80bc96 )
by Daan van
07:27
created

Utils::addStrings()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 17
rs 9.4286
cc 3
eloc 11
nc 3
nop 5
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) {
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...
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 = array();
88
        foreach (self::xpQuery($signatureElement, './ds:KeyInfo/ds:X509Data/ds:X509Certificate') as $certNode) {
89
            $certData = trim($certNode->textContent);
90
            $certData = str_replace(array("\r", "\n", "\t", ' '), '', $certData);
91
            $certificates[] = $certData;
92
        }
93
94
        $ret = array(
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) {
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...
119
            return $key;
120
        }
121
122
        $keyInfo = openssl_pkey_get_details($key->key);
123
        if ($keyInfo === false) {
124
            throw new \Exception('Unable to get key details from XMLSecurityKey.');
125
        }
126
        if (!isset($keyInfo['key'])) {
127
            throw new \Exception('Missing key in public key details.');
128
        }
129
130
        $newKey = new XMLSecurityKey($algorithm, array('type'=>$type));
131
        $newKey->loadKey($keyInfo['key']);
132
133
        return $newKey;
134
    }
135
136
137
    /**
138
     * Check a signature against a key.
139
     *
140
     * An exception is thrown if we are unable to validate the signature.
141
     *
142
     * @param array          $info The information returned by the validateElement()-function.
143
     * @param XMLSecurityKey $key  The publickey that should validate the Signature object.
144
     * @throws \Exception
145
     */
146
    public static function validateSignature(array $info, XMLSecurityKey $key)
147
    {
148
        assert('array_key_exists("Signature", $info)');
149
150
        /** @var XMLSecurityDSig $objXMLSecDSig */
151
        $objXMLSecDSig = $info['Signature'];
152
153
        $sigMethod = self::xpQuery($objXMLSecDSig->sigNode, './ds:SignedInfo/ds:SignatureMethod');
154
        if (empty($sigMethod)) {
155
            throw new \Exception('Missing SignatureMethod element.');
156
        }
157
        $sigMethod = $sigMethod[0];
158
        if (!$sigMethod->hasAttribute('Algorithm')) {
159
            throw new \Exception('Missing Algorithm-attribute on SignatureMethod element.');
160
        }
161
        $algo = $sigMethod->getAttribute('Algorithm');
162
163
        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...
164
            $key = self::castKey($key, $algo);
165
        }
166
167
        /* Check the signature. */
168
        if (! $objXMLSecDSig->verify($key)) {
169
            throw new \Exception("Unable to validate Signature");
170
        }
171
    }
172
173
174
    /**
175
     * Do an XPath query on an XML node.
176
     *
177
     * @param  \DOMNode $node  The XML node.
178
     * @param  string  $query The query.
179
     * @return \DOMElement[]    Array with matching DOM nodes.
180
     */
181
    public static function xpQuery(\DOMNode $node, $query)
182
    {
183
        assert('is_string($query)');
184
        static $xpCache = null;
185
186
        if ($node instanceof \DOMDocument) {
187
            $doc = $node;
188
        } else {
189
            $doc = $node->ownerDocument;
190
        }
191
192
        if ($xpCache === null || !$xpCache->document->isSameNode($doc)) {
193
            $xpCache = new \DOMXPath($doc);
194
            $xpCache->registerNamespace('soap-env', Constants::NS_SOAP);
195
            $xpCache->registerNamespace('saml_protocol', Constants::NS_SAMLP);
196
            $xpCache->registerNamespace('saml_assertion', Constants::NS_SAML);
197
            $xpCache->registerNamespace('saml_metadata', Constants::NS_MD);
198
            $xpCache->registerNamespace('ds', XMLSecurityDSig::XMLDSIGNS);
199
            $xpCache->registerNamespace('xenc', XMLSecEnc::XMLENCNS);
200
        }
201
202
        $results = $xpCache->query($query, $node);
203
        $ret = array();
204
        for ($i = 0; $i < $results->length; $i++) {
205
            $ret[$i] = $results->item($i);
206
        }
207
208
        return $ret;
209
    }
210
211
212
    /**
213
     * Make an exact copy the specific \DOMElement.
214
     *
215
     * @param  \DOMElement      $element The element we should copy.
216
     * @param  \DOMElement|null $parent  The target parent element.
217
     * @return \DOMElement      The copied element.
218
     */
219
    public static function copyElement(\DOMElement $element, \DOMElement $parent = null)
220
    {
221
        if ($parent === null) {
222
            $document = DOMDocumentFactory::create();
223
        } else {
224
            $document = $parent->ownerDocument;
225
        }
226
227
        $namespaces = array();
228
        for ($e = $element; $e !== null; $e = $e->parentNode) {
229
            foreach (Utils::xpQuery($e, './namespace::*') as $ns) {
230
                $prefix = $ns->localName;
231
                if ($prefix === 'xml' || $prefix === 'xmlns') {
232
                    continue;
233
                }
234
                $uri = $ns->nodeValue;
235
                if (!isset($namespaces[$prefix])) {
236
                    $namespaces[$prefix] = $uri;
237
                }
238
            }
239
        }
240
241
        /** @var \DOMElement $newElement */
242
        $newElement = $document->importNode($element, true);
243
        if ($parent !== null) {
244
            /* We need to append the child to the parent before we add the namespaces. */
245
            $parent->appendChild($newElement);
246
        }
247
248
        foreach ($namespaces as $prefix => $uri) {
249
            $newElement->setAttributeNS($uri, $prefix . ':__ns_workaround__', 'tmp');
250
            $newElement->removeAttributeNS($uri, '__ns_workaround__');
251
        }
252
253
        return $newElement;
254
    }
255
256
257
    /**
258
     * Parse a boolean attribute.
259
     *
260
     * @param  \DOMElement $node          The element we should fetch the attribute from.
261
     * @param  string     $attributeName The name of the attribute.
262
     * @param  mixed      $default       The value that should be returned if the attribute doesn't exist.
263
     * @return bool|mixed The value of the attribute, or $default if the attribute doesn't exist.
264
     * @throws \Exception
265
     */
266
    public static function parseBoolean(\DOMElement $node, $attributeName, $default = null)
267
    {
268
        assert('is_string($attributeName)');
269
270
        if (!$node->hasAttribute($attributeName)) {
271
            return $default;
272
        }
273
        $value = $node->getAttribute($attributeName);
274
        switch (strtolower($value)) {
275
            case '0':
276
            case 'false':
277
                return false;
278
            case '1':
279
            case 'true':
280
                return true;
281
            default:
282
                throw new \Exception('Invalid value of boolean attribute ' . var_export($attributeName, true) . ': ' . var_export($value, true));
283
        }
284
    }
285
286
287
    /**
288
     * Create a NameID element.
289
     *
290
     * The NameId array can have the following elements: 'Value', 'Format',
291
     *   'NameQualifier, 'SPNameQualifier'
292
     *
293
     * Only the 'Value'-element is required.
294
     *
295
     * @param \DOMElement $node   The DOM node we should append the NameId to.
296
     * @param array      $nameId The name identifier.
297
     */
298
    public static function addNameId(\DOMElement $node, array $nameId)
299
    {
300
        assert('array_key_exists("Value", $nameId)');
301
302
        $xml = Utils::addString($node, Constants::NS_SAML, 'saml:NameID', $nameId['Value']);
303
304
        if (array_key_exists('NameQualifier', $nameId) && $nameId['NameQualifier'] !== null) {
305
            $xml->setAttribute('NameQualifier', $nameId['NameQualifier']);
306
        }
307 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...
308
            $xml->setAttribute('SPNameQualifier', $nameId['SPNameQualifier']);
309
        }
310 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...
311
            $xml->setAttribute('Format', $nameId['Format']);
312
        }
313
    }
314
315
    /**
316
     * Parse a NameID element.
317
     *
318
     * @param  \DOMElement $xml The DOM element we should parse.
319
     * @return array      The parsed name identifier.
320
     */
321
    public static function parseNameId(\DOMElement $xml)
322
    {
323
        $ret = array('Value' => trim($xml->textContent));
324
325
        foreach (array('NameQualifier', 'SPNameQualifier', 'Format') as $attr) {
326
            if ($xml->hasAttribute($attr)) {
327
                $ret[$attr] = $xml->getAttribute($attr);
328
            }
329
        }
330
331
        return $ret;
332
    }
333
334
    /**
335
     * Insert a Signature-node.
336
     *
337
     * @param XMLSecurityKey $key           The key we should use to sign the message.
338
     * @param array          $certificates  The certificates we should add to the signature node.
339
     * @param \DOMElement     $root          The XML node we should sign.
340
     * @param \DOMNode        $insertBefore  The XML element we should insert the signature element before.
341
     */
342
    public static function insertSignature(
343
        XMLSecurityKey $key,
344
        array $certificates,
345
        \DOMElement $root,
346
        \DOMNode $insertBefore = null
347
    ) {
348
        $objXMLSecDSig = new XMLSecurityDSig();
349
        $objXMLSecDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N);
350
351
        switch ($key->type) {
352
            case XMLSecurityKey::RSA_SHA256:
353
                $type = XMLSecurityDSig::SHA256;
354
                break;
355
            case XMLSecurityKey::RSA_SHA384:
356
                $type = XMLSecurityDSig::SHA384;
357
                break;
358
            case XMLSecurityKey::RSA_SHA512:
359
                $type = XMLSecurityDSig::SHA512;
360
                break;
361
            default:
362
                $type = XMLSecurityDSig::SHA1;
363
        }
364
365
        $objXMLSecDSig->addReferenceList(
366
            array($root),
367
            $type,
368
            array('http://www.w3.org/2000/09/xmldsig#enveloped-signature', XMLSecurityDSig::EXC_C14N),
369
            array('id_name' => 'ID', 'overwrite' => false)
370
        );
371
372
        $objXMLSecDSig->sign($key);
373
374
        foreach ($certificates as $certificate) {
375
            $objXMLSecDSig->add509Cert($certificate, true);
376
        }
377
378
        $objXMLSecDSig->insertSignature($root, $insertBefore);
0 ignored issues
show
Documentation introduced by
$root is of type object<DOMElement>, but the function expects a object<RobRichards\XMLSecLibs\DOMNode>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Bug introduced by
It seems like $insertBefore defined by parameter $insertBefore on line 346 can also be of type object<DOMNode>; however, RobRichards\XMLSecLibs\X...DSig::insertSignature() does only seem to accept object<RobRichards\XMLSecLibs\DOMNode>|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
379
    }
380
381
    /**
382
     * Decrypt an encrypted element.
383
     *
384
     * This is an internal helper function.
385
     *
386
     * @param  \DOMElement     $encryptedData The encrypted data.
387
     * @param  XMLSecurityKey $inputKey      The decryption key.
388
     * @param  array          &$blacklist    Blacklisted decryption algorithms.
389
     * @return \DOMElement     The decrypted element.
390
     * @throws \Exception
391
     */
392
    private static function doDecryptElement(\DOMElement $encryptedData, XMLSecurityKey $inputKey, array &$blacklist)
393
    {
394
        $enc = new XMLSecEnc();
395
396
        $enc->setNode($encryptedData);
397
        $enc->type = $encryptedData->getAttribute("Type");
398
399
        $symmetricKey = $enc->locateKey($encryptedData);
400
        if (!$symmetricKey) {
401
            throw new \Exception('Could not locate key algorithm in encrypted data.');
402
        }
403
404
        $symmetricKeyInfo = $enc->locateKeyInfo($symmetricKey);
405
        if (!$symmetricKeyInfo) {
406
            throw new \Exception('Could not locate <dsig:KeyInfo> for the encrypted key.');
407
        }
408
409
        $inputKeyAlgo = $inputKey->getAlgorith();
410
        if ($symmetricKeyInfo->isEncrypted) {
411
            $symKeyInfoAlgo = $symmetricKeyInfo->getAlgorith();
412
413 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...
414
                throw new \Exception('Algorithm disabled: ' . var_export($symKeyInfoAlgo, true));
415
            }
416
417
            if ($symKeyInfoAlgo === XMLSecurityKey::RSA_OAEP_MGF1P && $inputKeyAlgo === XMLSecurityKey::RSA_1_5) {
418
                /*
419
                 * The RSA key formats are equal, so loading an RSA_1_5 key
420
                 * into an RSA_OAEP_MGF1P key can be done without problems.
421
                 * We therefore pretend that the input key is an
422
                 * RSA_OAEP_MGF1P key.
423
                 */
424
                $inputKeyAlgo = XMLSecurityKey::RSA_OAEP_MGF1P;
425
            }
426
427
            /* Make sure that the input key format is the same as the one used to encrypt the key. */
428 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...
429
                throw new \Exception(
430
                    'Algorithm mismatch between input key and key used to encrypt ' .
431
                    ' the symmetric key for the message. Key was: ' .
432
                    var_export($inputKeyAlgo, true) . '; message was: ' .
433
                    var_export($symKeyInfoAlgo, true)
434
                );
435
            }
436
437
            /** @var XMLSecEnc $encKey */
438
            $encKey = $symmetricKeyInfo->encryptedCtx;
439
            $symmetricKeyInfo->key = $inputKey->key;
440
441
            $keySize = $symmetricKey->getSymmetricKeySize();
442
            if ($keySize === null) {
443
                /* To protect against "key oracle" attacks, we need to be able to create a
444
                 * symmetric key, and for that we need to know the key size.
445
                 */
446
                throw new \Exception('Unknown key size for encryption algorithm: ' . var_export($symmetricKey->type, true));
447
            }
448
449
            try {
450
                $key = $encKey->decryptKey($symmetricKeyInfo);
451
                if (strlen($key) != $keySize) {
452
                    throw new \Exception(
453
                        'Unexpected key size (' . strlen($key) * 8 . 'bits) for encryption algorithm: ' .
454
                        var_export($symmetricKey->type, true)
455
                    );
456
                }
457
            } catch (\Exception $e) {
458
                /* We failed to decrypt this key. Log it, and substitute a "random" key. */
459
                Utils::getContainer()->getLogger()->error('Failed to decrypt symmetric key: ' . $e->getMessage());
460
                /* Create a replacement key, so that it looks like we fail in the same way as if the key was correctly padded. */
461
462
                /* We base the symmetric key on the encrypted key and private key, so that we always behave the
463
                 * same way for a given input key.
464
                 */
465
                $encryptedKey = $encKey->getCipherValue();
466
                $pkey = openssl_pkey_get_details($symmetricKeyInfo->key);
467
                $pkey = sha1(serialize($pkey), true);
468
                $key = sha1($encryptedKey . $pkey, true);
469
470
                /* Make sure that the key has the correct length. */
471
                if (strlen($key) > $keySize) {
472
                    $key = substr($key, 0, $keySize);
473
                } elseif (strlen($key) < $keySize) {
474
                    $key = str_pad($key, $keySize);
475
                }
476
            }
477
            $symmetricKey->loadkey($key);
478
        } else {
479
            $symKeyAlgo = $symmetricKey->getAlgorith();
480
            /* Make sure that the input key has the correct format. */
481 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...
482
                throw new \Exception(
483
                    'Algorithm mismatch between input key and key in message. ' .
484
                    'Key was: ' . var_export($inputKeyAlgo, true) . '; message was: ' .
485
                    var_export($symKeyAlgo, true)
486
                );
487
            }
488
            $symmetricKey = $inputKey;
489
        }
490
491
        $algorithm = $symmetricKey->getAlgorith();
492 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...
493
            throw new \Exception('Algorithm disabled: ' . var_export($algorithm, true));
494
        }
495
496
        /** @var string $decrypted */
497
        $decrypted = $enc->decryptNode($symmetricKey, false);
498
499
        /*
500
         * This is a workaround for the case where only a subset of the XML
501
         * tree was serialized for encryption. In that case, we may miss the
502
         * namespaces needed to parse the XML.
503
         */
504
        $xml = '<root xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" '.
505
                     'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' .
506
            $decrypted .
507
            '</root>';
508
509
        try {
510
            $newDoc = DOMDocumentFactory::fromString($xml);
511
        } catch (RuntimeException $e) {
512
            throw new \Exception('Failed to parse decrypted XML. Maybe the wrong sharedkey was used?', 0, $e);
513
        }
514
515
        $decryptedElement = $newDoc->firstChild->firstChild;
516
        if ($decryptedElement === null) {
517
            throw new \Exception('Missing encrypted element.');
518
        }
519
520
        if (!($decryptedElement instanceof \DOMElement)) {
521
            throw new \Exception('Decrypted element was not actually a \DOMElement.');
522
        }
523
524
        return $decryptedElement;
525
    }
526
527
    /**
528
     * Decrypt an encrypted element.
529
     *
530
     * @param  \DOMElement     $encryptedData The encrypted data.
531
     * @param  XMLSecurityKey $inputKey      The decryption key.
532
     * @param  array          $blacklist     Blacklisted decryption algorithms.
533
     * @return \DOMElement     The decrypted element.
534
     * @throws \Exception
535
     */
536
    public static function decryptElement(\DOMElement $encryptedData, XMLSecurityKey $inputKey, array $blacklist = array())
537
    {
538
        try {
539
            return self::doDecryptElement($encryptedData, $inputKey, $blacklist);
540
        } catch (\Exception $e) {
541
            /*
542
             * Something went wrong during decryption, but for security
543
             * reasons we cannot tell the user what failed.
544
             */
545
            Utils::getContainer()->getLogger()->error('Decryption failed: ' . $e->getMessage());
546
            throw new \Exception('Failed to decrypt XML element.', 0, $e);
547
        }
548
    }
549
550
    /**
551
     * Extract localized strings from a set of nodes.
552
     *
553
     * @param  \DOMElement $parent       The element that contains the localized strings.
554
     * @param  string     $namespaceURI The namespace URI the localized strings should have.
555
     * @param  string     $localName    The localName of the localized strings.
556
     * @return array      Localized strings.
557
     */
558
    public static function extractLocalizedStrings(\DOMElement $parent, $namespaceURI, $localName)
559
    {
560
        assert('is_string($namespaceURI)');
561
        assert('is_string($localName)');
562
563
        $ret = array();
564
        for ($node = $parent->firstChild; $node !== null; $node = $node->nextSibling) {
565
            if ($node->namespaceURI !== $namespaceURI || $node->localName !== $localName) {
566
                continue;
567
            }
568
569
            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...
570
                $language = $node->getAttribute('xml:lang');
571
            } else {
572
                $language = 'en';
573
            }
574
            $ret[$language] = trim($node->textContent);
575
        }
576
577
        return $ret;
578
    }
579
580
    /**
581
     * Extract strings from a set of nodes.
582
     *
583
     * @param  \DOMElement $parent       The element that contains the localized strings.
584
     * @param  string     $namespaceURI The namespace URI the string elements should have.
585
     * @param  string     $localName    The localName of the string elements.
586
     * @return array      The string values of the various nodes.
587
     */
588
    public static function extractStrings(\DOMElement $parent, $namespaceURI, $localName)
589
    {
590
        assert('is_string($namespaceURI)');
591
        assert('is_string($localName)');
592
593
        $ret = array();
594
        for ($node = $parent->firstChild; $node !== null; $node = $node->nextSibling) {
595
            if ($node->namespaceURI !== $namespaceURI || $node->localName !== $localName) {
596
                continue;
597
            }
598
            $ret[] = trim($node->textContent);
599
        }
600
601
        return $ret;
602
    }
603
604
    /**
605
     * Append string element.
606
     *
607
     * @param  \DOMElement $parent    The parent element we should append the new nodes to.
608
     * @param  string     $namespace The namespace of the created element.
609
     * @param  string     $name      The name of the created element.
610
     * @param  string     $value     The value of the element.
611
     * @return \DOMElement The generated element.
612
     */
613
    public static function addString(\DOMElement $parent, $namespace, $name, $value)
614
    {
615
        assert('is_string($namespace)');
616
        assert('is_string($name)');
617
        assert('is_string($value)');
618
619
        $doc = $parent->ownerDocument;
620
621
        $n = $doc->createElementNS($namespace, $name);
622
        $n->appendChild($doc->createTextNode($value));
623
        $parent->appendChild($n);
624
625
        return $n;
626
    }
627
628
    /**
629
     * Append string elements.
630
     *
631
     * @param \DOMElement $parent    The parent element we should append the new nodes to.
632
     * @param string     $namespace The namespace of the created elements
633
     * @param string     $name      The name of the created elements
634
     * @param bool       $localized Whether the strings are localized, and should include the xml:lang attribute.
635
     * @param array      $values    The values we should create the elements from.
636
     */
637
    public static function addStrings(\DOMElement $parent, $namespace, $name, $localized, array $values)
638
    {
639
        assert('is_string($namespace)');
640
        assert('is_string($name)');
641
        assert('is_bool($localized)');
642
643
        $doc = $parent->ownerDocument;
644
645
        foreach ($values as $index => $value) {
646
            $n = $doc->createElementNS($namespace, $name);
647
            $n->appendChild($doc->createTextNode($value));
648
            if ($localized) {
649
                $n->setAttribute('xml:lang', $index);
650
            }
651
            $parent->appendChild($n);
652
        }
653
    }
654
655
    /**
656
     * Create a KeyDescriptor with the given certificate.
657
     *
658
     * @param  string                     $x509Data The certificate, as a base64-encoded DER data.
659
     * @return \SAML2\XML\md\KeyDescriptor The keydescriptor.
660
     */
661
    public static function createKeyDescriptor($x509Data)
662
    {
663
        assert('is_string($x509Data)');
664
665
        $x509Certificate = new X509Certificate();
666
        $x509Certificate->certificate = $x509Data;
667
668
        $x509Data = new X509Data();
669
        $x509Data->data[] = $x509Certificate;
670
671
        $keyInfo = new KeyInfo();
672
        $keyInfo->info[] = $x509Data;
673
674
        $keyDescriptor = new KeyDescriptor();
675
        $keyDescriptor->KeyInfo = $keyInfo;
676
677
        return $keyDescriptor;
678
    }
679
680
    /**
681
     * This function converts a SAML2 timestamp on the form
682
     * yyyy-mm-ddThh:mm:ss(\.s+)?Z to a UNIX timestamp. The sub-second
683
     * part is ignored.
684
     *
685
     * Andreas comments:
686
     *  I got this timestamp from Shibboleth 1.3 IdP: 2008-01-17T11:28:03.577Z
687
     *  Therefore I added to possibility to have microseconds to the format.
688
     * Added: (\.\\d{1,3})? to the regex.
689
     *
690
     * Note that we always require a 'Z' timezone for the dateTime to be valid.
691
     * This is not in the SAML spec but that's considered to be a bug in the
692
     * spec. See https://github.com/simplesamlphp/saml2/pull/36 for some
693
     * background.
694
     *
695
     * @param string $time The time we should convert.
696
     * @return int Converted to a unix timestamp.
697
     * @throws \Exception
698
     */
699
    public static function xsDateTimeToTimestamp($time)
700
    {
701
        $matches = array();
702
703
        // We use a very strict regex to parse the timestamp.
704
        $regex = '/^(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d)T(\\d\\d):(\\d\\d):(\\d\\d)(?:\\.\\d+)?Z$/D';
705
        if (preg_match($regex, $time, $matches) == 0) {
706
            throw new \Exception(
707
                'Invalid SAML2 timestamp passed to xsDateTimeToTimestamp: ' . $time
708
            );
709
        }
710
711
        // Extract the different components of the time from the  matches in the regex.
712
        // intval will ignore leading zeroes in the string.
713
        $year   = intval($matches[1]);
714
        $month  = intval($matches[2]);
715
        $day    = intval($matches[3]);
716
        $hour   = intval($matches[4]);
717
        $minute = intval($matches[5]);
718
        $second = intval($matches[6]);
719
720
        // We use gmmktime because the timestamp will always be given
721
        //in UTC.
722
        $ts = gmmktime($hour, $minute, $second, $month, $day, $year);
723
724
        return $ts;
725
    }
726
727
    /**
728
     * @return \SAML2\Compat\Ssp\Container
729
     */
730
    public static function getContainer()
731
    {
732
        return ContainerSingleton::getInstance();
733
    }
734
}
735