/** * @param array $legacyResponse * @return EngineBlock_Saml2_ResponseAnnotationDecorator */ public function fromOldFormat(array $legacyResponse) { $legacyResponse = EngineBlock_Corto_XmlToArray::registerNamespaces($legacyResponse); $xml = EngineBlock_Corto_XmlToArray::array2xml($legacyResponse); $document = new DOMDocument(); $document->loadXML($xml); $response = new SAML2_Response($document->firstChild); $annotatedResponse = new EngineBlock_Saml2_ResponseAnnotationDecorator($response); return $this->addPrivateVars($annotatedResponse, $legacyResponse); }
public function serve($serviceName) { // Get the configuration for EngineBlock in it's IdP role. $engineIdpEntityId = $this->_server->getUrl('idpMetadataService'); $engineIdpEntity = $this->_server->getRepository()->fetchIdentityProviderByEntityId($engineIdpEntityId); $edugainEntities = array(); $ssoServiceReplacer = new ServiceReplacer($engineIdpEntity, 'SingleSignOnService', ServiceReplacer::REQUIRED); $slServiceReplacer = new ServiceReplacer($engineIdpEntity, 'SingleLogoutService', ServiceReplacer::OPTIONAL); $remoteEntities = $this->_server->getRepository()->findEntitiesPublishableInEdugain(); foreach ($remoteEntities as $entity) { // Use EngineBlock certificates $entity->certificates = $engineIdpEntity->certificates; // Ignore the NameIDFormats the IdP supports, any requests made on this endpoint will use EngineBlock // NameIDs, so advertise that. unset($entity->nameIdFormat); $entity->supportedNameIdFormats = $engineIdpEntity->supportedNameIdFormats; // For IdP's replace the SingleSignService with the one from EB if ($entity instanceof IdentityProvider) { // Replace service locations and bindings with those of EB $transparentSsoUrl = $this->_server->getUrl('singleSignOnService', $entity->entityId); $ssoServiceReplacer->replace($entity, $transparentSsoUrl); $transparentSlUrl = $this->_server->getUrl('singleLogoutService'); $slServiceReplacer->replace($entity, $transparentSlUrl); } $entity->contactPersons = $engineIdpEntity->contactPersons; $entity = $this->_addRequestAttributes($entity); $edugainEntities[] = $entity; } // Map the IdP configuration to a Corto XMLToArray structured document array $mapper = new EngineBlock_Corto_Mapper_Metadata_EdugainDocument($this->_server->getNewId(\OpenConext\Component\EngineBlockFixtures\IdFrame::ID_USAGE_SAML2_METADATA), $this->_server->timeStamp($this->_server->getConfig('metadataValidUntilSeconds', 86400)), true); $document = $mapper->setEntities($edugainEntities)->map(); // Sign the document $document = $this->_server->sign($document); // Convert the document to XML $xml = EngineBlock_Corto_XmlToArray::array2xml($document); // If debugging is enabled then validate it according to the schema if ($this->_server->getConfig('debug', false)) { $validator = new EngineBlock_Xml_Validator('http://docs.oasis-open.org/security/saml/v2.0/saml-schema-metadata-2.0.xsd'); $validator->validate($xml); } // The spec dictates we use a custom mimetype, but debugging is easier with a normal mimetype // also no single SP / IdP complains over this. //$this->_server->sendHeader('Content-Type', 'application/samlmetadata+xml'); $this->_server->sendHeader('Content-Type', 'application/xml'); $this->_server->sendOutput($xml); }
/** * Validates xml against a given schema * * @param string $xml * @return void * @throws EngineBlock_Exception in case validating itself fails or if xml does not validate */ public function validate($xml) { if (!ini_get('allow_url_fopen')) { throw new EngineBlock_Exception('Failed validating XML, url_fopen is not allowed'); } // Load schema $schemaXml = @file_get_contents($this->_schemaLocation); if ($schemaXml === false) { throw new EngineBlock_Exception('Failed validating XML, schema url could not be opened: "' . $this->_schemaLocation . '"'); } $schemaXml = $this->_absolutizeSchemaLocations($schemaXml, $this->_schemaLocation); $dom = new DOMDocument(); $dom->loadXML($xml); if (!@$dom->schemaValidateSource($schemaXml)) { $errorInfo = error_get_last(); $errorMessage = $errorInfo['message']; // @todo improve parsing message by creating custom exceptions for which know that structure of messages $parsedErrorMessage = preg_replace('/\\{[^}]*\\}/', '', $errorMessage); echo '<pre>' . htmlentities(EngineBlock_Corto_XmlToArray::formatXml($xml)) . '</pre>'; throw new EngineBlock_Exception("Metadata XML doesn't validate against schema at '{$schemaXml}', gives error:: '{$parsedErrorMessage}'"); } }
public function serve($serviceName) { // Get the configuration for EngineBlock in it's IdP / SP role without the VO. $this->_server->setProcessingMode(); $engineEntityId = $this->_server->getUrl($serviceName); $this->_server->unsetProcessingMode(); $engineEntity = $this->_server->getRepository()->fetchEntityByEntityId($engineEntityId); // Override the EntityID and SSO location to optionally append VO id $externalEngineEntityId = $this->_server->getUrl($serviceName); $engineEntity->entityId = $externalEngineEntityId; if ($serviceName === 'idpMetadataService') { $ssoServiceReplacer = new ServiceReplacer($engineEntity, 'SingleSignOnService', ServiceReplacer::REQUIRED); $ssoLocation = $this->_server->getUrl('singleSignOnService'); $ssoServiceReplacer->replace($engineEntity, $ssoLocation); } // Override Single Logout Service Location with generated url $slServiceReplacer = new ServiceReplacer($engineEntity, 'SingleLogoutService', ServiceReplacer::OPTIONAL); $slLocation = $this->_server->getUrl('singleLogoutService'); $slServiceReplacer->replace($engineEntity, $slLocation); // Map the IdP configuration to a Corto XMLToArray structured document array $mapper = new EngineBlock_Corto_Mapper_Metadata_EdugainDocument($this->_server->getNewId(IdFrame::ID_USAGE_SAML2_METADATA), $this->_server->timeStamp($this->_server->getConfig('metadataValidUntilSeconds', 86400)), false); $document = $mapper->setEntity($engineEntity)->map(); // Sign the document $document = $this->_server->sign($document); // Convert the document to XML $xml = EngineBlock_Corto_XmlToArray::array2xml($document); // If debugging is enabled then validate it according to the schema if ($this->_server->getConfig('debug', false)) { $validator = new EngineBlock_Xml_Validator('http://docs.oasis-open.org/security/saml/v2.0/saml-schema-metadata-2.0.xsd'); $validator->validate($xml); } // The spec dictates we use a custom mimetype, but debugging is easier with a normal mimetype // also no single SP / IdP complains over this. //$this->_server->sendHeader('Content-Type', 'application/samlmetadata+xml'); $this->_server->sendHeader('Content-Type', 'application/xml'); $this->_server->sendOutput($xml); }
/** * @dataProvider xmlOutputProvider */ public function testArrayToXml($phpFile, $xmlFile) { $phpInput = (require $phpFile); $expectedXmlOutput = file_get_contents($xmlFile); $this->assertEquals($expectedXmlOutput, EngineBlock_Corto_XmlToArray::array2xml($phpInput)); }
/** * Sign a Corto_XmlToArray array with XML. * * @param $element Element to sign * @return array Signed element */ public function sign(array $element) { $signingKeyPair = $this->getSigningCertificates(); $signature = array('__t' => 'ds:Signature', '_xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'ds:SignedInfo' => array('__t' => 'ds:SignedInfo', '_xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'ds:CanonicalizationMethod' => array('_Algorithm' => 'http://www.w3.org/2001/10/xml-exc-c14n#'), 'ds:SignatureMethod' => array('_Algorithm' => 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'), 'ds:Reference' => array(0 => array('_URI' => '__placeholder__', 'ds:Transforms' => array('ds:Transform' => array(array('_Algorithm' => 'http://www.w3.org/2000/09/xmldsig#enveloped-signature'), array('_Algorithm' => 'http://www.w3.org/2001/10/xml-exc-c14n#'))), 'ds:DigestMethod' => array('_Algorithm' => 'http://www.w3.org/2000/09/xmldsig#sha1'), 'ds:DigestValue' => array('__v' => '__placeholder__')))), 'ds:SignatureValue' => array('__v' => '__placeholder__'), 'ds:KeyInfo' => array('ds:X509Data' => array('ds:X509Certificate' => array('__v' => $signingKeyPair->getCertificate()->toCertData())))); // Convert the XMl object to actual XML and get a reference to what we're about to sign $canonicalXmlDom = new DOMDocument(); $canonicalXmlDom->loadXML(EngineBlock_Corto_XmlToArray::array2xml($element)); // Note that the current element may not be the first or last, because we might include comments, so look for // the actual XML element $xpath = new DOMXPath($canonicalXmlDom); $nodes = $xpath->query('/*[@ID="' . $element['_ID'] . '"]'); if ($nodes->length < 1) { throw new EngineBlock_Corto_ProxyServer_Exception("Unable to sign message can't find element with id to sign?", EngineBlock_Corto_ProxyServer_Exception::CODE_NOTICE); } $canonicalXmlDom = $nodes->item(0); // Now do 'exclusive no-comments' XML cannonicalization $canonicalXml = $canonicalXmlDom->C14N(true, false); // Hash it, encode it in Base64 and include that as the 'Reference' $signature['ds:SignedInfo']['ds:Reference'][0]['ds:DigestValue']['__v'] = base64_encode(sha1($canonicalXml, true)); $signature['ds:SignedInfo']['ds:Reference'][0]['_URI'] = "#" . $element['_ID']; // Now we start the actual signing, instead of signing the entire (possibly large) document, // we only sign the 'SignedInfo' which includes the 'Reference' hash $canonicalXml2Dom = new DOMDocument(); $canonicalXml2Dom->loadXML(EngineBlock_Corto_XmlToArray::array2xml($signature['ds:SignedInfo'])); $canonicalXml2 = $canonicalXml2Dom->firstChild->C14N(true, false); $signatureValue = null; $signatureValue = $signingKeyPair->getPrivateKey()->sign($canonicalXml2); $signature['ds:SignatureValue']['__v'] = base64_encode($signatureValue); $element['ds:Signature'] = $signature; $element[EngineBlock_Corto_XmlToArray::PRIVATE_PFX]['Signed'] = true; return $element; }
public function serve($serviceName) { // Fetch SP Entity Descriptor for the SP Entity ID that is fetched from the request $request = EngineBlock_ApplicationSingleton::getInstance()->getHttpRequest(); $spEntityId = $request->getQueryParameter('sp-entity-id'); if ($spEntityId) { // See if an sp-entity-id was specified for which we need to use sp specific metadata $spEntity = $this->_server->getRepository()->fetchServiceProviderByEntityId($spEntityId); } // Get the configuration for EngineBlock in it's IdP role. $engineIdpEntityId = $this->_server->getUrl('idpMetadataService'); $engineIdentityProvider = $this->_server->getRepository()->fetchIdentityProviderByEntityId($engineIdpEntityId); $idpEntities = array(); // Note that Shibboleth likes to see it's self in the metadata, so if an sp-entity-id was passed along // we make sure the first thing is the Service Provider if (isset($spEntity)) { $idpEntities[] = $spEntity; } $ssoServiceReplacer = new ServiceReplacer($engineIdentityProvider, 'SingleSignOnService', ServiceReplacer::REQUIRED); $slServiceReplacer = new ServiceReplacer($engineIdentityProvider, 'SingleLogoutService', ServiceReplacer::OPTIONAL); if (isset($spEntity)) { $identityProviders = $this->_server->getRepository()->findIdentityProvidersByEntityId($this->_server->getRepository()->findAllowedIdpEntityIdsForSp($spEntity)); } else { $identityProviders = $this->_server->getRepository()->findIdentityProviders(); } foreach ($identityProviders as $entity) { // Don't add ourselves if ($entity->entityId === $engineIdentityProvider->entityId) { continue; } if ($entity->hidden) { continue; } // Use EngineBlock certificates $entity->certificates = $engineIdentityProvider->certificates; // Ignore the NameIDFormats the IdP supports, any requests made on this endpoint will use EngineBlock // NameIDs, so advertise that. unset($entity->nameIdFormat); $entity->supportedNameIdFormats = $engineIdentityProvider->supportedNameIdFormats; // Replace service locations and bindings with those of EB $transparentSsoUrl = $this->_server->getUrl('singleSignOnService', $entity->entityId); $ssoServiceReplacer->replace($entity, $transparentSsoUrl); $transparentSlUrl = $this->_server->getUrl('singleLogoutService'); $slServiceReplacer->replace($entity, $transparentSlUrl); $entity->contactPersons = $engineIdentityProvider->contactPersons; $idpEntities[] = $entity; } // Map the IdP configuration to a Corto XMLToArray structured document array $mapper = new EngineBlock_Corto_Mapper_Metadata_EdugainDocument($this->_server->getNewId(\OpenConext\Component\EngineBlockFixtures\IdFrame::ID_USAGE_SAML2_METADATA), $this->_server->timeStamp($this->_server->getConfig('metadataValidUntilSeconds', 86400)), false); $document = $mapper->setEntities($idpEntities)->map(); // Sign the document $document = $this->_server->sign($document); // Convert the document to XML $xml = EngineBlock_Corto_XmlToArray::array2xml($document); // If debugging is enabled then validate it according to the schema if ($this->_server->getConfig('debug', false)) { $validator = new EngineBlock_Xml_Validator('http://docs.oasis-open.org/security/saml/v2.0/saml-schema-metadata-2.0.xsd'); $validator->validate($xml); } // The spec dictates we use a custom mimetype, but debugging is easier with a normal mimetype // also no single SP / IdP complains over this. //$this->_server->sendHeader('Content-Type', 'application/samlmetadata+xml'); $this->_server->sendHeader('Content-Type', 'application/xml'); $this->_server->sendOutput($xml); }
protected static function _xml2array(&$elements, $level = 1, $namespaceMapping = array()) { $newElement = array(); while (isset($elements[self::$counter])) { $value = $elements[self::$counter]; self::$counter++; if ($value['type'] == 'close') { return $newElement; } elseif ($value['type'] == 'cdata') { continue; } $hashedAttributes = array(); $tagName = $value['tag']; if (isset($value['attributes']) && ($attributes = $value['attributes'])) { foreach ($attributes as $attributeKey => $attributeValue) { unset($attributes[$attributeKey]); $attributeKey = self::_mapNamespacesToSaml($attributeKey); $hashedAttributes[self::ATTRIBUTE_PFX . $attributeKey] = $attributeValue; } } $complete = array(); $tagName = self::_mapNamespacesToSaml($tagName); $complete[self::TAG_NAME_PFX] = $tagName; if ($hashedAttributes) { $complete = array_merge($complete, $hashedAttributes); } if (isset($value['value']) && ($attributeValue = trim($value['value']))) { $complete[self::VALUE_PFX] = $attributeValue; } if ($value['type'] == 'open') { $cs = self::_xml2array($elements, $level + 1, $namespaceMapping); foreach ($cs as $c) { $tagName = $c[self::TAG_NAME_PFX]; unset($c[self::TAG_NAME_PFX]); if (!isset(self::$_singulars[$tagName])) { $complete[$tagName][] = $c; } else { $complete[$tagName] = $c; unset($complete[$tagName][self::TAG_NAME_PFX]); } } } $newElement[] = $complete; } self::$counter = 0; return $newElement; }