diff --git a/bin/parseMetadata.php b/bin/parseMetadata.php index d21ae978a7ee3031832d417565ebf978624b6ab6..d74511a94bc6e1367fde2b17644117ba80638698 100755 --- a/bin/parseMetadata.php +++ b/bin/parseMetadata.php @@ -130,7 +130,9 @@ function processFile($filename) { foreach($entities as $entity) { addMetadata($filename, $entity->getMetadata1xSP(), 'shib13-sp-remote'); + addMetadata($filename, $entity->getMetadata1xIdP(), 'shib13-idp-remote'); addMetadata($filename, $entity->getMetadata20SP(), 'saml20-sp-remote'); + addMetadata($filename, $entity->getMetadata20IdP(), 'saml20-idp-remote'); } } diff --git a/lib/SimpleSAML/Metadata/SAMLParser.php b/lib/SimpleSAML/Metadata/SAMLParser.php index 8056490d0cc4ff57293d1e4aa8a4f07e4c60f43a..898d1284ecf636170fe50b5a47c9603dc5949362 100644 --- a/lib/SimpleSAML/Metadata/SAMLParser.php +++ b/lib/SimpleSAML/Metadata/SAMLParser.php @@ -32,6 +32,11 @@ class SimpleSAML_Metadata_SAMLParser { ); + /** + * This is the binding used to send authentication requests in SAML 1.x. + */ + const SAML_1x_AUTHN_REQUEST = 'urn:mace:shibboleth:1.0:profiles:AuthnRequest'; + /** * This is the binding used for browser post in SAML 1.x. */ @@ -59,18 +64,22 @@ class SimpleSAML_Metadata_SAMLParser { /** * This is an array with the processed SPSSODescriptor elements we have found in this * metadata file. - * Each element in the array is an associative array with the following elements: - * - 'protocols': Array with the protocols this SPSSODescriptor supports. + * Each element in the array is an associative array with the elements from parseSSODescriptor and: * - 'assertionConsumerServices': Array with the SP's assertion consumer services. * Each assertion consumer service is stored as an associative array with the * elements that parseGenericEndpoint returns. - * - 'singleLogoutServices': Array with the SP's single logout service endpoints. Each endpoint is stored - * as an associative array with the elements that parseGenericEndpoint returns. - * - 'nameIDFormats': The NameIDFormats that the SP accepts. This may be an empty array. */ private $spDescriptors; + /** + * This is an array with the processed IDPSSODescriptor elements we have found. + * Each element in the array is an associative array with the elements from parseSSODescriptor and: + * - 'singleSignOnServices': Array with the IdP's single signon service endpoints. Each endpoint is stored + * as an associative array with the elements that parseGenericEndpoint returns. + */ + private $idpDescriptors; + /** * This is the constructor for the SAMLParser class. @@ -79,6 +88,7 @@ class SimpleSAML_Metadata_SAMLParser { */ private function __construct($entityElement) { $this->spDescriptors = array(); + $this->idpDescriptors = array(); assert('$entityElement instanceof DOMElement'); @@ -104,6 +114,10 @@ class SimpleSAML_Metadata_SAMLParser { if(SimpleSAML_Utilities::isDOMElementOfType($child, 'SPSSODescriptor', '@md') === TRUE) { $this->processSPSSODescriptor($child); } + + if(SimpleSAML_Utilities::isDOMElementOfType($child, 'IDPSSODescriptor', '@md') === TRUE) { + $this->processIDPSSODescriptor($child); + } } } @@ -290,6 +304,62 @@ class SimpleSAML_Metadata_SAMLParser { } + /** + * This function returns the metadata for SAML 2.0 IdPs in the format simpleSAMLphp expects. + * This is an associative array with the following fields: + * - 'entityID': The entity id of the entity described in the metadata. + * - 'name': Autogenerated name for this entity. Currently set to the entityID. + * - 'SingleSignOnService': String with the url of the SSO service which supports the redirect binding. + * - 'SingleLogoutService': String with the url where we should send logout requests/responses. + * - 'certFingerprint': Fingerprint of the X509Certificate from the metadata. + * + * Metadata must be loaded with one of the parse functions before this function can be called. + * + * @return Associative array with metadata or NULL if we are unable to generate metadata for a SAML 1.x IdP. + */ + public function getMetadata1xIdP() { + + $ret = array(); + + $ret['entityID'] = $this->entityID; + + $ret['name'] = $this->entityID; + + /* Find IdP information which supports the SAML 1.x protocol. */ + $idp = $this->getIdPDescriptors(self::$SAML1xProtocols); + if(count($idp) === 0) { + return NULL; + } + + /* We currently only look at the first IDP descriptor which supports SAML 1.x. */ + $idp = $idp[0]; + + /* Find the SSO service endpoint. */ + $sso = $this->getDefaultEndpoint($idp['singleSignOnServices'], array(self::SAML_1x_AUTHN_REQUEST)); + if($sso === NULL) { + throw new Exception('Could not find any valid SingleSignOnService endpoint.'); + } + $ret['SingleSignOnService'] = $sso['location']; + + /* Find the certificate fingerprint. */ + foreach($idp['keys'] as $key) { + if($key['type'] !== 'X509Certificate') { + continue; + } + + $certData = base64_decode($key['X509Certificate']); + if($certData === FALSE) { + throw new Exception('Unable to parse base64 encoded certificate data.'); + } + + $ret['certFingerprint'] = sha1($certData); + break; + } + + return $ret; + } + + /** * This function returns the metadata for SAML 2.0 SPs in the format simpleSAMLphp expects. * This is an associative array with the following fields: @@ -301,7 +371,7 @@ class SimpleSAML_Metadata_SAMLParser { * * Metadata must be loaded with one of the parse functions before this function can be called. * - * @return Associative array with metadata or NULL if we are unable to generate metadata for a SAML 1.x SP. + * @return Associative array with metadata or NULL if we are unable to generate metadata for a SAML 2.x SP. */ public function getMetadata20SP() { @@ -330,13 +400,13 @@ class SimpleSAML_Metadata_SAMLParser { /* Find the single logout service endpoint. */ - $acs = $this->getDefaultEndpoint($spd['singleLogoutServices'], array(self::SAML_20_REDIRECT_BINDING)); - if($acs === NULL) { + $slo = $this->getDefaultEndpoint($spd['singleLogoutServices'], array(self::SAML_20_REDIRECT_BINDING)); + if($slo === NULL) { throw new Exception('Could not find any valid SingleLogoutService.' . - ' simpleSAMLphp currently supports only the http-redirect binding for SAML 1.0 logout.'); + ' simpleSAMLphp currently supports only the http-redirect binding for SAML 2.0 logout.'); } - $ret['SingleLogoutService'] = $acs['location']; + $ret['SingleLogoutService'] = $slo['location']; /* Find the NameIDFormat. This may not exists. */ @@ -350,36 +420,136 @@ class SimpleSAML_Metadata_SAMLParser { /** - * This function extracts metadata from a SPSSODescriptor element. + * This function returns the metadata for SAML 2.0 IdPs in the format simpleSAMLphp expects. + * This is an associative array with the following fields: + * - 'entityID': The entity id of the entity described in the metadata. + * - 'name': Autogenerated name for this entity. Currently set to the entityID. + * - 'SingleSignOnService': String with the url of the SSO service which supports the redirect binding. + * - 'SingleLogoutService': String with the url where we should send logout requests/responses. + * - 'certFingerprint': Fingerprint of the X509Certificate from the metadata. * - * @param $element The element which should be parsed. + * Metadata must be loaded with one of the parse functions before this function can be called. + * + * @return Associative array with metadata or NULL if we are unable to generate metadata for a SAML 2.0 IdP. */ - private function processSPSSODescriptor($element) { - assert('$element instanceof DOMElement'); + public function getMetadata20IdP() { - $sp = array(); + $ret = array(); - $sp['protocols'] = self::getSupportedProtocols($element); + $ret['entityID'] = $this->entityID; - /* Find all AssertionConsumerService elements. */ - $sp['assertionConsumerServices'] = array(); - $acs = SimpleSAML_Utilities::getDOMChildren($element, 'AssertionConsumerService', '@md'); - foreach($acs as $child) { - $sp['assertionConsumerServices'][] = self::parseAssertionConsumerService($child); + $ret['name'] = $this->entityID; + + /* Find IdP information which supports the SAML 2.0 protocol. */ + $idp = $this->getIdPDescriptors(self::$SAML20Protocols); + if(count($idp) === 0) { + return NULL; + } + + /* We currently only look at the first IDP descriptor which supports SAML 2.0. */ + $idp = $idp[0]; + + /* Find the SSO service endpoint. */ + $sso = $this->getDefaultEndpoint($idp['singleSignOnServices'], array(self::SAML_20_REDIRECT_BINDING)); + if($sso === NULL) { + throw new Exception('Could not find any valid SingleSignOnService endpoint.'); + } + $ret['SingleSignOnService'] = $sso['location']; + + + /* Find the single logout service endpoint. */ + $slo = $this->getDefaultEndpoint($idp['singleLogoutServices'], array(self::SAML_20_REDIRECT_BINDING)); + if($slo === NULL) { + throw new Exception('Could not find any valid SingleLogoutService.' . + ' simpleSAMLphp currently supports only the http-redirect binding for SAML 2.0 logout.'); } + $ret['SingleLogoutService'] = $slo['location']; + + + /* Find the certificate fingerprint. */ + foreach($idp['keys'] as $key) { + if($key['type'] !== 'X509Certificate') { + continue; + } + + $certData = base64_decode($key['X509Certificate']); + if($certData === FALSE) { + throw new Exception('Unable to parse base64 encoded certificate data.'); + } + + $ret['certFingerprint'] = sha1($certData); + break; + } + + return $ret; + } + + + /** + * This function extracts metadata from a SSODescriptor element. + * + * The returned associative array has the following elements: + * - 'protocols': Array with the protocols this SSODescriptor supports. + * - 'singleLogoutServices': Array with the single logout service endpoints. Each endpoint is stored + * as an associative array with the elements that parseGenericEndpoint returns. + * - 'nameIDFormats': The NameIDFormats supported by this SSODescriptor. This may be an empty array. + * - 'keys': Array of associative arrays with the elements from parseKeyDescriptor: + * + * @param $element The element we should extract metadata from. + * @return Associative array with metadata we have extracted from this element. + */ + private static function parseSSODescriptor($element) { + + assert('$element instanceof DOMElement'); + + $sd = array(); + + $sd['protocols'] = self::getSupportedProtocols($element); /* Find all SingleLogoutService elements. */ - $sp['singleLogoutServices'] = array(); + $sd['singleLogoutServices'] = array(); $sls = SimpleSAML_Utilities::getDOMChildren($element, 'SingleLogoutService', '@md'); foreach($sls as $child) { - $sp['singleLogoutServices'][] = self::parseSingleLogoutService($child); + $sd['singleLogoutServices'][] = self::parseSingleLogoutService($child); } /* Process NameIDFormat elements. */ - $sp['nameIDFormats'] = array(); + $sd['nameIDFormats'] = array(); $nif = SimpleSAML_Utilities::getDOMChildren($element, 'NameIDFormat', '@md'); if(count($nif) > 0) { - $sp['nameIDFormats'][] = self::parseNameIDFormat($nif[0]); + $sd['nameIDFormats'][] = self::parseNameIDFormat($nif[0]); + } + + /* Process KeyDescriptor elements. */ + $sd['keys'] = array(); + $keys = SimpleSAML_Utilities::getDOMChildren($element, 'KeyDescriptor', '@md'); + foreach($keys as $kd) { + $key = self::parseKeyDescriptor($kd); + if($key !== NULL) { + $sd['keys'][] = $key; + } + } + + + return $sd; + } + + + /** + * This function extracts metadata from a SPSSODescriptor element. + * + * @param $element The element which should be parsed. + */ + private function processSPSSODescriptor($element) { + assert('$element instanceof DOMElement'); + + $sp = self::parseSSODescriptor($element); + + /* Find all AssertionConsumerService elements. */ + $sp['assertionConsumerServices'] = array(); + $acs = SimpleSAML_Utilities::getDOMChildren($element, 'AssertionConsumerService', '@md'); + foreach($acs as $child) { + $sp['assertionConsumerServices'][] = self::parseAssertionConsumerService($child); } @@ -387,6 +557,28 @@ class SimpleSAML_Metadata_SAMLParser { } + /** + * This function extracts metadata from a IDPSSODescriptor element. + * + * @param $element The element which should be parsed. + */ + private function processIDPSSODescriptor($element) { + assert('$element instanceof DOMElement'); + + $idp = self::parseSSODescriptor($element); + + /* Find all SingleSignOnService elements. */ + $idp['singleSignOnServices'] = array(); + $acs = SimpleSAML_Utilities::getDOMChildren($element, 'SingleSignOnService', '@md'); + foreach($acs as $child) { + $idp['singleSignOnServices'][] = self::parseSingleSignOnService($child); + } + + + $this->idpDescriptors[] = $idp; + } + + /** * This function parses AssertionConsumerService elements. * @@ -414,27 +606,28 @@ class SimpleSAML_Metadata_SAMLParser { /** - * This function parses NameIDFormat elements. + * This function parses SingleSignOnService elements. * * @param $element The element which should be parsed. - * @return URN with the supported NameIDFormat. + * @return Associative array with the data we have extracted from the SingleLogoutService element. */ - private static function parseNameIDFormat($element) { + private static function parseSingleSignOnService($element) { assert('$element instanceof DOMElement'); - $fmt = ''; + return self::parseGenericEndpoint($element, FALSE); + } - for($i = 0; $i < $element->childNodes->length; $i++) { - $child = $element->childNodes->item($i); - if(!($child instanceof DOMText)) { - throw new Exception('NameIDFormat contained a non-text child node.'); - } - $fmt .= $child->wholeText; - } + /** + * This function parses NameIDFormat elements. + * + * @param $element The element which should be parsed. + * @return URN with the supported NameIDFormat. + */ + private static function parseNameIDFormat($element) { + assert('$element instanceof DOMElement'); - $fmt = trim($fmt); - return $fmt; + return SimpleSAML_Utilities::getDOMText($element); } @@ -497,6 +690,65 @@ class SimpleSAML_Metadata_SAMLParser { } + /** + * This function parses a KeyDescriptor element. It currently only supports keys with a single + * X509 certificate. + * + * The associative array for a key can contain: + * - 'encryption': Indicates wheter this key can be used for encryption. + * - 'signing': Indicates wheter this key can be used for signing. + * - 'type: The type of the key. 'X509Certificate' is the only key type we support. + * - 'X509Certificate': The contents of the first X509Certificate element (if the type is 'X509Certificate '). + * + * @param $kd The KeyDescriptor element. + * @return Associative array describing the key, or NULL if this is an unsupported key. + */ + private static function parseKeyDescriptor($kd) { + assert('$kd instanceof DOMElement'); + + $r = array(); + + if($kd->hasAttribute('use')) { + $use = $kd->getAttribute('use'); + if($use === 'encryption') { + $r['encryption'] = TRUE; + $r['signing'] = FALSE; + } elseif($use === 'signing') { + $r['encryption'] = FALSE; + $r['signing'] = TRUE; + } else { + throw new Exception('Invalid use-value for KeyDescriptor: ' . $use); + } + } else { + $r['encryption'] = TRUE; + $r['signing'] = TRUE; + } + + $keyInfo = SimpleSAML_Utilities::getDOMChildren($kd, 'KeyInfo', '@ds'); + if(count($keyInfo) === 0) { + throw new Exception('Missing required KeyInfo field for KeyDescriptor.'); + } + $keyInfo = $keyInfo[0]; + + $X509Data = SimpleSAML_Utilities::getDOMChildren($keyInfo, 'X509Data', '@ds'); + if(count($X509Data) === 0) { + return NULL; + } + $X509Data = $X509Data[0]; + + $X509Certificate = SimpleSAML_Utilities::getDOMChildren($X509Data, 'X509Certificate', '@ds'); + if(count($X509Certificate) === 0) { + return NULL; + } + $X509Certificate = $X509Certificate[0]; + + $r['type'] = 'X509Certificate'; + $r['X509Certificate'] = SimpleSAML_Utilities::getDOMText($X509Certificate); + + return $r; + } + + /** * This function attempts to locate the default endpoint which supports one of the given bindings. * @@ -572,6 +824,28 @@ class SimpleSAML_Metadata_SAMLParser { } + /** + * This function finds IdP descriptors which supports one of the given protocols. + * + * @param $protocols Array with the protocols we accept. + * @return Array with IdP descriptors which supports one of the given protocols. + */ + private function getIdPDescriptors($protocols) { + assert('is_array($protocols)'); + + $ret = array(); + + foreach($this->idpDescriptors as $idpd) { + $sharedProtocols = array_intersect($protocols, $idpd['protocols']); + if(count($sharedProtocols) > 0) { + $ret[] = $idpd; + } + } + + return $ret; + } + + /** * This function locates the EntityDescriptor node in a DOMDocument. This node should * be the first (and only) node in the document.