diff --git a/config/config-template.php b/config/config-template.php index dfb80660c36b609587a9ec0336b4daa2f595d9a3..ebdd972583a68ee982d12a78b217e30bfe83b03d 100644 --- a/config/config-template.php +++ b/config/config-template.php @@ -283,6 +283,15 @@ $config = array ( */ 'auth.auto.delay_login' => 0, + + /* + * This option enables signing of all messages sent with the + * HTTP-Redirect binding. The default value is false. To enable, set + * this option to true, and add a 'privatekey' element to the entity + * (IdP or SP) which is sending the message. + */ + 'binding.httpredirect.sign' => true, + ); diff --git a/lib/SimpleSAML/Bindings/SAML20/HTTPRedirect.php b/lib/SimpleSAML/Bindings/SAML20/HTTPRedirect.php index 6d777b639150eba3cb45a7b46dc760a916f78f48..d50144c4fa4c6bab7f4564f5b14fdb859883b15f 100644 --- a/lib/SimpleSAML/Bindings/SAML20/HTTPRedirect.php +++ b/lib/SimpleSAML/Bindings/SAML20/HTTPRedirect.php @@ -29,8 +29,76 @@ class SimpleSAML_Bindings_SAML20_HTTPRedirect { $this->configuration = $configuration; $this->metadata = $metadatastore; } - - public function getRedirectURL($request, $remoteentityid, $relayState = null, $endpoint = 'SingleSignOnService', $direction = 'SAMLRequest', $mode = 'SP') { + + + public function signQuery($query, $md) { + + /* Check if signing of HTTP-Redirect messages is enabled. */ + if($this->configuration->getValue('binding.httpredirect.sign', false) !== true) { + return $query; + } + + /* Don't attempt to sign the query if no private key is set in the metadata. */ + if(!array_key_exists('privatekey', $md) || $md['privatekey'] === NULL) { + return $query; + } + + + /* Load the private key. */ + + $privatekey = $this->configuration->getBaseDir() . 'cert/' . $md['privatekey']; + if (!file_exists($privatekey)) { + throw new Exception('Could not find private key file [' . $privatekey . '] which is needed to sign the request.'); + } + + $keydata = file_get_contents($privatekey); + if($keydata === FALSE) { + throw new Exception('Unable to load private key file: ' . $privatekey); + } + + $keyid = openssl_pkey_get_private($keydata); + if($keyid === FALSE) { + throw new Exception('OpenSSL was unable to parse the private key from the following file: ' . $privatekey); + } + + /* Make sure that the loaded key is a RSA key. */ + $keydetails = openssl_pkey_get_details($keyid); + if($keydetails === FALSE) { + throw new Exception('Unable to get key details of already loaded key.'); + } + if($keydetails['type'] !== OPENSSL_KEYTYPE_RSA) { + throw new Exception('Private key used to sign the query string isn\'t a RSA key. Key was loaded from the following file: ' . $privatekey); + } + + + /* Sign the query string. According to the specification, the string which should be + * signed is the concatenation of the following query parameters (in order): + * - SAMLRequest/SAMLResponse + * - RelayState (if present) + * - SigAlg + * + * We assume that the query string now contains only the two first parameters. + */ + + /* Append the signature algorithm. We always use RSA-SHA1. */ + $algURI = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'; + $query = $query . "&" . "SigAlg=" . urlencode($algURI); + + /* Sign the query string. The default hash algorithm of openssl_sign is (fortunately) SHA1. */ + if(!openssl_sign($query, $signature, $keyid)) { + throw new Exception('OpenSSL was unable to sign the query string.'); + } + + /* Free the key we used. */ + openssl_pkey_free($keyid); + + /* Return the signed query string. */ + $query = $query . "&" . "Signature=" . urlencode(base64_encode($signature)); + return $query; + } + + + public function getRedirectURL($request, $localentityid, $remoteentityid, $relayState = null, $endpoint = 'SingleSignOnService', $direction = 'SAMLRequest', $mode = 'SP') { if (!in_array($mode, array('SP', 'IdP'))) { throw new Exception('mode parameter of sendMessage() must be either SP or IdP'); @@ -51,21 +119,28 @@ class SimpleSAML_Bindings_SAML20_HTTPRedirect { if (!isset($idpTargetUrl) or $idpTargetUrl == '') { throw new Exception('Could not find endpoint [' .$endpoint . '] in metadata for [' . $remoteentityid . '] (looking in ' . $metadataset . ')'); } - - $encodedRequest = urlencode( base64_encode( gzdeflate( $request ) )); - - $redirectURL = $idpTargetUrl . "?" . $direction . "=" . $encodedRequest; + + $request = urlencode( base64_encode( gzdeflate( $request ) )); + $request = $direction . "=" . $request; if (isset($relayState)) { - $redirectURL .= "&RelayState=" . urlencode($relayState); + $request .= "&RelayState=" . urlencode($relayState); } + + $metadataset = 'saml20-sp-hosted'; + if ($mode == 'IdP') { + $metadataset = 'saml20-idp-hosted'; + } + $localmd = $this->metadata->getMetaData($localentityid, $metadataset); + $request = $this->signQuery($request, $localmd); + $redirectURL = $idpTargetUrl . "?" . $request; + return $redirectURL; } - - public function sendMessage($request, $remoteentityid, $relayState = null, $endpoint = 'SingleSignOnService', $direction = 'SAMLRequest', $mode = 'SP') { + public function sendMessage($request, $localentityid, $remoteentityid, $relayState = null, $endpoint = 'SingleSignOnService', $direction = 'SAMLRequest', $mode = 'SP') { - $redirectURL = $this->getRedirectURL($request, $remoteentityid, $relayState, $endpoint, $direction, $mode); + $redirectURL = $this->getRedirectURL($request, $localentityid, $remoteentityid, $relayState, $endpoint, $direction, $mode); if ($this->configuration->getValue('debug')) { diff --git a/lib/SimpleSAML/XML/SAML20/AuthnRequest.php b/lib/SimpleSAML/XML/SAML20/AuthnRequest.php index b804e00713fa8aaa80f6e030ab26fc19e6763e7f..cba1a2e20a05ffe67b232a8e6ef0c57355f601b9 100644 --- a/lib/SimpleSAML/XML/SAML20/AuthnRequest.php +++ b/lib/SimpleSAML/XML/SAML20/AuthnRequest.php @@ -132,7 +132,7 @@ class SimpleSAML_XML_SAML20_AuthnRequest { } - public function generate($spentityid) { + public function generate($spentityid, $destination) { $md = $this->metadata->getMetaData($spentityid); $id = self::generateID(); @@ -154,6 +154,7 @@ class SimpleSAML_XML_SAML20_AuthnRequest { "IssueInstant=\"" . $issueInstant . "\" " . "ForceAuthn=\"false\" " . "IsPassive=\"false\" " . + "Destination=\"" . $destination . "\" " . "ProtocolBinding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" " . "AssertionConsumerServiceURL=\"" . $assertionConsumerServiceURL . "\">\n" . "<saml:Issuer " . diff --git a/lib/SimpleSAML/XML/SAML20/LogoutRequest.php b/lib/SimpleSAML/XML/SAML20/LogoutRequest.php index a4bbd294a872ca14b4b4e8ac6cc041f2c178f9e9..137ff0c985be3e527b7d2b6aa9576be5d7d00043 100644 --- a/lib/SimpleSAML/XML/SAML20/LogoutRequest.php +++ b/lib/SimpleSAML/XML/SAML20/LogoutRequest.php @@ -141,6 +141,7 @@ class SimpleSAML_XML_SAML20_LogoutRequest { "xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" " . "ID=\"" . $id . "\" " . "Version=\"2.0\" " . + "Destination=\"" . $destination . "\" " . "IssueInstant=\"" . $issueInstant . "\"> " . "<saml:Issuer " . "xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">" . diff --git a/metadata-templates/saml20-sp-hosted.php b/metadata-templates/saml20-sp-hosted.php index 01517f0794103d1cfec8eea1268a5e44e5261826..cf429df10fda445d2874902ddaa7fab331f0b149 100644 --- a/metadata-templates/saml20-sp-hosted.php +++ b/metadata-templates/saml20-sp-hosted.php @@ -15,7 +15,22 @@ $metadata = array( 'host' => 'sp.example.org', 'spNameQualifier' => 'sp.example.org', 'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', - 'ForceAuthn' => 'false' + 'ForceAuthn' => 'false', + + /* + * This option configures the name of a file which contains a + * RSA key for this service provider. The file must be located + * in the cert-directory of the SimpleSAMLPHP installation. + * + * This key will be used to sign all outgoing authentication- + * requests, logoutrequests and logoutresponses (everything + * that uses the HTTP-Redirect binding). + * + * To enable signing, set this option to a private key file + * and enable the 'binding.httpredirect.sign' global option. + */ + 'privatekey' => 'server.pem', + ) ); diff --git a/www/saml2/idp/SingleLogoutService.php b/www/saml2/idp/SingleLogoutService.php index 3b12d99637d8bd82f73c88572e8413be9afaee1b..2fa3b21c03e9db84f9757d7107b4c0f946a90382 100644 --- a/www/saml2/idp/SingleLogoutService.php +++ b/www/saml2/idp/SingleLogoutService.php @@ -52,7 +52,7 @@ if (isset($_GET['SAMLRequest'])) { /* Send the response using the HTTP-Redirect binding. */ $binding = new SimpleSAML_Bindings_SAML20_HTTPRedirect($config, $metadata); - $binding->sendMessage($responseText, $spentityid, $relayState, + $binding->sendMessage($responseText, $idpentityid, $spentityid, $relayState, 'SingleLogoutService', 'SAMLResponse', 'IdP'); exit; } @@ -114,7 +114,7 @@ if ($spentityid) { } //$request, $remoteentityid, $relayState = null, $endpoint = 'SingleLogoutService', $direction = 'SAMLRequest', $mode = 'SP' - $httpredirect->sendMessage($req, $spentityid, $relayState, 'SingleLogoutService', 'SAMLRequest', 'IdP'); + $httpredirect->sendMessage($req, $idpentityid, $spentityid, $relayState, 'SingleLogoutService', 'SAMLRequest', 'IdP'); exit(); @@ -165,7 +165,7 @@ try { } //$request, $remoteentityid, $relayState = null, $endpoint = 'SingleLogoutService', $direction = 'SAMLRequest', $mode = 'SP' - $httpredirect->sendMessage($logoutResponseXML, $logoutrequest->getIssuer(), $relayState, 'SingleLogoutService', 'SAMLResponse', 'IdP'); + $httpredirect->sendMessage($logoutResponseXML, $idpentityid, $logoutrequest->getIssuer(), $relayState, 'SingleLogoutService', 'SAMLResponse', 'IdP'); } catch(Exception $exception) { diff --git a/www/saml2/sp/SingleLogoutService.php b/www/saml2/sp/SingleLogoutService.php index 671733d422f1339c87060d56c5942bdad7c55753..53cf3c1f1a44314174dd756c4db2a91978ebc8b6 100644 --- a/www/saml2/sp/SingleLogoutService.php +++ b/www/saml2/sp/SingleLogoutService.php @@ -77,7 +77,7 @@ if (isset($_GET['SAMLRequest'])) { 'SP me (' . $responder . ') is sending logout response to IdP (' . $requester . ')'); // Send the Logout response using HTTP POST binding. - $httpredirect->sendMessage($logoutResponseXML, $requester, $logoutrequest->getRelayState(), 'SingleLogoutServiceResponse', 'SAMLResponse'); + $httpredirect->sendMessage($logoutResponseXML, $responser, $requester, $logoutrequest->getRelayState(), 'SingleLogoutServiceResponse', 'SAMLResponse'); } elseif(isset($_GET['SAMLResponse'])) { diff --git a/www/saml2/sp/initSLO.php b/www/saml2/sp/initSLO.php index 193f352551fe2bddf4df36c3645f874be6d679d5..7ded295484608de03857248f0343547547c99be5 100644 --- a/www/saml2/sp/initSLO.php +++ b/www/saml2/sp/initSLO.php @@ -46,7 +46,7 @@ if (isset($session) ) { 'SP (' . $spentityid . ') is sending logout request to IdP (' . $idpentityid . ')'); //$request, $remoteentityid, $relayState = null, $endpoint = 'SingleLogoutService', $direction = 'SAMLRequest', $mode = 'SP' - $httpredirect->sendMessage($req, $idpentityid, $relayState, 'SingleLogoutService', 'SAMLRequest', 'SP'); + $httpredirect->sendMessage($req, $spentityid, $idpentityid, $relayState, 'SingleLogoutService', 'SAMLRequest', 'SP'); } catch(Exception $exception) { diff --git a/www/saml2/sp/initSSO.php b/www/saml2/sp/initSSO.php index c78a49b87838ecac11a62c212f703959939744fa..5a8ff34b4d12e16223439259973f96942a6be092 100644 --- a/www/saml2/sp/initSSO.php +++ b/www/saml2/sp/initSSO.php @@ -64,7 +64,9 @@ if (!isset($session) || !$session->isValid() ) { try { $sr = new SimpleSAML_XML_SAML20_AuthnRequest($config, $metadata); - $req = $sr->generate($spentityid); + $md = $metadata->getMetaData($idpentityid, 'saml20-idp-remote'); + $req = $sr->generate($spentityid, $md['SingleSignOnService']); + $httpredirect = new SimpleSAML_Bindings_SAML20_HTTPRedirect($config, $metadata); @@ -76,7 +78,7 @@ if (!isset($session) || !$session->isValid() ) { $logger->log(LOG_NOTICE, $session->getTrackID(), 'SAML2.0', 'SP.initSSO', 'AuthnRequest', $idpentityid, 'SP (' . $spentityid . ') is sending authenticatino request to IdP (' . $idpentityid . ')'); - $httpredirect->sendMessage($req, $idpentityid, $relayState); + $httpredirect->sendMessage($req, $spentityid, $idpentityid, $relayState); } catch(Exception $exception) {