ILIAS  release_5-3 Revision v5.3.23-19-g915713cf615
Utils.php
Go to the documentation of this file.
1<?php
2
3namespace SAML2;
4
14
20class Utils
21{
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;
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 = 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
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, array(
123 XMLSecurityKey::RSA_1_5,
124 XMLSecurityKey::RSA_SHA1,
125 XMLSecurityKey::RSA_SHA256,
126 XMLSecurityKey::RSA_SHA384,
127 XMLSecurityKey::RSA_SHA512
128 ))) {
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, array('type'=>$type));
141 $newKey->loadKey($keyInfo['key']);
142
143 return $newKey;
144 }
145
146
156 public static function validateSignature(array $info, XMLSecurityKey $key)
157 {
158 assert(array_key_exists("Signature", $info));
159
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_SHA1 && $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
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 = array();
214 for ($i = 0; $i < $results->length; $i++) {
215 $ret[$i] = $results->item($i);
216 }
217
218 return $ret;
219 }
220
221
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 = array();
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
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
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
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->NameQualifier = $nameId['NameQualifier'];
324 }
325 if (array_key_exists('SPNameQualifier', $nameId) && $nameId['SPNameQualifier'] !== null) {
326 $nid->SPNameQualifier = $nameId['SPNameQualifier'];
327 }
328 if (array_key_exists('Format', $nameId) && $nameId['Format'] !== null) {
329 $nid->Format = $nameId['Format'];
330 }
331
332 $nid->toXML($node);
333 }
334
343 public static function parseNameId(\DOMElement $xml)
344 {
345 $ret = array('Value' => trim($xml->textContent));
346
347 foreach (array('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
364 public static function insertSignature(
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 array($root),
389 $type,
390 array('http://www.w3.org/2000/09/xmldsig#enveloped-signature', XMLSecurityDSig::EXC_C14N),
391 array('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
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
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: ' .
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
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
558 public static function decryptElement(\DOMElement $encryptedData, XMLSecurityKey $inputKey, array $blacklist = array())
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
580 public static function extractLocalizedStrings(\DOMElement $parent, $namespaceURI, $localName)
581 {
582 assert(is_string($namespaceURI));
583 assert(is_string($localName));
584
585 $ret = array();
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
610 public static function extractStrings(\DOMElement $parent, $namespaceURI, $localName)
611 {
612 assert(is_string($namespaceURI));
613 assert(is_string($localName));
614
615 $ret = array();
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
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
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
683 public static function createKeyDescriptor($x509Data)
684 {
685 assert(is_string($x509Data));
686
687 $x509Certificate = new X509Certificate();
688 $x509Certificate->certificate = $x509Data;
689
690 $x509Data = new X509Data();
691 $x509Data->data[] = $x509Certificate;
692
693 $keyInfo = new KeyInfo();
694 $keyInfo->info[] = $x509Data;
695
696 $keyDescriptor = new KeyDescriptor();
697 $keyDescriptor->KeyInfo = $keyInfo;
698
699 return $keyDescriptor;
700 }
701
721 public static function xsDateTimeToTimestamp($time)
722 {
723 $matches = array();
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
752 public static function getContainer()
753 {
754 return ContainerSingleton::getInstance();
755 }
756}
$n
Definition: RandomTest.php:85
An exception for terminatinating execution or to throw for unit testing.
getSymmetricKeySize()
Retrieve the key size for the symmetric encryption algorithm.
static createKeyDescriptor($x509Data)
Create a KeyDescriptor with the given certificate.
Definition: Utils.php:683
static xpQuery(\DOMNode $node, $query)
Do an XPath query on an XML node.
Definition: Utils.php:191
static addNameId(\DOMElement $node, array $nameId)
Create a NameID element.
Definition: Utils.php:314
static addString(\DOMElement $parent, $namespace, $name, $value)
Append string element.
Definition: Utils.php:635
static castKey(XMLSecurityKey $key, $algorithm, $type='public')
Helper function to convert a XMLSecurityKey to the correct algorithm.
Definition: Utils.php:112
static parseBoolean(\DOMElement $node, $attributeName, $default=null)
Parse a boolean attribute.
Definition: Utils.php:276
static decryptElement(\DOMElement $encryptedData, XMLSecurityKey $inputKey, array $blacklist=array())
Decrypt an encrypted element.
Definition: Utils.php:558
static addStrings(\DOMElement $parent, $namespace, $name, $localized, array $values)
Append string elements.
Definition: Utils.php:659
static extractLocalizedStrings(\DOMElement $parent, $namespaceURI, $localName)
Extract localized strings from a set of nodes.
Definition: Utils.php:580
static xsDateTimeToTimestamp($time)
This function converts a SAML2 timestamp on the form yyyy-mm-ddThh:mm:ss(.s+)?Z to a UNIX timestamp.
Definition: Utils.php:721
static parseNameId(\DOMElement $xml)
Parse a NameID element.
Definition: Utils.php:343
static extractStrings(\DOMElement $parent, $namespaceURI, $localName)
Extract strings from a set of nodes.
Definition: Utils.php:610
static getContainer()
Definition: Utils.php:752
static insertSignature(XMLSecurityKey $key, array $certificates, \DOMElement $root, \DOMNode $insertBefore=null)
Insert a Signature-node.
Definition: Utils.php:364
$key
Definition: croninfo.php:18
$i
Definition: disco.tpl.php:19
if($err=$client->getError()) $namespace
if(@file_exists(dirname(__FILE__).'/lang/eng.php')) $certificate
Definition: example_052.php:77
$time
Definition: cron.php:21
if($format !==null) $name
Definition: metadata.php:146
$index
Definition: metadata.php:60
$xml
Definition: metadata.php:240
$nameId
Definition: saml2-acs.php:138
$info
Definition: index.php:5
$certificates
Definition: metarefresh.php:39
$ret
Definition: parser.php:6
$query
$type
$algo
Definition: pwgen.php:34
$results
Definition: svg-scanner.php:47