ILIAS  release_5-4 Revision v5.4.26-12-gabc799a52e6
Utils.php
Go to the documentation of this file.
1 <?php
2 
3 namespace SAML2;
4 
14 
20 class 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  ), 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, 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_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 
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(
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  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 }
if($err=$client->getError()) $namespace
getSymmetricKeySize()
Retrieve the key size for the symmetric encryption algorithm.
$type
static extractLocalizedStrings(\DOMElement $parent, $namespaceURI, $localName)
Extract localized strings from a set of nodes.
Definition: Utils.php:580
$certificates
Definition: metarefresh.php:39
$index
Definition: metadata.php:60
static extractStrings(\DOMElement $parent, $namespaceURI, $localName)
Extract strings from a set of nodes.
Definition: Utils.php:610
$nameId
Definition: saml2-acs.php:138
$algo
Definition: pwgen.php:34
if(@file_exists(dirname(__FILE__).'/lang/eng.php')) $certificate
Definition: example_052.php:77
$time
Definition: cron.php:21
static insertSignature(XMLSecurityKey $key, array $certificates, \DOMElement $root, \DOMNode $insertBefore=null)
Insert a Signature-node.
Definition: Utils.php:364
static decryptElement(\DOMElement $encryptedData, XMLSecurityKey $inputKey, array $blacklist=array())
Decrypt an encrypted element.
Definition: Utils.php:558
$values
static createKeyDescriptor($x509Data)
Create a KeyDescriptor with the given certificate.
Definition: Utils.php:683
static addStrings(\DOMElement $parent, $namespace, $name, $localized, array $values)
Append string elements.
Definition: Utils.php:659
static addString(\DOMElement $parent, $namespace, $name, $value)
Append string element.
Definition: Utils.php:635
static parseBoolean(\DOMElement $node, $attributeName, $default=null)
Parse a boolean attribute.
Definition: Utils.php:276
$query
$n
Definition: RandomTest.php:85
$root
Definition: sabredav.php:45
static xpQuery(\DOMNode $node, $query)
Do an XPath query on an XML node.
Definition: Utils.php:191
$default
Definition: build.php:20
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
$results
Definition: svg-scanner.php:47
$ret
Definition: parser.php:6
$i
Definition: disco.tpl.php:19
$info
Definition: index.php:5
static parseNameId(\DOMElement $xml)
Parse a NameID element.
Definition: Utils.php:343
$key
Definition: croninfo.php:18
static castKey(XMLSecurityKey $key, $algorithm, $type='public')
Helper function to convert a XMLSecurityKey to the correct algorithm.
Definition: Utils.php:112
static addNameId(\DOMElement $node, array $nameId)
Create a NameID element.
Definition: Utils.php:314
static getContainer()
Definition: Utils.php:752