diff --git a/lib/SAML2/Binding.php b/lib/SAML2/Binding.php index a6b672b3cdfb37f9327d63e37c190a334c44999d..cc5f1ccb9083a329330be97bb5ae94e28f34895c 100644 --- a/lib/SAML2/Binding.php +++ b/lib/SAML2/Binding.php @@ -34,6 +34,8 @@ abstract class SAML2_Binding { return new SAML2_HTTPRedirect(); case SAML2_Const::BINDING_HTTP_ARTIFACT: return new SAML2_HTTPArtifact(); + case SAML2_Const::BINDING_HOK_SSO: + return new SAML2_HTTPPost(); default: throw new Exception('Unsupported binding: ' . var_export($urn, TRUE)); } diff --git a/lib/SAML2/Const.php b/lib/SAML2/Const.php index d44a28e0383488b811329770812c425186ba01dc..c5361867becb0b98de4770ef115e920c1277841c 100644 --- a/lib/SAML2/Const.php +++ b/lib/SAML2/Const.php @@ -39,12 +39,22 @@ class SAML2_Const { */ const BINDING_SOAP = 'urn:oasis:names:tc:SAML:2.0:bindings:SOAP'; + /** + * The URN for the Holder-of-Key Web Browser SSO Profile binding + */ + const BINDING_HOK_SSO = 'urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser'; + /** * Bearer subject confirmation method. */ const CM_BEARER = 'urn:oasis:names:tc:SAML:2.0:cm:bearer'; + /** + * Holder-of-Key subject confirmation method. + */ + const CM_HOK = 'urn:oasis:names:tc:SAML:2.0:cm:holder-of-key'; + /** * The URN for the unspecified attribute NameFormat. @@ -103,6 +113,10 @@ class SAML2_Const { */ const NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'; + /** + * The namespace for the SAML 2 HoK Web Browser SSO Profile. + */ + const NS_HOK = 'urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser'; /** * Top-level status code indicating successful processing of the request. diff --git a/lib/SAML2/XML/saml/SubjectConfirmationData.php b/lib/SAML2/XML/saml/SubjectConfirmationData.php index 74fac0a2c2fcfebf60706a51310b9b07dd77b121..1c28c65c8df37559c5977d1a8893763eb64efd6f 100644 --- a/lib/SAML2/XML/saml/SubjectConfirmationData.php +++ b/lib/SAML2/XML/saml/SubjectConfirmationData.php @@ -49,7 +49,18 @@ class SAML2_XML_saml_SubjectConfirmationData { /** - * Initialize (and parse? a SubjectConfirmationData element. + * The various key information elements. + * + * Array with various elements describing this key. + * Unknown elements will be represented by SAML2_XML_Chunk. + * + * @var array + */ + public $info = array(); + + + /** + * Initialize (and parse) a SubjectConfirmationData element. * * @param DOMElement|NULL $xml The XML element we should load. */ @@ -74,6 +85,23 @@ class SAML2_XML_saml_SubjectConfirmationData { if ($xml->hasAttribute('Address')) { $this->Address = $xml->getAttribute('Address'); } + for ($n = $xml->firstChild; $n !== NULL; $n = $n->nextSibling) { + if (!($n instanceof DOMElement)) { + continue; + } + if ($n->namespaceURI !== XMLSecurityDSig::XMLDSIGNS) { + $this->info[] = new SAML2_XML_Chunk($n); + continue; + } + switch ($n->localName) { + case 'KeyInfo': + $this->info[] = new SAML2_XML_ds_KeyInfo($n); + break; + default: + $this->info[] = new SAML2_XML_Chunk($n); + break; + } + } } @@ -108,6 +136,9 @@ class SAML2_XML_saml_SubjectConfirmationData { if (isset($this->Address)) { $e->setAttribute('Address', $this->Address); } + foreach ($this->info as $n) { + $n->toXML($e); + } return $e; } diff --git a/lib/SimpleSAML/Metadata/SAMLBuilder.php b/lib/SimpleSAML/Metadata/SAMLBuilder.php index c15cbca1a86288559c5c4e6964736ba8cb8eb4c8..3373d0f64987df7419d88c73ae0a84efd071dda8 100644 --- a/lib/SimpleSAML/Metadata/SAMLBuilder.php +++ b/lib/SimpleSAML/Metadata/SAMLBuilder.php @@ -190,6 +190,9 @@ class SimpleSAML_Metadata_SAMLBuilder { if (isset($ep['ResponseLocation'])) { $t->ResponseLocation = $ep['ResponseLocation']; } + if (isset($ep['hoksso:ProtocolBinding'])) { + $t->setAttributeNS(SAML2_Const::NS_HOK, 'hoksso:ProtocolBinding', SAML2_Const::BINDING_HTTP_REDIRECT); + } if ($indexed) { if (!isset($ep['index'])) { diff --git a/modules/saml/lib/IdP/SAML2.php b/modules/saml/lib/IdP/SAML2.php index 17d01b3ca1792072ed088c6c01df91d797436f53..15434a0362197ec6a14717c4bf47e358f43fbbd0 100644 --- a/modules/saml/lib/IdP/SAML2.php +++ b/modules/saml/lib/IdP/SAML2.php @@ -226,6 +226,9 @@ class sspmod_saml_IdP_SAML2 { if ($idpMetadata->getBoolean('saml20.sendartifact', FALSE)) { $supportedBindings[] = SAML2_Const::BINDING_HTTP_ARTIFACT; } + if ($idpMetadata->getBoolean('saml20.hok.assertion', FALSE)) { + $supportedBindings[] = SAML2_Const::BINDING_HOK_SSO; + } if (isset($_REQUEST['spentityid'])) { /* IdP initiated authentication. */ @@ -763,11 +766,47 @@ class sspmod_saml_IdP_SAML2 { $a->setSessionIndex(SimpleSAML_Utilities::generateID()); $sc = new SAML2_XML_saml_SubjectConfirmation(); - $sc->Method = SAML2_Const::CM_BEARER; $sc->SubjectConfirmationData = new SAML2_XML_saml_SubjectConfirmationData(); $sc->SubjectConfirmationData->NotOnOrAfter = time() + $assertionLifetime; $sc->SubjectConfirmationData->Recipient = $state['saml:ConsumerURL']; $sc->SubjectConfirmationData->InResponseTo = $state['saml:RequestId']; + + /* ProtcolBinding of SP's <AuthnRequest> overwrites IdP hosted metadata configuration. */ + $hokAssertion = NULL; + if ($state['saml:Binding'] === SAML2_Const::BINDING_HOK_SSO) { + $hokAssertion = TRUE; + } + if ($hokAssertion === NULL) { + $hokAssertion = $idpMetadata->getBoolean('saml20.hok.assertion', FALSE); + } + + if ($hokAssertion) { + /* Holder-of-Key */ + $sc->Method = SAML2_Const::CM_HOK; + if (SimpleSAML_Utilities::isHTTPS()) { + if (isset($_SERVER['SSL_CLIENT_CERT']) && !empty($_SERVER['SSL_CLIENT_CERT'])) { + /* Extract certificate data (if this is a certificate). */ + $clientCert = $_SERVER['SSL_CLIENT_CERT']; + $pattern = '/^-----BEGIN CERTIFICATE-----([^-]*)^-----END CERTIFICATE-----/m'; + if (preg_match($pattern, $clientCert, $matches)) { + /* We have a client certificate from the browser which we add to the HoK assertion. */ + $x509Certificate = new SAML2_XML_ds_X509Certificate(); + $x509Certificate->certificate = str_replace(array("\r", "\n", " "), '', $matches[1]); + + $x509Data = new SAML2_XML_ds_X509Data(); + $x509Data->data[] = $x509Certificate; + + $keyInfo = new SAML2_XML_ds_KeyInfo(); + $keyInfo->info[] = $x509Data; + + $sc->SubjectConfirmationData->info[] = $keyInfo; + } else throw new SimpleSAML_Error_Exception('Error creating HoK assertion: No valid client certificate provided during TLS handshake with IdP'); + } else throw new SimpleSAML_Error_Exception('Error creating HoK assertion: No client certificate provided during TLS handshake with IdP'); + } else throw new SimpleSAML_Error_Exception('Error creating HoK assertion: No HTTPS connection to IdP, but required for Holder-of-Key SSO'); + } else { + /* Bearer */ + $sc->Method = SAML2_Const::CM_BEARER; + } $a->setSubjectConfirmation(array($sc)); /* Add attributes. */ diff --git a/modules/saml/lib/Message.php b/modules/saml/lib/Message.php index 65ebff37ff76feb60cfc441c14870cc9f1688c0c..105c54cd3efb04d7a457e0ad641c99117b5bb191 100644 --- a/modules/saml/lib/Message.php +++ b/modules/saml/lib/Message.php @@ -373,17 +373,12 @@ class sspmod_saml_Message { )); } - $dst = $idpMetadata->getDefaultEndpoint('SingleSignOnService', array(SAML2_Const::BINDING_HTTP_REDIRECT)); - $dst = $dst['Location']; - - $ar->setIssuer($spMetadata->getString('entityid')); - $ar->setDestination($dst); - $ar->setForceAuthn($spMetadata->getBoolean('ForceAuthn', FALSE)); $ar->setIsPassive($spMetadata->getBoolean('IsPassive', FALSE)); $protbind = $spMetadata->getValueValidate('ProtocolBinding', array( SAML2_Const::BINDING_HTTP_POST, + SAML2_Const::BINDING_HOK_SSO, SAML2_Const::BINDING_HTTP_ARTIFACT, SAML2_Const::BINDING_HTTP_REDIRECT, ), SAML2_Const::BINDING_HTTP_POST); @@ -391,6 +386,17 @@ class sspmod_saml_Message { /* Shoaib - setting the appropriate binding based on parameter in sp-metadata defaults to HTTP_POST */ $ar->setProtocolBinding($protbind); + /* Select appropriate SSO endpoint */ + if ($protbind === SAML2_Const::BINDING_HOK_SSO) { + $dst = $idpMetadata->getDefaultEndpoint('SingleSignOnService', array(SAML2_Const::BINDING_HOK_SSO)); + } else { + $dst = $idpMetadata->getDefaultEndpoint('SingleSignOnService', array(SAML2_Const::BINDING_HTTP_REDIRECT)); + } + $dst = $dst['Location']; + + $ar->setIssuer($spMetadata->getString('entityid')); + $ar->setDestination($dst); + if ($spMetadata->hasValue('AuthnContextClassRef')) { $accr = $spMetadata->getArrayizeString('AuthnContextClassRef'); $ar->setRequestedAuthnContext(array('AuthnContextClassRef' => $accr)); @@ -559,11 +565,84 @@ class sspmod_saml_Message { $found = FALSE; $lastError = 'No SubjectConfirmation element in Subject.'; foreach ($assertion->getSubjectConfirmation() as $sc) { - if ($sc->Method !== SAML2_Const::CM_BEARER) { + if ($sc->Method !== SAML2_Const::CM_BEARER && $sc->Method !== SAML2_Const::CM_HOK) { $lastError = 'Invalid Method on SubjectConfirmation: ' . var_export($sc->Method, TRUE); continue; } + + /* Is SSO with HoK enabled? IdP remote metadata overwrites SP metadata configuration. */ + $hok = $idpMetadata->getBoolean('saml20.hok.assertion', NULL); + if ($hok === NULL) { + $protocolBinding = $spMetadata->getString('ProtocolBinding', SAML2_Const::BINDING_HTTP_POST); + if ($protocolBinding === SAML2_Const::BINDING_HOK_SSO) { + $hok = TRUE; + } else { + $hok = FALSE; + } + } + if ($sc->Method === SAML2_Const::CM_BEARER && $hok) { + $lastError = 'Bearer SubjectConfirmation received, but Holder-of-Key SubjectConfirmation needed'; + continue; + } + $scd = $sc->SubjectConfirmationData; + if ($sc->Method === SAML2_Const::CM_HOK) { + /* Check HoK Assertion */ + if (SimpleSAML_Utilities::isHTTPS() === FALSE) { + $lastError = 'No HTTPS connection, but required for Holder-of-Key SSO'; + continue; + } + if (isset($_SERVER['SSL_CLIENT_CERT']) && empty($_SERVER['SSL_CLIENT_CERT'])) { + $lastError = 'No client certificate provided during TLS Handshake with SP'; + continue; + } + /* Extract certificate data (if this is a certificate). */ + $clientCert = $_SERVER['SSL_CLIENT_CERT']; + $pattern = '/^-----BEGIN CERTIFICATE-----([^-]*)^-----END CERTIFICATE-----/m'; + if (preg_match($pattern, $clientCert, $matches) === FALSE) { + $lastError = 'No valid client certificate provided during TLS Handshake with SP'; + continue; + } + /* We have a valid client certificate from the browser. */ + $clientCert = str_replace(array("\r", "\n", " "), '', $matches[1]); + + foreach ($scd->info as $thing) { + if($thing instanceof SAML2_XML_ds_KeyInfo) { + $keyInfo[]=$thing; + } + } + if (count($keyInfo)!=1) { + $lastError = 'Error validating Holder-of-Key assertion: Only one <ds:KeyInfo> element in <SubjectConfirmationData> allowed'; + continue; + } + + foreach ($keyInfo[0]->info as $thing) { + if($thing instanceof SAML2_XML_ds_X509Data) { + $x509data[]=$thing; + } + } + if (count($x509data)!=1) { + $lastError = 'Error validating Holder-of-Key assertion: Only one <ds:X509Data> element in <ds:KeyInfo> within <SubjectConfirmationData> allowed'; + continue; + } + + foreach ($x509data[0]->data as $thing) { + if($thing instanceof SAML2_XML_ds_X509Certificate) { + $x509cert[]=$thing; + } + } + if (count($x509cert)!=1) { + $lastError = 'Error validating Holder-of-Key assertion: Only one <ds:X509Certificate> element in <ds:X509Data> within <SubjectConfirmationData> allowed'; + continue; + } + + $HoKCertificate = $x509cert[0]->certificate; + if ($HoKCertificate !== $clientCert) { + $lastError = 'Provided client certificate does not match the certificate bound to the Holder-of-Key assertion'; + continue; + } + } + if ($scd->NotBefore && $scd->NotBefore > time() + 60) { $lastError = 'NotBefore in SubjectConfirmationData is in the future: ' . $scd->NotBefore; continue; diff --git a/modules/saml/www/sp/metadata.php b/modules/saml/www/sp/metadata.php index 1f0d31bc301ecbea6222b9992b5bad2b3fb7b3e3..b19a392193cd3e95757dbc56a6ded8e8e424240b 100644 --- a/modules/saml/www/sp/metadata.php +++ b/modules/saml/www/sp/metadata.php @@ -72,6 +72,13 @@ $acs->Binding = 'urn:oasis:names:tc:SAML:1.0:profiles:artifact-01'; $acs->Location = SimpleSAML_Module::getModuleURL('saml/sp/saml1-acs.php/' . $sourceId . '/artifact'); $sp->AssertionConsumerService[] = $acs; +$acs = new SAML2_XML_md_IndexedEndpointType(); +$acs->index = 4; +$acs->Binding = 'urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser'; +$acs->ProtocolBinding = SAML2_Const::BINDING_HTTP_POST; +$acs->Location = SimpleSAML_Module::getModuleURL('saml/sp/saml2-acs.php/' . $sourceId); +$sp->AssertionConsumerService[] = $acs; + $keys = array(); $certInfo = SimpleSAML_Utilities::loadPublicKey($spconfig, FALSE, 'new_'); if ($certInfo !== NULL && array_key_exists('certData', $certInfo)) { diff --git a/www/saml2/idp/metadata.php b/www/saml2/idp/metadata.php index 3821cb2feeeddddc90557c573f753cf57ee20f22..e9ee76513211920742632a4cae8116e0f7d6a83b 100644 --- a/www/saml2/idp/metadata.php +++ b/www/saml2/idp/metadata.php @@ -60,7 +60,9 @@ try { $metaArray = array( 'metadata-set' => 'saml20-idp-remote', 'entityid' => $idpentityid, - 'SingleSignOnService' => $metadata->getGenerated('SingleSignOnService', 'saml20-idp-hosted'), + 'SingleSignOnService' => array(0 => array( + 'Binding' => SAML2_Const::BINDING_HTTP_REDIRECT, + 'Location' => $metadata->getGenerated('SingleSignOnService', 'saml20-idp-hosted'))), 'SingleLogoutService' => $metadata->getGenerated('SingleLogoutService', 'saml20-idp-hosted'), ); @@ -79,6 +81,14 @@ try { ); } + if ($idpmeta->getBoolean('saml20.hok.assertion', FALSE)) { + /* Prepend HoK SSO Service endpoint. */ + array_unshift($metaArray['SingleSignOnService'], array( + 'hoksso:ProtocolBinding' => SAML2_Const::BINDING_HTTP_REDIRECT, + 'Binding' => SAML2_Const::BINDING_HOK_SSO, + 'Location' => SimpleSAML_Utilities::getBaseURL() . 'saml2/idp/SSOService.php')); + } + $metaArray['NameIDFormat'] = $idpmeta->getString('NameIDFormat', 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'); if ($idpmeta->hasValue('OrganizationName')) {