diff --git a/modules/saml/lib/Message.php b/modules/saml/lib/Message.php index 5c6369b1289a253b0179ec33fd5a7275aae74ab9..36d0e28b1be68881f81aff2f58c849d205c542a4 100644 --- a/modules/saml/lib/Message.php +++ b/modules/saml/lib/Message.php @@ -1,804 +1,855 @@ <?php +use RobRichards\XMLSecLibs\XMLSecurityKey; /** - * Common code for building SAML 2 messages based on the - * available metadata. + * Common code for building SAML 2 messages based on the available metadata. * * @package SimpleSAMLphp */ -class sspmod_saml_Message { - - /** - * Add signature key and sender certificate to an element (Message or Assertion). - * - * @param SimpleSAML_Configuration $srcMetadata The metadata of the sender. - * @param SimpleSAML_Configuration $dstMetadata The metadata of the recipient. - * @param \SAML2\Message $element The element we should add the data to. - */ - public static function addSign(SimpleSAML_Configuration $srcMetadata, SimpleSAML_Configuration $dstMetadata, \SAML2\SignedElement $element) { - - $dstPrivateKey = $dstMetadata->getString('signature.privatekey', NULL); - - if ($dstPrivateKey !== NULL) { - $keyArray = SimpleSAML\Utils\Crypto::loadPrivateKey($dstMetadata, TRUE, 'signature.'); - $certArray = SimpleSAML\Utils\Crypto::loadPublicKey($dstMetadata, FALSE, 'signature.'); - } else { - $keyArray = SimpleSAML\Utils\Crypto::loadPrivateKey($srcMetadata, TRUE); - $certArray = SimpleSAML\Utils\Crypto::loadPublicKey($srcMetadata, FALSE); - } - - $algo = $dstMetadata->getString('signature.algorithm', NULL); - if ($algo === NULL) { - /* - * In the NIST Special Publication 800-131A, SHA-1 became deprecated for generating - * new digital signatures in 2011, and will be explicitly disallowed starting the 1st - * of January, 2014. We'll keep this as a default for the next release and mark it - * as deprecated, as part of the transition to SHA-256. - * - * See http://csrc.nist.gov/publications/nistpubs/800-131A/sp800-131A.pdf for more info. - * - * TODO: change default to XMLSecurityKey::RSA_SHA256. - */ - $algo = $srcMetadata->getString('signature.algorithm', XMLSecurityKey::RSA_SHA1); - } - - $privateKey = new XMLSecurityKey($algo, array('type' => 'private')); - if (array_key_exists('password', $keyArray)) { - $privateKey->passphrase = $keyArray['password']; - } - $privateKey->loadKey($keyArray['PEM'], FALSE); - - $element->setSignatureKey($privateKey); - - if ($certArray === NULL) { - // We don't have a certificate to add - return; - } - - if (!array_key_exists('PEM', $certArray)) { - // We have a public key with only a fingerprint. - return; - } - - $element->setCertificates(array($certArray['PEM'])); - } - - - /** - * Add signature key and and senders certificate to message. - * - * @param SimpleSAML_Configuration $srcMetadata The metadata of the sender. - * @param SimpleSAML_Configuration $dstMetadata The metadata of the recipient. - * @param \SAML2\Message $message The message we should add the data to. - */ - private static function addRedirectSign(SimpleSAML_Configuration $srcMetadata, SimpleSAML_Configuration $dstMetadata, \SAML2\Message $message) { - - if ($message instanceof \SAML2\LogoutRequest || $message instanceof \SAML2\LogoutResponse) { - $signingEnabled = $srcMetadata->getBoolean('sign.logout', NULL); - if ($signingEnabled === NULL) { - $signingEnabled = $dstMetadata->getBoolean('sign.logout', NULL); - } - } elseif ($message instanceof \SAML2\AuthnRequest) { - $signingEnabled = $srcMetadata->getBoolean('sign.authnrequest', NULL); - if ($signingEnabled === NULL) { - $signingEnabled = $dstMetadata->getBoolean('sign.authnrequest', NULL); - } - } - - if ($signingEnabled === NULL) { - $signingEnabled = $dstMetadata->getBoolean('redirect.sign', NULL); - if ($signingEnabled === NULL) { - $signingEnabled = $srcMetadata->getBoolean('redirect.sign', FALSE); - } - } - if (!$signingEnabled) { - return; - } - - self::addSign($srcMetadata, $dstMetadata, $message); - } - - - /** - * Find the certificate used to sign a message or assertion. - * - * An exception is thrown if we are unable to locate the certificate. - * - * @param array $certFingerprints The fingerprints we are looking for. - * @param array $certificates Array of certificates. - * @return string Certificate, in PEM-format. - */ - private static function findCertificate(array $certFingerprints, array $certificates) { - - $candidates = array(); - - foreach ($certificates as $cert) { - $fp = strtolower(sha1(base64_decode($cert))); - if (!in_array($fp, $certFingerprints, TRUE)) { - $candidates[] = $fp; - continue; - } - - /* We have found a matching fingerprint. */ - $pem = "-----BEGIN CERTIFICATE-----\n" . - chunk_split($cert, 64) . - "-----END CERTIFICATE-----\n"; - return $pem; - } - - $candidates = "'" . implode("', '", $candidates) . "'"; - $fps = "'" . implode("', '", $certFingerprints) . "'"; - throw new SimpleSAML_Error_Exception('Unable to find a certificate matching the configured ' . - 'fingerprint. Candidates: ' . $candidates . '; certFingerprint: ' . $fps . '.'); - } - - - /** - * Check the signature on a SAML2 message or assertion. - * - * @param SimpleSAML_Configuration $srcMetadata The metadata of the sender. - * @param \SAML2\SignedElement $element Either a \SAML2\Response or a \SAML2\Assertion. - */ - public static function checkSign(SimpleSAML_Configuration $srcMetadata, \SAML2\SignedElement $element) { - - /* Find the public key that should verify signatures by this entity. */ - $keys = $srcMetadata->getPublicKeys('signing'); - if ($keys !== NULL) { - $pemKeys = array(); - foreach ($keys as $key) { - switch ($key['type']) { - case 'X509Certificate': - $pemKeys[] = "-----BEGIN CERTIFICATE-----\n" . - chunk_split($key['X509Certificate'], 64) . - "-----END CERTIFICATE-----\n"; - break; - default: - SimpleSAML\Logger::debug('Skipping unknown key type: ' . $key['type']); - } - } - - } elseif ($srcMetadata->hasValue('certFingerprint')) { - SimpleSAML\Logger::notice( - "Validating certificates by fingerprint is deprecated. Please use " . - "certData or certificate options in your remote metadata configuration." - ); - - $certFingerprint = $srcMetadata->getArrayizeString('certFingerprint'); - foreach ($certFingerprint as &$fp) { - $fp = strtolower(str_replace(':', '', $fp)); - } - - $certificates = $element->getCertificates(); - - /* - * We don't have the full certificate stored. Try to find it - * in the message or the assertion instead. - */ - if (count($certificates) === 0) { - /* We need the full certificate in order to match it against the fingerprint. */ - SimpleSAML\Logger::debug('No certificate in message when validating against fingerprint.'); - return FALSE; - } else { - SimpleSAML\Logger::debug('Found ' . count($certificates) . ' certificates in ' . get_class($element)); - } - - $pemCert = self::findCertificate($certFingerprint, $certificates); - $pemKeys = array($pemCert); - } else { - throw new SimpleSAML_Error_Exception( - 'Missing certificate in metadata for ' . - var_export($srcMetadata->getString('entityid'), TRUE)); - } - - SimpleSAML\Logger::debug('Has ' . count($pemKeys) . ' candidate keys for validation.'); - - $lastException = NULL; - foreach ($pemKeys as $i => $pem) { - $key = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, array('type'=>'public')); - $key->loadKey($pem); - - try { - /* - * Make sure that we have a valid signature on either the response - * or the assertion. - */ - $res = $element->validate($key); - if ($res) { - SimpleSAML\Logger::debug('Validation with key #' . $i . ' succeeded.'); - return TRUE; - } - SimpleSAML\Logger::debug('Validation with key #' . $i . ' failed without exception.'); - } catch (Exception $e) { - SimpleSAML\Logger::debug('Validation with key #' . $i . ' failed with exception: ' . $e->getMessage()); - $lastException = $e; - } - } - - /* We were unable to validate the signature with any of our keys. */ - if ($lastException !== NULL) { - throw $lastException; - } else { - return FALSE; - } - } - - - /** - * Check signature on a SAML2 message if enabled. - * - * @param SimpleSAML_Configuration $srcMetadata The metadata of the sender. - * @param SimpleSAML_Configuration $dstMetadata The metadata of the recipient. - * @param \SAML2\Message $message The message we should check the signature on. - */ - public static function validateMessage( - SimpleSAML_Configuration $srcMetadata, - SimpleSAML_Configuration $dstMetadata, - \SAML2\Message $message - ) { - - if ($message instanceof \SAML2\LogoutRequest || $message instanceof \SAML2\LogoutResponse) { - $enabled = $srcMetadata->getBoolean('validate.logout', NULL); - if ($enabled === NULL) { - $enabled = $dstMetadata->getBoolean('validate.logout', NULL); - } - } elseif ($message instanceof \SAML2\AuthnRequest) { - $enabled = $srcMetadata->getBoolean('validate.authnrequest', NULL); - if ($enabled === NULL) { - $enabled = $dstMetadata->getBoolean('validate.authnrequest', NULL); - } - } - - if ($enabled === NULL) { - $enabled = $srcMetadata->getBoolean('redirect.validate', NULL); - if ($enabled === NULL) { - $enabled = $dstMetadata->getBoolean('redirect.validate', FALSE); - } - } - - if (!$enabled) { - return; - } - - if (!self::checkSign($srcMetadata, $message)) { - throw new SimpleSAML_Error_Exception('Validation of received messages enabled, but no signature found on message.'); - } - } - - - /** - * Retrieve the decryption keys from metadata. - * - * @param SimpleSAML_Configuration $srcMetadata The metadata of the sender (IdP). - * @param SimpleSAML_Configuration $dstMetadata The metadata of the recipient (SP). - * @return array Array of decryption keys. - */ - public static function getDecryptionKeys(SimpleSAML_Configuration $srcMetadata, - SimpleSAML_Configuration $dstMetadata) { - - $sharedKey = $srcMetadata->getString('sharedkey', NULL); - if ($sharedKey !== NULL) { - $key = new XMLSecurityKey(XMLSecurityKey::AES128_CBC); - $key->loadKey($sharedKey); - return array($key); - } - - $keys = array(); - - /* Load the new private key if it exists. */ - $keyArray = SimpleSAML\Utils\Crypto::loadPrivateKey($dstMetadata, FALSE, 'new_'); - if ($keyArray !== NULL) { - assert('isset($keyArray["PEM"])'); - - $key = new XMLSecurityKey(XMLSecurityKey::RSA_1_5, array('type'=>'private')); - if (array_key_exists('password', $keyArray)) { - $key->passphrase = $keyArray['password']; - } - $key->loadKey($keyArray['PEM']); - $keys[] = $key; - } - - /* Find the existing private key. */ - $keyArray = SimpleSAML\Utils\Crypto::loadPrivateKey($dstMetadata, TRUE); - assert('isset($keyArray["PEM"])'); - - $key = new XMLSecurityKey(XMLSecurityKey::RSA_1_5, array('type'=>'private')); - if (array_key_exists('password', $keyArray)) { - $key->passphrase = $keyArray['password']; - } - $key->loadKey($keyArray['PEM']); - $keys[] = $key; - - return $keys; - } - - - /** - * Retrieve blacklisted algorithms. - * - * Remote configuration overrides local configuration. - * - * @param SimpleSAML_Configuration $srcMetadata The metadata of the sender. - * @param SimpleSAML_Configuration $dstMetadata The metadata of the recipient. - * @return array Array of blacklisted algorithms. - */ - public static function getBlacklistedAlgorithms(SimpleSAML_Configuration $srcMetadata, - SimpleSAML_Configuration $dstMetadata) { - - $blacklist = $srcMetadata->getArray('encryption.blacklisted-algorithms', NULL); - if ($blacklist === NULL) { - $blacklist = $dstMetadata->getArray('encryption.blacklisted-algorithms', array(XMLSecurityKey::RSA_1_5)); - } - return $blacklist; - } - - - /** - * Decrypt an assertion. - * - * This function takes in a \SAML2\Assertion and decrypts it if it is encrypted. - * If it is unencrypted, and encryption is enabled in the metadata, an exception - * will be throws. - * - * @param SimpleSAML_Configuration $srcMetadata The metadata of the sender (IdP). - * @param SimpleSAML_Configuration $dstMetadata The metadata of the recipient (SP). - * @param \SAML2\Assertion|\SAML2\EncryptedAssertion $assertion The assertion we are decrypting. - * @return \SAML2\Assertion The assertion. - */ - private static function decryptAssertion(SimpleSAML_Configuration $srcMetadata, - SimpleSAML_Configuration $dstMetadata, $assertion) { - assert('$assertion instanceof \SAML2\Assertion || $assertion instanceof \SAML2\EncryptedAssertion'); - - if ($assertion instanceof \SAML2\Assertion) { - $encryptAssertion = $srcMetadata->getBoolean('assertion.encryption', NULL); - if ($encryptAssertion === NULL) { - $encryptAssertion = $dstMetadata->getBoolean('assertion.encryption', FALSE); - } - if ($encryptAssertion) { - /* The assertion was unencrypted, but we have encryption enabled. */ - throw new Exception('Received unencrypted assertion, but encryption was enabled.'); - } - - return $assertion; - } - - try { - $keys = self::getDecryptionKeys($srcMetadata, $dstMetadata); - } catch (Exception $e) { - throw new SimpleSAML_Error_Exception('Error decrypting assertion: ' . $e->getMessage()); - } - - $blacklist = self::getBlacklistedAlgorithms($srcMetadata, $dstMetadata); - - $lastException = NULL; - foreach ($keys as $i => $key) { - try { - $ret = $assertion->getAssertion($key, $blacklist); - SimpleSAML\Logger::debug('Decryption with key #' . $i . ' succeeded.'); - return $ret; - } catch (Exception $e) { - SimpleSAML\Logger::debug('Decryption with key #' . $i . ' failed with exception: ' . $e->getMessage()); - $lastException = $e; - } - } - throw $lastException; - } - - - /** - * Retrieve the status code of a response as a sspmod_saml_Error. - * - * @param \SAML2\StatusResponse $response The response. - * @return sspmod_saml_Error The error. - */ - public static function getResponseError(\SAML2\StatusResponse $response) { - - $status = $response->getStatus(); - return new sspmod_saml_Error($status['Code'], $status['SubCode'], $status['Message']); - } - - - /** - * Build an authentication request based on information in the metadata. - * - * @param SimpleSAML_Configuration $spMetadata The metadata of the service provider. - * @param SimpleSAML_Configuration $idpMetadata The metadata of the identity provider. - */ - public static function buildAuthnRequest(SimpleSAML_Configuration $spMetadata, SimpleSAML_Configuration $idpMetadata) { - - $ar = new \SAML2\AuthnRequest(); - - // get the NameIDPolicy to apply. IdP metadata has precedence. - $nameIdPolicy = array(); - if ($idpMetadata->hasValue('NameIDPolicy')) { - $nameIdPolicy = $idpMetadata->getValue('NameIDPolicy'); - } elseif ($spMetadata->hasValue('NameIDPolicy')) { - $nameIdPolicy = $spMetadata->getValue('NameIDPolicy'); - } - - if (!is_array($nameIdPolicy)) { - // handle old configurations where 'NameIDPolicy' was used to specify just the format - $nameIdPolicy = array('Format' => $nameIdPolicy); - } - - $nameIdPolicy_cf = SimpleSAML_Configuration::loadFromArray($nameIdPolicy); - $policy = array( - 'Format' => $nameIdPolicy_cf->getString('Format', \SAML2\Constants::NAMEID_TRANSIENT), - 'AllowCreate' => $nameIdPolicy_cf->getBoolean('AllowCreate', true), - ); - $spNameQualifier = $nameIdPolicy_cf->getString('SPNameQualifier', false); - if ($spNameQualifier !== false) { - $policy['SPNameQualifier'] = $spNameQualifier; - } - $ar->setNameIdPolicy($policy); - - $ar->setForceAuthn($spMetadata->getBoolean('ForceAuthn', FALSE)); - $ar->setIsPassive($spMetadata->getBoolean('IsPassive', FALSE)); - - $protbind = $spMetadata->getValueValidate('ProtocolBinding', array( - \SAML2\Constants::BINDING_HTTP_POST, - \SAML2\Constants::BINDING_HOK_SSO, - \SAML2\Constants::BINDING_HTTP_ARTIFACT, - \SAML2\Constants::BINDING_HTTP_REDIRECT, - ), \SAML2\Constants::BINDING_HTTP_POST); - - /* Shoaib - setting the appropriate binding based on parameter in sp-metadata defaults to HTTP_POST */ - $ar->setProtocolBinding($protbind); - - $ar->setIssuer($spMetadata->getString('entityid')); - - $ar->setAssertionConsumerServiceIndex($spMetadata->getInteger('AssertionConsumerServiceIndex', NULL)); - $ar->setAttributeConsumingServiceIndex($spMetadata->getInteger('AttributeConsumingServiceIndex', NULL)); - - if ($spMetadata->hasValue('AuthnContextClassRef')) { - $accr = $spMetadata->getArrayizeString('AuthnContextClassRef'); - $comp = $spMetadata->getValueValidate('AuthnContextComparison', array( - \SAML2\Constants::COMPARISON_EXACT, - \SAML2\Constants::COMPARISON_MINIMUM, - \SAML2\Constants::COMPARISON_MAXIMUM, - \SAML2\Constants::COMPARISON_BETTER, - ), \SAML2\Constants::COMPARISON_EXACT); - $ar->setRequestedAuthnContext(array('AuthnContextClassRef' => $accr, 'Comparison' => $comp)); - } - - self::addRedirectSign($spMetadata, $idpMetadata, $ar); - - return $ar; - } - - - /** - * Build a logout request based on information in the metadata. - * - * @param SimpleSAML_Configuration $srcMetadata The metadata of the sender. - * @param SimpleSAML_Configuration $dstpMetadata The metadata of the recipient. - */ - public static function buildLogoutRequest(SimpleSAML_Configuration $srcMetadata, SimpleSAML_Configuration $dstMetadata) { - - $lr = new \SAML2\LogoutRequest(); - $lr->setIssuer($srcMetadata->getString('entityid')); - - self::addRedirectSign($srcMetadata, $dstMetadata, $lr); - - return $lr; - } - - - /** - * Build a logout response based on information in the metadata. - * - * @param SimpleSAML_Configuration $srcMetadata The metadata of the sender. - * @param SimpleSAML_Configuration $dstpMetadata The metadata of the recipient. - */ - public static function buildLogoutResponse(SimpleSAML_Configuration $srcMetadata, SimpleSAML_Configuration $dstMetadata) { - - $lr = new \SAML2\LogoutResponse(); - $lr->setIssuer($srcMetadata->getString('entityid')); - - self::addRedirectSign($srcMetadata, $dstMetadata, $lr); - - return $lr; - } - - - /** - * Process a response message. - * - * If the response is an error response, we will throw a sspmod_saml_Error - * exception with the error. - * - * @param SimpleSAML_Configuration $spMetadata The metadata of the service provider. - * @param SimpleSAML_Configuration $idpMetadata The metadata of the identity provider. - * @param \SAML2\Response $response The response. - * @return array Array with \SAML2\Assertion objects, containing valid assertions from the response. - */ - public static function processResponse( - SimpleSAML_Configuration $spMetadata, SimpleSAML_Configuration $idpMetadata, - \SAML2\Response $response - ) { - - if (!$response->isSuccess()) { - throw self::getResponseError($response); - } - - /* Validate Response-element destination. */ - $currentURL = \SimpleSAML\Utils\HTTP::getSelfURLNoQuery(); - $msgDestination = $response->getDestination(); - if ($msgDestination !== NULL && $msgDestination !== $currentURL) { - throw new Exception('Destination in response doesn\'t match the current URL. Destination is "' . - $msgDestination . '", current URL is "' . $currentURL . '".'); - } - - $responseSigned = self::checkSign($idpMetadata, $response); - - /* - * When we get this far, the response itself is valid. - * We only need to check signatures and conditions of the response. - */ - - $assertion = $response->getAssertions(); - if (empty($assertion)) { - throw new SimpleSAML_Error_Exception('No assertions found in response from IdP.'); - } - - $ret = array(); - foreach ($assertion as $a) { - $ret[] = self::processAssertion($spMetadata, $idpMetadata, $response, $a, $responseSigned); - } - - return $ret; - } - - - /** - * Process an assertion in a response. - * - * Will throw an exception if it is invalid. - * - * @param SimpleSAML_Configuration $spMetadata The metadata of the service provider. - * @param SimpleSAML_Configuration $idpMetadata The metadata of the identity provider. - * @param \SAML2\Response $response The response containing the assertion. - * @param \SAML2\Assertion|\SAML2\EncryptedAssertion $assertion The assertion. - * @param bool $responseSigned Whether the response is signed. - * @return \SAML2\Assertion The assertion, if it is valid. - */ - private static function processAssertion( - SimpleSAML_Configuration $spMetadata, SimpleSAML_Configuration $idpMetadata, - \SAML2\Response $response, $assertion, $responseSigned - ) { - assert('$assertion instanceof \SAML2\Assertion || $assertion instanceof \SAML2\EncryptedAssertion'); - assert('is_bool($responseSigned)'); - - $assertion = self::decryptAssertion($idpMetadata, $spMetadata, $assertion); - - if (!self::checkSign($idpMetadata, $assertion)) { - if (!$responseSigned) { - throw new SimpleSAML_Error_Exception('Neither the assertion nor the response was signed.'); - } - } - /* At least one valid signature found. */ - - $currentURL = \SimpleSAML\Utils\HTTP::getSelfURLNoQuery(); - - - /* Check various properties of the assertion. */ - - $notBefore = $assertion->getNotBefore(); - if ($notBefore !== NULL && $notBefore > time() + 60) { - throw new SimpleSAML_Error_Exception('Received an assertion that is valid in the future. Check clock synchronization on IdP and SP.'); - } - - $notOnOrAfter = $assertion->getNotOnOrAfter(); - if ($notOnOrAfter !== NULL && $notOnOrAfter <= time() - 60) { - throw new SimpleSAML_Error_Exception('Received an assertion that has expired. Check clock synchronization on IdP and SP.'); - } - - $sessionNotOnOrAfter = $assertion->getSessionNotOnOrAfter(); - if ($sessionNotOnOrAfter !== NULL && $sessionNotOnOrAfter <= time() - 60) { - throw new SimpleSAML_Error_Exception('Received an assertion with a session that has expired. Check clock synchronization on IdP and SP.'); - } - - $validAudiences = $assertion->getValidAudiences(); - if ($validAudiences !== NULL) { - $spEntityId = $spMetadata->getString('entityid'); - if (!in_array($spEntityId, $validAudiences, TRUE)) { - $candidates = '[' . implode('], [', $validAudiences) . ']'; - throw new SimpleSAML_Error_Exception('This SP [' . $spEntityId . '] is not a valid audience for the assertion. Candidates were: ' . $candidates); - } - } - - $found = FALSE; - $lastError = 'No SubjectConfirmation element in Subject.'; - $validSCMethods = array(\SAML2\Constants::CM_BEARER, \SAML2\Constants::CM_HOK, \SAML2\Constants::CM_VOUCHES); - foreach ($assertion->getSubjectConfirmation() as $sc) { - if (!in_array($sc->Method, $validSCMethods)) { - $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) { - $hok = $spMetadata->getBoolean('saml20.hok.assertion', FALSE); - } - if ($sc->Method === \SAML2\Constants::CM_BEARER && $hok) { - $lastError = 'Bearer SubjectConfirmation received, but Holder-of-Key SubjectConfirmation needed'; - continue; - } - if ($sc->Method === \SAML2\Constants::CM_HOK && !$hok) { - $lastError = 'Holder-of-Key SubjectConfirmation received, but the Holder-of-Key profile is not enabled.'; - continue; - } - - $scd = $sc->SubjectConfirmationData; - if ($sc->Method === \SAML2\Constants::CM_HOK) { - /* Check HoK Assertion */ - if (\SimpleSAML\Utils\HTTP::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)) { - $lastError = 'Error while looking for client certificate during TLS handshake with SP, the client certificate does not ' - . 'have the expected structure'; - 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 no SubjectConfirmationData then don't do anything. - if ($scd === null) { - $lastError = 'No SubjectConfirmationData provided'; - continue; - } - - if ($scd->NotBefore && $scd->NotBefore > time() + 60) { - $lastError = 'NotBefore in SubjectConfirmationData is in the future: ' . $scd->NotBefore; - continue; - } - if ($scd->NotOnOrAfter && $scd->NotOnOrAfter <= time() - 60) { - $lastError = 'NotOnOrAfter in SubjectConfirmationData is in the past: ' . $scd->NotOnOrAfter; - continue; - } - if ($scd->Recipient !== NULL && $scd->Recipient !== $currentURL) { - $lastError = 'Recipient in SubjectConfirmationData does not match the current URL. Recipient is ' . - var_export($scd->Recipient, TRUE) . ', current URL is ' . var_export($currentURL, TRUE) . '.'; - continue; - } - if ($scd->InResponseTo !== NULL && $response->getInResponseTo() !== NULL && $scd->InResponseTo !== $response->getInResponseTo()) { - $lastError = 'InResponseTo in SubjectConfirmationData does not match the Response. Response has ' . - var_export($response->getInResponseTo(), TRUE) . ', SubjectConfirmationData has ' . var_export($scd->InResponseTo, TRUE) . '.'; - continue; - } - $found = TRUE; - break; - } - if (!$found) { - throw new SimpleSAML_Error_Exception('Error validating SubjectConfirmation in Assertion: ' . $lastError); - } - - /* As far as we can tell, the assertion is valid. */ - - - /* Maybe we need to base64 decode the attributes in the assertion? */ - if ($idpMetadata->getBoolean('base64attributes', FALSE)) { - $attributes = $assertion->getAttributes(); - $newAttributes = array(); - foreach ($attributes as $name => $values) { - $newAttributes[$name] = array(); - foreach ($values as $value) { - foreach(explode('_', $value) AS $v) { - $newAttributes[$name][] = base64_decode($v); - } - } - } - $assertion->setAttributes($newAttributes); - } - - - /* Decrypt the NameID element if it is encrypted. */ - if ($assertion->isNameIdEncrypted()) { - try { - $keys = self::getDecryptionKeys($idpMetadata, $spMetadata); - } catch (Exception $e) { - throw new SimpleSAML_Error_Exception('Error decrypting NameID: ' . $e->getMessage()); - } - - $blacklist = self::getBlacklistedAlgorithms($idpMetadata, $spMetadata); - - $lastException = NULL; - foreach ($keys as $i => $key) { - try { - $assertion->decryptNameId($key, $blacklist); - SimpleSAML\Logger::debug('Decryption with key #' . $i . ' succeeded.'); - $lastException = NULL; - break; - } catch (Exception $e) { - SimpleSAML\Logger::debug('Decryption with key #' . $i . ' failed with exception: ' . $e->getMessage()); - $lastException = $e; - } - } - if ($lastException !== NULL) { - throw $lastException; - } - } - - return $assertion; - } - - - /** - * Retrieve the encryption key for the given entity. - * - * @param SimpleSAML_Configuration $metadata The metadata of the entity. - * @return XMLSecurityKey The encryption key. - */ - public static function getEncryptionKey(SimpleSAML_Configuration $metadata) { - - $sharedKey = $metadata->getString('sharedkey', NULL); - if ($sharedKey !== NULL) { - $key = new XMLSecurityKey(XMLSecurityKey::AES128_CBC); - $key->loadKey($sharedKey); - return $key; - } - - $keys = $metadata->getPublicKeys('encryption', TRUE); - foreach ($keys as $key) { - switch ($key['type']) { - case 'X509Certificate': - $pemKey = "-----BEGIN CERTIFICATE-----\n" . - chunk_split($key['X509Certificate'], 64) . - "-----END CERTIFICATE-----\n"; - $key = new XMLSecurityKey(XMLSecurityKey::RSA_OAEP_MGF1P, array('type'=>'public')); - $key->loadKey($pemKey); - return $key; - } - } - - throw new SimpleSAML_Error_Exception('No supported encryption key in ' . var_export($metadata->getString('entityid'), TRUE)); - } - +class sspmod_saml_Message +{ + + /** + * Add signature key and sender certificate to an element (Message or Assertion). + * + * @param SimpleSAML_Configuration $srcMetadata The metadata of the sender. + * @param SimpleSAML_Configuration $dstMetadata The metadata of the recipient. + * @param \SAML2\SignedElement $element The element we should add the data to. + */ + public static function addSign( + SimpleSAML_Configuration $srcMetadata, + SimpleSAML_Configuration $dstMetadata, + \SAML2\SignedElement $element + ) { + $dstPrivateKey = $dstMetadata->getString('signature.privatekey', null); + + if ($dstPrivateKey !== null) { + $keyArray = SimpleSAML\Utils\Crypto::loadPrivateKey($dstMetadata, true, 'signature.'); + $certArray = SimpleSAML\Utils\Crypto::loadPublicKey($dstMetadata, false, 'signature.'); + } else { + $keyArray = SimpleSAML\Utils\Crypto::loadPrivateKey($srcMetadata, true); + $certArray = SimpleSAML\Utils\Crypto::loadPublicKey($srcMetadata, false); + } + + $algo = $dstMetadata->getString('signature.algorithm', null); + if ($algo === null) { + /* + * In the NIST Special Publication 800-131A, SHA-1 became deprecated for generating + * new digital signatures in 2011, and will be explicitly disallowed starting the 1st + * of January, 2014. We'll keep this as a default for the next release and mark it + * as deprecated, as part of the transition to SHA-256. + * + * See http://csrc.nist.gov/publications/nistpubs/800-131A/sp800-131A.pdf for more info. + * + * TODO: change default to XMLSecurityKey::RSA_SHA256. + */ + $algo = $srcMetadata->getString('signature.algorithm', XMLSecurityKey::RSA_SHA1); + } + + $privateKey = new XMLSecurityKey($algo, array('type' => 'private')); + if (array_key_exists('password', $keyArray)) { + $privateKey->passphrase = $keyArray['password']; + } + $privateKey->loadKey($keyArray['PEM'], false); + + $element->setSignatureKey($privateKey); + + if ($certArray === null) { + // we don't have a certificate to add + return; + } + + if (!array_key_exists('PEM', $certArray)) { + // we have a public key with only a fingerprint + return; + } + + $element->setCertificates(array($certArray['PEM'])); + } + + + /** + * Add signature key and and senders certificate to message. + * + * @param SimpleSAML_Configuration $srcMetadata The metadata of the sender. + * @param SimpleSAML_Configuration $dstMetadata The metadata of the recipient. + * @param \SAML2\Message $message The message we should add the data to. + */ + private static function addRedirectSign( + SimpleSAML_Configuration $srcMetadata, + SimpleSAML_Configuration $dstMetadata, + \SAML2\Message $message + ) { + + $signingEnabled = null; + if ($message instanceof \SAML2\LogoutRequest || $message instanceof \SAML2\LogoutResponse) { + $signingEnabled = $srcMetadata->getBoolean('sign.logout', null); + if ($signingEnabled === null) { + $signingEnabled = $dstMetadata->getBoolean('sign.logout', null); + } + } elseif ($message instanceof \SAML2\AuthnRequest) { + $signingEnabled = $srcMetadata->getBoolean('sign.authnrequest', null); + if ($signingEnabled === null) { + $signingEnabled = $dstMetadata->getBoolean('sign.authnrequest', null); + } + } + + if ($signingEnabled === null) { + $signingEnabled = $dstMetadata->getBoolean('redirect.sign', null); + if ($signingEnabled === null) { + $signingEnabled = $srcMetadata->getBoolean('redirect.sign', false); + } + } + if (!$signingEnabled) { + return; + } + + self::addSign($srcMetadata, $dstMetadata, $message); + } + + + /** + * Find the certificate used to sign a message or assertion. + * + * An exception is thrown if we are unable to locate the certificate. + * + * @param array $certFingerprints The fingerprints we are looking for. + * @param array $certificates Array of certificates. + * + * @return string Certificate, in PEM-format. + * + * @throws SimpleSAML_Error_Exception if we cannot find the certificate matching the fingerprint. + */ + private static function findCertificate(array $certFingerprints, array $certificates) + { + $candidates = array(); + + foreach ($certificates as $cert) { + $fp = strtolower(sha1(base64_decode($cert))); + if (!in_array($fp, $certFingerprints, true)) { + $candidates[] = $fp; + continue; + } + + /* We have found a matching fingerprint. */ + $pem = "-----BEGIN CERTIFICATE-----\n". + chunk_split($cert, 64). + "-----END CERTIFICATE-----\n"; + return $pem; + } + + $candidates = "'".implode("', '", $candidates)."'"; + $fps = "'".implode("', '", $certFingerprints)."'"; + throw new SimpleSAML_Error_Exception('Unable to find a certificate matching the configured '. + 'fingerprint. Candidates: '.$candidates.'; certFingerprint: '.$fps.'.'); + } + + + /** + * Check the signature on a SAML2 message or assertion. + * + * @param SimpleSAML_Configuration $srcMetadata The metadata of the sender. + * @param \SAML2\SignedElement $element Either a \SAML2\Response or a \SAML2\Assertion. + * @return boolean True if the signature is correct, false otherwise. + * + * @throws \SimpleSAML_Error_Exception if there is not certificate in the metadata for the entity. + * @throws \Exception if the signature validation fails with an exception. + */ + public static function checkSign(SimpleSAML_Configuration $srcMetadata, \SAML2\SignedElement $element) + { + // find the public key that should verify signatures by this entity + $keys = $srcMetadata->getPublicKeys('signing'); + if ($keys !== null) { + $pemKeys = array(); + foreach ($keys as $key) { + switch ($key['type']) { + case 'X509Certificate': + $pemKeys[] = "-----BEGIN CERTIFICATE-----\n". + chunk_split($key['X509Certificate'], 64). + "-----END CERTIFICATE-----\n"; + break; + default: + SimpleSAML\Logger::debug('Skipping unknown key type: '.$key['type']); + } + } + } elseif ($srcMetadata->hasValue('certFingerprint')) { + SimpleSAML\Logger::notice( + "Validating certificates by fingerprint is deprecated. Please use ". + "certData or certificate options in your remote metadata configuration." + ); + + $certFingerprint = $srcMetadata->getArrayizeString('certFingerprint'); + foreach ($certFingerprint as &$fp) { + $fp = strtolower(str_replace(':', '', $fp)); + } + + $certificates = $element->getCertificates(); + + // we don't have the full certificate stored. Try to find it in the message or the assertion instead + if (count($certificates) === 0) { + /* We need the full certificate in order to match it against the fingerprint. */ + SimpleSAML\Logger::debug('No certificate in message when validating against fingerprint.'); + return false; + } else { + SimpleSAML\Logger::debug('Found '.count($certificates).' certificates in '.get_class($element)); + } + + $pemCert = self::findCertificate($certFingerprint, $certificates); + $pemKeys = array($pemCert); + } else { + throw new SimpleSAML_Error_Exception( + 'Missing certificate in metadata for '. + var_export($srcMetadata->getString('entityid'), true) + ); + } + + SimpleSAML\Logger::debug('Has '.count($pemKeys).' candidate keys for validation.'); + + $lastException = null; + foreach ($pemKeys as $i => $pem) { + $key = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, array('type' => 'public')); + $key->loadKey($pem); + + try { + // make sure that we have a valid signature on either the response or the assertion + $res = $element->validate($key); + if ($res) { + SimpleSAML\Logger::debug('Validation with key #'.$i.' succeeded.'); + return true; + } + SimpleSAML\Logger::debug('Validation with key #'.$i.' failed without exception.'); + } catch (Exception $e) { + SimpleSAML\Logger::debug('Validation with key #'.$i.' failed with exception: '.$e->getMessage()); + $lastException = $e; + } + } + + // we were unable to validate the signature with any of our keys + if ($lastException !== null) { + throw $lastException; + } else { + return false; + } + } + + + /** + * Check signature on a SAML2 message if enabled. + * + * @param SimpleSAML_Configuration $srcMetadata The metadata of the sender. + * @param SimpleSAML_Configuration $dstMetadata The metadata of the recipient. + * @param \SAML2\Message $message The message we should check the signature on. + * + * @throws \SimpleSAML_Error_Exception if message validation is enabled, but there is no signature in the message. + */ + public static function validateMessage( + SimpleSAML_Configuration $srcMetadata, + SimpleSAML_Configuration $dstMetadata, + \SAML2\Message $message + ) { + $enabled = null; + if ($message instanceof \SAML2\LogoutRequest || $message instanceof \SAML2\LogoutResponse) { + $enabled = $srcMetadata->getBoolean('validate.logout', null); + if ($enabled === null) { + $enabled = $dstMetadata->getBoolean('validate.logout', null); + } + } elseif ($message instanceof \SAML2\AuthnRequest) { + $enabled = $srcMetadata->getBoolean('validate.authnrequest', null); + if ($enabled === null) { + $enabled = $dstMetadata->getBoolean('validate.authnrequest', null); + } + } + + if ($enabled === null) { + $enabled = $srcMetadata->getBoolean('redirect.validate', null); + if ($enabled === null) { + $enabled = $dstMetadata->getBoolean('redirect.validate', false); + } + } + + if (!$enabled) { + return; + } + + if (!self::checkSign($srcMetadata, $message)) { + throw new SimpleSAML_Error_Exception( + 'Validation of received messages enabled, but no signature found on message.' + ); + } + } + + + /** + * Retrieve the decryption keys from metadata. + * + * @param SimpleSAML_Configuration $srcMetadata The metadata of the sender (IdP). + * @param SimpleSAML_Configuration $dstMetadata The metadata of the recipient (SP). + * + * @return array Array of decryption keys. + */ + public static function getDecryptionKeys( + SimpleSAML_Configuration $srcMetadata, + SimpleSAML_Configuration $dstMetadata + ) { + $sharedKey = $srcMetadata->getString('sharedkey', null); + if ($sharedKey !== null) { + $key = new XMLSecurityKey(XMLSecurityKey::AES128_CBC); + $key->loadKey($sharedKey); + return array($key); + } + + $keys = array(); + + // load the new private key if it exists + $keyArray = SimpleSAML\Utils\Crypto::loadPrivateKey($dstMetadata, false, 'new_'); + if ($keyArray !== null) { + assert('isset($keyArray["PEM"])'); + + $key = new XMLSecurityKey(XMLSecurityKey::RSA_1_5, array('type' => 'private')); + if (array_key_exists('password', $keyArray)) { + $key->passphrase = $keyArray['password']; + } + $key->loadKey($keyArray['PEM']); + $keys[] = $key; + } + + // find the existing private key + $keyArray = SimpleSAML\Utils\Crypto::loadPrivateKey($dstMetadata, true); + assert('isset($keyArray["PEM"])'); + + $key = new XMLSecurityKey(XMLSecurityKey::RSA_1_5, array('type' => 'private')); + if (array_key_exists('password', $keyArray)) { + $key->passphrase = $keyArray['password']; + } + $key->loadKey($keyArray['PEM']); + $keys[] = $key; + + return $keys; + } + + + /** + * Retrieve blacklisted algorithms. + * + * Remote configuration overrides local configuration. + * + * @param SimpleSAML_Configuration $srcMetadata The metadata of the sender. + * @param SimpleSAML_Configuration $dstMetadata The metadata of the recipient. + * + * @return array Array of blacklisted algorithms. + */ + public static function getBlacklistedAlgorithms( + SimpleSAML_Configuration $srcMetadata, + SimpleSAML_Configuration $dstMetadata + ) { + $blacklist = $srcMetadata->getArray('encryption.blacklisted-algorithms', null); + if ($blacklist === null) { + $blacklist = $dstMetadata->getArray('encryption.blacklisted-algorithms', array(XMLSecurityKey::RSA_1_5)); + } + return $blacklist; + } + + + /** + * Decrypt an assertion. + * + * @param SimpleSAML_Configuration $srcMetadata The metadata of the sender (IdP). + * @param SimpleSAML_Configuration $dstMetadata The metadata of the recipient (SP). + * @param \SAML2\Assertion|\SAML2\EncryptedAssertion $assertion The assertion we are decrypting. + * + * @return \SAML2\Assertion The assertion. + * + * @throws \SimpleSAML_Error_Exception if encryption is enabled but the assertion is not encrypted, or if we cannot + * get the decryption keys. + * @throws \Exception if decryption fails for whatever reason. + */ + private static function decryptAssertion( + SimpleSAML_Configuration $srcMetadata, + SimpleSAML_Configuration $dstMetadata, + $assertion + ) { + assert('$assertion instanceof \SAML2\Assertion || $assertion instanceof \SAML2\EncryptedAssertion'); + + if ($assertion instanceof \SAML2\Assertion) { + $encryptAssertion = $srcMetadata->getBoolean('assertion.encryption', null); + if ($encryptAssertion === null) { + $encryptAssertion = $dstMetadata->getBoolean('assertion.encryption', false); + } + if ($encryptAssertion) { + /* The assertion was unencrypted, but we have encryption enabled. */ + throw new Exception('Received unencrypted assertion, but encryption was enabled.'); + } + + return $assertion; + } + + try { + $keys = self::getDecryptionKeys($srcMetadata, $dstMetadata); + } catch (Exception $e) { + throw new SimpleSAML_Error_Exception('Error decrypting assertion: '.$e->getMessage()); + } + + $blacklist = self::getBlacklistedAlgorithms($srcMetadata, $dstMetadata); + + $lastException = null; + foreach ($keys as $i => $key) { + try { + $ret = $assertion->getAssertion($key, $blacklist); + SimpleSAML\Logger::debug('Decryption with key #'.$i.' succeeded.'); + return $ret; + } catch (Exception $e) { + SimpleSAML\Logger::debug('Decryption with key #'.$i.' failed with exception: '.$e->getMessage()); + $lastException = $e; + } + } + throw $lastException; + } + + + /** + * Retrieve the status code of a response as a sspmod_saml_Error. + * + * @param \SAML2\StatusResponse $response The response. + * + * @return sspmod_saml_Error The error. + */ + public static function getResponseError(\SAML2\StatusResponse $response) + { + $status = $response->getStatus(); + return new sspmod_saml_Error($status['Code'], $status['SubCode'], $status['Message']); + } + + + /** + * Build an authentication request based on information in the metadata. + * + * @param SimpleSAML_Configuration $spMetadata The metadata of the service provider. + * @param SimpleSAML_Configuration $idpMetadata The metadata of the identity provider. + * @return \SAML2\AuthnRequest An authentication request object. + */ + public static function buildAuthnRequest( + SimpleSAML_Configuration $spMetadata, + SimpleSAML_Configuration $idpMetadata + ) { + $ar = new \SAML2\AuthnRequest(); + + // get the NameIDPolicy to apply. IdP metadata has precedence. + $nameIdPolicy = array(); + if ($idpMetadata->hasValue('NameIDPolicy')) { + $nameIdPolicy = $idpMetadata->getValue('NameIDPolicy'); + } elseif ($spMetadata->hasValue('NameIDPolicy')) { + $nameIdPolicy = $spMetadata->getValue('NameIDPolicy'); + } + + if (!is_array($nameIdPolicy)) { + // handle old configurations where 'NameIDPolicy' was used to specify just the format + $nameIdPolicy = array('Format' => $nameIdPolicy); + } + + $nameIdPolicy_cf = SimpleSAML_Configuration::loadFromArray($nameIdPolicy); + $policy = array( + 'Format' => $nameIdPolicy_cf->getString('Format', \SAML2\Constants::NAMEID_TRANSIENT), + 'AllowCreate' => $nameIdPolicy_cf->getBoolean('AllowCreate', true), + ); + $spNameQualifier = $nameIdPolicy_cf->getString('SPNameQualifier', false); + if ($spNameQualifier !== false) { + $policy['SPNameQualifier'] = $spNameQualifier; + } + $ar->setNameIdPolicy($policy); + + $ar->setForceAuthn($spMetadata->getBoolean('ForceAuthn', false)); + $ar->setIsPassive($spMetadata->getBoolean('IsPassive', false)); + + $protbind = $spMetadata->getValueValidate('ProtocolBinding', array( + \SAML2\Constants::BINDING_HTTP_POST, + \SAML2\Constants::BINDING_HOK_SSO, + \SAML2\Constants::BINDING_HTTP_ARTIFACT, + \SAML2\Constants::BINDING_HTTP_REDIRECT, + ), \SAML2\Constants::BINDING_HTTP_POST); + + // Shoaib: setting the appropriate binding based on parameter in sp-metadata defaults to HTTP_POST + $ar->setProtocolBinding($protbind); + $ar->setIssuer($spMetadata->getString('entityid')); + $ar->setAssertionConsumerServiceIndex($spMetadata->getInteger('AssertionConsumerServiceIndex', null)); + $ar->setAttributeConsumingServiceIndex($spMetadata->getInteger('AttributeConsumingServiceIndex', null)); + + if ($spMetadata->hasValue('AuthnContextClassRef')) { + $accr = $spMetadata->getArrayizeString('AuthnContextClassRef'); + $comp = $spMetadata->getValueValidate('AuthnContextComparison', array( + \SAML2\Constants::COMPARISON_EXACT, + \SAML2\Constants::COMPARISON_MINIMUM, + \SAML2\Constants::COMPARISON_MAXIMUM, + \SAML2\Constants::COMPARISON_BETTER, + ), \SAML2\Constants::COMPARISON_EXACT); + $ar->setRequestedAuthnContext(array('AuthnContextClassRef' => $accr, 'Comparison' => $comp)); + } + + self::addRedirectSign($spMetadata, $idpMetadata, $ar); + + return $ar; + } + + + /** + * Build a logout request based on information in the metadata. + * + * @param SimpleSAML_Configuration $srcMetadata The metadata of the sender. + * @param SimpleSAML_Configuration $dstMetadata The metadata of the recipient. + * @return \SAML2\LogoutRequest A logout request object. + */ + public static function buildLogoutRequest( + SimpleSAML_Configuration $srcMetadata, + SimpleSAML_Configuration $dstMetadata + ) { + $lr = new \SAML2\LogoutRequest(); + $lr->setIssuer($srcMetadata->getString('entityid')); + + self::addRedirectSign($srcMetadata, $dstMetadata, $lr); + + return $lr; + } + + + /** + * Build a logout response based on information in the metadata. + * + * @param SimpleSAML_Configuration $srcMetadata The metadata of the sender. + * @param SimpleSAML_Configuration $dstMetadata The metadata of the recipient. + * @return \SAML2\LogoutResponse A logout response object. + */ + public static function buildLogoutResponse( + SimpleSAML_Configuration $srcMetadata, + SimpleSAML_Configuration $dstMetadata + ) { + $lr = new \SAML2\LogoutResponse(); + $lr->setIssuer($srcMetadata->getString('entityid')); + + self::addRedirectSign($srcMetadata, $dstMetadata, $lr); + + return $lr; + } + + + /** + * Process a response message. + * + * If the response is an error response, we will throw a sspmod_saml_Error exception with the error. + * + * @param SimpleSAML_Configuration $spMetadata The metadata of the service provider. + * @param SimpleSAML_Configuration $idpMetadata The metadata of the identity provider. + * @param \SAML2\Response $response The response. + * + * @return array Array with \SAML2\Assertion objects, containing valid assertions from the response. + * + * @throws \SimpleSAML_Error_Exception if there are no assertions in the response. + * @throws \Exception if the destination of the response does not match the current URL. + */ + public static function processResponse( + SimpleSAML_Configuration $spMetadata, + SimpleSAML_Configuration $idpMetadata, + \SAML2\Response $response + ) { + if (!$response->isSuccess()) { + throw self::getResponseError($response); + } + + // validate Response-element destination + $currentURL = \SimpleSAML\Utils\HTTP::getSelfURLNoQuery(); + $msgDestination = $response->getDestination(); + if ($msgDestination !== null && $msgDestination !== $currentURL) { + throw new Exception('Destination in response doesn\'t match the current URL. Destination is "'. + $msgDestination.'", current URL is "'.$currentURL.'".'); + } + + $responseSigned = self::checkSign($idpMetadata, $response); + + /* + * When we get this far, the response itself is valid. + * We only need to check signatures and conditions of the response. + */ + $assertion = $response->getAssertions(); + if (empty($assertion)) { + throw new SimpleSAML_Error_Exception('No assertions found in response from IdP.'); + } + + $ret = array(); + foreach ($assertion as $a) { + $ret[] = self::processAssertion($spMetadata, $idpMetadata, $response, $a, $responseSigned); + } + + return $ret; + } + + + /** + * Process an assertion in a response. + * + * @param SimpleSAML_Configuration $spMetadata The metadata of the service provider. + * @param SimpleSAML_Configuration $idpMetadata The metadata of the identity provider. + * @param \SAML2\Response $response The response containing the assertion. + * @param \SAML2\Assertion|\SAML2\EncryptedAssertion $assertion The assertion. + * @param bool $responseSigned Whether the response is signed. + * + * @return \SAML2\Assertion The assertion, if it is valid. + * + * @throws \SimpleSAML_Error_Exception if an error occurs while trying to validate the assertion, or if a assertion + * is not signed and it should be, or if we are unable to decrypt the NameID due to a local failure (missing or + * invalid decryption key). + * @throws \Exception if we couldn't decrypt the NameID for unexpected reasons. + */ + private static function processAssertion( + SimpleSAML_Configuration $spMetadata, + SimpleSAML_Configuration $idpMetadata, + \SAML2\Response $response, + $assertion, + $responseSigned + ) { + assert('$assertion instanceof \SAML2\Assertion || $assertion instanceof \SAML2\EncryptedAssertion'); + assert('is_bool($responseSigned)'); + + $assertion = self::decryptAssertion($idpMetadata, $spMetadata, $assertion); + + if (!self::checkSign($idpMetadata, $assertion)) { + if (!$responseSigned) { + throw new SimpleSAML_Error_Exception('Neither the assertion nor the response was signed.'); + } + } // at least one valid signature found + + $currentURL = \SimpleSAML\Utils\HTTP::getSelfURLNoQuery(); + + // check various properties of the assertion + $notBefore = $assertion->getNotBefore(); + if ($notBefore !== null && $notBefore > time() + 60) { + throw new SimpleSAML_Error_Exception( + 'Received an assertion that is valid in the future. Check clock synchronization on IdP and SP.' + ); + } + $notOnOrAfter = $assertion->getNotOnOrAfter(); + if ($notOnOrAfter !== null && $notOnOrAfter <= time() - 60) { + throw new SimpleSAML_Error_Exception( + 'Received an assertion that has expired. Check clock synchronization on IdP and SP.' + ); + } + $sessionNotOnOrAfter = $assertion->getSessionNotOnOrAfter(); + if ($sessionNotOnOrAfter !== null && $sessionNotOnOrAfter <= time() - 60) { + throw new SimpleSAML_Error_Exception( + 'Received an assertion with a session that has expired. Check clock synchronization on IdP and SP.' + ); + } + $validAudiences = $assertion->getValidAudiences(); + if ($validAudiences !== null) { + $spEntityId = $spMetadata->getString('entityid'); + if (!in_array($spEntityId, $validAudiences, true)) { + $candidates = '['.implode('], [', $validAudiences).']'; + throw new SimpleSAML_Error_Exception('This SP ['.$spEntityId. + '] is not a valid audience for the assertion. Candidates were: '.$candidates); + } + } + + $found = false; + $lastError = 'No SubjectConfirmation element in Subject.'; + $validSCMethods = array(\SAML2\Constants::CM_BEARER, \SAML2\Constants::CM_HOK, \SAML2\Constants::CM_VOUCHES); + foreach ($assertion->getSubjectConfirmation() as $sc) { + if (!in_array($sc->Method, $validSCMethods)) { + $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) { + $hok = $spMetadata->getBoolean('saml20.hok.assertion', false); + } + if ($sc->Method === \SAML2\Constants::CM_BEARER && $hok) { + $lastError = 'Bearer SubjectConfirmation received, but Holder-of-Key SubjectConfirmation needed'; + continue; + } + if ($sc->Method === \SAML2\Constants::CM_HOK && !$hok) { + $lastError = 'Holder-of-Key SubjectConfirmation received, '. + 'but the Holder-of-Key profile is not enabled.'; + continue; + } + + $scd = $sc->SubjectConfirmationData; + if ($sc->Method === \SAML2\Constants::CM_HOK) { + // check HoK Assertion + if (\SimpleSAML\Utils\HTTP::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)) { + $lastError = 'Error while looking for client certificate during TLS handshake with SP, the client '. + 'certificate does not have the expected structure'; + continue; + } + // we have a valid client certificate from the browser + $clientCert = str_replace(array("\r", "\n", " "), '', $matches[1]); + + $keyInfo = array(); + 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; + } + + $x509data = array(); + 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; + } + + $x509cert = array(); + 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 no SubjectConfirmationData then don't do anything. + if ($scd === null) { + $lastError = 'No SubjectConfirmationData provided'; + continue; + } + + if ($scd->NotBefore && $scd->NotBefore > time() + 60) { + $lastError = 'NotBefore in SubjectConfirmationData is in the future: '.$scd->NotBefore; + continue; + } + if ($scd->NotOnOrAfter && $scd->NotOnOrAfter <= time() - 60) { + $lastError = 'NotOnOrAfter in SubjectConfirmationData is in the past: '.$scd->NotOnOrAfter; + continue; + } + if ($scd->Recipient !== null && $scd->Recipient !== $currentURL) { + $lastError = 'Recipient in SubjectConfirmationData does not match the current URL. Recipient is '. + var_export($scd->Recipient, true).', current URL is '.var_export($currentURL, true).'.'; + continue; + } + if ($scd->InResponseTo !== null && $response->getInResponseTo() !== null && + $scd->InResponseTo !== $response->getInResponseTo() + ) { + $lastError = 'InResponseTo in SubjectConfirmationData does not match the Response. Response has '. + var_export($response->getInResponseTo(), true). + ', SubjectConfirmationData has '.var_export($scd->InResponseTo, true).'.'; + continue; + } + $found = true; + break; + } + if (!$found) { + throw new SimpleSAML_Error_Exception('Error validating SubjectConfirmation in Assertion: '.$lastError); + } // as far as we can tell, the assertion is valid + + // maybe we need to base64 decode the attributes in the assertion? + if ($idpMetadata->getBoolean('base64attributes', false)) { + $attributes = $assertion->getAttributes(); + $newAttributes = array(); + foreach ($attributes as $name => $values) { + $newAttributes[$name] = array(); + foreach ($values as $value) { + foreach (explode('_', $value) as $v) { + $newAttributes[$name][] = base64_decode($v); + } + } + } + $assertion->setAttributes($newAttributes); + } + + // decrypt the NameID element if it is encrypted + if ($assertion->isNameIdEncrypted()) { + try { + $keys = self::getDecryptionKeys($idpMetadata, $spMetadata); + } catch (Exception $e) { + throw new SimpleSAML_Error_Exception('Error decrypting NameID: '.$e->getMessage()); + } + + $blacklist = self::getBlacklistedAlgorithms($idpMetadata, $spMetadata); + + $lastException = null; + foreach ($keys as $i => $key) { + try { + $assertion->decryptNameId($key, $blacklist); + SimpleSAML\Logger::debug('Decryption with key #'.$i.' succeeded.'); + $lastException = null; + break; + } catch (Exception $e) { + SimpleSAML\Logger::debug('Decryption with key #'.$i.' failed with exception: '.$e->getMessage()); + $lastException = $e; + } + } + if ($lastException !== null) { + throw $lastException; + } + } + + return $assertion; + } + + + /** + * Retrieve the encryption key for the given entity. + * + * @param SimpleSAML_Configuration $metadata The metadata of the entity. + * + * @return \RobRichards\XMLSecLibs\XMLSecurityKey The encryption key. + * + * @throws \SimpleSAML_Error_Exception if there is no supported encryption key in the metadata of this entity. + */ + public static function getEncryptionKey(SimpleSAML_Configuration $metadata) + { + + $sharedKey = $metadata->getString('sharedkey', null); + if ($sharedKey !== null) { + $key = new XMLSecurityKey(XMLSecurityKey::AES128_CBC); + $key->loadKey($sharedKey); + return $key; + } + + $keys = $metadata->getPublicKeys('encryption', true); + foreach ($keys as $key) { + switch ($key['type']) { + case 'X509Certificate': + $pemKey = "-----BEGIN CERTIFICATE-----\n". + chunk_split($key['X509Certificate'], 64). + "-----END CERTIFICATE-----\n"; + $key = new XMLSecurityKey(XMLSecurityKey::RSA_OAEP_MGF1P, array('type' => 'public')); + $key->loadKey($pemKey); + return $key; + } + } + + throw new SimpleSAML_Error_Exception('No supported encryption key in '. + var_export($metadata->getString('entityid'), true)); + } }