From 33c68d56e7f51db60d64628736244bc9cc1ee099 Mon Sep 17 00:00:00 2001 From: Sergio Gomez <sergio@uco.es> Date: Mon, 27 Mar 2017 20:43:06 +0200 Subject: [PATCH] SimpleSAML_XML_* classes refactorized to PSR-2 --- lib/SimpleSAML/XML/Errors.php | 244 +++--- lib/SimpleSAML/XML/Parser.php | 118 +-- lib/SimpleSAML/XML/Shib13/AuthnRequest.php | 78 +- lib/SimpleSAML/XML/Shib13/AuthnResponse.php | 798 ++++++++++---------- lib/SimpleSAML/XML/Signer.php | 361 ++++----- lib/SimpleSAML/XML/Validator.php | 649 ++++++++-------- 6 files changed, 1143 insertions(+), 1105 deletions(-) diff --git a/lib/SimpleSAML/XML/Errors.php b/lib/SimpleSAML/XML/Errors.php index a6d14dd9f..48d93b73d 100644 --- a/lib/SimpleSAML/XML/Errors.php +++ b/lib/SimpleSAML/XML/Errors.php @@ -12,124 +12,128 @@ namespace SimpleSAML\XML; -class Errors { - - /** - * This is an stack of error logs. The topmost element is the one we are - * currently working on. - */ - private static $errorStack = array(); - - /** - * This is the xml error state we had before we began logging. - */ - private static $xmlErrorState; - - - /** - * Append current XML errors to the the current stack level. - */ - private static function addErrors() { - - $currentErrors = libxml_get_errors(); - libxml_clear_errors(); - - $level = count(self::$errorStack) - 1; - self::$errorStack[$level] = array_merge(self::$errorStack[$level], $currentErrors); - } - - - /** - * Start error logging. - * - * A call to this function will begin a new error logging context. Every call must have - * a corresponding call to end(). - */ - public static function begin() { - - // Check whether the error access functions are present - if(!function_exists('libxml_use_internal_errors')) { - return; - } - - if(count(self::$errorStack) === 0) { - // No error logging is currently in progress. Initialize it. - self::$xmlErrorState = libxml_use_internal_errors(TRUE); - libxml_clear_errors(); - } else { - /* We have already started error logging. Append the current errors to the - * list of errors in this level. - */ - self::addErrors(); - } - - // Add a new level to the error stack - self::$errorStack[] = array(); - } - - - /** - * End error logging. - * - * @return array An array with the LibXMLErrors which has occurred since begin() was called. - */ - public static function end() { - - // Check whether the error access functions are present - if(!function_exists('libxml_use_internal_errors')) { - // Pretend that no errors occurred - return array(); - } - - // Add any errors which may have occurred - self::addErrors(); - - - $ret = array_pop(self::$errorStack); - - if(count(self::$errorStack) === 0) { - // Disable our error logging and restore the previous state - libxml_use_internal_errors(self::$xmlErrorState); - } - - return $ret; - } - - - /** - * Format an error as a string. - * - * This function formats the given LibXMLError object as a string. - * - * @param $error \LibXMLError The LibXMLError which should be formatted. - * @return string A string representing the given LibXMLError. - */ - public static function formatError($error) { - assert('$error instanceof LibXMLError'); - return 'level=' . $error->level . ',code=' . $error->code . ',line=' . $error->line . ',col=' . $error->column . - ',msg=' . trim($error->message); - } - - - /** - * Format a list of errors as a string. - * - * This fucntion takes an array of LibXMLError objects and creates a string with all the errors. - * Each error will be separated by a newline, and the string will end with a newline-character. - * - * @param $errors array An array of errors. - * @return string A string representing the errors. An empty string will be returned if there were no - * errors in the array. - */ - public static function formatErrors($errors) { - assert('is_array($errors)'); - - $ret = ''; - foreach($errors as $error) { - $ret .= self::formatError($error) . "\n"; - } - - return $ret; - } - +class Errors +{ + + /** + * This is an stack of error logs. The topmost element is the one we are + * currently working on. + */ + private static $errorStack = array(); + + /** + * This is the xml error state we had before we began logging. + */ + private static $xmlErrorState; + + + /** + * Append current XML errors to the the current stack level. + */ + private static function addErrors() + { + $currentErrors = libxml_get_errors(); + libxml_clear_errors(); + + $level = count(self::$errorStack) - 1; + self::$errorStack[$level] = array_merge(self::$errorStack[$level], $currentErrors); + } + + + /** + * Start error logging. + * + * A call to this function will begin a new error logging context. Every call must have + * a corresponding call to end(). + */ + public static function begin() + { + + // Check whether the error access functions are present + if (!function_exists('libxml_use_internal_errors')) { + return; + } + + if (count(self::$errorStack) === 0) { + // No error logging is currently in progress. Initialize it. + self::$xmlErrorState = libxml_use_internal_errors(true); + libxml_clear_errors(); + } else { + /* We have already started error logging. Append the current errors to the + * list of errors in this level. + */ + self::addErrors(); + } + + // Add a new level to the error stack + self::$errorStack[] = array(); + } + + + /** + * End error logging. + * + * @return array An array with the LibXMLErrors which has occurred since begin() was called. + */ + public static function end() + { + + // Check whether the error access functions are present + if (!function_exists('libxml_use_internal_errors')) { + // Pretend that no errors occurred + return array(); + } + + // Add any errors which may have occurred + self::addErrors(); + + + $ret = array_pop(self::$errorStack); + + if (count(self::$errorStack) === 0) { + // Disable our error logging and restore the previous state + libxml_use_internal_errors(self::$xmlErrorState); + } + + return $ret; + } + + + /** + * Format an error as a string. + * + * This function formats the given LibXMLError object as a string. + * + * @param $error \LibXMLError The LibXMLError which should be formatted. + * @return string A string representing the given LibXMLError. + */ + public static function formatError($error) + { + assert('$error instanceof LibXMLError'); + return 'level=' . $error->level . ',code=' . $error->code . ',line=' . $error->line . ',col=' . $error->column . + ',msg=' . trim($error->message); + } + + + /** + * Format a list of errors as a string. + * + * This fucntion takes an array of LibXMLError objects and creates a string with all the errors. + * Each error will be separated by a newline, and the string will end with a newline-character. + * + * @param $errors array An array of errors. + * @return string A string representing the errors. An empty string will be returned if there were no + * errors in the array. + */ + public static function formatErrors($errors) + { + assert('is_array($errors)'); + + $ret = ''; + foreach ($errors as $error) { + $ret .= self::formatError($error) . "\n"; + } + + return $ret; + } } diff --git a/lib/SimpleSAML/XML/Parser.php b/lib/SimpleSAML/XML/Parser.php index d36beb4fd..b43fe49a7 100644 --- a/lib/SimpleSAML/XML/Parser.php +++ b/lib/SimpleSAML/XML/Parser.php @@ -9,59 +9,69 @@ namespace SimpleSAML\XML; -class Parser { +class Parser +{ + public $simplexml = null; - var $simplexml = null; - - function __construct($xml) {; - $this->simplexml = new \SimpleXMLElement($xml); - $this->simplexml->registerXPathNamespace('saml2', 'urn:oasis:names:tc:SAML:2.0:assertion'); - $this->simplexml->registerXPathNamespace('saml2meta', 'urn:oasis:names:tc:SAML:2.0:metadata'); - $this->simplexml->registerXPathNamespace('ds', 'http://www.w3.org/2000/09/xmldsig#'); - - } - - public static function fromSimpleXMLElement(\SimpleXMLElement $element) { - - // Traverse all existing namespaces in element - $namespaces = $element->getNamespaces(); - foreach ($namespaces AS $prefix => $ns) { - $element[(($prefix === '') ? 'xmlns' : 'xmlns:' . $prefix)] = $ns; - } - - /* Create a new parser with the xml document where the namespace definitions - * are added. - */ - $parser = new Parser($element->asXML()); - return $parser; - - } - - public function getValueDefault($xpath, $defvalue) { - try { - return $this->getValue($xpath, true); - } catch (\Exception $e) { - return $defvalue; - } - } - - public function getValue($xpath, $required = false) { - - $result = $this->simplexml->xpath($xpath); - if (! $result or !is_array($result)) { - if ($required) throw new \Exception('Could not get value from XML document using the following XPath expression: ' . $xpath); - else return null; - } - return (string) $result[0]; - } - - public function getValueAlternatives(array $xpath, $required = false) { - foreach ($xpath AS $x) { - $seek = $this->getValue($x); - if ($seek) return $seek; - } - if ($required) throw new \Exception('Could not get value from XML document using multiple alternative XPath expressions.'); - else return null; - } - + public function __construct($xml) + { + ; + $this->simplexml = new \SimpleXMLElement($xml); + $this->simplexml->registerXPathNamespace('saml2', 'urn:oasis:names:tc:SAML:2.0:assertion'); + $this->simplexml->registerXPathNamespace('saml2meta', 'urn:oasis:names:tc:SAML:2.0:metadata'); + $this->simplexml->registerXPathNamespace('ds', 'http://www.w3.org/2000/09/xmldsig#'); + } + + public static function fromSimpleXMLElement(\SimpleXMLElement $element) + { + + // Traverse all existing namespaces in element + $namespaces = $element->getNamespaces(); + foreach ($namespaces as $prefix => $ns) { + $element[(($prefix === '') ? 'xmlns' : 'xmlns:' . $prefix)] = $ns; + } + + /* Create a new parser with the xml document where the namespace definitions + * are added. + */ + $parser = new Parser($element->asXML()); + return $parser; + } + + public function getValueDefault($xpath, $defvalue) + { + try { + return $this->getValue($xpath, true); + } catch (\Exception $e) { + return $defvalue; + } + } + + public function getValue($xpath, $required = false) + { + $result = $this->simplexml->xpath($xpath); + if (! $result or !is_array($result)) { + if ($required) { + throw new \Exception('Could not get value from XML document using the following XPath expression: ' . $xpath); + } else { + return null; + } + } + return (string) $result[0]; + } + + public function getValueAlternatives(array $xpath, $required = false) + { + foreach ($xpath as $x) { + $seek = $this->getValue($x); + if ($seek) { + return $seek; + } + } + if ($required) { + throw new \Exception('Could not get value from XML document using multiple alternative XPath expressions.'); + } else { + return null; + } + } } diff --git a/lib/SimpleSAML/XML/Shib13/AuthnRequest.php b/lib/SimpleSAML/XML/Shib13/AuthnRequest.php index 47fecc60e..f52fea212 100644 --- a/lib/SimpleSAML/XML/Shib13/AuthnRequest.php +++ b/lib/SimpleSAML/XML/Shib13/AuthnRequest.php @@ -1,7 +1,7 @@ <?php /** - * The Shibboleth 1.3 Authentication Request. Not part of SAML 1.1, + * The Shibboleth 1.3 Authentication Request. Not part of SAML 1.1, * but an extension using query paramters no XML. * * @author Andreas Ã…kre Solberg, UNINETT AS. <andreas.solberg@uninett.no> @@ -10,40 +10,44 @@ namespace SimpleSAML\XML\Shib13; -class AuthnRequest { - - private $issuer = null; - private $relayState = null; - - public function setRelayState($relayState) { - $this->relayState = $relayState; - } - - public function getRelayState() { - return $this->relayState; - } - - public function setIssuer($issuer) { - $this->issuer = $issuer; - } - public function getIssuer() { - return $this->issuer; - } - - public function createRedirect($destination, $shire) { - $metadata = \SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); - $idpmetadata = $metadata->getMetaDataConfig($destination, 'shib13-idp-remote'); - - $desturl = $idpmetadata->getDefaultEndpoint('SingleSignOnService', array('urn:mace:shibboleth:1.0:profiles:AuthnRequest')); - $desturl = $desturl['Location']; - - $target = $this->getRelayState(); - - $url = $desturl . '?' . - 'providerId=' . urlencode($this->getIssuer()) . - '&shire=' . urlencode($shire) . - (isset($target) ? '&target=' . urlencode($target) : ''); - return $url; - } - +class AuthnRequest +{ + private $issuer = null; + private $relayState = null; + + public function setRelayState($relayState) + { + $this->relayState = $relayState; + } + + public function getRelayState() + { + return $this->relayState; + } + + public function setIssuer($issuer) + { + $this->issuer = $issuer; + } + public function getIssuer() + { + return $this->issuer; + } + + public function createRedirect($destination, $shire) + { + $metadata = \SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); + $idpmetadata = $metadata->getMetaDataConfig($destination, 'shib13-idp-remote'); + + $desturl = $idpmetadata->getDefaultEndpoint('SingleSignOnService', array('urn:mace:shibboleth:1.0:profiles:AuthnRequest')); + $desturl = $desturl['Location']; + + $target = $this->getRelayState(); + + $url = $desturl . '?' . + 'providerId=' . urlencode($this->getIssuer()) . + '&shire=' . urlencode($shire) . + (isset($target) ? '&target=' . urlencode($target) : ''); + return $url; + } } diff --git a/lib/SimpleSAML/XML/Shib13/AuthnResponse.php b/lib/SimpleSAML/XML/Shib13/AuthnResponse.php index 17ae3b90b..a8f4fc32b 100644 --- a/lib/SimpleSAML/XML/Shib13/AuthnResponse.php +++ b/lib/SimpleSAML/XML/Shib13/AuthnResponse.php @@ -1,5 +1,5 @@ <?php - + /** * A Shibboleth 1.3 authentication response. * @@ -9,7 +9,6 @@ namespace SimpleSAML\XML\Shib13; - use SAML2\DOMDocumentFactory; use SAML2\Utils; use SimpleSAML\Utils\Config; @@ -17,140 +16,146 @@ use SimpleSAML\Utils\Random; use SimpleSAML\Utils\Time; use SimpleSAML\XML\Validator; -class AuthnResponse { +class AuthnResponse +{ - /** - * @var Validator This variable contains an XML validator for this message. - */ + /** + * @var Validator This variable contains an XML validator for this message. + */ private $validator = null; - /** - * Whether this response was validated by some external means (e.g. SSL). - * - * @var bool - */ - private $messageValidated = FALSE; - - - const SHIB_PROTOCOL_NS = 'urn:oasis:names:tc:SAML:1.0:protocol'; - const SHIB_ASSERT_NS = 'urn:oasis:names:tc:SAML:1.0:assertion'; - - - /** - * The DOMDocument which represents this message. - * - * @var \DOMDocument - */ - private $dom; - - /** - * The relaystate which is associated with this response. - * - * @var string|NULL - */ - private $relayState = null; - - - /** - * Set whether this message was validated externally. - * - * @param bool $messageValidated TRUE if the message is already validated, FALSE if not. - */ - public function setMessageValidated($messageValidated) { - assert('is_bool($messageValidated)'); - - $this->messageValidated = $messageValidated; - } - - - public function setXML($xml) { - assert('is_string($xml)'); + /** + * Whether this response was validated by some external means (e.g. SSL). + * + * @var bool + */ + private $messageValidated = false; - try { - $this->dom = DOMDocumentFactory::fromString(str_replace ("\r", "", $xml)); - } catch(\Exception $e) { - throw new \Exception('Unable to parse AuthnResponse XML.'); - } - } - public function setRelayState($relayState) { - $this->relayState = $relayState; - } + const SHIB_PROTOCOL_NS = 'urn:oasis:names:tc:SAML:1.0:protocol'; + const SHIB_ASSERT_NS = 'urn:oasis:names:tc:SAML:1.0:assertion'; - public function getRelayState() { - return $this->relayState; - } - public function validate() { - assert('$this->dom instanceof DOMDocument'); + /** + * The DOMDocument which represents this message. + * + * @var \DOMDocument + */ + private $dom; - if ($this->messageValidated) { - // This message was validated externally - return TRUE; - } + /** + * The relaystate which is associated with this response. + * + * @var string|NULL + */ + private $relayState = null; - // Validate the signature - $this->validator = new Validator($this->dom, array('ResponseID', 'AssertionID')); - // Get the issuer of the response - $issuer = $this->getIssuer(); + /** + * Set whether this message was validated externally. + * + * @param bool $messageValidated TRUE if the message is already validated, FALSE if not. + */ + public function setMessageValidated($messageValidated) + { + assert('is_bool($messageValidated)'); + + $this->messageValidated = $messageValidated; + } + + + public function setXML($xml) + { + assert('is_string($xml)'); + + try { + $this->dom = DOMDocumentFactory::fromString(str_replace("\r", "", $xml)); + } catch (\Exception $e) { + throw new \Exception('Unable to parse AuthnResponse XML.'); + } + } + + public function setRelayState($relayState) + { + $this->relayState = $relayState; + } + + public function getRelayState() + { + return $this->relayState; + } + + public function validate() + { + assert('$this->dom instanceof DOMDocument'); + + if ($this->messageValidated) { + // This message was validated externally + return true; + } + + // Validate the signature + $this->validator = new Validator($this->dom, array('ResponseID', 'AssertionID')); + + // Get the issuer of the response + $issuer = $this->getIssuer(); + + // Get the metadata of the issuer + $metadata = \SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); + $md = $metadata->getMetaDataConfig($issuer, 'shib13-idp-remote'); + + $publicKeys = $md->getPublicKeys('signing'); + if ($publicKeys !== null) { + $certFingerprints = array(); + foreach ($publicKeys as $key) { + if ($key['type'] !== 'X509Certificate') { + continue; + } + $certFingerprints[] = sha1(base64_decode($key['X509Certificate'])); + } + $this->validator->validateFingerprint($certFingerprints); + } elseif ($md->hasValue('certFingerprint')) { + $certFingerprints = $md->getArrayizeString('certFingerprint'); + + // Validate the fingerprint + $this->validator->validateFingerprint($certFingerprints); + } elseif ($md->hasValue('caFile')) { + // Validate against CA + $this->validator->validateCA(Config::getCertPath($md->getString('caFile'))); + } else { + throw new \SimpleSAML_Error_Exception('Missing certificate in Shibboleth 1.3 IdP Remote metadata for identity provider [' . $issuer . '].'); + } + + return true; + } + + + /* Checks if the given node is validated by the signature on this response. + * + * Returns: + * TRUE if the node is validated or FALSE if not. + */ + private function isNodeValidated($node) + { + if ($this->messageValidated) { + // This message was validated externally + return true; + } - // Get the metadata of the issuer - $metadata = \SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); - $md = $metadata->getMetaDataConfig($issuer, 'shib13-idp-remote'); + if ($this->validator === null) { + return false; + } - $publicKeys = $md->getPublicKeys('signing'); - if ($publicKeys !== NULL) { - $certFingerprints = array(); - foreach ($publicKeys as $key) { - if ($key['type'] !== 'X509Certificate') { - continue; - } - $certFingerprints[] = sha1(base64_decode($key['X509Certificate'])); - } - $this->validator->validateFingerprint($certFingerprints); - } elseif ($md->hasValue('certFingerprint')) { - $certFingerprints = $md->getArrayizeString('certFingerprint'); + // Convert the node to a DOM node if it is an element from SimpleXML + if ($node instanceof \SimpleXMLElement) { + $node = dom_import_simplexml($node); + } - // Validate the fingerprint - $this->validator->validateFingerprint($certFingerprints); - } elseif ($md->hasValue('caFile')) { - // Validate against CA - $this->validator->validateCA(Config::getCertPath($md->getString('caFile'))); - } else { - throw new \SimpleSAML_Error_Exception('Missing certificate in Shibboleth 1.3 IdP Remote metadata for identity provider [' . $issuer . '].'); - } + assert('$node instanceof DOMNode'); - return true; - } - - - /* Checks if the given node is validated by the signature on this response. - * - * Returns: - * TRUE if the node is validated or FALSE if not. - */ - private function isNodeValidated($node) { - - if ($this->messageValidated) { - // This message was validated externally - return TRUE; - } - - if($this->validator === NULL) { - return FALSE; - } - - // Convert the node to a DOM node if it is an element from SimpleXML - if($node instanceof \SimpleXMLElement) { - $node = dom_import_simplexml($node); - } - - assert('$node instanceof DOMNode'); - - return $this->validator->isNodeValidated($node); - } + return $this->validator->isNodeValidated($node); + } /** @@ -161,139 +166,138 @@ class AuthnResponse { * then the query will be relative to the root of the response. * @return \DOMNodeList */ - private function doXPathQuery($query, $node = NULL) { - assert('is_string($query)'); - assert('$this->dom instanceof DOMDocument'); - - if($node === NULL) { - $node = $this->dom->documentElement; - } - - assert('$node instanceof DOMNode'); - - $xPath = new \DOMXpath($this->dom); - $xPath->registerNamespace('shibp', self::SHIB_PROTOCOL_NS); - $xPath->registerNamespace('shib', self::SHIB_ASSERT_NS); - - return $xPath->query($query, $node); - } - - /** - * Retrieve the session index of this response. - * - * @return string|NULL The session index of this response. - */ - function getSessionIndex() { - assert('$this->dom instanceof DOMDocument'); - - $query = '/shibp:Response/shib:Assertion/shib:AuthnStatement'; - $nodelist = $this->doXPathQuery($query); - if ($node = $nodelist->item(0)) { - return $node->getAttribute('SessionIndex'); - } - - return NULL; - } - - - public function getAttributes() { - - $metadata = \SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); - $md = $metadata->getMetadata($this->getIssuer(), 'shib13-idp-remote'); - $base64 = isset($md['base64attributes']) ? $md['base64attributes'] : false; - - if (! ($this->dom instanceof \DOMDocument) ) { - return array(); - } - - $attributes = array(); - - $assertions = $this->doXPathQuery('/shibp:Response/shib:Assertion'); - - foreach ($assertions AS $assertion) { - - if(!$this->isNodeValidated($assertion)) { - throw new \Exception('Shib13 AuthnResponse contained an unsigned assertion.'); - } + private function doXPathQuery($query, $node = null) + { + assert('is_string($query)'); + assert('$this->dom instanceof DOMDocument'); - $conditions = $this->doXPathQuery('shib:Conditions', $assertion); - if ($conditions && $conditions->length > 0) { - $condition = $conditions->item(0); + if ($node === null) { + $node = $this->dom->documentElement; + } - $start = $condition->getAttribute('NotBefore'); - $end = $condition->getAttribute('NotOnOrAfter'); + assert('$node instanceof DOMNode'); - if ($start && $end) { - if (!self::checkDateConditions($start, $end)) { - error_log('Date check failed ... (from ' . $start . ' to ' . $end . ')'); - continue; - } - } - } + $xPath = new \DOMXpath($this->dom); + $xPath->registerNamespace('shibp', self::SHIB_PROTOCOL_NS); + $xPath->registerNamespace('shib', self::SHIB_ASSERT_NS); - $attribute_nodes = $this->doXPathQuery('shib:AttributeStatement/shib:Attribute/shib:AttributeValue', $assertion); - /** @var \DOMElement $attribute */ - foreach($attribute_nodes as $attribute) { + return $xPath->query($query, $node); + } - $value = $attribute->textContent; - $name = $attribute->parentNode->getAttribute('AttributeName'); - - if ($attribute->hasAttribute('Scope')) { - $scopePart = '@' . $attribute->getAttribute('Scope'); - } else { - $scopePart = ''; - } - - if(!is_string($name)) { - throw new \Exception('Shib13 Attribute node without an AttributeName.'); - } - - if(!array_key_exists($name, $attributes)) { - $attributes[$name] = array(); - } - - if ($base64) { - $encodedvalues = explode('_', $value); - foreach($encodedvalues AS $v) { - $attributes[$name][] = base64_decode($v) . $scopePart; - } - } else { - $attributes[$name][] = $value . $scopePart; - } - } - } - - return $attributes; - } - - - public function getIssuer() { - - $query = '/shibp:Response/shib:Assertion/@Issuer'; - $nodelist = $this->doXPathQuery($query); - - if ($attr = $nodelist->item(0)) { - return $attr->nodeValue; - } else { - throw new \Exception('Could not find Issuer field in Authentication response'); - } - - } - - public function getNameID() { - - $nameID = array(); - - $query = '/shibp:Response/shib:Assertion/shib:AuthenticationStatement/shib:Subject/shib:NameIdentifier'; - $nodelist = $this->doXPathQuery($query); - - if ($node = $nodelist->item(0)) { - $nameID["Value"] = $node->nodeValue; - $nameID["Format"] = $node->getAttribute('Format'); - } - - return $nameID; - } + /** + * Retrieve the session index of this response. + * + * @return string|NULL The session index of this response. + */ + public function getSessionIndex() + { + assert('$this->dom instanceof DOMDocument'); + + $query = '/shibp:Response/shib:Assertion/shib:AuthnStatement'; + $nodelist = $this->doXPathQuery($query); + if ($node = $nodelist->item(0)) { + return $node->getAttribute('SessionIndex'); + } + + return null; + } + + + public function getAttributes() + { + $metadata = \SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); + $md = $metadata->getMetadata($this->getIssuer(), 'shib13-idp-remote'); + $base64 = isset($md['base64attributes']) ? $md['base64attributes'] : false; + + if (! ($this->dom instanceof \DOMDocument)) { + return array(); + } + + $attributes = array(); + + $assertions = $this->doXPathQuery('/shibp:Response/shib:Assertion'); + + foreach ($assertions as $assertion) { + if (!$this->isNodeValidated($assertion)) { + throw new \Exception('Shib13 AuthnResponse contained an unsigned assertion.'); + } + + $conditions = $this->doXPathQuery('shib:Conditions', $assertion); + if ($conditions && $conditions->length > 0) { + $condition = $conditions->item(0); + + $start = $condition->getAttribute('NotBefore'); + $end = $condition->getAttribute('NotOnOrAfter'); + + if ($start && $end) { + if (!self::checkDateConditions($start, $end)) { + error_log('Date check failed ... (from ' . $start . ' to ' . $end . ')'); + continue; + } + } + } + + $attribute_nodes = $this->doXPathQuery('shib:AttributeStatement/shib:Attribute/shib:AttributeValue', $assertion); + /** @var \DOMElement $attribute */ + foreach ($attribute_nodes as $attribute) { + $value = $attribute->textContent; + $name = $attribute->parentNode->getAttribute('AttributeName'); + + if ($attribute->hasAttribute('Scope')) { + $scopePart = '@' . $attribute->getAttribute('Scope'); + } else { + $scopePart = ''; + } + + if (!is_string($name)) { + throw new \Exception('Shib13 Attribute node without an AttributeName.'); + } + + if (!array_key_exists($name, $attributes)) { + $attributes[$name] = array(); + } + + if ($base64) { + $encodedvalues = explode('_', $value); + foreach ($encodedvalues as $v) { + $attributes[$name][] = base64_decode($v) . $scopePart; + } + } else { + $attributes[$name][] = $value . $scopePart; + } + } + } + + return $attributes; + } + + + public function getIssuer() + { + $query = '/shibp:Response/shib:Assertion/@Issuer'; + $nodelist = $this->doXPathQuery($query); + + if ($attr = $nodelist->item(0)) { + return $attr->nodeValue; + } else { + throw new \Exception('Could not find Issuer field in Authentication response'); + } + } + + public function getNameID() + { + $nameID = array(); + + $query = '/shibp:Response/shib:Assertion/shib:AuthenticationStatement/shib:Subject/shib:NameIdentifier'; + $nodelist = $this->doXPathQuery($query); + + if ($node = $nodelist->item(0)) { + $nameID["Value"] = $node->nodeValue; + $nameID["Format"] = $node->getAttribute('Format'); + } + + return $nameID; + } /** @@ -305,69 +309,69 @@ class AuthnResponse { * @param array|NULL $attributes The attributes which should be included in the response. * @return string The response. */ - public function generate(\SimpleSAML_Configuration $idp, \SimpleSAML_Configuration $sp, $shire, $attributes) { - assert('is_string($shire)'); - assert('$attributes === NULL || is_array($attributes)'); - - if ($sp->hasValue('scopedattributes')) { - $scopedAttributes = $sp->getArray('scopedattributes'); - } elseif ($idp->hasValue('scopedattributes')) { - $scopedAttributes = $idp->getArray('scopedattributes'); - } else { - $scopedAttributes = array(); - } - - $id = Random::generateID(); - - $issueInstant = Time::generateTimestamp(); - - // 30 seconds timeskew back in time to allow differing clocks - $notBefore = Time::generateTimestamp(time() - 30); - - - $assertionExpire = Time::generateTimestamp(time() + 60 * 5);# 5 minutes - $assertionid = Random::generateID(); - - $spEntityId = $sp->getString('entityid'); - - $audience = $sp->getString('audience', $spEntityId); - $base64 = $sp->getBoolean('base64attributes', FALSE); - - $namequalifier = $sp->getString('NameQualifier', $spEntityId); - $nameid = Random::generateID(); - $subjectNode = - '<Subject>' . - '<NameIdentifier' . - ' Format="urn:mace:shibboleth:1.0:nameIdentifier"' . - ' NameQualifier="' . htmlspecialchars($namequalifier) . '"' . - '>' . - htmlspecialchars($nameid) . - '</NameIdentifier>' . - '<SubjectConfirmation>' . - '<ConfirmationMethod>' . - 'urn:oasis:names:tc:SAML:1.0:cm:bearer' . - '</ConfirmationMethod>' . - '</SubjectConfirmation>' . - '</Subject>'; - - $encodedattributes = ''; - - if (is_array($attributes)) { - - $encodedattributes .= '<AttributeStatement>'; - $encodedattributes .= $subjectNode; - - foreach ($attributes AS $name => $value) { - $encodedattributes .= $this->enc_attribute($name, $value, $base64, $scopedAttributes); - } - - $encodedattributes .= '</AttributeStatement>'; - } - - /* - * The SAML 1.1 response message - */ - $response = '<Response xmlns="urn:oasis:names:tc:SAML:1.0:protocol" + public function generate(\SimpleSAML_Configuration $idp, \SimpleSAML_Configuration $sp, $shire, $attributes) + { + assert('is_string($shire)'); + assert('$attributes === NULL || is_array($attributes)'); + + if ($sp->hasValue('scopedattributes')) { + $scopedAttributes = $sp->getArray('scopedattributes'); + } elseif ($idp->hasValue('scopedattributes')) { + $scopedAttributes = $idp->getArray('scopedattributes'); + } else { + $scopedAttributes = array(); + } + + $id = Random::generateID(); + + $issueInstant = Time::generateTimestamp(); + + // 30 seconds timeskew back in time to allow differing clocks + $notBefore = Time::generateTimestamp(time() - 30); + + + $assertionExpire = Time::generateTimestamp(time() + 60 * 5);# 5 minutes + $assertionid = Random::generateID(); + + $spEntityId = $sp->getString('entityid'); + + $audience = $sp->getString('audience', $spEntityId); + $base64 = $sp->getBoolean('base64attributes', false); + + $namequalifier = $sp->getString('NameQualifier', $spEntityId); + $nameid = Random::generateID(); + $subjectNode = + '<Subject>' . + '<NameIdentifier' . + ' Format="urn:mace:shibboleth:1.0:nameIdentifier"' . + ' NameQualifier="' . htmlspecialchars($namequalifier) . '"' . + '>' . + htmlspecialchars($nameid) . + '</NameIdentifier>' . + '<SubjectConfirmation>' . + '<ConfirmationMethod>' . + 'urn:oasis:names:tc:SAML:1.0:cm:bearer' . + '</ConfirmationMethod>' . + '</SubjectConfirmation>' . + '</Subject>'; + + $encodedattributes = ''; + + if (is_array($attributes)) { + $encodedattributes .= '<AttributeStatement>'; + $encodedattributes .= $subjectNode; + + foreach ($attributes as $name => $value) { + $encodedattributes .= $this->enc_attribute($name, $value, $base64, $scopedAttributes); + } + + $encodedattributes .= '</AttributeStatement>'; + } + + /* + * The SAML 1.1 response message + */ + $response = '<Response xmlns="urn:oasis:names:tc:SAML:1.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" IssueInstant="' . $issueInstant. '" @@ -386,96 +390,94 @@ class AuthnResponse { </Conditions> <AuthenticationStatement AuthenticationInstant="' . $issueInstant. '" AuthenticationMethod="urn:oasis:names:tc:SAML:1.0:am:unspecified">' . - $subjectNode . ' + $subjectNode . ' </AuthenticationStatement> ' . $encodedattributes . ' </Assertion> </Response>'; - return $response; - } - - - /** - * Format a shib13 attribute. - * - * @param string $name Name of the attribute. - * @param array $values Values of the attribute (as an array of strings). - * @param bool $base64 Whether the attriubte values should be base64-encoded. - * @param array $scopedAttributes Array of attributes names which are scoped. - * @return string The attribute encoded as an XML-string. - */ - private function enc_attribute($name, $values, $base64, $scopedAttributes) { - assert('is_string($name)'); - assert('is_array($values)'); - assert('is_bool($base64)'); - assert('is_array($scopedAttributes)'); - - if (in_array($name, $scopedAttributes, TRUE)) { - $scoped = TRUE; - } else { - $scoped = FALSE; - } - - $attr = '<Attribute AttributeName="' . htmlspecialchars($name) . '" AttributeNamespace="urn:mace:shibboleth:1.0:attributeNamespace:uri">'; - foreach ($values AS $value) { - - $scopePart = ''; - if ($scoped) { - $tmp = explode('@', $value, 2); - if (count($tmp) === 2) { - $value = $tmp[0]; - $scopePart = ' Scope="' . htmlspecialchars($tmp[1]) . '"'; - } - } - - if ($base64) { - $value = base64_encode($value); - } - - $attr .= '<AttributeValue' . $scopePart . '>' . htmlspecialchars($value) . '</AttributeValue>'; - } - $attr .= '</Attribute>'; - - return $attr; - } - - /** - * Check if we are currently between the given date & time conditions. - * - * Note that this function allows a 10-minute leap from the initial time as marked by $start. - * - * @param string|null $start A SAML2 timestamp marking the start of the period to check. Defaults to null, in which - * case there's no limitations in the past. - * @param string|null $end A SAML2 timestamp marking the end of the period to check. Defaults to null, in which - * case there's no limitations in the future. - * - * @return bool True if the current time belongs to the period specified by $start and $end. False otherwise. - * - * @see \SAML2\Utils::xsDateTimeToTimestamp. - * - * @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no> - * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> - */ - protected static function checkDateConditions($start = null, $end = null) - { - $currentTime = time(); - - if (!empty($start)) { - $startTime = Utils::xsDateTimeToTimestamp($start); - // allow for a 10 minute difference in time - if (($startTime < 0) || (($startTime - 600) > $currentTime)) { - return false; - } - } - if (!empty($end)) { - $endTime = Utils::xsDateTimeToTimestamp($end); - if (($endTime < 0) || ($endTime <= $currentTime)) { - return false; - } - } - return true; - } + return $response; + } -} + /** + * Format a shib13 attribute. + * + * @param string $name Name of the attribute. + * @param array $values Values of the attribute (as an array of strings). + * @param bool $base64 Whether the attriubte values should be base64-encoded. + * @param array $scopedAttributes Array of attributes names which are scoped. + * @return string The attribute encoded as an XML-string. + */ + private function enc_attribute($name, $values, $base64, $scopedAttributes) + { + assert('is_string($name)'); + assert('is_array($values)'); + assert('is_bool($base64)'); + assert('is_array($scopedAttributes)'); + + if (in_array($name, $scopedAttributes, true)) { + $scoped = true; + } else { + $scoped = false; + } + + $attr = '<Attribute AttributeName="' . htmlspecialchars($name) . '" AttributeNamespace="urn:mace:shibboleth:1.0:attributeNamespace:uri">'; + foreach ($values as $value) { + $scopePart = ''; + if ($scoped) { + $tmp = explode('@', $value, 2); + if (count($tmp) === 2) { + $value = $tmp[0]; + $scopePart = ' Scope="' . htmlspecialchars($tmp[1]) . '"'; + } + } + + if ($base64) { + $value = base64_encode($value); + } + + $attr .= '<AttributeValue' . $scopePart . '>' . htmlspecialchars($value) . '</AttributeValue>'; + } + $attr .= '</Attribute>'; + + return $attr; + } + + /** + * Check if we are currently between the given date & time conditions. + * + * Note that this function allows a 10-minute leap from the initial time as marked by $start. + * + * @param string|null $start A SAML2 timestamp marking the start of the period to check. Defaults to null, in which + * case there's no limitations in the past. + * @param string|null $end A SAML2 timestamp marking the end of the period to check. Defaults to null, in which + * case there's no limitations in the future. + * + * @return bool True if the current time belongs to the period specified by $start and $end. False otherwise. + * + * @see \SAML2\Utils::xsDateTimeToTimestamp. + * + * @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no> + * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> + */ + protected static function checkDateConditions($start = null, $end = null) + { + $currentTime = time(); + + if (!empty($start)) { + $startTime = Utils::xsDateTimeToTimestamp($start); + // allow for a 10 minute difference in time + if (($startTime < 0) || (($startTime - 600) > $currentTime)) { + return false; + } + } + if (!empty($end)) { + $endTime = Utils::xsDateTimeToTimestamp($end); + if (($endTime < 0) || ($endTime <= $currentTime)) { + return false; + } + } + return true; + } +} diff --git a/lib/SimpleSAML/XML/Signer.php b/lib/SimpleSAML/XML/Signer.php index 9e5622bab..b62c94a13 100644 --- a/lib/SimpleSAML/XML/Signer.php +++ b/lib/SimpleSAML/XML/Signer.php @@ -15,100 +15,103 @@ use RobRichards\XMLSecLibs\XMLSecurityDSig; use RobRichards\XMLSecLibs\XMLSecurityKey; use SimpleSAML\Utils\Config; -class Signer { +class Signer +{ - /** - * @var string The name of the ID attribute. - */ - private $idAttrName; + /** + * @var string The name of the ID attribute. + */ + private $idAttrName; /** * @var XMLSecurityKey|bool The private key (as an XMLSecurityKey). */ - private $privateKey; - - /** - * @var string The certificate (as text). - */ - private $certificate; - - - /** - * @var string Extra certificates which should be included in the response. - */ - private $extraCertificates; - - - /** - * Constructor for the metadata signer. - * - * You can pass an list of options as key-value pairs in the array. This allows you to initialize - * a metadata signer in one call. - * - * The following keys are recognized: - * - privatekey The file with the private key, relative to the cert-directory. - * - privatekey_pass The passphrase for the private key. - * - certificate The file with the certificate, relative to the cert-directory. - * - privatekey_array The private key, as an array returned from SimpleSAML_Utilities::loadPrivateKey. - * - publickey_array The public key, as an array returned from SimpleSAML_Utilities::loadPublicKey. - * - id The name of the ID attribute. - * - * @param $options array Associative array with options for the constructor. Defaults to an empty array. - */ - public function __construct($options = array()) { - assert('is_array($options)'); - - $this->idAttrName = FALSE; - $this->privateKey = FALSE; - $this->certificate = FALSE; - $this->extraCertificates = array(); - - if(array_key_exists('privatekey', $options)) { - $pass = NULL; - if(array_key_exists('privatekey_pass', $options)) { - $pass = $options['privatekey_pass']; - } - - $this->loadPrivateKey($options['privatekey'], $pass); - } - - if(array_key_exists('certificate', $options)) { - $this->loadCertificate($options['certificate']); - } - - if (array_key_exists('privatekey_array', $options)) { - $this->loadPrivateKeyArray($options['privatekey_array']); - } - - if (array_key_exists('publickey_array', $options)) { - $this->loadPublicKeyArray($options['publickey_array']); - } - - if(array_key_exists('id', $options)) { - $this->setIdAttribute($options['id']); - } - } - - - /** - * Set the private key from an array. - * - * This function loads the private key from an array matching what is returned - * by SimpleSAML_Utilities::loadPrivateKey(...). - * - * @param array $privatekey The private key. - */ - public function loadPrivateKeyArray($privatekey) { - assert('is_array($privatekey)'); - assert('array_key_exists("PEM", $privatekey)'); - - $this->privateKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, array('type' => 'private')); - if (array_key_exists('password', $privatekey)) { - $this->privateKey->passphrase = $privatekey['password']; - } - $this->privateKey->loadKey($privatekey['PEM'], FALSE); - } + private $privateKey; + + /** + * @var string The certificate (as text). + */ + private $certificate; + + + /** + * @var string Extra certificates which should be included in the response. + */ + private $extraCertificates; + + + /** + * Constructor for the metadata signer. + * + * You can pass an list of options as key-value pairs in the array. This allows you to initialize + * a metadata signer in one call. + * + * The following keys are recognized: + * - privatekey The file with the private key, relative to the cert-directory. + * - privatekey_pass The passphrase for the private key. + * - certificate The file with the certificate, relative to the cert-directory. + * - privatekey_array The private key, as an array returned from SimpleSAML_Utilities::loadPrivateKey. + * - publickey_array The public key, as an array returned from SimpleSAML_Utilities::loadPublicKey. + * - id The name of the ID attribute. + * + * @param $options array Associative array with options for the constructor. Defaults to an empty array. + */ + public function __construct($options = array()) + { + assert('is_array($options)'); + + $this->idAttrName = false; + $this->privateKey = false; + $this->certificate = false; + $this->extraCertificates = array(); + + if (array_key_exists('privatekey', $options)) { + $pass = null; + if (array_key_exists('privatekey_pass', $options)) { + $pass = $options['privatekey_pass']; + } + + $this->loadPrivateKey($options['privatekey'], $pass); + } + + if (array_key_exists('certificate', $options)) { + $this->loadCertificate($options['certificate']); + } + + if (array_key_exists('privatekey_array', $options)) { + $this->loadPrivateKeyArray($options['privatekey_array']); + } + + if (array_key_exists('publickey_array', $options)) { + $this->loadPublicKeyArray($options['publickey_array']); + } + + if (array_key_exists('id', $options)) { + $this->setIdAttribute($options['id']); + } + } + + + /** + * Set the private key from an array. + * + * This function loads the private key from an array matching what is returned + * by SimpleSAML_Utilities::loadPrivateKey(...). + * + * @param array $privatekey The private key. + */ + public function loadPrivateKeyArray($privatekey) + { + assert('is_array($privatekey)'); + assert('array_key_exists("PEM", $privatekey)'); + + $this->privateKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, array('type' => 'private')); + if (array_key_exists('password', $privatekey)) { + $this->privateKey->passphrase = $privatekey['password']; + } + $this->privateKey->loadKey($privatekey['PEM'], false); + } /** @@ -122,25 +125,26 @@ class Signer { * key is unencrypted. * @throws \Exception */ - public function loadPrivateKey($file, $pass = NULL) { - assert('is_string($file)'); - assert('is_string($pass) || is_null($pass)'); - - $keyFile = Config::getCertPath($file); - if (!file_exists($keyFile)) { - throw new \Exception('Could not find private key file "' . $keyFile . '".'); - } - $keyData = file_get_contents($keyFile); - if($keyData === FALSE) { - throw new \Exception('Unable to read private key file "' . $keyFile . '".'); - } - - $privatekey = array('PEM' => $keyData); - if($pass !== NULL) { - $privatekey['password'] = $pass; - } - $this->loadPrivateKeyArray($privatekey); - } + public function loadPrivateKey($file, $pass = null) + { + assert('is_string($file)'); + assert('is_string($pass) || is_null($pass)'); + + $keyFile = Config::getCertPath($file); + if (!file_exists($keyFile)) { + throw new \Exception('Could not find private key file "' . $keyFile . '".'); + } + $keyData = file_get_contents($keyFile); + if ($keyData === false) { + throw new \Exception('Unable to read private key file "' . $keyFile . '".'); + } + + $privatekey = array('PEM' => $keyData); + if ($pass !== null) { + $privatekey['password'] = $pass; + } + $this->loadPrivateKeyArray($privatekey); + } /** @@ -152,17 +156,18 @@ class Signer { * @param array $publickey The public key. * @throws \Exception */ - public function loadPublicKeyArray($publickey) { - assert('is_array($publickey)'); + public function loadPublicKeyArray($publickey) + { + assert('is_array($publickey)'); - if (!array_key_exists('PEM', $publickey)) { - // We have a public key with only a fingerprint - throw new \Exception('Tried to add a certificate fingerprint in a signature.'); - } + if (!array_key_exists('PEM', $publickey)) { + // We have a public key with only a fingerprint + throw new \Exception('Tried to add a certificate fingerprint in a signature.'); + } - // For now, we only assume that the public key is an X509 certificate - $this->certificate = $publickey['PEM']; - } + // For now, we only assume that the public key is an X509 certificate + $this->certificate = $publickey['PEM']; + } /** @@ -175,31 +180,33 @@ class Signer { * the cert-directory. * @throws \Exception */ - public function loadCertificate($file) { - assert('is_string($file)'); + public function loadCertificate($file) + { + assert('is_string($file)'); - $certFile = Config::getCertPath($file); - if (!file_exists($certFile)) { - throw new \Exception('Could not find certificate file "' . $certFile . '".'); - } + $certFile = Config::getCertPath($file); + if (!file_exists($certFile)) { + throw new \Exception('Could not find certificate file "' . $certFile . '".'); + } - $this->certificate = file_get_contents($certFile); - if($this->certificate === FALSE) { - throw new \Exception('Unable to read certificate file "' . $certFile . '".'); - } - } + $this->certificate = file_get_contents($certFile); + if ($this->certificate === false) { + throw new \Exception('Unable to read certificate file "' . $certFile . '".'); + } + } - /** - * Set the attribute name for the ID value. - * - * @param $idAttrName string The name of the attribute which contains the id. - */ - public function setIDAttribute($idAttrName) { - assert('is_string($idAttrName)'); + /** + * Set the attribute name for the ID value. + * + * @param $idAttrName string The name of the attribute which contains the id. + */ + public function setIDAttribute($idAttrName) + { + assert('is_string($idAttrName)'); - $this->idAttrName = $idAttrName; - } + $this->idAttrName = $idAttrName; + } /** @@ -211,21 +218,22 @@ class Signer { * @param $file string The file which contains the certificate, relative to the cert-directory. * @throws \Exception */ - public function addCertificate($file) { - assert('is_string($file)'); + public function addCertificate($file) + { + assert('is_string($file)'); - $certFile = Config::getCertPath($file); - if (!file_exists($certFile)) { - throw new \Exception('Could not find extra certificate file "' . $certFile . '".'); - } + $certFile = Config::getCertPath($file); + if (!file_exists($certFile)) { + throw new \Exception('Could not find extra certificate file "' . $certFile . '".'); + } - $certificate = file_get_contents($certFile); - if($certificate === FALSE) { - throw new \Exception('Unable to read extra certificate file "' . $certFile . '".'); - } + $certificate = file_get_contents($certFile); + if ($certificate === false) { + throw new \Exception('Unable to read extra certificate file "' . $certFile . '".'); + } - $this->extraCertificates[] = $certificate; - } + $this->extraCertificates[] = $certificate; + } /** @@ -240,42 +248,43 @@ class Signer { * $insertInto. * @throws \Exception */ - public function sign($node, $insertInto, $insertBefore = NULL) { - assert('$node instanceof DOMElement'); - assert('$insertInto instanceof DOMElement'); - assert('is_null($insertBefore) || $insertBefore instanceof DOMElement ' . - '|| $insertBefore instanceof DOMComment || $insertBefore instanceof DOMText'); + public function sign($node, $insertInto, $insertBefore = null) + { + assert('$node instanceof DOMElement'); + assert('$insertInto instanceof DOMElement'); + assert('is_null($insertBefore) || $insertBefore instanceof DOMElement ' . + '|| $insertBefore instanceof DOMComment || $insertBefore instanceof DOMText'); - if($this->privateKey === FALSE) { - throw new \Exception('Private key not set.'); - } + if ($this->privateKey === false) { + throw new \Exception('Private key not set.'); + } - $objXMLSecDSig = new XMLSecurityDSig(); - $objXMLSecDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N); + $objXMLSecDSig = new XMLSecurityDSig(); + $objXMLSecDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N); - $options = array(); - if($this->idAttrName !== FALSE) { - $options['id_name'] = $this->idAttrName; - } + $options = array(); + if ($this->idAttrName !== false) { + $options['id_name'] = $this->idAttrName; + } - $objXMLSecDSig->addReferenceList(array($node), XMLSecurityDSig::SHA1, - array('http://www.w3.org/2000/09/xmldsig#enveloped-signature', XMLSecurityDSig::EXC_C14N), - $options); + $objXMLSecDSig->addReferenceList(array($node), XMLSecurityDSig::SHA1, + array('http://www.w3.org/2000/09/xmldsig#enveloped-signature', XMLSecurityDSig::EXC_C14N), + $options); - $objXMLSecDSig->sign($this->privateKey); + $objXMLSecDSig->sign($this->privateKey); - if($this->certificate !== FALSE) { - // Add the certificate to the signature - $objXMLSecDSig->add509Cert($this->certificate, TRUE); - } + if ($this->certificate !== false) { + // Add the certificate to the signature + $objXMLSecDSig->add509Cert($this->certificate, true); + } - // Add extra certificates - foreach($this->extraCertificates as $certificate) { - $objXMLSecDSig->add509Cert($certificate, TRUE); - } + // Add extra certificates + foreach ($this->extraCertificates as $certificate) { + $objXMLSecDSig->add509Cert($certificate, true); + } - $objXMLSecDSig->insertSignature($insertInto, $insertBefore); - } + $objXMLSecDSig->insertSignature($insertInto, $insertBefore); + } } diff --git a/lib/SimpleSAML/XML/Validator.php b/lib/SimpleSAML/XML/Validator.php index c04067bbe..09c2a077e 100644 --- a/lib/SimpleSAML/XML/Validator.php +++ b/lib/SimpleSAML/XML/Validator.php @@ -3,7 +3,7 @@ /** * This class implements helper functions for XML validation. * - * @author Olav Morken, UNINETT AS. + * @author Olav Morken, UNINETT AS. * @package SimpleSAMLphp */ @@ -13,18 +13,19 @@ use RobRichards\XMLSecLibs\XMLSecEnc; use RobRichards\XMLSecLibs\XMLSecurityDSig; use SimpleSAML\Logger; -class Validator { +class Validator +{ - /** - * This variable contains the X509 certificate the XML document - * was signed with, or NULL if it wasn't signed with an X509 certificate. - */ - private $x509Certificate; + /** + * This variable contains the X509 certificate the XML document + * was signed with, or NULL if it wasn't signed with an X509 certificate. + */ + private $x509Certificate; - /** - * This variable contains the nodes which are signed. - */ - private $validNodes = null; + /** + * This variable contains the nodes which are signed. + */ + private $validNodes = null; /** @@ -45,147 +46,151 @@ class Validator { * @param array|bool $publickey The public key / certificate which should be used to validate the XML node. * @throws \Exception */ - public function __construct($xmlNode, $idAttribute = NULL, $publickey = FALSE) { - assert('$xmlNode instanceof \DOMNode'); - - if ($publickey === NULL) { - $publickey = FALSE; - } elseif(is_string($publickey)) { - $publickey = array( - 'PEM' => $publickey, - ); - } else { - assert('$publickey === FALSE || is_array($publickey)'); - } - - // Create an XML security object - $objXMLSecDSig = new XMLSecurityDSig(); - - // Add the id attribute if the user passed in an id attribute - if($idAttribute !== NULL) { - if (is_string($idAttribute)) { - $objXMLSecDSig->idKeys[] = $idAttribute; - } elseif (is_array($idAttribute)) { - foreach ($idAttribute AS $ida) - $objXMLSecDSig->idKeys[] = $ida; - } - } - - // Locate the XMLDSig Signature element to be used - $signatureElement = $objXMLSecDSig->locateSignature($xmlNode); - if (!$signatureElement) { - throw new \Exception('Could not locate XML Signature element.'); - } - - // Canonicalize the XMLDSig SignedInfo element in the message - $objXMLSecDSig->canonicalizeSignedInfo(); - - // Validate referenced xml nodes - if (!$objXMLSecDSig->validateReference()) { - throw new \Exception('XMLsec: digest validation failed'); - } - - - // Find the key used to sign the document - $objKey = $objXMLSecDSig->locateKey(); - if (empty($objKey)) { - throw new \Exception('Error loading key to handle XML signature'); - } - - // Load the key data - if ($publickey !== FALSE && array_key_exists('PEM', $publickey)) { - // We have PEM data for the public key / certificate - $objKey->loadKey($publickey['PEM']); - } else { - // No PEM data. Search for key in signature - - if (!XMLSecEnc::staticLocateKeyInfo($objKey, $signatureElement)) { - throw new \Exception('Error finding key data for XML signature validation.'); - } - - if ($publickey !== FALSE) { - /* $publickey is set, and should therefore contain one or more fingerprints. - * Check that the response contains a certificate with a matching - * fingerprint. - */ - assert('is_array($publickey["certFingerprint"])'); - - $certificate = $objKey->getX509Certificate(); - if ($certificate === NULL) { - // Wasn't signed with an X509 certificate - throw new \Exception('Message wasn\'t signed with an X509 certificate,' . - ' and no public key was provided in the metadata.'); - } - - self::validateCertificateFingerprint($certificate, $publickey['certFingerprint']); - // Key OK - } - } - - // Check the signature - if ($objXMLSecDSig->verify($objKey) !== 1) { - throw new \Exception("Unable to validate Signature"); - } - - // Extract the certificate - $this->x509Certificate = $objKey->getX509Certificate(); - - // Find the list of validated nodes - $this->validNodes = $objXMLSecDSig->getValidatedNodes(); - } - - - /** - * Retrieve the X509 certificate which was used to sign the XML. - * - * This function will return the certificate as a PEM-encoded string. If the XML - * wasn't signed by an X509 certificate, NULL will be returned. - * - * @return string The certificate as a PEM-encoded string, or NULL if not signed with an X509 certificate. - */ - public function getX509Certificate() { - return $this->x509Certificate; - } - - - /** - * Calculates the fingerprint of an X509 certificate. - * - * @param $x509cert string The certificate as a base64-encoded string. The string may optionally - * be framed with '-----BEGIN CERTIFICATE-----' and '-----END CERTIFICATE-----'. - * @return string The fingerprint as a 40-character lowercase hexadecimal number. NULL is returned if the - * argument isn't an X509 certificate. - */ - private static function calculateX509Fingerprint($x509cert) { - assert('is_string($x509cert)'); - - $lines = explode("\n", $x509cert); - - $data = ''; - - foreach($lines as $line) { - // Remove '\r' from end of line if present - $line = rtrim($line); - if($line === '-----BEGIN CERTIFICATE-----') { - // Delete junk from before the certificate - $data = ''; - } elseif($line === '-----END CERTIFICATE-----') { - // Ignore data after the certificate - break; - } elseif($line === '-----BEGIN PUBLIC KEY-----') { - // This isn't an X509 certificate - return NULL; - } else { - // Append the current line to the certificate data - $data .= $line; - } - } - - /* $data now contains the certificate as a base64-encoded string. The fingerprint - * of the certificate is the sha1-hash of the certificate. - */ - return strtolower(sha1(base64_decode($data))); - } + public function __construct($xmlNode, $idAttribute = null, $publickey = false) + { + assert('$xmlNode instanceof \DOMNode'); + + if ($publickey === null) { + $publickey = false; + } elseif (is_string($publickey)) { + $publickey = array( + 'PEM' => $publickey, + ); + } else { + assert('$publickey === FALSE || is_array($publickey)'); + } + + // Create an XML security object + $objXMLSecDSig = new XMLSecurityDSig(); + + // Add the id attribute if the user passed in an id attribute + if ($idAttribute !== null) { + if (is_string($idAttribute)) { + $objXMLSecDSig->idKeys[] = $idAttribute; + } elseif (is_array($idAttribute)) { + foreach ($idAttribute as $ida) { + $objXMLSecDSig->idKeys[] = $ida; + } + } + } + + // Locate the XMLDSig Signature element to be used + $signatureElement = $objXMLSecDSig->locateSignature($xmlNode); + if (!$signatureElement) { + throw new \Exception('Could not locate XML Signature element.'); + } + + // Canonicalize the XMLDSig SignedInfo element in the message + $objXMLSecDSig->canonicalizeSignedInfo(); + + // Validate referenced xml nodes + if (!$objXMLSecDSig->validateReference()) { + throw new \Exception('XMLsec: digest validation failed'); + } + + + // Find the key used to sign the document + $objKey = $objXMLSecDSig->locateKey(); + if (empty($objKey)) { + throw new \Exception('Error loading key to handle XML signature'); + } + + // Load the key data + if ($publickey !== false && array_key_exists('PEM', $publickey)) { + // We have PEM data for the public key / certificate + $objKey->loadKey($publickey['PEM']); + } else { + // No PEM data. Search for key in signature + + if (!XMLSecEnc::staticLocateKeyInfo($objKey, $signatureElement)) { + throw new \Exception('Error finding key data for XML signature validation.'); + } + + if ($publickey !== false) { + /* $publickey is set, and should therefore contain one or more fingerprints. + * Check that the response contains a certificate with a matching + * fingerprint. + */ + assert('is_array($publickey["certFingerprint"])'); + + $certificate = $objKey->getX509Certificate(); + if ($certificate === null) { + // Wasn't signed with an X509 certificate + throw new \Exception('Message wasn\'t signed with an X509 certificate,' . + ' and no public key was provided in the metadata.'); + } + + self::validateCertificateFingerprint($certificate, $publickey['certFingerprint']); + // Key OK + } + } + + // Check the signature + if ($objXMLSecDSig->verify($objKey) !== 1) { + throw new \Exception("Unable to validate Signature"); + } + + // Extract the certificate + $this->x509Certificate = $objKey->getX509Certificate(); + + // Find the list of validated nodes + $this->validNodes = $objXMLSecDSig->getValidatedNodes(); + } + + + /** + * Retrieve the X509 certificate which was used to sign the XML. + * + * This function will return the certificate as a PEM-encoded string. If the XML + * wasn't signed by an X509 certificate, NULL will be returned. + * + * @return string The certificate as a PEM-encoded string, or NULL if not signed with an X509 certificate. + */ + public function getX509Certificate() + { + return $this->x509Certificate; + } + + + /** + * Calculates the fingerprint of an X509 certificate. + * + * @param $x509cert string The certificate as a base64-encoded string. The string may optionally + * be framed with '-----BEGIN CERTIFICATE-----' and '-----END CERTIFICATE-----'. + * @return string The fingerprint as a 40-character lowercase hexadecimal number. NULL is returned if the + * argument isn't an X509 certificate. + */ + private static function calculateX509Fingerprint($x509cert) + { + assert('is_string($x509cert)'); + + $lines = explode("\n", $x509cert); + + $data = ''; + + foreach ($lines as $line) { + // Remove '\r' from end of line if present + $line = rtrim($line); + if ($line === '-----BEGIN CERTIFICATE-----') { + // Delete junk from before the certificate + $data = ''; + } elseif ($line === '-----END CERTIFICATE-----') { + // Ignore data after the certificate + break; + } elseif ($line === '-----BEGIN PUBLIC KEY-----') { + // This isn't an X509 certificate + return null; + } else { + // Append the current line to the certificate data + $data .= $line; + } + } + + /* $data now contains the certificate as a base64-encoded string. The fingerprint + * of the certificate is the sha1-hash of the certificate. + */ + return strtolower(sha1(base64_decode($data))); + } /** @@ -198,31 +203,31 @@ class Validator { * @param array $fingerprints The valid fingerprints. * @throws \Exception */ - private static function validateCertificateFingerprint($certificate, $fingerprints) { - assert('is_string($certificate)'); - assert('is_array($fingerprints)'); - - $certFingerprint = self::calculateX509Fingerprint($certificate); - if ($certFingerprint === NULL) { - // Couldn't calculate fingerprint from X509 certificate. Should not happen. - throw new \Exception('Unable to calculate fingerprint from X509' . - ' certificate. Maybe it isn\'t an X509 certificate?'); - } - - foreach ($fingerprints as $fp) { - assert('is_string($fp)'); - - if ($fp === $certFingerprint) { - // The fingerprints matched - return; - } - - } - - // None of the fingerprints matched. Throw an exception describing the error. - throw new \Exception('Invalid fingerprint of certificate. Expected one of [' . - implode('], [', $fingerprints) . '], but got [' . $certFingerprint . ']'); - } + private static function validateCertificateFingerprint($certificate, $fingerprints) + { + assert('is_string($certificate)'); + assert('is_array($fingerprints)'); + + $certFingerprint = self::calculateX509Fingerprint($certificate); + if ($certFingerprint === null) { + // Couldn't calculate fingerprint from X509 certificate. Should not happen. + throw new \Exception('Unable to calculate fingerprint from X509' . + ' certificate. Maybe it isn\'t an X509 certificate?'); + } + + foreach ($fingerprints as $fp) { + assert('is_string($fp)'); + + if ($fp === $certFingerprint) { + // The fingerprints matched + return; + } + } + + // None of the fingerprints matched. Throw an exception describing the error. + throw new \Exception('Invalid fingerprint of certificate. Expected one of [' . + implode('], [', $fingerprints) . '], but got [' . $certFingerprint . ']'); + } /** @@ -236,52 +241,54 @@ class Validator { * or an array of fingerprints. * @throws \Exception */ - public function validateFingerprint($fingerprints) { - assert('is_string($fingerprints) || is_array($fingerprints)'); + public function validateFingerprint($fingerprints) + { + assert('is_string($fingerprints) || is_array($fingerprints)'); - if($this->x509Certificate === NULL) { - throw new \Exception('Key used to sign the message was not an X509 certificate.'); - } + if ($this->x509Certificate === null) { + throw new \Exception('Key used to sign the message was not an X509 certificate.'); + } - if(!is_array($fingerprints)) { - $fingerprints = array($fingerprints); - } + if (!is_array($fingerprints)) { + $fingerprints = array($fingerprints); + } - // Normalize the fingerprints - foreach($fingerprints as &$fp) { - assert('is_string($fp)'); + // Normalize the fingerprints + foreach ($fingerprints as &$fp) { + assert('is_string($fp)'); - // Make sure that the fingerprint is in the correct format - $fp = strtolower(str_replace(":", "", $fp)); - } + // Make sure that the fingerprint is in the correct format + $fp = strtolower(str_replace(":", "", $fp)); + } - self::validateCertificateFingerprint($this->x509Certificate, $fingerprints); - } - - - /** - * This function checks if the given XML node was signed. - * - * @param $node \DOMNode The XML node which we should verify that was signed. - * - * @return bool TRUE if this node (or a parent node) was signed. FALSE if not. - */ - public function isNodeValidated($node) { - assert('$node instanceof \DOMNode'); - - while($node !== NULL) { - if(in_array($node, $this->validNodes)) { - return TRUE; - } + self::validateCertificateFingerprint($this->x509Certificate, $fingerprints); + } - $node = $node->parentNode; - } - /* Neither this node nor any of the parent nodes could be found in the list of - * signed nodes. - */ - return FALSE; - } + /** + * This function checks if the given XML node was signed. + * + * @param $node \DOMNode The XML node which we should verify that was signed. + * + * @return bool TRUE if this node (or a parent node) was signed. FALSE if not. + */ + public function isNodeValidated($node) + { + assert('$node instanceof \DOMNode'); + + while ($node !== null) { + if (in_array($node, $this->validNodes)) { + return true; + } + + $node = $node->parentNode; + } + + /* Neither this node nor any of the parent nodes could be found in the list of + * signed nodes. + */ + return false; + } /** @@ -292,47 +299,48 @@ class Validator { * @param $caFile string File with trusted certificates, in PEM-format. * @throws \Exception */ - public function validateCA($caFile) { + public function validateCA($caFile) + { + assert('is_string($caFile)'); - assert('is_string($caFile)'); + if ($this->x509Certificate === null) { + throw new \Exception('Key used to sign the message was not an X509 certificate.'); + } - if($this->x509Certificate === NULL) { - throw new \Exception('Key used to sign the message was not an X509 certificate.'); - } - - self::validateCertificate($this->x509Certificate, $caFile); - } + self::validateCertificate($this->x509Certificate, $caFile); + } - /** - * Validate a certificate against a CA file, by using the builtin - * openssl_x509_checkpurpose function - * - * @param string $certificate The certificate, in PEM format. - * @param string $caFile File with trusted certificates, in PEM-format. - * @return boolean|string TRUE on success, or a string with error messages if it failed. - * @deprecated - */ - private static function validateCABuiltIn($certificate, $caFile) { - assert('is_string($certificate)'); - assert('is_string($caFile)'); + /** + * Validate a certificate against a CA file, by using the builtin + * openssl_x509_checkpurpose function + * + * @param string $certificate The certificate, in PEM format. + * @param string $caFile File with trusted certificates, in PEM-format. + * @return boolean|string TRUE on success, or a string with error messages if it failed. + * @deprecated + */ + private static function validateCABuiltIn($certificate, $caFile) + { + assert('is_string($certificate)'); + assert('is_string($caFile)'); - // Clear openssl errors - while(openssl_error_string() !== FALSE); + // Clear openssl errors + while (openssl_error_string() !== false); - $res = openssl_x509_checkpurpose($certificate, X509_PURPOSE_ANY, array($caFile)); + $res = openssl_x509_checkpurpose($certificate, X509_PURPOSE_ANY, array($caFile)); - $errors = ''; - // Log errors - while( ($error = openssl_error_string()) !== FALSE) { - $errors .= ' [' . $error . ']'; - } + $errors = ''; + // Log errors + while (($error = openssl_error_string()) !== false) { + $errors .= ' [' . $error . ']'; + } - if($res !== TRUE) { - return $errors; - } + if ($res !== true) { + return $errors; + } - return TRUE; - } + return true; + } /** @@ -348,51 +356,52 @@ class Validator { * @throws \Exception * @deprecated */ - private static function validateCAExec($certificate, $caFile) { - assert('is_string($certificate)'); - assert('is_string($caFile)'); - - $command = array( - 'openssl', 'verify', - '-CAfile', $caFile, - '-purpose', 'any', - ); - - $cmdline = ''; - foreach($command as $c) { - $cmdline .= escapeshellarg($c) . ' '; - } - - $cmdline .= '2>&1'; - $descSpec = array( - 0 => array('pipe', 'r'), - 1 => array('pipe', 'w'), - ); - $process = proc_open($cmdline, $descSpec, $pipes); - if (!is_resource($process)) { - throw new \Exception('Failed to execute verification command: ' . $cmdline); - } - - if (fwrite($pipes[0], $certificate) === FALSE) { - throw new \Exception('Failed to write certificate for verification.'); - } - fclose($pipes[0]); - - $out = ''; - while (!feof($pipes[1])) { - $line = trim(fgets($pipes[1])); - if(strlen($line) > 0) { - $out .= ' [' . $line . ']'; - } - } - fclose($pipes[1]); - - $status = proc_close($process); - if ($status !== 0 || $out !== ' [stdin: OK]') { - return $out; - } - - return TRUE; + private static function validateCAExec($certificate, $caFile) + { + assert('is_string($certificate)'); + assert('is_string($caFile)'); + + $command = array( + 'openssl', 'verify', + '-CAfile', $caFile, + '-purpose', 'any', + ); + + $cmdline = ''; + foreach ($command as $c) { + $cmdline .= escapeshellarg($c) . ' '; + } + + $cmdline .= '2>&1'; + $descSpec = array( + 0 => array('pipe', 'r'), + 1 => array('pipe', 'w'), + ); + $process = proc_open($cmdline, $descSpec, $pipes); + if (!is_resource($process)) { + throw new \Exception('Failed to execute verification command: ' . $cmdline); + } + + if (fwrite($pipes[0], $certificate) === false) { + throw new \Exception('Failed to write certificate for verification.'); + } + fclose($pipes[0]); + + $out = ''; + while (!feof($pipes[1])) { + $line = trim(fgets($pipes[1])); + if (strlen($line) > 0) { + $out .= ' [' . $line . ']'; + } + } + fclose($pipes[1]); + + $status = proc_close($process); + if ($status !== 0 || $out !== ' [stdin: OK]') { + return $out; + } + + return true; } @@ -406,30 +415,30 @@ class Validator { * @throws \Exception * @deprecated */ - public static function validateCertificate($certificate, $caFile) { - assert('is_string($certificate)'); - assert('is_string($caFile)'); - - if (!file_exists($caFile)) { - throw new \Exception('Could not load CA file: ' . $caFile); - } - - Logger::debug('Validating certificate against CA file: ' . var_export($caFile, TRUE)); - - $resBuiltin = self::validateCABuiltIn($certificate, $caFile); - if ($resBuiltin !== TRUE) { - Logger::debug('Failed to validate with internal function: ' . var_export($resBuiltin, TRUE)); - - $resExternal = self::validateCAExec($certificate, $caFile); - if ($resExternal !== TRUE) { - Logger::debug('Failed to validate with external function: ' . var_export($resExternal, TRUE)); - throw new \Exception('Could not verify certificate against CA file "' - . $caFile . '". Internal result:' . $resBuiltin . - ' External result:' . $resExternal); - } - } - - Logger::debug('Successfully validated certificate.'); - } - + public static function validateCertificate($certificate, $caFile) + { + assert('is_string($certificate)'); + assert('is_string($caFile)'); + + if (!file_exists($caFile)) { + throw new \Exception('Could not load CA file: ' . $caFile); + } + + Logger::debug('Validating certificate against CA file: ' . var_export($caFile, true)); + + $resBuiltin = self::validateCABuiltIn($certificate, $caFile); + if ($resBuiltin !== true) { + Logger::debug('Failed to validate with internal function: ' . var_export($resBuiltin, true)); + + $resExternal = self::validateCAExec($certificate, $caFile); + if ($resExternal !== true) { + Logger::debug('Failed to validate with external function: ' . var_export($resExternal, true)); + throw new \Exception('Could not verify certificate against CA file "' + . $caFile . '". Internal result:' . $resBuiltin . + ' External result:' . $resExternal); + } + } + + Logger::debug('Successfully validated certificate.'); + } } -- GitLab