Newer
Older
namespace SimpleSAML\Module\saml;
Jaime Pérez
committed
use RobRichards\XMLSecLibs\XMLSecurityKey;
use SAML2\Assertion;
use SAML2\AuthnRequest;
Jaime Pérez Crespo
committed
use SAML2\Constants;
use SAML2\EncryptedAssertion;
use SAML2\LogoutRequest;
use SAML2\LogoutResponse;
use SAML2\Response;
use SAML2\SignedElement;
use SAML2\StatusResponse;
use SAML2\XML\ds\KeyInfo;
use SAML2\XML\ds\X509Certificate;
use SAML2\XML\ds\X509Data;
Jaime Pérez Crespo
committed
use SAML2\XML\saml\Issuer;
use SimpleSAML\Configuration;
use SimpleSAML\Error as SSP_Error;
use SimpleSAML\Logger;
use SimpleSAML\Utils;
Jaime Pérez
committed
* Common code for building SAML 2 messages based on the available metadata.
Jaime Pérez
committed
{
/**
* 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.
Jaime Pérez
committed
* @param \SAML2\SignedElement $element The element we should add the data to.
Jaime Pérez
committed
*/
public static function addSign(
Configuration $srcMetadata,
Configuration $dstMetadata,
SignedElement $element
Jaime Pérez
committed
) {
$dstPrivateKey = $dstMetadata->getString('signature.privatekey', null);
if ($dstPrivateKey !== null) {
$keyArray = Utils\Crypto::loadPrivateKey($dstMetadata, true, 'signature.');
$certArray = Utils\Crypto::loadPublicKey($dstMetadata, false, 'signature.');
Jaime Pérez
committed
} else {
$keyArray = Utils\Crypto::loadPrivateKey($srcMetadata, true);
$certArray = Utils\Crypto::loadPublicKey($srcMetadata, false);
Jaime Pérez
committed
}
$algo = $dstMetadata->getString('signature.algorithm', null);
if ($algo === null) {
$algo = $srcMetadata->getString('signature.algorithm', XMLSecurityKey::RSA_SHA256);
Jaime Pérez
committed
}
$privateKey = new XMLSecurityKey($algo, ['type' => 'private']);
Jaime Pérez
committed
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([$certArray['PEM']]);
Jaime Pérez
committed
}
/**
* 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.
Jaime Pérez
committed
* @param \SAML2\Message $message The message we should add the data to.
Jaime Pérez
committed
*/
private static function addRedirectSign(
Configuration $srcMetadata,
Configuration $dstMetadata,
Jaime Pérez
committed
\SAML2\Message $message
) {
$signingEnabled = null;
if ($message instanceof LogoutRequest || $message instanceof LogoutResponse) {
Jaime Pérez
committed
$signingEnabled = $srcMetadata->getBoolean('sign.logout', null);
if ($signingEnabled === null) {
$signingEnabled = $dstMetadata->getBoolean('sign.logout', null);
}
} elseif ($message instanceof AuthnRequest) {
Jaime Pérez
committed
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
$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.
Jaime Pérez
committed
*/
private static function findCertificate(array $certFingerprints, array $certificates)
{
Jaime Pérez
committed
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 SSP_Error\Exception('Unable to find a certificate matching the configured '.
Jaime Pérez
committed
'fingerprint. Candidates: '.$candidates.'; certFingerprint: '.$fps.'.');
}
/**
* Check the signature on a SAML2 message or assertion.
*
* @param \SimpleSAML\Configuration $srcMetadata The metadata of the sender.
Jaime Pérez
committed
* @param \SAML2\SignedElement $element Either a \SAML2\Response or a \SAML2\Assertion.
* @return bool True if the signature is correct, false otherwise.
Jaime Pérez
committed
*
* @throws \SimpleSAML\Error\Exception if there is not certificate in the metadata for the entity.
Jaime Pérez
committed
* @throws \Exception if the signature validation fails with an exception.
*/
public static function checkSign(Configuration $srcMetadata, SignedElement $element)
Jaime Pérez
committed
{
// find the public key that should verify signatures by this entity
$keys = $srcMetadata->getPublicKeys('signing');
if (!empty($keys)) {
Jaime Pérez
committed
foreach ($keys as $key) {
switch ($key['type']) {
case 'X509Certificate':
$pemKeys[] = "-----BEGIN CERTIFICATE-----\n".
chunk_split($key['X509Certificate'], 64).
"-----END CERTIFICATE-----\n";
break;
default:
Logger::debug('Skipping unknown key type: '.$key['type']);
Jaime Pérez
committed
}
}
} elseif ($srcMetadata->hasValue('certFingerprint')) {
Logger::notice(
Jaime Pérez
committed
"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. */
Logger::debug('No certificate in message when validating against fingerprint.');
Jaime Pérez
committed
return false;
} else {
Logger::debug('Found '.count($certificates).' certificates in '.get_class($element));
Jaime Pérez
committed
}
$pemCert = self::findCertificate($certFingerprint, $certificates);
Jaime Pérez
committed
} else {
throw new SSP_Error\Exception(
Jaime Pérez
committed
'Missing certificate in metadata for '.
var_export($srcMetadata->getString('entityid'), true)
);
}
Logger::debug('Has '.count($pemKeys).' candidate keys for validation.');
Jaime Pérez
committed
$lastException = null;
foreach ($pemKeys as $i => $pem) {
$key = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, ['type' => 'public']);
Jaime Pérez
committed
$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) {
Logger::debug('Validation with key #'.$i.' succeeded.');
Jaime Pérez
committed
return true;
}
Logger::debug('Validation with key #'.$i.' failed without exception.');
Logger::debug('Validation with key #'.$i.' failed with exception: '.$e->getMessage());
Jaime Pérez
committed
$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.
Jaime Pérez
committed
* @param \SAML2\Message $message The message we should check the signature on.
Jaime Pérez
committed
*
* @throws \SimpleSAML\Error\Exception if message validation is enabled, but there is no signature in the message.
Jaime Pérez
committed
*/
public static function validateMessage(
Configuration $srcMetadata,
Configuration $dstMetadata,
Jaime Pérez
committed
\SAML2\Message $message
) {
$enabled = null;
if ($message instanceof LogoutRequest || $message instanceof LogoutResponse) {
Jaime Pérez
committed
$enabled = $srcMetadata->getBoolean('validate.logout', null);
if ($enabled === null) {
$enabled = $dstMetadata->getBoolean('validate.logout', null);
}
} elseif ($message instanceof AuthnRequest) {
Jaime Pérez
committed
$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 SSP_Error\Exception(
Jaime Pérez
committed
'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).
Jaime Pérez
committed
*
* @return array Array of decryption keys.
*/
public static function getDecryptionKeys(
Configuration $srcMetadata,
Configuration $dstMetadata
Jaime Pérez
committed
) {
$sharedKey = $srcMetadata->getString('sharedkey', null);
if ($sharedKey !== null) {
$key = new XMLSecurityKey(XMLSecurityKey::AES128_CBC);
$key->loadKey($sharedKey);
Jaime Pérez
committed
}
Jaime Pérez
committed
// load the new private key if it exists
$keyArray = Utils\Crypto::loadPrivateKey($dstMetadata, false, 'new_');
Jaime Pérez
committed
if ($keyArray !== null) {
Jaime Pérez
committed
$key = new XMLSecurityKey(XMLSecurityKey::RSA_1_5, ['type' => 'private']);
Jaime Pérez
committed
if (array_key_exists('password', $keyArray)) {
$key->passphrase = $keyArray['password'];
}
$key->loadKey($keyArray['PEM']);
$keys[] = $key;
}
// find the existing private key
$keyArray = Utils\Crypto::loadPrivateKey($dstMetadata, true);
Jaime Pérez
committed
$key = new XMLSecurityKey(XMLSecurityKey::RSA_1_5, ['type' => 'private']);
Jaime Pérez
committed
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.
Jaime Pérez
committed
*
* @return array Array of blacklisted algorithms.
*/
public static function getBlacklistedAlgorithms(
Configuration $srcMetadata,
Configuration $dstMetadata
Jaime Pérez
committed
) {
$blacklist = $srcMetadata->getArray('encryption.blacklisted-algorithms', null);
if ($blacklist === null) {
$blacklist = $dstMetadata->getArray('encryption.blacklisted-algorithms', [XMLSecurityKey::RSA_1_5]);
Jaime Pérez
committed
}
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).
Jaime Pérez
committed
* @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
Jaime Pérez
committed
* get the decryption keys.
* @throws \Exception if decryption fails for whatever reason.
*/
private static function decryptAssertion(
Configuration $srcMetadata,
Configuration $dstMetadata,
Jaime Pérez
committed
$assertion
) {
assert($assertion instanceof Assertion || $assertion instanceof EncryptedAssertion);
Jaime Pérez
committed
if ($assertion instanceof Assertion) {
Jaime Pérez
committed
$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.');
Jaime Pérez
committed
}
return $assertion;
}
try {
$keys = self::getDecryptionKeys($srcMetadata, $dstMetadata);
throw new SSP_Error\Exception('Error decrypting assertion: '.$e->getMessage());
Jaime Pérez
committed
}
$blacklist = self::getBlacklistedAlgorithms($srcMetadata, $dstMetadata);
$lastException = null;
foreach ($keys as $i => $key) {
try {
$ret = $assertion->getAssertion($key, $blacklist);
Logger::debug('Decryption with key #'.$i.' succeeded.');
Jaime Pérez
committed
return $ret;
Logger::debug('Decryption with key #'.$i.' failed with exception: '.$e->getMessage());
Jaime Pérez
committed
$lastException = $e;
}
}
/**
* The annotation below is not working - See vimeo/psalm#1909
* @psalm-suppress InvalidThrow
* @var \Exception $lastException
*/
Jaime Pérez
committed
throw $lastException;
}
/**
* Decrypt any encrypted attributes in 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\Assertion $assertion The assertion containing any possibly encrypted attributes.
*
* @return void
*
* @throws \SimpleSAML\Error\Exception if we cannot get the decryption keys or decryption fails.
*/
private static function decryptAttributes(
Configuration $srcMetadata,
Configuration $dstMetadata,
Assertion &$assertion
) {
if (!$assertion->hasEncryptedAttributes()) {
return;
}
try {
$keys = self::getDecryptionKeys($srcMetadata, $dstMetadata);
throw new SSP_Error\Exception('Error decrypting attributes: '.$e->getMessage());
}
$blacklist = self::getBlacklistedAlgorithms($srcMetadata, $dstMetadata);
$error = true;
foreach ($keys as $i => $key) {
try {
$assertion->decryptAttributes($key, $blacklist);
Logger::debug('Attribute decryption with key #'.$i.' succeeded.');
$error = false;
break;
Logger::debug('Attribute decryption failed with exception: '.$e->getMessage());
}
}
if ($error) {
throw new SSP_Error\Exception('Could not decrypt the attributes');
}
}
Jaime Pérez
committed
/**
* Retrieve the status code of a response as a \SimpleSAML\Module\saml\Error.
Jaime Pérez
committed
*
* @param \SAML2\StatusResponse $response The response.
*
* @return \SimpleSAML\Module\saml\Error The error.
Jaime Pérez
committed
*/
public static function getResponseError(StatusResponse $response)
Jaime Pérez
committed
{
$status = $response->getStatus();
return new \SimpleSAML\Module\saml\Error($status['Code'], $status['SubCode'], $status['Message']);
Jaime Pérez
committed
}
/**
* 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.
Jaime Pérez
committed
* @return \SAML2\AuthnRequest An authentication request object.
*/
public static function buildAuthnRequest(
Configuration $spMetadata,
Configuration $idpMetadata
Jaime Pérez
committed
) {
$ar = new AuthnRequest();
Jaime Pérez
committed
// get the NameIDPolicy to apply. IdP metadata has precedence.
Jaime Pérez Crespo
committed
$nameIdPolicy = null;
Jaime Pérez
committed
if ($idpMetadata->hasValue('NameIDPolicy')) {
$nameIdPolicy = $idpMetadata->getValue('NameIDPolicy');
} elseif ($spMetadata->hasValue('NameIDPolicy')) {
$nameIdPolicy = $spMetadata->getValue('NameIDPolicy');
}
$policy = Utils\Config\Metadata::parseNameIdPolicy($nameIdPolicy);
Jaime Pérez Crespo
committed
if ($policy !== null) {
// either we have a policy set, or we used the transient default
$ar->setNameIdPolicy($policy);
Jaime Pérez
committed
}
$ar->setForceAuthn($spMetadata->getBoolean('ForceAuthn', false));
$ar->setIsPassive($spMetadata->getBoolean('IsPassive', false));
$protbind = $spMetadata->getValueValidate('ProtocolBinding', [
Constants::BINDING_HTTP_POST,
Constants::BINDING_HOK_SSO,
Constants::BINDING_HTTP_ARTIFACT,
Constants::BINDING_HTTP_REDIRECT,
], Constants::BINDING_HTTP_POST);
Jaime Pérez
committed
// Shoaib: setting the appropriate binding based on parameter in sp-metadata defaults to HTTP_POST
$ar->setProtocolBinding($protbind);
$issuer = new Issuer();
$issuer->setValue($spMetadata->getString('entityid'));
$ar->setIssuer($issuer);
Jaime Pérez
committed
$ar->setAssertionConsumerServiceIndex($spMetadata->getInteger('AssertionConsumerServiceIndex', null));
$ar->setAttributeConsumingServiceIndex($spMetadata->getInteger('AttributeConsumingServiceIndex', null));
if ($spMetadata->hasValue('AuthnContextClassRef')) {
$accr = $spMetadata->getArrayizeString('AuthnContextClassRef');
$comp = $spMetadata->getValueValidate('AuthnContextComparison', [
Constants::COMPARISON_EXACT,
Constants::COMPARISON_MINIMUM,
Constants::COMPARISON_MAXIMUM,
Constants::COMPARISON_BETTER,
], Constants::COMPARISON_EXACT);
$ar->setRequestedAuthnContext(['AuthnContextClassRef' => $accr, 'Comparison' => $comp]);
Jaime Pérez
committed
}
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.
Jaime Pérez
committed
* @return \SAML2\LogoutRequest A logout request object.
*/
public static function buildLogoutRequest(
Configuration $srcMetadata,
Configuration $dstMetadata
Jaime Pérez
committed
) {
$lr = new LogoutRequest();
Jaime Pérez Crespo
committed
$issuer = new Issuer();
$issuer->setValue($srcMetadata->getString('entityid'));
$issuer->setFormat(Constants::NAMEID_ENTITY);
$lr->setIssuer($issuer);
Jaime Pérez
committed
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.
Jaime Pérez
committed
* @return \SAML2\LogoutResponse A logout response object.
*/
public static function buildLogoutResponse(
Configuration $srcMetadata,
Configuration $dstMetadata
Jaime Pérez
committed
) {
$lr = new LogoutResponse();
$issuer = new Issuer();
$issuer->setValue($srcMetadata->getString('entityid'));
$issuer->setFormat(Constants::NAMEID_ENTITY);
$lr->setIssuer($issuer);
Jaime Pérez
committed
self::addRedirectSign($srcMetadata, $dstMetadata, $lr);
return $lr;
}
/**
* Process a response message.
*
* If the response is an error response, we will throw a \SimpleSAML\Module\saml\Error exception with the error.
Jaime Pérez
committed
*
* @param \SimpleSAML\Configuration $spMetadata The metadata of the service provider.
* @param \SimpleSAML\Configuration $idpMetadata The metadata of the identity provider.
Jaime Pérez
committed
* @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.
Jaime Pérez
committed
* @throws \Exception if the destination of the response does not match the current URL.
*/
public static function processResponse(
Configuration $spMetadata,
Configuration $idpMetadata,
Response $response
Jaime Pérez
committed
) {
if (!$response->isSuccess()) {
throw self::getResponseError($response);
}
// validate Response-element destination
$currentURL = Utils\HTTP::getSelfURLNoQuery();
Jaime Pérez
committed
$msgDestination = $response->getDestination();
if ($msgDestination !== null && $msgDestination !== $currentURL) {
throw new \Exception('Destination in response doesn\'t match the current URL. Destination is "'.
Jaime Pérez
committed
$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 SSP_Error\Exception('No assertions found in response from IdP.');
Jaime Pérez
committed
}
Jaime Pérez
committed
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.
Jaime Pérez
committed
* @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
Jaime Pérez
committed
* 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(
Configuration $spMetadata,
Configuration $idpMetadata,
Response $response,
Jaime Pérez
committed
$assertion,
$responseSigned
) {
assert($assertion instanceof Assertion || $assertion instanceof EncryptedAssertion);
Jaime Pérez
committed
$assertion = self::decryptAssertion($idpMetadata, $spMetadata, $assertion);
self::decryptAttributes($idpMetadata, $spMetadata, $assertion);
Jaime Pérez
committed
if (!self::checkSign($idpMetadata, $assertion)) {
if (!$responseSigned) {
throw new SSP_Error\Exception('Neither the assertion nor the response was signed.');
Jaime Pérez
committed
}
} // at least one valid signature found
$currentURL = Utils\HTTP::getSelfURLNoQuery();
Jaime Pérez
committed
// check various properties of the assertion
$config = Configuration::getInstance();
$allowed_clock_skew = $config->getInteger('assertion.allowed_clock_skew', 180);
$options = [
'options' => [
'default' => 180,
'min_range' => 180,
'max_range' => 300,
],
];
$allowed_clock_skew = filter_var($allowed_clock_skew, FILTER_VALIDATE_INT, $options);
Jaime Pérez
committed
$notBefore = $assertion->getNotBefore();
if ($notBefore !== null && $notBefore > time() + $allowed_clock_skew) {
throw new SSP_Error\Exception(
Jaime Pérez
committed
'Received an assertion that is valid in the future. Check clock synchronization on IdP and SP.'
);
}
$notOnOrAfter = $assertion->getNotOnOrAfter();
if ($notOnOrAfter !== null && $notOnOrAfter <= time() - $allowed_clock_skew) {
throw new SSP_Error\Exception(
Jaime Pérez
committed
'Received an assertion that has expired. Check clock synchronization on IdP and SP.'
);
}
$sessionNotOnOrAfter = $assertion->getSessionNotOnOrAfter();
if ($sessionNotOnOrAfter !== null && $sessionNotOnOrAfter <= time() - $allowed_clock_skew) {
throw new SSP_Error\Exception(
Jaime Pérez
committed
'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 SSP_Error\Exception('This SP ['.$spEntityId.
Jaime Pérez
committed
'] is not a valid audience for the assertion. Candidates were: '.$candidates);
}
}
$found = false;
$lastError = 'No SubjectConfirmation element in Subject.';
$validSCMethods = [Constants::CM_BEARER, Constants::CM_HOK, Constants::CM_VOUCHES];
Jaime Pérez
committed
foreach ($assertion->getSubjectConfirmation() as $sc) {
Jaime Pérez Crespo
committed
$method = $sc->getMethod();
if (!in_array($method, $validSCMethods, true)) {
$lastError = 'Invalid Method on SubjectConfirmation: '.var_export($method, true);
Jaime Pérez
committed
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 ($method === Constants::CM_BEARER && $hok) {
Jaime Pérez
committed
$lastError = 'Bearer SubjectConfirmation received, but Holder-of-Key SubjectConfirmation needed';
continue;
}
if ($method === Constants::CM_HOK && !$hok) {
Jaime Pérez
committed
$lastError = 'Holder-of-Key SubjectConfirmation received, '.
'but the Holder-of-Key profile is not enabled.';
continue;
}
Jaime Pérez Crespo
committed
$scd = $sc->getSubjectConfirmationData();
if ($method === Constants::CM_HOK) {
Jaime Pérez
committed
// check HoK Assertion
if (Utils\HTTP::isHTTPS() === false) {
Jaime Pérez
committed
$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(["\r", "\n", " "], '', $matches[1]);
Jaime Pérez
committed
if ($thing instanceof KeyInfo) {
Jaime Pérez
committed
$keyInfo[] = $thing;
}
}
if (count($keyInfo) != 1) {
$lastError = 'Error validating Holder-of-Key assertion: Only one <ds:KeyInfo> element in '.
'<SubjectConfirmationData> allowed';
continue;
}
if ($thing instanceof X509Data) {
Jaime Pérez
committed
$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;
}
if ($thing instanceof X509Certificate) {
Jaime Pérez
committed
$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;
}
Jaime Pérez
committed
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;
}
Jaime Pérez Crespo
committed
$notBefore = $scd->getNotBefore();
if (is_int($notBefore) && $notBefore > time() + 60) {
Jaime Pérez Crespo
committed
$lastError = 'NotBefore in SubjectConfirmationData is in the future: '.$notBefore;
Jaime Pérez
committed
continue;
}
Jaime Pérez Crespo
committed
$notOnOrAfter = $scd->getNotOnOrAfter();
if (is_int($notOnOrAfter) && $notOnOrAfter <= time() - 60) {
Jaime Pérez Crespo
committed
$lastError = 'NotOnOrAfter in SubjectConfirmationData is in the past: '.$notOnOrAfter;
Jaime Pérez
committed
continue;
}
Jaime Pérez Crespo
committed
$recipient = $scd->getRecipient();
if ($recipient !== null && $recipient !== $currentURL) {
Jaime Pérez
committed
$lastError = 'Recipient in SubjectConfirmationData does not match the current URL. Recipient is '.
Jaime Pérez Crespo
committed
var_export($recipient, true).', current URL is '.var_export($currentURL, true).'.';
Jaime Pérez
committed
continue;
}
Jaime Pérez Crespo
committed
$inResponseTo = $scd->getInResponseTo();
if ($inResponseTo !== null && $response->getInResponseTo() !== null &&
$inResponseTo !== $response->getInResponseTo()
Jaime Pérez
committed
) {
$lastError = 'InResponseTo in SubjectConfirmationData does not match the Response. Response has '.
var_export($response->getInResponseTo(), true).
Jaime Pérez Crespo
committed
', SubjectConfirmationData has '.var_export($inResponseTo, true).'.';
Jaime Pérez
committed
continue;
}
$found = true;
break;
}
if (!$found) {
throw new SSP_Error\Exception('Error validating SubjectConfirmation in Assertion: '.$lastError);
Jaime Pérez
committed
} // 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();
Jaime Pérez
committed
foreach ($attributes as $name => $values) {
Jaime Pérez
committed
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);
throw new SSP_Error\Exception('Error decrypting NameID: '.$e->getMessage());
Jaime Pérez
committed
}
$blacklist = self::getBlacklistedAlgorithms($idpMetadata, $spMetadata);
$lastException = null;
foreach ($keys as $i => $key) {
try {
$assertion->decryptNameId($key, $blacklist);
Logger::debug('Decryption with key #'.$i.' succeeded.');
Jaime Pérez
committed
$lastException = null;
break;
Logger::debug('Decryption with key #'.$i.' failed with exception: '.$e->getMessage());
Jaime Pérez
committed
$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.
Jaime Pérez
committed
*
* @return \RobRichards\XMLSecLibs\XMLSecurityKey The encryption key.
*
* @throws \SimpleSAML\Error\Exception if there is no supported encryption key in the metadata of this entity.
Jaime Pérez
committed
*/
public static function getEncryptionKey(Configuration $metadata)
Jaime Pérez
committed
{
$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, ['type' => 'public']);
Jaime Pérez
committed
$key->loadKey($pemKey);
return $key;
}
}
throw new SSP_Error\Exception('No supported encryption key in '.
Jaime Pérez
committed
var_export($metadata->getString('entityid'), true));
}