Viewing file: Utils.php (20.21 KB) -rwxrwxr-x Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
<?php
/** * Helper functions for the SAML2 library. * * @package simpleSAMLphp * @version $Id$ */ class SAML2_Utils {
/** * Check the Signature in a XML element. * * This function expects the XML element to contain a Signature-element * which contains a reference to the XML-element. This is common for both * messages and assertions. * * Note that this function only validates the element itself. It does not * check this against any local keys. * * If no Signature-element is located, this function will return FALSE. All * other validation errors result in an exception. On successful validation * an array will be returned. This array contains the information required to * check the signature against a public key. * * @param DOMElement $root The element which should be validated. * @return array|FALSE An array with information about the Signature-element. */ public static function validateElement(DOMElement $root) {
/* Create an XML security object. */ $objXMLSecDSig = new XMLSecurityDSig();
/* Both SAML messages and SAML assertions use the 'ID' attribute. */ $objXMLSecDSig->idKeys[] = 'ID';
/* Locate the XMLDSig Signature element to be used. */ $signatureElement = self::xpQuery($root, './ds:Signature'); if (count($signatureElement) === 0) { /* We don't have a signature element ot validate. */ return FALSE; } elseif (count($signatureElement) > 1) { throw new Exception('XMLSec: more than one signature element in root.'); } $signatureElement = $signatureElement[0]; $objXMLSecDSig->sigNode = $signatureElement;
/* Canonicalize the XMLDSig SignedInfo element in the message. */ $objXMLSecDSig->canonicalizeSignedInfo();
/* Validate referenced xml nodes. */ if (!$objXMLSecDSig->validateReference()) { throw new Exception('XMLsec: digest validation failed'); }
/* Check that $root is one of the signed nodes. */ $rootSigned = FALSE; foreach ($objXMLSecDSig->getValidatedNodes() as $signedNode) { if ($signedNode->isSameNode($root)) { $rootSigned = TRUE; break; } elseif ($root->parentNode instanceof DOMDocument && $signedNode->isSameNode($root->ownerDocument)) { /* $root is the root element of a signed document. */ $rootSigned = TRUE; break; } } if (!$rootSigned) { throw new Exception('XMLSec: The root element is not signed.'); }
/* Now we extract all available X509 certificates in the signature element. */ $certificates = array(); foreach (self::xpQuery($signatureElement, './ds:KeyInfo/ds:X509Data/ds:X509Certificate') as $certNode) { $certData = trim($certNode->textContent); $certData = str_replace(array("\r", "\n", "\t", ' '), '', $certData); $certificates[] = $certData; }
$ret = array( 'Signature' => $objXMLSecDSig, 'Certificates' => $certificates, );
return $ret; }
/** * Helper function to convert a XMLSecurityKey to the correct algorithm. * * @param XMLSecurityKey $key The key. * @param string $algorithm The desired algorithm. * @param string $types Public or private key, defaults to public. * @return XMLSecurityKey The new key. */ public static function castKey(XMLSecurityKey $key, $algorithm, $type = 'public') { assert('is_string($algorithm)'); assert('$type === "public" || $type === "private"');
// do nothing if algorithm is already the type of the key if ($key->type === $algorithm) { return $key; }
$keyInfo = openssl_pkey_get_details($key->key); if ($keyInfo === FALSE) { throw new Exception('Unable to get key details from XMLSecurityKey.'); } if (!isset($keyInfo['key'])) { throw new Exception('Missing key in public key details.'); }
$newKey = new XMLSecurityKey($algorithm, array('type'=>$type)); $newKey->loadKey($keyInfo['key']); return $newKey; }
/** * Check a signature against a key. * * An exception is thrown if we are unable to validate the signature. * * @param array $info The information returned by the validateElement()-function. * @param XMLSecurityKey $key The publickey that should validate the Signature object. */ public static function validateSignature(array $info, XMLSecurityKey $key) { assert('array_key_exists("Signature", $info)');
$objXMLSecDSig = $info['Signature'];
$sigMethod = self::xpQuery($objXMLSecDSig->sigNode, './ds:SignedInfo/ds:SignatureMethod'); if (empty($sigMethod)) { throw new Exception('Missing SignatureMethod element.'); } $sigMethod = $sigMethod[0]; if (!$sigMethod->hasAttribute('Algorithm')) { throw new Exception('Missing Algorithm-attribute on SignatureMethod element.'); } $algo = $sigMethod->getAttribute('Algorithm');
if ($key->type === XMLSecurityKey::RSA_SHA1 && $algo !== $key->type) { $key = self::castKey($key, $algo); }
/* Check the signature. */ if (! $objXMLSecDSig->verify($key)) { throw new Exception("Unable to validate Signature"); } }
/** * Do an XPath query on an XML node. * * @param DOMNode $node The XML node. * @param string $query The query. * @return array Array with matching DOM nodes. */ public static function xpQuery(DOMNode $node, $query) { assert('is_string($query)'); static $xpCache = NULL;
if ($node instanceof DOMDocument) { $doc = $node; } else { $doc = $node->ownerDocument; }
if ($xpCache === NULL || !$xpCache->document->isSameNode($doc)) { $xpCache = new DOMXPath($doc); $xpCache->registerNamespace('soap-env', SAML2_Const::NS_SOAP); $xpCache->registerNamespace('saml_protocol', SAML2_Const::NS_SAMLP); $xpCache->registerNamespace('saml_assertion', SAML2_Const::NS_SAML); $xpCache->registerNamespace('saml_metadata', SAML2_Const::NS_MD); $xpCache->registerNamespace('ds', XMLSecurityDSig::XMLDSIGNS); $xpCache->registerNamespace('xenc', XMLSecEnc::XMLENCNS); }
$results = $xpCache->query($query, $node); $ret = array(); for ($i = 0; $i < $results->length; $i++) { $ret[$i] = $results->item($i); }
return $ret; }
/** * Make an exact copy the specific DOMElement. * * @param DOMElement $element The element we should copy. * @param DOMElement|NULL $parent The target parent element. * @return DOMElement The copied element. */ public static function copyElement(DOMElement $element, DOMElement $parent = NULL) {
if ($parent === NULL) { $document = new DOMDocument(); } else { $document = $parent->ownerDocument; }
$namespaces = array(); for ($e = $element; $e !== NULL; $e = $e->parentNode) { foreach (SAML2_Utils::xpQuery($e, './namespace::*') as $ns) { $prefix = $ns->localName; if ($prefix === 'xml' || $prefix === 'xmlns') { continue; } $uri = $ns->nodeValue; if (!isset($namespaces[$prefix])) { $namespaces[$prefix] = $uri; } } }
$newElement = $document->importNode($element, TRUE); if ($parent !== NULL) { /* We need to append the child to the parent before we add the namespaces. */ $parent->appendChild($newElement); }
foreach ($namespaces as $prefix => $uri) { $newElement->setAttributeNS($uri, $prefix . ':__ns_workaround__', 'tmp'); $newElement->removeAttributeNS($uri, '__ns_workaround__'); }
return $newElement; }
/** * Parse a boolean attribute. * * @param DOMElement $node The element we should fetch the attribute from. * @param string $attributeName The name of the attribute. * @param mixed $default The value that should be returned if the attribute doesn't exist. * @return bool|mixed The value of the attribute, or $default if the attribute doesn't exist. */ public static function parseBoolean(DOMElement $node, $attributeName, $default = NULL) { assert('is_string($attributeName)');
if (!$node->hasAttribute($attributeName)) { return $default; } $value = $node->getAttribute($attributeName); switch (strtolower($value)) { case '0': case 'false': return FALSE; case '1': case 'true': return TRUE; default: throw new Exception('Invalid value of boolean attribute ' . var_export($attributeName, TRUE) . ': ' . var_export($value, TRUE)); } }
/** * Create a NameID element. * * The NameId array can have the following elements: 'Value', 'Format', * 'NameQualifier, 'SPNameQualifier' * * Only the 'Value'-element is required. * * @param DOMElement $node The DOM node we should append the NameId to. * @param array $nameId The name identifier. */ public static function addNameId(DOMElement $node, array $nameId) { assert('array_key_exists("Value", $nameId)');
$xml = SAML2_Utils::addString($node, SAML2_Const::NS_SAML, 'saml:NameID', $nameId['Value']);
if (array_key_exists('NameQualifier', $nameId) && $nameId['NameQualifier'] !== NULL) { $xml->setAttribute('NameQualifier', $nameId['NameQualifier']); } if (array_key_exists('SPNameQualifier', $nameId) && $nameId['SPNameQualifier'] !== NULL) { $xml->setAttribute('SPNameQualifier', $nameId['SPNameQualifier']); } if (array_key_exists('Format', $nameId) && $nameId['Format'] !== NULL) { $xml->setAttribute('Format', $nameId['Format']); } }
/** * Parse a NameID element. * * @param DOMElement $xml The DOM element we should parse. * @return array The parsed name identifier. */ public static function parseNameId(DOMElement $xml) {
$ret = array('Value' => trim($xml->textContent));
foreach (array('NameQualifier', 'SPNameQualifier', 'Format') as $attr) { if ($xml->hasAttribute($attr)) { $ret[$attr] = $xml->getAttribute($attr); } }
return $ret; }
/** * Insert a Signature-node. * * @param XMLSecurityKey $key The key we should use to sign the message. * @param array $certificates The certificates we should add to the signature node. * @param DOMElement $root The XML node we should sign. * @param DomElement $insertBefore The XML element we should insert the signature element before. */ public static function insertSignature(XMLSecurityKey $key, array $certificates, DOMElement $root, DOMNode $insertBefore = NULL) {
$objXMLSecDSig = new XMLSecurityDSig(); $objXMLSecDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N);
switch ($key->type) { case XMLSecurityKey::RSA_SHA256: $type = XMLSecurityDSig::SHA256; break; case XMLSecurityKey::RSA_SHA384: $type = XMLSecurityDSig::SHA384; break; case XMLSecurityKey::RSA_SHA512: $type = XMLSecurityDSig::SHA512; break; default: $type = XMLSecurityDSig::SHA1; }
$objXMLSecDSig->addReferenceList( array($root), $type, array('http://www.w3.org/2000/09/xmldsig#enveloped-signature', XMLSecurityDSig::EXC_C14N), array('id_name' => 'ID', 'overwrite' => FALSE) );
$objXMLSecDSig->sign($key);
foreach ($certificates as $certificate) { $objXMLSecDSig->add509Cert($certificate, TRUE); }
$objXMLSecDSig->insertSignature($root, $insertBefore);
}
/** * Decrypt an encrypted element. * * This is an internal helper function. * * @param DOMElement $encryptedData The encrypted data. * @param XMLSecurityKey $inputKey The decryption key. * @param array &$blacklist Blacklisted decryption algorithms. * @return DOMElement The decrypted element. */ private static function _decryptElement(DOMElement $encryptedData, XMLSecurityKey $inputKey, array &$blacklist) {
$enc = new XMLSecEnc();
$enc->setNode($encryptedData); $enc->type = $encryptedData->getAttribute("Type");
$symmetricKey = $enc->locateKey($encryptedData); if (!$symmetricKey) { throw new Exception('Could not locate key algorithm in encrypted data.'); }
$symmetricKeyInfo = $enc->locateKeyInfo($symmetricKey); if (!$symmetricKeyInfo) { throw new Exception('Could not locate <dsig:KeyInfo> for the encrypted key.'); }
$inputKeyAlgo = $inputKey->getAlgorith(); if ($symmetricKeyInfo->isEncrypted) { $symKeyInfoAlgo = $symmetricKeyInfo->getAlgorith();
if (in_array($symKeyInfoAlgo, $blacklist, TRUE)) { throw new Exception('Algorithm disabled: ' . var_export($symKeyInfoAlgo, TRUE)); }
if ($symKeyInfoAlgo === XMLSecurityKey::RSA_OAEP_MGF1P && $inputKeyAlgo === XMLSecurityKey::RSA_1_5) { /* * The RSA key formats are equal, so loading an RSA_1_5 key * into an RSA_OAEP_MGF1P key can be done without problems. * We therefore pretend that the input key is an * RSA_OAEP_MGF1P key. */ $inputKeyAlgo = XMLSecurityKey::RSA_OAEP_MGF1P; }
/* Make sure that the input key format is the same as the one used to encrypt the key. */ if ($inputKeyAlgo !== $symKeyInfoAlgo) { throw new Exception('Algorithm mismatch between input key and key used to encrypt ' . ' the symmetric key for the message. Key was: ' . var_export($inputKeyAlgo, TRUE) . '; message was: ' . var_export($symKeyInfoAlgo, TRUE)); }
$encKey = $symmetricKeyInfo->encryptedCtx; $symmetricKeyInfo->key = $inputKey->key;
$keySize = $symmetricKey->getSymmetricKeySize(); if ($keySize === NULL) { /* To protect against "key oracle" attacks, we need to be able to create a * symmetric key, and for that we need to know the key size. */ throw new Exception('Unknown key size for encryption algorithm: ' . var_export($symmetricKey->type, TRUE)); }
try { $key = $encKey->decryptKey($symmetricKeyInfo); if (strlen($key) != $keySize) { throw new Exception('Unexpected key size (' . strlen($key) * 8 . 'bits) for encryption algorithm: ' . var_export($symmetricKey->type, TRUE)); } } catch (Exception $e) { /* We failed to decrypt this key. Log it, and substitute a "random" key. */ SimpleSAML_Logger::error('Failed to decrypt symmetric key: ' . $e->getMessage()); /* Create a replacement key, so that it looks like we fail in the same way as if the key was correctly padded. */
/* We base the symmetric key on the encrypted key and private key, so that we always behave the * same way for a given input key. */ $encryptedKey = $encKey->getCipherValue(); $pkey = openssl_pkey_get_details($symmetricKeyInfo->key); $pkey = sha1(serialize($pkey), TRUE); $key = sha1($encryptedKey . $pkey, TRUE);
/* Make sure that the key has the correct length. */ if (strlen($key) > $keySize) { $key = substr($key, 0, $keySize); } elseif (strlen($key) < $keySize) { $key = str_pad($key, $keySize); } } $symmetricKey->loadkey($key);
} else { $symKeyAlgo = $symmetricKey->getAlgorith(); /* Make sure that the input key has the correct format. */ if ($inputKeyAlgo !== $symKeyAlgo) { throw new Exception('Algorithm mismatch between input key and key in message. ' . 'Key was: ' . var_export($inputKeyAlgo, TRUE) . '; message was: ' . var_export($symKeyAlgo, TRUE)); } $symmetricKey = $inputKey; }
$algorithm = $symmetricKey->getAlgorith(); if (in_array($algorithm, $blacklist, TRUE)) { throw new Exception('Algorithm disabled: ' . var_export($algorithm, TRUE)); }
$decrypted = $enc->decryptNode($symmetricKey, FALSE);
/* * This is a workaround for the case where only a subset of the XML * tree was serialized for encryption. In that case, we may miss the * namespaces needed to parse the XML. */ $xml = '<root xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'.$decrypted.'</root>'; $newDoc = new DOMDocument(); if (!@$newDoc->loadXML($xml)) { throw new Exception('Failed to parse decrypted XML. Maybe the wrong sharedkey was used?'); } $decryptedElement = $newDoc->firstChild->firstChild; if ($decryptedElement === NULL) { throw new Exception('Missing encrypted element.'); }
if (!($decryptedElement instanceof DOMElement)) { throw new Exception('Decrypted element was not actually a DOMElement.'); }
return $decryptedElement; }
/** * Decrypt an encrypted element. * * @param DOMElement $encryptedData The encrypted data. * @param XMLSecurityKey $inputKey The decryption key. * @param array $blacklist Blacklisted decryption algorithms. * @return DOMElement The decrypted element. */ public static function decryptElement(DOMElement $encryptedData, XMLSecurityKey $inputKey, array $blacklist = array()) {
try { return self::_decryptElement($encryptedData, $inputKey, $blacklist); } catch (Exception $e) { /* * Something went wrong during decryption, but for security * reasons we cannot tell the user what failed. */ SimpleSAML_Logger::error('Decryption failed: ' . $e->getMessage()); throw new Exception('Failed to decrypt XML element.'); } }
/** * Extract localized strings from a set of nodes. * * @param DOMElement $parent The element that contains the localized strings. * @param string $namespaceURI The namespace URI the localized strings should have. * @param string $localName The localName of the localized strings. * @return array Localized strings. */ public static function extractLocalizedStrings(DOMElement $parent, $namespaceURI, $localName) { assert('is_string($namespaceURI)'); assert('is_string($localName)');
$ret = array(); for ($node = $parent->firstChild; $node !== NULL; $node = $node->nextSibling) { if ($node->namespaceURI !== $namespaceURI || $node->localName !== $localName) { continue; }
if ($node->hasAttribute('xml:lang')) { $language = $node->getAttribute('xml:lang'); } else { $language = 'en'; } $ret[$language] = trim($node->textContent); }
return $ret; }
/** * Extract strings from a set of nodes. * * @param DOMElement $parent The element that contains the localized strings. * @param string $namespaceURI The namespace URI the string elements should have. * @param string $localName The localName of the string elements. * @return array The string values of the various nodes. */ public static function extractStrings(DOMElement $parent, $namespaceURI, $localName) { assert('is_string($namespaceURI)'); assert('is_string($localName)');
$ret = array(); for ($node = $parent->firstChild; $node !== NULL; $node = $node->nextSibling) { if ($node->namespaceURI !== $namespaceURI || $node->localName !== $localName) { continue; } $ret[] = trim($node->textContent); }
return $ret; }
/** * Append string element. * * @param DOMElement $parent The parent element we should append the new nodes to. * @param string $namespace The namespace of the created element. * @param string $name The name of the created element. * @param string $value The value of the element. * @return DOMElement The generated element. */ public static function addString(DOMElement $parent, $namespace, $name, $value) { assert('is_string($namespace)'); assert('is_string($name)'); assert('is_string($value)');
$doc = $parent->ownerDocument;
$n = $doc->createElementNS($namespace, $name); $n->appendChild($doc->createTextNode($value)); $parent->appendChild($n);
return $n; }
/** * Append string elements. * * @param DOMElement $parent The parent element we should append the new nodes to. * @param string $namespace The namespace of the created elements * @param string $name The name of the created elements * @param bool $localized Whether the strings are localized, and should include the xml:lang attribute. * @param array $values The values we should create the elements from. */ public static function addStrings(DOMElement $parent, $namespace, $name, $localized, array $values) { assert('is_string($namespace)'); assert('is_string($name)'); assert('is_bool($localized)');
$doc = $parent->ownerDocument;
foreach ($values as $index => $value) { $n = $doc->createElementNS($namespace, $name); $n->appendChild($doc->createTextNode($value)); if ($localized) { $n->setAttribute('xml:lang', $index); } $parent->appendChild($n); } }
/** * Create a KeyDescriptor with the given certificate. * * @param string $x509Data The certificate, as a base64-encoded DER data. * @return SAML2_XML_md_KeyDescriptor The keydescriptor. */ public static function createKeyDescriptor($x509Data) { assert('is_string($x509Data)');
$x509Certificate = new SAML2_XML_ds_X509Certificate(); $x509Certificate->certificate = $x509Data;
$x509Data = new SAML2_XML_ds_X509Data(); $x509Data->data[] = $x509Certificate;
$keyInfo = new SAML2_XML_ds_KeyInfo(); $keyInfo->info[] = $x509Data;
$keyDescriptor = new SAML2_XML_md_KeyDescriptor(); $keyDescriptor->KeyInfo = $keyInfo;
return $keyDescriptor; }
}
|