diff --git a/lib/SimpleSAML/Metadata/SAMLParser.php b/lib/SimpleSAML/Metadata/SAMLParser.php index ebb3591d08022cda5689e1f46fc62e39bd17994a..ffeca749f44ded1bd131452822134603fedb53f3 100644 --- a/lib/SimpleSAML/Metadata/SAMLParser.php +++ b/lib/SimpleSAML/Metadata/SAMLParser.php @@ -1,5 +1,6 @@ <?php + /** * This is class for parsing of SAML 1.x and SAML 2.0 metadata. * @@ -11,1371 +12,1444 @@ * parseDescriptorsFile, parseDescriptorsString or parseDescriptorsElement methods. These functions will return * an array of SAMLParser elements where each element represents an EntityDescriptor-element. */ -class SimpleSAML_Metadata_SAMLParser { - - /** - * This is the list of SAML 1.x protocols. - */ - private static $SAML1xProtocols = array( - 'urn:oasis:names:tc:SAML:1.0:protocol', - 'urn:oasis:names:tc:SAML:1.1:protocol', - ); - - - /** - * This is the list with the SAML 2.0 protocol. - */ - private static $SAML20Protocols = array( - 'urn:oasis:names:tc:SAML:2.0:protocol', - ); - - - /** - * This is the entity id we find in the metadata. - */ - private $entityId; - - - /** - * 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 elements from parseSSODescriptor and: - * - 'AssertionConsumerService': Array with the SP's assertion consumer services. - * Each assertion consumer service is stored as an associative array with the - * elements that parseGenericEndpoint returns. - */ - 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: - * - 'SingleSignOnService': Array with the IdP's single sign on service endpoints. Each endpoint is stored - * as an associative array with the elements that parseGenericEndpoint returns. - */ - private $idpDescriptors; - - - /** - * List of attribute authorities we have found. - * - * @var array - */ - private $attributeAuthorityDescriptors = array(); - - - /** - * This is an associative array with the organization name for this entity. The key of - * the associative array is the language code, while the value is a string with the - * organization name. - */ - private $organizationName = array(); - - - /** - * This is an associative array with the organization display name for this entity. The key of - * the associative array is the language code, while the value is a string with the - * organization display name. - */ - private $organizationDisplayName = array(); - - - /** - * This is an associative array with the organization URI for this entity. The key of - * the associative array is the language code, while the value is the URI. - */ - private $organizationURL = array(); - - - /** - * This is an array of the Contact Persons of this entity. - */ - private $contacts = array(); - - - private $scopes; - private $entityAttributes; - private $attributes; - private $tags; - private $uiInfo; - private $discoHints; - - - /** - * This is an array of elements that may be used to validate this element. - * - * @var array - */ - private $validators = array(); - - - /** - * The original EntityDescriptor element for this entity, as a base64 encoded string. - */ - private $entityDescriptor; - - - /** - * This is the constructor for the SAMLParser class. - * - * @param SAML2_XML_md_EntityDescriptor $entityElement The EntityDescriptor. - * @param int|NULL $maxExpireTime The unix timestamp for when this entity should expire, or NULL if unknown. - * @param array $validators An array of parent elements that may validate this element. - */ - private function __construct(SAML2_XML_md_EntityDescriptor $entityElement, $maxExpireTime, array $validators = array()) { - assert('is_null($maxExpireTime) || is_int($maxExpireTime)'); - - $this->spDescriptors = array(); - $this->idpDescriptors = array(); - - $e = $entityElement->toXML(); - $e = $e->ownerDocument->saveXML($e); - $this->entityDescriptor = base64_encode($e); - $this->entityId = $entityElement->entityID; - - $expireTime = self::getExpireTime($entityElement, $maxExpireTime); - - $this->validators = $validators; - $this->validators[] = $entityElement; - - - /* Process Extensions element, if it exists. */ - $ext = self::processExtensions($entityElement); - $this->scopes = $ext['scope']; - $this->tags = $ext['tags']; - $this->entityAttributes = $ext['EntityAttributes']; - - /* Look over the RoleDescriptors. */ - foreach ($entityElement->RoleDescriptor as $child) { - - if ($child instanceof SAML2_XML_md_SPSSODescriptor) { - $this->processSPSSODescriptor($child, $expireTime); - } elseif ($child instanceof SAML2_XML_md_IDPSSODescriptor) { - $this->processIDPSSODescriptor($child, $expireTime); - } elseif ($child instanceof SAML2_XML_md_AttributeAuthorityDescriptor) { - $this->processAttributeAuthorityDescriptor($child, $expireTime); - } - } - - if ($entityElement->Organization) { - $this->processOrganization($entityElement->Organization); - } - - if(!empty($entityElement->ContactPerson)) { - foreach($entityElement->ContactPerson as $contact) { - $this->processContactPerson($contact); - } - } - } - - - /** - * This function parses a file which contains XML encoded metadata. - * - * @param string $file The path to the file which contains the metadata. - * @return SimpleSAML_Metadata_SAMLParser An instance of this class with the metadata loaded. - * @throws Exception If the file does not parse as XML. - */ - public static function parseFile($file) { - $doc = new DOMDocument(); - - $data = \SimpleSAML\Utils\HTTP::fetch($file); - - $res = $doc->loadXML($data); - if($res !== TRUE) { - throw new Exception('Failed to read XML from file: ' . $file); - } - - return self::parseDocument($doc); - } - - - /** - * This function parses a string which contains XML encoded metadata. - * - * @param string $metadata A string which contains XML encoded metadata. - * @return SimpleSAML_Metadata_SAMLParser An instance of this class with the metadata loaded. - * @throws Exception If the string does not parse as XML. - */ - public static function parseString($metadata) { - $doc = new DOMDocument(); - - $res = $doc->loadXML($metadata); - if($res !== TRUE) { - throw new Exception('Failed to parse XML string.'); - } - - return self::parseDocument($doc); - } - - - /** - * This function parses a DOMDocument which is assumed to contain a single EntityDescriptor element. - * - * @param DOMDocument $document The DOMDocument which contains the EntityDescriptor element. - * @return SimpleSAML_Metadata_SAMLParser An instance of this class with the metadata loaded. - */ - public static function parseDocument($document) { - assert('$document instanceof DOMDocument'); - - $entityElement = self::findEntityDescriptor($document); - - return self::parseElement($entityElement); - } - - - /** - * This function parses a SAML2_XML_md_EntityDescriptor object which represents a EntityDescriptor element. - * - * @param SAML2_XML_md_EntityDescriptor $entityElement A SAML2_XML_md_EntityDescriptor object which represents a EntityDescriptor element. - * @return SimpleSAML_Metadata_SAMLParser An instance of this class with the metadata loaded. - */ - public static function parseElement($entityElement) { - assert('$entityElement instanceof SAML2_XML_md_EntityDescriptor'); - - return new SimpleSAML_Metadata_SAMLParser($entityElement, NULL); - } - - - /** - * This function parses a file where the root node is either an EntityDescriptor element or an - * EntitiesDescriptor element. In both cases it will return an associative array of SAMLParser instances. If - * the file contains a single EntityDescriptorElement, then the array will contain a single SAMLParser - * instance. - * - * @param string $file The path to the file which contains the EntityDescriptor or EntitiesDescriptor element. - * @return SimpleSAML_Metadata_SAMLParser[] An array of SAMLParser instances. - * @throws Exception If the file does not parse as XML. - */ - public static function parseDescriptorsFile($file) { - - if ($file === NULL) throw new Exception('Cannot open file NULL. File name not specified.'); - - $data = \SimpleSAML\Utils\HTTP::fetch($file); - - $doc = new DOMDocument(); - $res = $doc->loadXML($data); - if($res !== TRUE) { - throw new Exception('Failed to read XML from file: ' . $file); - } - if ($doc->documentElement === NULL) throw new Exception('Opened file is not an XML document: ' . $file); - - return self::parseDescriptorsElement($doc->documentElement); - } - - - /** - * This function parses a string with XML data. The root node of the XML data is expected to be either an - * EntityDescriptor element or an EntitiesDescriptor element. It will return an associative array of - * SAMLParser instances. - * - * @param string $string The string with XML data. - * @return SimpleSAML_Metadata_SAMLParser[] An associative array of SAMLParser instances. The key of the array will be the entity id. - * @throws Exception If the string does not parse as XML. - */ - public static function parseDescriptorsString($string) { - - $doc = new DOMDocument(); - - $res = $doc->loadXML($string); - if($res !== TRUE) { - throw new Exception('Failed to parse XML string.'); - } - - return self::parseDescriptorsElement($doc->documentElement); - } - - - /** - * This function parses a DOMElement which represents either an EntityDescriptor element or an - * EntitiesDescriptor element. It will return an associative array of SAMLParser instances in both cases. - * - * @param DOMElement|NULL $element The DOMElement which contains the EntityDescriptor element or the EntitiesDescriptor element. - * @return SimpleSAML_Metadata_SAMLParser[] An associative array of SAMLParser instances. The key of the array will be the entity id. - * @throws Exception if the document is empty or the root is an unexpected node. - */ - public static function parseDescriptorsElement(DOMElement $element = NULL) { - - if($element === NULL) { - throw new Exception('Document was empty.'); - } - - assert('$element instanceof DOMElement'); - - if (SimpleSAML\Utils\XML::isDOMElementOfType($element, 'EntityDescriptor', '@md') === TRUE) { - return self::processDescriptorsElement(new SAML2_XML_md_EntityDescriptor($element)); - } elseif (SimpleSAML\Utils\XML::isDOMElementOfType($element, 'EntitiesDescriptor', '@md') === TRUE) { - return self::processDescriptorsElement(new SAML2_XML_md_EntitiesDescriptor($element)); - } else { - throw new Exception('Unexpected root node: [' . $element->namespaceURI . ']:' . - $element->localName); - } - } - - - /** - * - * @param SAML2_XML_md_EntityDescriptor|SAML2_XML_md_EntitiesDescriptor $element The element we should process. - * @param int|NULL $maxExpireTime The maximum expiration time of the entities. - * @param array $validators The parent-elements that may be signed. - * @return SimpleSAML_Metadata_SAMLParser[] Array of SAMLParser instances. - */ - private static function processDescriptorsElement($element, $maxExpireTime = NULL, array $validators = array()) { - assert('is_null($maxExpireTime) || is_int($maxExpireTime)'); - - if ($element instanceof SAML2_XML_md_EntityDescriptor) { - $ret = new SimpleSAML_Metadata_SAMLParser($element, $maxExpireTime, $validators); - return array($ret->getEntityId() => $ret); - } - - assert('$element instanceof SAML2_XML_md_EntitiesDescriptor'); - - - $expTime = self::getExpireTime($element, $maxExpireTime); - - $validators[] = $element; - - $ret = array(); - foreach ($element->children as $child) { - $ret += self::processDescriptorsElement($child, $expTime, $validators); - } - - return $ret; - } - - - /** - * Determine how long a given element can be cached. - * - * This function looks for the 'validUntil' attribute to determine - * how long a given XML-element is valid. It returns this as a unix timestamp. - * - * @param mixed $element The element we should determine the expiry time of. - * @param int|NULL $maxExpireTime The maximum expiration time. - * @return int The unix timestamp for when the element should expire. Will be NULL if no - * limit is set for the element. - */ - private static function getExpireTime($element, $maxExpireTime) { - /* validUntil may be NULL */ - $expire = $element->validUntil; - - if ( $maxExpireTime !== NULL && ($expire === NULL || $maxExpireTime < $expire) ) { - $expire = $maxExpireTime; - } - - return $expire; - } - - - /** - * This function returns the entity id of this parsed entity. - * - * @return string The entity id of this parsed entity. - */ - public function getEntityId() { - return $this->entityId; - } - - - private function getMetadataCommon() { - $ret = array(); - $ret['entityid'] = $this->entityId; - $ret['entityDescriptor'] = $this->entityDescriptor; - - - /* - * Add organizational metadata - */ - if (!empty($this->organizationName)) { - $ret['description'] = $this->organizationName; - $ret['OrganizationName'] = $this->organizationName; - } - if (!empty($this->organizationDisplayName)) { - $ret['name'] = $this->organizationDisplayName; - $ret['OrganizationDisplayName'] = $this->organizationDisplayName; - } - if (!empty($this->organizationURL)) { - $ret['url'] = $this->organizationURL; - $ret['OrganizationURL'] = $this->organizationURL; - } - - /* - * Add contact metadata - */ - $ret['contacts'] = $this->contacts; - - return $ret; - } - - - /** - * Add data parsed from extensions to metadata. - * - * @param array &$metadata The metadata that should be updated. - * @param array $roleDescriptor The parsed role descriptor. - */ - private function addExtensions(array &$metadata, array $roleDescriptor) { - assert('array_key_exists("scope", $roleDescriptor)'); - assert('array_key_exists("tags", $roleDescriptor)'); - - $scopes = array_merge($this->scopes, array_diff($roleDescriptor['scope'], $this->scopes)); - if (!empty($scopes)) { - $metadata['scope'] = $scopes; - } - - $tags = array_merge($this->tags, array_diff($roleDescriptor['tags'], $this->tags)); - if (!empty($tags)) { - $metadata['tags'] = $tags; - } - - if (!empty($this->entityAttributes)) { - $metadata['EntityAttributes'] = $this->entityAttributes; - } - - if (!empty($roleDescriptor['UIInfo'])) { - $metadata['UIInfo'] = $roleDescriptor['UIInfo']; - } - - if (!empty($roleDescriptor['DiscoHints'])) { - $metadata['DiscoHints'] = $roleDescriptor['DiscoHints']; - } - } - - - /** - * This function returns the metadata for SAML 1.x SPs 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. - * - 'AssertionConsumerService': String with the URL of the assertion consumer service which supports - * the browser-post binding. - * - 'certData': X509Certificate for entity (if present). - * - * Metadata must be loaded with one of the parse functions before this function can be called. - * - * @return array An associative array with metadata or NULL if we are unable to generate metadata for a SAML 1.x SP. - */ - public function getMetadata1xSP() { - - $ret = $this->getMetadataCommon(); - $ret['metadata-set'] = 'shib13-sp-remote'; - - - /* Find SP information which supports one of the SAML 1.x protocols. */ - $spd = $this->getSPDescriptors(self::$SAML1xProtocols); - if(count($spd) === 0) { - return NULL; - } - - /* We currently only look at the first SPDescriptor which supports SAML 1.x. */ - $spd = $spd[0]; - - /* Add expire time to metadata. */ - if (array_key_exists('expire', $spd)) { - $ret['expire'] = $spd['expire']; - } - - /* Find the assertion consumer service endpoints. */ - $ret['AssertionConsumerService'] = $spd['AssertionConsumerService']; - - /* Add the list of attributes the SP should receive. */ - if (array_key_exists('attributes', $spd)) { - $ret['attributes'] = $spd['attributes']; - } - if (array_key_exists('attributes.required', $spd)) { - $ret['attributes.required'] = $spd['attributes.required']; - } - if (array_key_exists('attributes.NameFormat', $spd)) { - $ret['attributes.NameFormat'] = $spd['attributes.NameFormat']; - } - - /* Add name & description. */ - if (array_key_exists('name', $spd)) { - $ret['name'] = $spd['name']; - } - if (array_key_exists('description', $spd)) { - $ret['description'] = $spd['description']; - } - - /* Add public keys. */ - if (!empty($spd['keys'])) { - $ret['keys'] = $spd['keys']; - } - - /* Add extensions. */ - $this->addExtensions($ret, $spd); - - // prioritize mdui:DisplayName as the name if available - if (!empty($ret['UIInfo']['DisplayName'])) { - $ret['name'] = $ret['UIInfo']['DisplayName']; - } - - return $ret; - } - - - /** - * This function returns the metadata for SAML 1.x 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': Auto generated name for this entity. Currently set to the entity id. - * - '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. - * - 'certData': X509Certificate for entity (if present). - * - '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 array An associative array with metadata or NULL if we are unable to generate metadata for a SAML 1.x IdP. - */ - public function getMetadata1xIdP() { - - $ret = $this->getMetadataCommon(); - $ret['metadata-set'] = 'shib13-idp-remote'; - - /* 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]; - - /* Add expire time to metadata. */ - if (array_key_exists('expire', $idp)) { - $ret['expire'] = $idp['expire']; - } - - /* Find the SSO service endpoints. */ - $ret['SingleSignOnService'] = $idp['SingleSignOnService']; - - /* Find the ArtifactResolutionService endpoint. */ - $ret['ArtifactResolutionService'] = $idp['ArtifactResolutionService']; - - /* Add public keys. */ - if (!empty($idp['keys'])) { - $ret['keys'] = $idp['keys']; - } - - /* Add extensions. */ - $this->addExtensions($ret, $idp); - - // prioritize mdui:DisplayName as the name if available - if (!empty($ret['UIInfo']['DisplayName'])) { - $ret['name'] = $ret['UIInfo']['DisplayName']; - } - - 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: - * - 'entityid': The entity id of the entity described in the metadata. - * - 'AssertionConsumerService': String with the URL of the assertion consumer service which supports - * the browser-post binding. - * - 'SingleLogoutService': String with the URL where we should send logout requests/responses. - * - 'NameIDFormat': The name ID format this SP expects. This may be unset. - * - 'certData': X509Certificate for entity (if present). - * - * Metadata must be loaded with one of the parse functions before this function can be called. - * - * @return array An associative array with metadata or NULL if we are unable to generate metadata for a SAML 2.x SP. - */ - public function getMetadata20SP() { - - $ret = $this->getMetadataCommon(); - $ret['metadata-set'] = 'saml20-sp-remote'; - - - /* Find SP information which supports the SAML 2.0 protocol. */ - $spd = $this->getSPDescriptors(self::$SAML20Protocols); - if(count($spd) === 0) { - return NULL; - } - - /* We currently only look at the first SPDescriptor which supports SAML 2.0. */ - $spd = $spd[0]; - - /* Add expire time to metadata. */ - if (array_key_exists('expire', $spd)) { - $ret['expire'] = $spd['expire']; - } - - /* Find the assertion consumer service endpoints. */ - $ret['AssertionConsumerService'] = $spd['AssertionConsumerService']; - - - /* Find the single logout service endpoint. */ - $ret['SingleLogoutService'] = $spd['SingleLogoutService']; - - - /* Find the NameIDFormat. This may not exists. */ - if(count($spd['nameIDFormats']) > 0) { - /* simpleSAMLphp currently only supports a single NameIDFormat pr. SP. We use the first one. */ - $ret['NameIDFormat'] = $spd['nameIDFormats'][0]; - } - - /* Add the list of attributes the SP should receive. */ - if (array_key_exists('attributes', $spd)) { - $ret['attributes'] = $spd['attributes']; - } - if (array_key_exists('attributes.required', $spd)) { - $ret['attributes.required'] = $spd['attributes.required']; - } - if (array_key_exists('attributes.NameFormat', $spd)) { - $ret['attributes.NameFormat'] = $spd['attributes.NameFormat']; - } - - /* Add name & description. */ - if (array_key_exists('name', $spd)) { - $ret['name'] = $spd['name']; - } - if (array_key_exists('description', $spd)) { - $ret['description'] = $spd['description']; - } - - /* Add public keys. */ - if (!empty($spd['keys'])) { - $ret['keys'] = $spd['keys']; - } - - /* Add validate.authnrequest. */ - if (array_key_exists('AuthnRequestsSigned', $spd)) { - $ret['validate.authnrequest'] = $spd['AuthnRequestsSigned']; - } - - /* Add saml20.sign.assertion. */ - if (array_key_exists('WantAssertionsSigned', $spd)) { - $ret['saml20.sign.assertion'] = $spd['WantAssertionsSigned']; - } - - /* Add extensions. */ - $this->addExtensions($ret, $spd); - - // prioritize mdui:DisplayName as the name if available - if (!empty($ret['UIInfo']['DisplayName'])) { - $ret['name'] = $ret['UIInfo']['DisplayName']; - } - - return $ret; - } - - - /** - * 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': Auto generated name for this entity. Currently set to the entity id. - * - '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). - * - 'SingleLogoutServiceResponse': String where we should send logout responses (if this is different from - * the 'SingleLogoutService' endpoint. - * - 'NameIDFormats': The name ID formats this IdP supports. - * - 'certData': X509Certificate for entity (if present). - * - '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 array An associative array with metadata or NULL if we are unable to generate metadata for a SAML 2.0 IdP. - */ - public function getMetadata20IdP() { - - $ret = $this->getMetadataCommon(); - $ret['metadata-set'] = 'saml20-idp-remote'; - - - /* 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]; - - /* Add expire time to metadata. */ - if (array_key_exists('expire', $idp)) { - $ret['expire'] = $idp['expire']; - } - - /* Enable redirect.sign if WantAuthnRequestsSigned is enabled. */ - if ($idp['WantAuthnRequestsSigned']) { - $ret['sign.authnrequest'] = TRUE; - } - - /* Find the SSO service endpoint. */ - $ret['SingleSignOnService'] = $idp['SingleSignOnService']; - - - /* Find the single logout service endpoint. */ - $ret['SingleLogoutService'] = $idp['SingleLogoutService']; - - /* Find the ArtifactResolutionService endpoint. */ - $ret['ArtifactResolutionService'] = $idp['ArtifactResolutionService']; - - /* Add supported nameIDFormats */ - $ret['NameIDFormats'] = $idp['nameIDFormats']; - - /* Add public keys. */ - if (!empty($idp['keys'])) { - $ret['keys'] = $idp['keys']; - } - - /* Add extensions. */ - $this->addExtensions($ret, $idp); - - // prioritize mdui:DisplayName as the name if available - if (!empty($ret['UIInfo']['DisplayName'])) { - $ret['name'] = $ret['UIInfo']['DisplayName']; - } - - return $ret; - } - - - /** - * Retrieve AttributeAuthorities from the metadata. - * - * @return array Array of AttributeAuthorityDescriptor entries. - */ - public function getAttributeAuthorities() { - - return $this->attributeAuthorityDescriptors; - } - - - /** - * Parse a RoleDescriptorType element. - * - * The returned associative array has the following elements: - * - 'protocols': Array with the protocols supported. - * - 'expire': Timestamp for when this descriptor expires. - * - 'keys': Array of associative arrays with the elements from parseKeyDescriptor. - * - * @param SAML2_XML_md_RoleDescriptor $element The element we should extract metadata from. - * @param int|NULL $expireTime The unix timestamp for when this element should expire, or - * NULL if unknown. - * @return array An associative array with metadata we have extracted from this element. - */ - private static function parseRoleDescriptorType(SAML2_XML_md_RoleDescriptor $element, $expireTime) { - assert('is_null($expireTime) || is_int($expireTime)'); - - $ret = array(); - - $expireTime = self::getExpireTime($element, $expireTime); - - if ($expireTime !== NULL) { - /* We have got an expire timestamp, either from this element, or one of the - * parent elements. - */ - $ret['expire'] = $expireTime; - } - - $ret['protocols'] = $element->protocolSupportEnumeration; - - /* Process KeyDescriptor elements. */ - $ret['keys'] = array(); - foreach ($element->KeyDescriptor as $kd) { - $key = self::parseKeyDescriptor($kd); - if($key !== NULL) { - $ret['keys'][] = $key; - } - } - - $ext = self::processExtensions($element); - $ret['scope'] = $ext['scope']; - $ret['tags'] = $ext['tags']; - $ret['EntityAttributes'] = $ext['EntityAttributes']; - $ret['UIInfo'] = $ext['UIInfo']; - $ret['DiscoHints'] = $ext['DiscoHints']; - - 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. - * - 'SingleLogoutService': 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 SAML2_XML_md_SSODescriptorType $element The element we should extract metadata from. - * @param int|NULL $expireTime The unix timestamp for when this element should expire, or - * NULL if unknown. - * @return array An associative array with metadata we have extracted from this element. - */ - private static function parseSSODescriptor(SAML2_XML_md_SSODescriptorType $element, $expireTime) { - assert('is_null($expireTime) || is_int($expireTime)'); - - $sd = self::parseRoleDescriptorType($element, $expireTime); - - /* Find all SingleLogoutService elements. */ - $sd['SingleLogoutService'] = self::extractEndpoints($element->SingleLogoutService); - - /* Find all ArtifactResolutionService elements. */ - $sd['ArtifactResolutionService'] = self::extractEndpoints($element->ArtifactResolutionService); - - - /* Process NameIDFormat elements. */ - $sd['nameIDFormats'] = $element->NameIDFormat; - - return $sd; - } - - - /** - * This function extracts metadata from a SPSSODescriptor element. - * - * @param SAML2_XML_md_SPSSODescriptor $element The element which should be parsed. - * @param int|NULL $expireTime The unix timestamp for when this element should expire, or - * NULL if unknown. - */ - private function processSPSSODescriptor(SAML2_XML_md_SPSSODescriptor $element, $expireTime) { - assert('is_null($expireTime) || is_int($expireTime)'); - - $sp = self::parseSSODescriptor($element, $expireTime); - - /* Find all AssertionConsumerService elements. */ - $sp['AssertionConsumerService'] = self::extractEndpoints($element->AssertionConsumerService); - - /* Find all the attributes and SP name... */ - $attcs = $element->AttributeConsumingService; - if (count($attcs) > 0) { - self::parseAttributeConsumerService($attcs[0], $sp); - } - - /* Check AuthnRequestsSigned */ - if ($element->AuthnRequestsSigned !== NULL) { - $sp['AuthnRequestsSigned'] = $element->AuthnRequestsSigned; - } - - /* Check WantAssertionsSigned */ - if ($element->WantAssertionsSigned !== NULL) { - $sp['WantAssertionsSigned'] = $element->WantAssertionsSigned; - } - - $this->spDescriptors[] = $sp; - } - - - /** - * This function extracts metadata from a IDPSSODescriptor element. - * - * @param SAML2_XML_md_IDPSSODescriptor $element The element which should be parsed. - * @param int|NULL $expireTime The unix timestamp for when this element should expire, or - * NULL if unknown. - */ - private function processIDPSSODescriptor(SAML2_XML_md_IDPSSODescriptor $element, $expireTime) { - assert('is_null($expireTime) || is_int($expireTime)'); - - $idp = self::parseSSODescriptor($element, $expireTime); - - /* Find all SingleSignOnService elements. */ - $idp['SingleSignOnService'] = self::extractEndpoints($element->SingleSignOnService); - - if ($element->WantAuthnRequestsSigned) { - $idp['WantAuthnRequestsSigned'] = TRUE; - } else { - $idp['WantAuthnRequestsSigned'] = FALSE; - } - - $this->idpDescriptors[] = $idp; - } - - - /** - * This function extracts metadata from a AttributeAuthorityDescriptor element. - * - * @param SAML2_XML_md_AttributeAuthorityDescriptor $element The element which should be parsed. - * @param int|NULL $expireTime The unix timestamp for when this element should expire, or - * NULL if unknown. - */ - private function processAttributeAuthorityDescriptor(SAML2_XML_md_AttributeAuthorityDescriptor $element, $expireTime) { - assert('is_null($expireTime) || is_int($expireTime)'); - - $aad = self::parseRoleDescriptorType($element, $expireTime); - $aad['entityid'] = $this->entityId; - $aad['metadata-set'] = 'attributeauthority-remote'; - - $aad['AttributeService'] = self::extractEndpoints($element->AttributeService); - $aad['AssertionIDRequestService'] = self::extractEndpoints($element->AssertionIDRequestService); - $aad['NameIDFormat'] = $element->NameIDFormat; - - $this->attributeAuthorityDescriptors[] = $aad; - } - - - /** - * Parse an Extensions element. - * - * @param mixed $element The element which contains the Extensions element. - * - * @return array An associative array with the extensions parsed. - */ - private static function processExtensions($element) { - - $ret = array( - 'scope' => array(), - 'tags' => array(), - 'EntityAttributes' => array(), - 'UIInfo' => array(), - 'DiscoHints' => array(), - ); - - foreach ($element->Extensions as $e) { - - if ($e instanceof SAML2_XML_shibmd_Scope) { - $ret['scope'][] = $e->scope; - continue; - } - - // Entity Attributes are only allowed at entity level extensions - // and not at RoleDescriptor level - if ($element instanceof SAML2_XML_md_EntityDescriptor) { - - if ($e instanceof SAML2_XML_mdattr_EntityAttributes && !empty($e->children)) { - - foreach($e->children AS $attr) { - - // Only saml:Attribute are currently supported here. The specifications also allows - // saml:Assertions, which more complex processing. - if ($attr instanceof SAML2_XML_saml_Attribute) { - if (empty($attr->Name) || empty($attr->AttributeValue)) continue; - - // Attribute names that is not URI is prefixed as this: '{nameformat}name' - $name = $attr->Name; - if(empty($attr->NameFormat)) { - $name = '{' . SAML2_Const::NAMEFORMAT_UNSPECIFIED . '}' . $attr->Name; - } elseif ($attr->NameFormat !== 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri') { - $name = '{' . $attr->NameFormat . '}' . $attr->Name; - } - - $values = array(); - foreach($attr->AttributeValue AS $attrvalue) { - $values[] = $attrvalue->getString(); - } - - $ret['EntityAttributes'][$name] = $values; - } - } - } - } - - // UIInfo elements are only allowed at RoleDescriptor level extensions - if ($element instanceof SAML2_XML_md_RoleDescriptor) { - - if ($e instanceof SAML2_XML_mdui_UIInfo) { - - $ret['UIInfo']['DisplayName'] = $e->DisplayName; - $ret['UIInfo']['Description'] = $e->Description; - $ret['UIInfo']['InformationURL'] = $e->InformationURL; - $ret['UIInfo']['PrivacyStatementURL'] = $e->PrivacyStatementURL; - - foreach($e->Keywords as $uiItem) { - if (!($uiItem instanceof SAML2_XML_mdui_Keywords) - || empty($uiItem->Keywords) - || empty($uiItem->lang)) - continue; - $ret['UIInfo']['Keywords'][$uiItem->lang] = $uiItem->Keywords; - } - foreach($e->Logo as $uiItem) { - if (!($uiItem instanceof SAML2_XML_mdui_Logo) - || empty($uiItem->url) - || empty($uiItem->height) - || empty($uiItem->width)) - continue; - $logo = array( - 'url' => $uiItem->url, - 'height' => $uiItem->height, - 'width' => $uiItem->width, - ); - if (!empty($uiItem->Lang)) { - $logo['lang'] = $uiItem->lang; - } - $ret['UIInfo']['Logo'][] = $logo; - } - } - } - - // DiscoHints elements are only allowed at IDPSSODescriptor level extensions - if ($element instanceof SAML2_XML_md_IDPSSODescriptor) { - - if ($e instanceof SAML2_XML_mdui_DiscoHints) { - $ret['DiscoHints']['IPHint'] = $e->IPHint; - $ret['DiscoHints']['DomainHint'] = $e->DomainHint; - $ret['DiscoHints']['GeolocationHint'] = $e->GeolocationHint; - } - } - - - if (!($e instanceof SAML2_XML_Chunk)) { - continue; - } - - - if ($e->localName === 'Attribute' && $e->namespaceURI === SAML2_Const::NS_SAML) { - $attribute = $e->getXML(); - - $name = $attribute->getAttribute('Name'); - $values = array_map( - array('SimpleSAML\Utils\XML', 'getDOMText'), +class SimpleSAML_Metadata_SAMLParser +{ + + /** + * This is the list of SAML 1.x protocols. + * + * @var string[] + */ + private static $SAML1xProtocols = array( + 'urn:oasis:names:tc:SAML:1.0:protocol', + 'urn:oasis:names:tc:SAML:1.1:protocol', + ); + + + /** + * This is the list with the SAML 2.0 protocol. + * + * @var string[] + */ + private static $SAML20Protocols = array( + 'urn:oasis:names:tc:SAML:2.0:protocol', + ); + + + /** + * This is the entity id we find in the metadata. + * + * @var string + */ + private $entityId; + + + /** + * 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 elements from parseSSODescriptor and: + * - 'AssertionConsumerService': Array with the SP's assertion consumer services. + * Each assertion consumer service is stored as an associative array with the + * elements that parseGenericEndpoint returns. + * + * @var 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: + * - 'SingleSignOnService': Array with the IdP's single sign on service endpoints. Each endpoint is stored + * as an associative array with the elements that parseGenericEndpoint returns. + * + * @var array[] + */ + private $idpDescriptors; + + + /** + * List of attribute authorities we have found. + * + * @var array + */ + private $attributeAuthorityDescriptors = array(); + + + /** + * This is an associative array with the organization name for this entity. The key of + * the associative array is the language code, while the value is a string with the + * organization name. + * + * @var string[] + */ + private $organizationName = array(); + + + /** + * This is an associative array with the organization display name for this entity. The key of + * the associative array is the language code, while the value is a string with the + * organization display name. + * + * @var string[] + */ + private $organizationDisplayName = array(); + + + /** + * This is an associative array with the organization URI for this entity. The key of + * the associative array is the language code, while the value is the URI. + * + * @var string[] + */ + private $organizationURL = array(); + + + /** + * This is an array of the Contact Persons of this entity. + * + * @var array[] + */ + private $contacts = array(); + + + /** + * @var array + */ + private $scopes; + + + /** + * @var array + */ + private $entityAttributes; + + + /** + * @var array + */ + private $tags; + + + /** + * This is an array of elements that may be used to validate this element. + * + * @var SAML2_SignedElementHelper[] + */ + private $validators = array(); + + + /** + * The original EntityDescriptor element for this entity, as a base64 encoded string. + * + * @var string + */ + private $entityDescriptor; + + + /** + * This is the constructor for the SAMLParser class. + * + * @param SAML2_XML_md_EntityDescriptor $entityElement The EntityDescriptor. + * @param int|NULL $maxExpireTime The unix timestamp for when this entity should expire, or + * NULL if unknown. + * @param array $validators An array of parent elements that may validate this element. + */ + private function __construct( + SAML2_XML_md_EntityDescriptor $entityElement, + $maxExpireTime, + array $validators = array() + ) { + assert('is_null($maxExpireTime) || is_int($maxExpireTime)'); + + $this->spDescriptors = array(); + $this->idpDescriptors = array(); + + $e = $entityElement->toXML(); + $e = $e->ownerDocument->saveXML($e); + $this->entityDescriptor = base64_encode($e); + $this->entityId = $entityElement->entityID; + + $expireTime = self::getExpireTime($entityElement, $maxExpireTime); + + $this->validators = $validators; + $this->validators[] = $entityElement; + + // process Extensions element, if it exists + $ext = self::processExtensions($entityElement); + $this->scopes = $ext['scope']; + $this->tags = $ext['tags']; + $this->entityAttributes = $ext['EntityAttributes']; + + // look over the RoleDescriptors + foreach ($entityElement->RoleDescriptor as $child) { + + if ($child instanceof SAML2_XML_md_SPSSODescriptor) { + $this->processSPSSODescriptor($child, $expireTime); + } elseif ($child instanceof SAML2_XML_md_IDPSSODescriptor) { + $this->processIDPSSODescriptor($child, $expireTime); + } elseif ($child instanceof SAML2_XML_md_AttributeAuthorityDescriptor) { + $this->processAttributeAuthorityDescriptor($child, $expireTime); + } + } + + if ($entityElement->Organization) { + $this->processOrganization($entityElement->Organization); + } + + if (!empty($entityElement->ContactPerson)) { + foreach ($entityElement->ContactPerson as $contact) { + $this->processContactPerson($contact); + } + } + } + + + /** + * This function parses a file which contains XML encoded metadata. + * + * @param string $file The path to the file which contains the metadata. + * + * @return SimpleSAML_Metadata_SAMLParser An instance of this class with the metadata loaded. + * @throws Exception If the file does not parse as XML. + */ + public static function parseFile($file) + { + $doc = new DOMDocument(); + + $data = \SimpleSAML\Utils\HTTP::fetch($file); + + $res = $doc->loadXML($data); + if ($res !== true) { + throw new Exception('Failed to read XML from file: '.$file); + } + + return self::parseDocument($doc); + } + + + /** + * This function parses a string which contains XML encoded metadata. + * + * @param string $metadata A string which contains XML encoded metadata. + * + * @return SimpleSAML_Metadata_SAMLParser An instance of this class with the metadata loaded. + * @throws Exception If the string does not parse as XML. + */ + public static function parseString($metadata) + { + $doc = new DOMDocument(); + + $res = $doc->loadXML($metadata); + if ($res !== true) { + throw new Exception('Failed to parse XML string.'); + } + + return self::parseDocument($doc); + } + + + /** + * This function parses a DOMDocument which is assumed to contain a single EntityDescriptor element. + * + * @param DOMDocument $document The DOMDocument which contains the EntityDescriptor element. + * + * @return SimpleSAML_Metadata_SAMLParser An instance of this class with the metadata loaded. + */ + public static function parseDocument($document) + { + assert('$document instanceof DOMDocument'); + + $entityElement = self::findEntityDescriptor($document); + + return self::parseElement($entityElement); + } + + + /** + * This function parses a SAML2_XML_md_EntityDescriptor object which represents a EntityDescriptor element. + * + * @param SAML2_XML_md_EntityDescriptor $entityElement A SAML2_XML_md_EntityDescriptor object which represents a + * EntityDescriptor element. + * + * @return SimpleSAML_Metadata_SAMLParser An instance of this class with the metadata loaded. + */ + public static function parseElement($entityElement) + { + assert('$entityElement instanceof SAML2_XML_md_EntityDescriptor'); + + return new SimpleSAML_Metadata_SAMLParser($entityElement, null); + } + + + /** + * This function parses a file where the root node is either an EntityDescriptor element or an + * EntitiesDescriptor element. In both cases it will return an associative array of SAMLParser instances. If + * the file contains a single EntityDescriptorElement, then the array will contain a single SAMLParser + * instance. + * + * @param string $file The path to the file which contains the EntityDescriptor or EntitiesDescriptor element. + * + * @return SimpleSAML_Metadata_SAMLParser[] An array of SAMLParser instances. + * @throws Exception If the file does not parse as XML. + */ + public static function parseDescriptorsFile($file) + { + + if ($file === null) { + throw new Exception('Cannot open file NULL. File name not specified.'); + } + + $data = \SimpleSAML\Utils\HTTP::fetch($file); + + $doc = new DOMDocument(); + $res = $doc->loadXML($data); + if ($res !== true) { + throw new Exception('Failed to read XML from file: '.$file); + } + if ($doc->documentElement === null) { + throw new Exception('Opened file is not an XML document: '.$file); + } + + return self::parseDescriptorsElement($doc->documentElement); + } + + + /** + * This function parses a string with XML data. The root node of the XML data is expected to be either an + * EntityDescriptor element or an EntitiesDescriptor element. It will return an associative array of + * SAMLParser instances. + * + * @param string $string The string with XML data. + * + * @return SimpleSAML_Metadata_SAMLParser[] An associative array of SAMLParser instances. The key of the array will + * be the entity id. + * @throws Exception If the string does not parse as XML. + */ + public static function parseDescriptorsString($string) + { + $doc = new DOMDocument(); + + $res = $doc->loadXML($string); + if ($res !== true) { + throw new Exception('Failed to parse XML string.'); + } + + return self::parseDescriptorsElement($doc->documentElement); + } + + + /** + * This function parses a DOMElement which represents either an EntityDescriptor element or an + * EntitiesDescriptor element. It will return an associative array of SAMLParser instances in both cases. + * + * @param DOMElement|NULL $element The DOMElement which contains the EntityDescriptor element or the + * EntitiesDescriptor element. + * + * @return SimpleSAML_Metadata_SAMLParser[] An associative array of SAMLParser instances. The key of the array will + * be the entity id. + * @throws Exception if the document is empty or the root is an unexpected node. + */ + public static function parseDescriptorsElement(DOMElement $element = null) + { + if ($element === null) { + throw new Exception('Document was empty.'); + } + + assert('$element instanceof DOMElement'); + + if (SimpleSAML\Utils\XML::isDOMElementOfType($element, 'EntityDescriptor', '@md') === true) { + return self::processDescriptorsElement(new SAML2_XML_md_EntityDescriptor($element)); + } elseif (SimpleSAML\Utils\XML::isDOMElementOfType($element, 'EntitiesDescriptor', '@md') === true) { + return self::processDescriptorsElement(new SAML2_XML_md_EntitiesDescriptor($element)); + } else { + throw new Exception('Unexpected root node: ['.$element->namespaceURI.']:'.$element->localName); + } + } + + + /** + * + * @param SAML2_XML_md_EntityDescriptor|SAML2_XML_md_EntitiesDescriptor $element The element we should process. + * @param int|NULL $maxExpireTime The maximum expiration time + * of the entities. + * @param array $validators The parent-elements that may be + * signed. + * + * @return SimpleSAML_Metadata_SAMLParser[] Array of SAMLParser instances. + */ + private static function processDescriptorsElement($element, $maxExpireTime = null, array $validators = array()) + { + assert('is_null($maxExpireTime) || is_int($maxExpireTime)'); + + if ($element instanceof SAML2_XML_md_EntityDescriptor) { + $ret = new SimpleSAML_Metadata_SAMLParser($element, $maxExpireTime, $validators); + return array($ret->getEntityId() => $ret); + } + + assert('$element instanceof SAML2_XML_md_EntitiesDescriptor'); + + $expTime = self::getExpireTime($element, $maxExpireTime); + + $validators[] = $element; + + $ret = array(); + foreach ($element->children as $child) { + $ret += self::processDescriptorsElement($child, $expTime, $validators); + } + + return $ret; + } + + + /** + * Determine how long a given element can be cached. + * + * This function looks for the 'validUntil' attribute to determine + * how long a given XML-element is valid. It returns this as a unix timestamp. + * + * @param mixed $element The element we should determine the expiry time of. + * @param int|NULL $maxExpireTime The maximum expiration time. + * + * @return int The unix timestamp for when the element should expire. Will be NULL if no + * limit is set for the element. + */ + private static function getExpireTime($element, $maxExpireTime) + { + // validUntil may be null + $expire = $element->validUntil; + + if ($maxExpireTime !== null && ($expire === null || $maxExpireTime < $expire)) { + $expire = $maxExpireTime; + } + + return $expire; + } + + + /** + * This function returns the entity id of this parsed entity. + * + * @return string The entity id of this parsed entity. + */ + public function getEntityId() + { + return $this->entityId; + } + + + private function getMetadataCommon() + { + $ret = array(); + $ret['entityid'] = $this->entityId; + $ret['entityDescriptor'] = $this->entityDescriptor; + + // add organizational metadata + if (!empty($this->organizationName)) { + $ret['description'] = $this->organizationName; + $ret['OrganizationName'] = $this->organizationName; + } + if (!empty($this->organizationDisplayName)) { + $ret['name'] = $this->organizationDisplayName; + $ret['OrganizationDisplayName'] = $this->organizationDisplayName; + } + if (!empty($this->organizationURL)) { + $ret['url'] = $this->organizationURL; + $ret['OrganizationURL'] = $this->organizationURL; + } + + //add contact metadata + $ret['contacts'] = $this->contacts; + + return $ret; + } + + + /** + * Add data parsed from extensions to metadata. + * + * @param array &$metadata The metadata that should be updated. + * @param array $roleDescriptor The parsed role descriptor. + */ + private function addExtensions(array &$metadata, array $roleDescriptor) + { + assert('array_key_exists("scope", $roleDescriptor)'); + assert('array_key_exists("tags", $roleDescriptor)'); + + $scopes = array_merge($this->scopes, array_diff($roleDescriptor['scope'], $this->scopes)); + if (!empty($scopes)) { + $metadata['scope'] = $scopes; + } + + $tags = array_merge($this->tags, array_diff($roleDescriptor['tags'], $this->tags)); + if (!empty($tags)) { + $metadata['tags'] = $tags; + } + + if (!empty($this->entityAttributes)) { + $metadata['EntityAttributes'] = $this->entityAttributes; + } + + if (!empty($roleDescriptor['UIInfo'])) { + $metadata['UIInfo'] = $roleDescriptor['UIInfo']; + } + + if (!empty($roleDescriptor['DiscoHints'])) { + $metadata['DiscoHints'] = $roleDescriptor['DiscoHints']; + } + } + + + /** + * This function returns the metadata for SAML 1.x SPs 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. + * - 'AssertionConsumerService': String with the URL of the assertion consumer service which supports + * the browser-post binding. + * - 'certData': X509Certificate for entity (if present). + * + * Metadata must be loaded with one of the parse functions before this function can be called. + * + * @return array An associative array with metadata or NULL if we are unable to generate metadata for a SAML 1.x SP. + */ + public function getMetadata1xSP() + { + $ret = $this->getMetadataCommon(); + $ret['metadata-set'] = 'shib13-sp-remote'; + + + // find SP information which supports one of the SAML 1.x protocols + $spd = $this->getSPDescriptors(self::$SAML1xProtocols); + if (count($spd) === 0) { + return null; + } + + // we currently only look at the first SPDescriptor which supports SAML 1.x + $spd = $spd[0]; + + // add expire time to metadata + if (array_key_exists('expire', $spd)) { + $ret['expire'] = $spd['expire']; + } + + // find the assertion consumer service endpoints + $ret['AssertionConsumerService'] = $spd['AssertionConsumerService']; + + // add the list of attributes the SP should receive + if (array_key_exists('attributes', $spd)) { + $ret['attributes'] = $spd['attributes']; + } + if (array_key_exists('attributes.required', $spd)) { + $ret['attributes.required'] = $spd['attributes.required']; + } + if (array_key_exists('attributes.NameFormat', $spd)) { + $ret['attributes.NameFormat'] = $spd['attributes.NameFormat']; + } + + // add name & description + if (array_key_exists('name', $spd)) { + $ret['name'] = $spd['name']; + } + if (array_key_exists('description', $spd)) { + $ret['description'] = $spd['description']; + } + + // add public keys + if (!empty($spd['keys'])) { + $ret['keys'] = $spd['keys']; + } + + // add extensions + $this->addExtensions($ret, $spd); + + // prioritize mdui:DisplayName as the name if available + if (!empty($ret['UIInfo']['DisplayName'])) { + $ret['name'] = $ret['UIInfo']['DisplayName']; + } + + return $ret; + } + + + /** + * This function returns the metadata for SAML 1.x 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': Auto generated name for this entity. Currently set to the entity id. + * - '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. + * - 'certData': X509Certificate for entity (if present). + * - '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 array An associative array with metadata or NULL if we are unable to generate metadata for a SAML 1.x + * IdP. + */ + public function getMetadata1xIdP() + { + $ret = $this->getMetadataCommon(); + $ret['metadata-set'] = 'shib13-idp-remote'; + + // 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]; + + // fdd expire time to metadata + if (array_key_exists('expire', $idp)) { + $ret['expire'] = $idp['expire']; + } + + // find the SSO service endpoints + $ret['SingleSignOnService'] = $idp['SingleSignOnService']; + + // find the ArtifactResolutionService endpoint + $ret['ArtifactResolutionService'] = $idp['ArtifactResolutionService']; + + // add public keys + if (!empty($idp['keys'])) { + $ret['keys'] = $idp['keys']; + } + + // add extensions + $this->addExtensions($ret, $idp); + + // prioritize mdui:DisplayName as the name if available + if (!empty($ret['UIInfo']['DisplayName'])) { + $ret['name'] = $ret['UIInfo']['DisplayName']; + } + + 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: + * - 'entityid': The entity id of the entity described in the metadata. + * - 'AssertionConsumerService': String with the URL of the assertion consumer service which supports + * the browser-post binding. + * - 'SingleLogoutService': String with the URL where we should send logout requests/responses. + * - 'NameIDFormat': The name ID format this SP expects. This may be unset. + * - 'certData': X509Certificate for entity (if present). + * + * Metadata must be loaded with one of the parse functions before this function can be called. + * + * @return array An associative array with metadata or NULL if we are unable to generate metadata for a SAML 2.x SP. + */ + public function getMetadata20SP() + { + $ret = $this->getMetadataCommon(); + $ret['metadata-set'] = 'saml20-sp-remote'; + + // find SP information which supports the SAML 2.0 protocol + $spd = $this->getSPDescriptors(self::$SAML20Protocols); + if (count($spd) === 0) { + return null; + } + + // we currently only look at the first SPDescriptor which supports SAML 2.0 + $spd = $spd[0]; + + // add expire time to metadata + if (array_key_exists('expire', $spd)) { + $ret['expire'] = $spd['expire']; + } + + // find the assertion consumer service endpoints + $ret['AssertionConsumerService'] = $spd['AssertionConsumerService']; + + + // find the single logout service endpoint + $ret['SingleLogoutService'] = $spd['SingleLogoutService']; + + + // find the NameIDFormat. This may not exist + if (count($spd['nameIDFormats']) > 0) { + // SimpleSAMLphp currently only supports a single NameIDFormat pr. SP. We use the first one + $ret['NameIDFormat'] = $spd['nameIDFormats'][0]; + } + + // add the list of attributes the SP should receive + if (array_key_exists('attributes', $spd)) { + $ret['attributes'] = $spd['attributes']; + } + if (array_key_exists('attributes.required', $spd)) { + $ret['attributes.required'] = $spd['attributes.required']; + } + if (array_key_exists('attributes.NameFormat', $spd)) { + $ret['attributes.NameFormat'] = $spd['attributes.NameFormat']; + } + + // add name & description + if (array_key_exists('name', $spd)) { + $ret['name'] = $spd['name']; + } + if (array_key_exists('description', $spd)) { + $ret['description'] = $spd['description']; + } + + // add public keys + if (!empty($spd['keys'])) { + $ret['keys'] = $spd['keys']; + } + + // add validate.authnrequest + if (array_key_exists('AuthnRequestsSigned', $spd)) { + $ret['validate.authnrequest'] = $spd['AuthnRequestsSigned']; + } + + // add saml20.sign.assertion + if (array_key_exists('WantAssertionsSigned', $spd)) { + $ret['saml20.sign.assertion'] = $spd['WantAssertionsSigned']; + } + + // add extensions + $this->addExtensions($ret, $spd); + + // prioritize mdui:DisplayName as the name if available + if (!empty($ret['UIInfo']['DisplayName'])) { + $ret['name'] = $ret['UIInfo']['DisplayName']; + } + + return $ret; + } + + + /** + * 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': Auto generated name for this entity. Currently set to the entity id. + * - '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). + * - 'SingleLogoutServiceResponse': String where we should send logout responses (if this is different from + * the 'SingleLogoutService' endpoint. + * - 'NameIDFormats': The name ID formats this IdP supports. + * - 'certData': X509Certificate for entity (if present). + * - '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 array An associative array with metadata or NULL if we are unable to generate metadata for a SAML 2.0 + * IdP. + */ + public function getMetadata20IdP() + { + $ret = $this->getMetadataCommon(); + $ret['metadata-set'] = 'saml20-idp-remote'; + + // 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]; + + // add expire time to metadata + if (array_key_exists('expire', $idp)) { + $ret['expire'] = $idp['expire']; + } + + // enable redirect.sign if WantAuthnRequestsSigned is enabled + if ($idp['WantAuthnRequestsSigned']) { + $ret['sign.authnrequest'] = true; + } + + // find the SSO service endpoint + $ret['SingleSignOnService'] = $idp['SingleSignOnService']; + + // find the single logout service endpoint + $ret['SingleLogoutService'] = $idp['SingleLogoutService']; + + // find the ArtifactResolutionService endpoint + $ret['ArtifactResolutionService'] = $idp['ArtifactResolutionService']; + + // add supported nameIDFormats + $ret['NameIDFormats'] = $idp['nameIDFormats']; + + // add public keys + if (!empty($idp['keys'])) { + $ret['keys'] = $idp['keys']; + } + + // add extensions + $this->addExtensions($ret, $idp); + + // prioritize mdui:DisplayName as the name if available + if (!empty($ret['UIInfo']['DisplayName'])) { + $ret['name'] = $ret['UIInfo']['DisplayName']; + } + + return $ret; + } + + + /** + * Retrieve AttributeAuthorities from the metadata. + * + * @return array Array of AttributeAuthorityDescriptor entries. + */ + public function getAttributeAuthorities() + { + return $this->attributeAuthorityDescriptors; + } + + + /** + * Parse a RoleDescriptorType element. + * + * The returned associative array has the following elements: + * - 'protocols': Array with the protocols supported. + * - 'expire': Timestamp for when this descriptor expires. + * - 'keys': Array of associative arrays with the elements from parseKeyDescriptor. + * + * @param SAML2_XML_md_RoleDescriptor $element The element we should extract metadata from. + * @param int|NULL $expireTime The unix timestamp for when this element should expire, or + * NULL if unknown. + * + * @return array An associative array with metadata we have extracted from this element. + */ + private static function parseRoleDescriptorType(SAML2_XML_md_RoleDescriptor $element, $expireTime) + { + assert('is_null($expireTime) || is_int($expireTime)'); + + $ret = array(); + + $expireTime = self::getExpireTime($element, $expireTime); + + if ($expireTime !== null) { + // we got an expired timestamp, either from this element or one of the parent elements + $ret['expire'] = $expireTime; + } + + $ret['protocols'] = $element->protocolSupportEnumeration; + + // process KeyDescriptor elements + $ret['keys'] = array(); + foreach ($element->KeyDescriptor as $kd) { + $key = self::parseKeyDescriptor($kd); + if ($key !== null) { + $ret['keys'][] = $key; + } + } + + $ext = self::processExtensions($element); + $ret['scope'] = $ext['scope']; + $ret['tags'] = $ext['tags']; + $ret['EntityAttributes'] = $ext['EntityAttributes']; + $ret['UIInfo'] = $ext['UIInfo']; + $ret['DiscoHints'] = $ext['DiscoHints']; + + 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. + * - 'SingleLogoutService': 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 SAML2_XML_md_SSODescriptorType $element The element we should extract metadata from. + * @param int|NULL $expireTime The unix timestamp for when this element should expire, or + * NULL if unknown. + * + * @return array An associative array with metadata we have extracted from this element. + */ + private static function parseSSODescriptor(SAML2_XML_md_SSODescriptorType $element, $expireTime) + { + assert('is_null($expireTime) || is_int($expireTime)'); + + $sd = self::parseRoleDescriptorType($element, $expireTime); + + // find all SingleLogoutService elements + $sd['SingleLogoutService'] = self::extractEndpoints($element->SingleLogoutService); + + // find all ArtifactResolutionService elements + $sd['ArtifactResolutionService'] = self::extractEndpoints($element->ArtifactResolutionService); + + + // process NameIDFormat elements + $sd['nameIDFormats'] = $element->NameIDFormat; + + return $sd; + } + + + /** + * This function extracts metadata from a SPSSODescriptor element. + * + * @param SAML2_XML_md_SPSSODescriptor $element The element which should be parsed. + * @param int|NULL $expireTime The unix timestamp for when this element should expire, or + * NULL if unknown. + */ + private function processSPSSODescriptor(SAML2_XML_md_SPSSODescriptor $element, $expireTime) + { + assert('is_null($expireTime) || is_int($expireTime)'); + + $sp = self::parseSSODescriptor($element, $expireTime); + + // find all AssertionConsumerService elements + $sp['AssertionConsumerService'] = self::extractEndpoints($element->AssertionConsumerService); + + // find all the attributes and SP name... + $attcs = $element->AttributeConsumingService; + if (count($attcs) > 0) { + self::parseAttributeConsumerService($attcs[0], $sp); + } + + // check AuthnRequestsSigned + if ($element->AuthnRequestsSigned !== null) { + $sp['AuthnRequestsSigned'] = $element->AuthnRequestsSigned; + } + + // check WantAssertionsSigned + if ($element->WantAssertionsSigned !== null) { + $sp['WantAssertionsSigned'] = $element->WantAssertionsSigned; + } + + $this->spDescriptors[] = $sp; + } + + + /** + * This function extracts metadata from a IDPSSODescriptor element. + * + * @param SAML2_XML_md_IDPSSODescriptor $element The element which should be parsed. + * @param int|NULL $expireTime The unix timestamp for when this element should expire, or + * NULL if unknown. + */ + private function processIDPSSODescriptor(SAML2_XML_md_IDPSSODescriptor $element, $expireTime) + { + assert('is_null($expireTime) || is_int($expireTime)'); + + $idp = self::parseSSODescriptor($element, $expireTime); + + // find all SingleSignOnService elements + $idp['SingleSignOnService'] = self::extractEndpoints($element->SingleSignOnService); + + if ($element->WantAuthnRequestsSigned) { + $idp['WantAuthnRequestsSigned'] = true; + } else { + $idp['WantAuthnRequestsSigned'] = false; + } + + $this->idpDescriptors[] = $idp; + } + + + /** + * This function extracts metadata from a AttributeAuthorityDescriptor element. + * + * @param SAML2_XML_md_AttributeAuthorityDescriptor $element The element which should be parsed. + * @param int|NULL $expireTime The unix timestamp for when this element should + * expire, or NULL if unknown. + */ + private function processAttributeAuthorityDescriptor( + SAML2_XML_md_AttributeAuthorityDescriptor $element, + $expireTime + ) { + assert('is_null($expireTime) || is_int($expireTime)'); + + $aad = self::parseRoleDescriptorType($element, $expireTime); + $aad['entityid'] = $this->entityId; + $aad['metadata-set'] = 'attributeauthority-remote'; + + $aad['AttributeService'] = self::extractEndpoints($element->AttributeService); + $aad['AssertionIDRequestService'] = self::extractEndpoints($element->AssertionIDRequestService); + $aad['NameIDFormat'] = $element->NameIDFormat; + + $this->attributeAuthorityDescriptors[] = $aad; + } + + + /** + * Parse an Extensions element. + * + * @param mixed $element The element which contains the Extensions element. + * + * @return array An associative array with the extensions parsed. + */ + private static function processExtensions($element) + { + $ret = array( + 'scope' => array(), + 'tags' => array(), + 'EntityAttributes' => array(), + 'UIInfo' => array(), + 'DiscoHints' => array(), + ); + + foreach ($element->Extensions as $e) { + + if ($e instanceof SAML2_XML_shibmd_Scope) { + $ret['scope'][] = $e->scope; + continue; + } + + // Entity Attributes are only allowed at entity level extensions and not at RoleDescriptor level + if ($element instanceof SAML2_XML_md_EntityDescriptor) { + if ($e instanceof SAML2_XML_mdattr_EntityAttributes && !empty($e->children)) { + foreach ($e->children as $attr) { + // only saml:Attribute are currently supported here. The specifications also allows + // saml:Assertions, which more complex processing. + if ($attr instanceof SAML2_XML_saml_Attribute) { + if (empty($attr->Name) || empty($attr->AttributeValue)) { + continue; + } + + // attribute names that is not URI is prefixed as this: '{nameformat}name' + $name = $attr->Name; + if (empty($attr->NameFormat)) { + $name = '{'.SAML2_Const::NAMEFORMAT_UNSPECIFIED.'}'.$attr->Name; + } elseif ($attr->NameFormat !== 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri') { + $name = '{'.$attr->NameFormat.'}'.$attr->Name; + } + + $values = array(); + foreach ($attr->AttributeValue as $attrvalue) { + $values[] = $attrvalue->getString(); + } + + $ret['EntityAttributes'][$name] = $values; + } + } + } + } + + // UIInfo elements are only allowed at RoleDescriptor level extensions + if ($element instanceof SAML2_XML_md_RoleDescriptor) { + if ($e instanceof SAML2_XML_mdui_UIInfo) { + + $ret['UIInfo']['DisplayName'] = $e->DisplayName; + $ret['UIInfo']['Description'] = $e->Description; + $ret['UIInfo']['InformationURL'] = $e->InformationURL; + $ret['UIInfo']['PrivacyStatementURL'] = $e->PrivacyStatementURL; + + foreach ($e->Keywords as $uiItem) { + if (!($uiItem instanceof SAML2_XML_mdui_Keywords) + || empty($uiItem->Keywords) + || empty($uiItem->lang) + ) { + continue; + } + $ret['UIInfo']['Keywords'][$uiItem->lang] = $uiItem->Keywords; + } + foreach ($e->Logo as $uiItem) { + if (!($uiItem instanceof SAML2_XML_mdui_Logo) + || empty($uiItem->url) + || empty($uiItem->height) + || empty($uiItem->width) + ) { + continue; + } + $logo = array( + 'url' => $uiItem->url, + 'height' => $uiItem->height, + 'width' => $uiItem->width, + ); + if (!empty($uiItem->Lang)) { + $logo['lang'] = $uiItem->lang; + } + $ret['UIInfo']['Logo'][] = $logo; + } + } + } + + // DiscoHints elements are only allowed at IDPSSODescriptor level extensions + if ($element instanceof SAML2_XML_md_IDPSSODescriptor) { + + if ($e instanceof SAML2_XML_mdui_DiscoHints) { + $ret['DiscoHints']['IPHint'] = $e->IPHint; + $ret['DiscoHints']['DomainHint'] = $e->DomainHint; + $ret['DiscoHints']['GeolocationHint'] = $e->GeolocationHint; + } + } + + if (!($e instanceof SAML2_XML_Chunk)) { + continue; + } + + if ($e->localName === 'Attribute' && $e->namespaceURI === SAML2_Const::NS_SAML) { + $attribute = $e->getXML(); + + $name = $attribute->getAttribute('Name'); + $values = array_map( + array('SimpleSAML\Utils\XML', 'getDOMText'), SimpleSAML\Utils\XML::getDOMChildren($attribute, 'AttributeValue', '@saml2') - ); - - if ($name === 'tags') { - foreach ($values as $tagname) { - if (!empty($tagname)) { - $ret['tags'][] = $tagname; - } - } - } - } - } - return $ret; - } - - - /** - * Parse and process a Organization element. - * - * @param SAML2_XML_md_Organization $element The Organization element. - */ - private function processOrganization(SAML2_XML_md_Organization $element) { - - $this->organizationName = $element->OrganizationName; - $this->organizationDisplayName = $element->OrganizationDisplayName; - $this->organizationURL = $element->OrganizationURL; - } - - /** - * Parse and process a ContactPerson element. - * - * @param SAML2_XML_md_ContactPerson $element The ContactPerson element. - */ - - private function processContactPerson(SAML2_XML_md_ContactPerson $element) { - - $contactPerson = array(); - if(!empty($element->contactType)) { - $contactPerson['contactType'] = $element->contactType; - } - if(!empty($element->Company)) { - $contactPerson['company'] = $element->Company; - } - if(!empty($element->GivenName)) { - $contactPerson['givenName'] = $element->GivenName; - } - if(!empty($element->SurName)) { - $contactPerson['surName'] = $element->SurName; - } - if(!empty($element->EmailAddress)) { - $contactPerson['emailAddress'] = $element->EmailAddress; - } - if(!empty($element->TelephoneNumber)) { - $contactPerson['telephoneNumber'] = $element->TelephoneNumber; - } - if(!empty($contactPerson)) { - $this->contacts[] = $contactPerson; - } - } - - - /** - * This function parses AttributeConsumerService elements. - */ - private static function parseAttributeConsumerService(SAML2_XML_md_AttributeConsumingService $element, &$sp) { - assert('is_array($sp)'); - - $sp['name'] = $element->ServiceName; - $sp['description'] = $element->ServiceDescription; - - $format = NULL; - $sp['attributes'] = array(); - $sp['attributes.required'] = array(); - foreach ($element->RequestedAttribute AS $child) { - $attrname = $child->Name; - $sp['attributes'][] = $attrname; - - if ($child->isRequired) { - $sp['attributes.required'][] = $attrname; - } - - if ($child->isRequired !== NULL && $child->isRequired === TRUE) { - $sp['attributes.required'][] = $attrname; - } - - if ($child->NameFormat !== NULL) { - $attrformat = $child->NameFormat; - } else { - $attrformat = SAML2_Const::NAMEFORMAT_UNSPECIFIED; - } - - if ($format === NULL) { - $format = $attrformat; - } elseif ($format !== $attrformat) { - $format = SAML2_Const::NAMEFORMAT_UNSPECIFIED; - } - } - - if (empty($sp['attributes'])) { - /* - * Really an invalid configuration - all AttributeConsumingServices - * should have one or more attributes. - */ - unset($sp['attributes']); - } - if (empty($sp['attributes.required'])) { - unset($sp['attributes.required']); - } - - if ($format !== SAML2_Const::NAMEFORMAT_UNSPECIFIED && $format !== NULL) { - $sp['attributes.NameFormat'] = $format; - } - } - - - /** - * This function is a generic endpoint element parser. - * - * The returned associative array has the following elements: - * - 'Binding': The binding this endpoint uses. - * - 'Location': The URL to this endpoint. - * - 'ResponseLocation': The URL where responses should be sent. This may not exist. - * - 'index': The index of this endpoint. This attribute is only for indexed endpoints. - * - 'isDefault': Whether this endpoint is the default endpoint for this type. This attribute may not exist. - * - * @param SAML2_XML_md_EndpointType $element The element which should be parsed. - * @return array An associative array with the data we have extracted from the element. - */ - private static function parseGenericEndpoint(SAML2_XML_md_EndpointType $element) { - - $ep = array(); - - $ep['Binding'] = $element->Binding; - $ep['Location'] = $element->Location; - - if ($element->ResponseLocation !== NULL) { - $ep['ResponseLocation'] = $element->ResponseLocation; - } - - if ($element instanceof SAML2_XML_md_IndexedEndpointType) { - $ep['index'] = $element->index; - - if ($element->isDefault !== NULL) { - $ep['isDefault'] = $element->isDefault; - } - } - - return $ep; - } - - - /** - * Extract generic endpoints. - * - * @param array $endpoints The endpoints we should parse. - * @return array Array of parsed endpoints. - */ - private static function extractEndpoints(array $endpoints) { - - $ret = array(); - foreach ($endpoints as $ep) { - $ret[] = self::parseGenericEndpoint($ep); - } - - return $ret; - } - - - /** - * 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 whether this key can be used for encryption. - * - 'signing': Indicates whether 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 SAML2_XML_md_KeyDescriptor $kd The KeyDescriptor element. - * @return array An associative array describing the key, or NULL if this is an unsupported key. - */ - private static function parseKeyDescriptor(SAML2_XML_md_KeyDescriptor $kd) { - - $r = array(); - - if ($kd->use === 'encryption') { - $r['encryption'] = TRUE; - $r['signing'] = FALSE; - } elseif($kd->use === 'signing') { - $r['encryption'] = FALSE; - $r['signing'] = TRUE; - } else { - $r['encryption'] = TRUE; - $r['signing'] = TRUE; - } - - $keyInfo = $kd->KeyInfo; - - foreach ($keyInfo->info as $i) { - if ($i instanceof SAML2_XML_ds_X509Data) { - foreach ($i->data as $d) { - if ($d instanceof SAML2_XML_ds_X509Certificate) { - $r['type'] = 'X509Certificate'; - $r['X509Certificate'] = $d->certificate; - return $r; - } - } - } - } - - return NULL; - } - - - /** - * This function finds SP descriptors which supports one of the given protocols. - * - * @param $protocols Array with the protocols we accept. - * @return Array with SP descriptors which supports one of the given protocols. - */ - private function getSPDescriptors($protocols) { - assert('is_array($protocols)'); - - $ret = array(); - - foreach($this->spDescriptors as $spd) { - $sharedProtocols = array_intersect($protocols, $spd['protocols']); - if(count($sharedProtocols) > 0) { - $ret[] = $spd; - } - } - - return $ret; - } - - - /** - * 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. - * - * This function will throw an exception if it is unable to locate the node. - * - * @param DOMDocument $doc The DOMDocument where we should find the EntityDescriptor node. - * @return SAML2_XML_md_EntityDescriptor The DOMEntity which represents the EntityDescriptor. - * @throws Exception If the document is empty or the first element is not an EntityDescriptor element. - */ - private static function findEntityDescriptor($doc) { - - assert('$doc instanceof DOMDocument'); - - /* Find the EntityDescriptor DOMElement. This should be the first (and only) child of the - * DOMDocument. - */ - $ed = $doc->documentElement; - - if($ed === NULL) { - throw new Exception('Failed to load SAML metadata from empty XML document.'); - } - - if (SimpleSAML\Utils\XML::isDOMElementOfType($ed, 'EntityDescriptor', '@md') === FALSE) { - throw new Exception('Expected first element in the metadata document to be an EntityDescriptor element.'); - } - - return new SAML2_XML_md_EntityDescriptor($ed); - } - - - /** - * If this EntityDescriptor was signed this function use the public key to check the signature. - * - * @param array $certificates One ore more certificates with the public key. This makes it possible - * to do a key rollover. - * @return boolean True if it is possible to check the signature with the certificate, false otherwise. - * @throws Exception If the certificate file cannot be found. - */ - public function validateSignature($certificates) { - foreach ($certificates as $cert) { - assert('is_string($cert)'); - $certFile = \SimpleSAML\Utils\Config::getCertPath($cert); - if (!file_exists($certFile)) { - throw new Exception('Could not find certificate file [' . $certFile . '], which is needed to validate signature'); - } - $certData = file_get_contents($certFile); - - foreach ($this->validators as $validator) { - $key = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, array('type'=>'public')); - $key->loadKey($certData); - try { - if ($validator->validate($key)) { - return TRUE; - } - } catch (Exception $e) { - /* This certificate does not sign this element. */ - } - } - } - SimpleSAML_Logger::debug('Could not validate signature'); - return FALSE; - } - - - /** - * This function checks if this EntityDescriptor was signed with a certificate with the - * given fingerprint. - * - * @param string $fingerprint Fingerprint of the certificate which should have been used to sign this - * EntityDescriptor. - * @return boolean True if it was signed with the certificate with the given fingerprint, false otherwise. - */ - public function validateFingerprint($fingerprint) { - assert('is_string($fingerprint)'); - - $fingerprint = strtolower(str_replace(":", "", $fingerprint)); - - $candidates = array(); - foreach ($this->validators as $validator) { - foreach ($validator->getValidatingCertificates() as $cert) { - - $fp = strtolower(sha1(base64_decode($cert))); - $candidates[] = $fp; - if ($fp === $fingerprint) { - return TRUE; - } - } - - - } - SimpleSAML_Logger::debug('Fingerprint was [' . $fingerprint . '] not one of [' . join(', ', $candidates). ']'); - return FALSE; - } - + ); + + if ($name === 'tags') { + foreach ($values as $tagname) { + if (!empty($tagname)) { + $ret['tags'][] = $tagname; + } + } + } + } + } + return $ret; + } + + + /** + * Parse and process a Organization element. + * + * @param SAML2_XML_md_Organization $element The Organization element. + */ + private function processOrganization(SAML2_XML_md_Organization $element) + { + $this->organizationName = $element->OrganizationName; + $this->organizationDisplayName = $element->OrganizationDisplayName; + $this->organizationURL = $element->OrganizationURL; + } + + + /** + * Parse and process a ContactPerson element. + * + * @param SAML2_XML_md_ContactPerson $element The ContactPerson element. + */ + + private function processContactPerson(SAML2_XML_md_ContactPerson $element) + { + $contactPerson = array(); + if (!empty($element->contactType)) { + $contactPerson['contactType'] = $element->contactType; + } + if (!empty($element->Company)) { + $contactPerson['company'] = $element->Company; + } + if (!empty($element->GivenName)) { + $contactPerson['givenName'] = $element->GivenName; + } + if (!empty($element->SurName)) { + $contactPerson['surName'] = $element->SurName; + } + if (!empty($element->EmailAddress)) { + $contactPerson['emailAddress'] = $element->EmailAddress; + } + if (!empty($element->TelephoneNumber)) { + $contactPerson['telephoneNumber'] = $element->TelephoneNumber; + } + if (!empty($contactPerson)) { + $this->contacts[] = $contactPerson; + } + } + + + /** + * This function parses AttributeConsumerService elements. + * + * @param SAML2_XML_md_AttributeConsumingService $element The AttributeConsumingService to parse. + * @param array $sp The array with the SP's metadata. + */ + private static function parseAttributeConsumerService(SAML2_XML_md_AttributeConsumingService $element, &$sp) + { + assert('is_array($sp)'); + + $sp['name'] = $element->ServiceName; + $sp['description'] = $element->ServiceDescription; + + $format = null; + $sp['attributes'] = array(); + $sp['attributes.required'] = array(); + foreach ($element->RequestedAttribute as $child) { + $attrname = $child->Name; + $sp['attributes'][] = $attrname; + + if ($child->isRequired) { + $sp['attributes.required'][] = $attrname; + } + + if ($child->isRequired !== null && $child->isRequired === true) { + $sp['attributes.required'][] = $attrname; + } + + if ($child->NameFormat !== null) { + $attrformat = $child->NameFormat; + } else { + $attrformat = SAML2_Const::NAMEFORMAT_UNSPECIFIED; + } + + if ($format === null) { + $format = $attrformat; + } elseif ($format !== $attrformat) { + $format = SAML2_Const::NAMEFORMAT_UNSPECIFIED; + } + } + + if (empty($sp['attributes'])) { + // a really invalid configuration: all AttributeConsumingServices should have one or more attributes + unset($sp['attributes']); + } + if (empty($sp['attributes.required'])) { + unset($sp['attributes.required']); + } + + if ($format !== SAML2_Const::NAMEFORMAT_UNSPECIFIED && $format !== null) { + $sp['attributes.NameFormat'] = $format; + } + } + + + /** + * This function is a generic endpoint element parser. + * + * The returned associative array has the following elements: + * - 'Binding': The binding this endpoint uses. + * - 'Location': The URL to this endpoint. + * - 'ResponseLocation': The URL where responses should be sent. This may not exist. + * - 'index': The index of this endpoint. This attribute is only for indexed endpoints. + * - 'isDefault': Whether this endpoint is the default endpoint for this type. This attribute may not exist. + * + * @param SAML2_XML_md_EndpointType $element The element which should be parsed. + * + * @return array An associative array with the data we have extracted from the element. + */ + private static function parseGenericEndpoint(SAML2_XML_md_EndpointType $element) + { + $ep = array(); + + $ep['Binding'] = $element->Binding; + $ep['Location'] = $element->Location; + + if ($element->ResponseLocation !== null) { + $ep['ResponseLocation'] = $element->ResponseLocation; + } + + if ($element instanceof SAML2_XML_md_IndexedEndpointType) { + $ep['index'] = $element->index; + + if ($element->isDefault !== null) { + $ep['isDefault'] = $element->isDefault; + } + } + + return $ep; + } + + + /** + * Extract generic endpoints. + * + * @param array $endpoints The endpoints we should parse. + * + * @return array Array of parsed endpoints. + */ + private static function extractEndpoints(array $endpoints) + { + $ret = array(); + foreach ($endpoints as $ep) { + $ret[] = self::parseGenericEndpoint($ep); + } + + return $ret; + } + + + /** + * 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 whether this key can be used for encryption. + * - 'signing': Indicates whether 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 SAML2_XML_md_KeyDescriptor $kd The KeyDescriptor element. + * + * @return array|null An associative array describing the key, or null if this is an unsupported key. + */ + private static function parseKeyDescriptor(SAML2_XML_md_KeyDescriptor $kd) + { + $r = array(); + + if ($kd->use === 'encryption') { + $r['encryption'] = true; + $r['signing'] = false; + } elseif ($kd->use === 'signing') { + $r['encryption'] = false; + $r['signing'] = true; + } else { + $r['encryption'] = true; + $r['signing'] = true; + } + + $keyInfo = $kd->KeyInfo; + + foreach ($keyInfo->info as $i) { + if ($i instanceof SAML2_XML_ds_X509Data) { + foreach ($i->data as $d) { + if ($d instanceof SAML2_XML_ds_X509Certificate) { + $r['type'] = 'X509Certificate'; + $r['X509Certificate'] = $d->certificate; + return $r; + } + } + } + } + + return null; + } + + + /** + * This function finds SP descriptors which supports one of the given protocols. + * + * @param $protocols Array with the protocols we accept. + * + * @return Array with SP descriptors which supports one of the given protocols. + */ + private function getSPDescriptors($protocols) + { + assert('is_array($protocols)'); + + $ret = array(); + + foreach ($this->spDescriptors as $spd) { + $sharedProtocols = array_intersect($protocols, $spd['protocols']); + if (count($sharedProtocols) > 0) { + $ret[] = $spd; + } + } + + return $ret; + } + + + /** + * 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. + * + * This function will throw an exception if it is unable to locate the node. + * + * @param DOMDocument $doc The DOMDocument where we should find the EntityDescriptor node. + * + * @return SAML2_XML_md_EntityDescriptor The DOMEntity which represents the EntityDescriptor. + * @throws Exception If the document is empty or the first element is not an EntityDescriptor element. + */ + private static function findEntityDescriptor($doc) + { + assert('$doc instanceof DOMDocument'); + + // find the EntityDescriptor DOMElement. This should be the first (and only) child of the DOMDocument + $ed = $doc->documentElement; + + if ($ed === null) { + throw new Exception('Failed to load SAML metadata from empty XML document.'); + } + + if (SimpleSAML\Utils\XML::isDOMElementOfType($ed, 'EntityDescriptor', '@md') === false) { + throw new Exception('Expected first element in the metadata document to be an EntityDescriptor element.'); + } + + return new SAML2_XML_md_EntityDescriptor($ed); + } + + + /** + * If this EntityDescriptor was signed this function use the public key to check the signature. + * + * @param array $certificates One ore more certificates with the public key. This makes it possible + * to do a key rollover. + * + * @return boolean True if it is possible to check the signature with the certificate, false otherwise. + * @throws Exception If the certificate file cannot be found. + */ + public function validateSignature($certificates) + { + foreach ($certificates as $cert) { + assert('is_string($cert)'); + $certFile = \SimpleSAML\Utils\Config::getCertPath($cert); + if (!file_exists($certFile)) { + throw new Exception( + 'Could not find certificate file ['.$certFile.'], which is needed to validate signature' + ); + } + $certData = file_get_contents($certFile); + + foreach ($this->validators as $validator) { + $key = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, array('type' => 'public')); + $key->loadKey($certData); + try { + if ($validator->validate($key)) { + return true; + } + } catch (Exception $e) { + // this certificate did not sign this element, skip + } + } + } + SimpleSAML_Logger::debug('Could not validate signature'); + return false; + } + + + /** + * This function checks if this EntityDescriptor was signed with a certificate with the + * given fingerprint. + * + * @param string $fingerprint Fingerprint of the certificate which should have been used to sign this + * EntityDescriptor. + * + * @return boolean True if it was signed with the certificate with the given fingerprint, false otherwise. + */ + public function validateFingerprint($fingerprint) + { + assert('is_string($fingerprint)'); + + $fingerprint = strtolower(str_replace(":", "", $fingerprint)); + + $candidates = array(); + foreach ($this->validators as $validator) { + foreach ($validator->getValidatingCertificates() as $cert) { + + $fp = strtolower(sha1(base64_decode($cert))); + $candidates[] = $fp; + if ($fp === $fingerprint) { + return true; + } + } + } + SimpleSAML_Logger::debug('Fingerprint was ['.$fingerprint.'] not one of ['.join(', ', $candidates).']'); + return false; + } }