From 1b2b06702002fb279e603163e9984cbf4569e029 Mon Sep 17 00:00:00 2001
From: Olav Morken <olav.morken@uninett.no>
Date: Tue, 2 Nov 2010 11:20:57 +0000
Subject: [PATCH] saml: Add support for key rollover.

git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@2617 44740490-163a-0410-bde0-09ae8108e29a
---
 docs/index.txt                       |  1 +
 modules/saml/docs/keyrollover.txt    | 68 +++++++++++++++++++++++++++
 modules/saml/lib/Message.php         | 69 ++++++++++++++++++++++------
 modules/saml/www/sp/metadata.php     | 44 ++++++++++++++++--
 modules/saml/www/sp/saml2-logout.php | 16 ++++++-
 www/saml2/idp/metadata.php           | 49 ++++++++++++++------
 www/shib13/idp/metadata.php          | 30 +++++++++---
 7 files changed, 237 insertions(+), 40 deletions(-)
 create mode 100644 modules/saml/docs/keyrollover.txt

diff --git a/docs/index.txt b/docs/index.txt
index d6227541c..67f79741c 100644
--- a/docs/index.txt
+++ b/docs/index.txt
@@ -27,6 +27,7 @@ SimpleSAMLphp Documentation
  * [Theming simpleSAMLphp](simplesamlphp-theming)
  * [simpleSAMLphp Modules](simplesamlphp-modules) - how to create own customized modules
  * [Installing third party modules with the pack.php tool](pack)
+ * [Key rollover](./saml:keyrollover)
 
 Documentation on specific simpleSAMLphp modules:
  
diff --git a/modules/saml/docs/keyrollover.txt b/modules/saml/docs/keyrollover.txt
new file mode 100644
index 000000000..a6cb9f147
--- /dev/null
+++ b/modules/saml/docs/keyrollover.txt
@@ -0,0 +1,68 @@
+Key rollover with simpleSAMLphp
+===============================
+
+This document gives a quick guide to doing key rollover with a simpleSAMLphp service provider or identity provider.
+
+
+1. Create the new key and certificate
+-------------------------------------
+
+First you must create the new key that you are going to use.
+To create a self signed certificate, you may use the following command:
+
+    cd cert
+    openssl req -new -x509 -days 3652 -nodes -out new.crt -keyout new.pem
+
+
+2. Add the new key to simpleSAMLphp
+-----------------------------------
+
+Where you add the new key depends on whether you are doing key rollover for a service provider or an identity provider.
+If you are doing key rollover for a service provider, the new key must be added to `config/authsources.php`.
+To do key rollover for an identity provider, you must add the new key to `metadata/saml20-idp-hosted.php` and/or `metadata/shib13-idp-hosted.php`.
+
+The new certificate and key is added to the configuration with the prefix `new_`:
+
+Example:
+
+    'default-sp' => array(
+        'saml:SP',
+        'privatekey' => 'old.pem',
+        'certificate' => 'old.crt',
+        'new_privatekey' => 'new.pem',
+        'new_certificate' => 'new.crt',
+    ),
+
+When the new key is added, simpleSAMLphp will attempt to use both the new key and the old key for decryption of messages, but only the old key will be used for signing messages.
+The metadata will be updated to list the new key for signing and encryption, and the old key will no longer listed as available for encryption.
+This ensures that both those entities that use your old metadata and those that use your new metadata will be able to send and receive messages from you.
+
+
+3. Distribute your new metadata
+-------------------------------
+
+Now, you need to make sure that all your peers are using your new metadata.
+How you go about this depends on how your peers have added your metadata.
+
+
+4. Remove the old key
+---------------------
+
+Once you are certain that all your peers are using the new metadata, you must remove the old key.
+Replace the existing `privatekey` and `certificate` options in your configuration with the `new_privatekey` and `new_certificate` options.
+
+Example:
+
+    'default-sp' => array(
+        'saml:SP',
+        'privatekey' => 'new.pem',
+        'certificate' => 'new.crt',
+    ),
+
+This will cause your old key to be removed from your metadata.
+
+
+5. Distribute your final metadata
+---------------------------------
+
+Now you need to update the metadata of all your peers again, so that your old signing certificate is removed.
diff --git a/modules/saml/lib/Message.php b/modules/saml/lib/Message.php
index 547a9f5f7..5552ce09b 100644
--- a/modules/saml/lib/Message.php
+++ b/modules/saml/lib/Message.php
@@ -260,35 +260,49 @@ class sspmod_saml_Message {
 
 
 	/**
-	 * Retrieve the decryption key from metadata.
+	 * 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 XMLSecurityKey  The decryption key.
+	 * @return array  Array of decryption keys.
 	 */
-	public static function getDecryptionKey(SimpleSAML_Configuration $srcMetadata,
+	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);
-		} else {
-			/* Find the private key we should use to decrypt messages to this SP. */
-			$keyArray = SimpleSAML_Utilities::loadPrivateKey($dstMetadata, TRUE);
-			if (!array_key_exists('PEM', $keyArray)) {
-				throw new Exception('Unable to locate key we should use to decrypt the message.');
-			}
+			return array($key);
+		}
+
+		$keys = array();
+
+		/* Load the new private key if it exists. */
+		$keyArray = SimpleSAML_Utilities::loadPrivateKey($dstMetadata, FALSE, 'new_');
+		if ($keyArray !== NULL) {
+			assert('isset($keyArray["PEM"])');
 
-			/* Extract the public key from the certificate for encryption. */
 			$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 $key;
+		/* Find the existing private key. */
+		$keyArray = SimpleSAML_Utilities::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;
 	}
 
 
@@ -322,12 +336,23 @@ class sspmod_saml_Message {
 		}
 
 		try {
-			$key = self::getDecryptionKey($srcMetadata, $dstMetadata);
+			$keys = self::getDecryptionKeys($srcMetadata, $dstMetadata);
 		} catch (Exception $e) {
 			throw new SimpleSAML_Error_Exception('Error decrypting assertion: ' . $e->getMessage());
 		}
 
-		return $assertion->getAssertion($key);
+		$lastException = NULL;
+		foreach ($keys as $i => $key) {
+			try {
+				$ret = $assertion->getAssertion($key);
+				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;
 	}
 
 
@@ -605,12 +630,26 @@ class sspmod_saml_Message {
 		/* Decrypt the NameID element if it is encrypted. */
 		if ($assertion->isNameIdEncrypted()) {
 			try {
-				$key = self::getDecryptionKey($idpMetadata, $spMetadata);
+				$key = self::getDecryptionKeys($idpMetadata, $spMetadata);
 			} catch (Exception $e) {
 				throw new SimpleSAML_Error_Exception('Error decrypting NameID: ' . $e->getMessage());
 			}
 
-			$assertion->decryptNameId($key);
+			$lastException = NULL;
+			foreach ($keys as $i => $key) {
+				try {
+					$assertion->decryptNameId($key);
+					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;
diff --git a/modules/saml/www/sp/metadata.php b/modules/saml/www/sp/metadata.php
index 1343c0cfe..3729d4c7b 100644
--- a/modules/saml/www/sp/metadata.php
+++ b/modules/saml/www/sp/metadata.php
@@ -72,8 +72,11 @@ $acs->Binding = 'urn:oasis:names:tc:SAML:1.0:profiles:artifact-01';
 $acs->Location = SimpleSAML_Module::getModuleURL('saml/sp/saml1-acs.php/' . $sourceId . '/artifact');
 $sp->AssertionConsumerService[] = $acs;
 
-$certInfo = SimpleSAML_Utilities::loadPublicKey($spconfig);
+$keys = array();
+$certInfo = SimpleSAML_Utilities::loadPublicKey($spconfig, FALSE, 'new_');
 if ($certInfo !== NULL && array_key_exists('certData', $certInfo)) {
+	$hasNewCert = TRUE;
+
 	$certData = $certInfo['certData'];
 	$kd = SAML2_Utils::createKeyDescriptor($certData);
 	$kd->use = 'signing';
@@ -82,6 +85,39 @@ if ($certInfo !== NULL && array_key_exists('certData', $certInfo)) {
 	$kd = SAML2_Utils::createKeyDescriptor($certData);
 	$kd->use = 'encryption';
 	$sp->KeyDescriptor[] = $kd;
+
+	$keys[] = array(
+		'type' => 'X509Certificate',
+		'signing' => TRUE,
+		'encryption' => TRUE,
+		'X509Certificate' => $certInfo['certData'],
+	);
+
+} else {
+	$hasNewCert = FALSE;
+}
+
+$certInfo = SimpleSAML_Utilities::loadPublicKey($spconfig);
+if ($certInfo !== NULL && array_key_exists('certData', $certInfo)) {
+	$certData = $certInfo['certData'];
+	$kd = SAML2_Utils::createKeyDescriptor($certData);
+	$kd->use = 'signing';
+	$sp->KeyDescriptor[] = $kd;
+
+	if (!$hasNewCert) {
+		/* Don't include the old certificate for encryption when we have a newer certificate. */
+		$kd = SAML2_Utils::createKeyDescriptor($certData);
+		$kd->use = 'encryption';
+		$sp->KeyDescriptor[] = $kd;
+	}
+
+	$keys[] = array(
+		'type' => 'X509Certificate',
+		'signing' => TRUE,
+		'encryption' => ($hasNewCert ? FALSE : TRUE),
+		'X509Certificate' => $certInfo['certData'],
+	);
+
 } else {
 	$certData = NULL;
 }
@@ -168,8 +204,10 @@ $xml = $ed->toXML();
 SimpleSAML_Utilities::formatDOMElement($xml);
 $xml = $xml->ownerDocument->saveXML($xml);
 
-if ($certData !== NULL) {
-	$metaArray20['certData'] = $certData;
+if (count($keys) === 1) {
+	$metaArray20['certData'] = $keys[0]['X509Certificate'];
+} elseif (count($keys) > 1) {
+	$metaArray20['keys'] = $keys;
 }
 
 if (array_key_exists('output', $_REQUEST) && $_REQUEST['output'] == 'xhtml') {
diff --git a/modules/saml/www/sp/saml2-logout.php b/modules/saml/www/sp/saml2-logout.php
index 85e42f588..40197282c 100644
--- a/modules/saml/www/sp/saml2-logout.php
+++ b/modules/saml/www/sp/saml2-logout.php
@@ -64,11 +64,23 @@ if ($message instanceof SAML2_LogoutResponse) {
 
 	if ($message->isNameIdEncrypted()) {
 		try {
-			$key = sspmod_saml_Message::getDecryptionKey($idpMetadata, $spMetadata);
+			$keys = sspmod_saml_Message::getDecryptionKeys($srcMetadata, $dstMetadata);
 		} catch (Exception $e) {
 			throw new SimpleSAML_Error_Exception('Error decrypting NameID: ' . $e->getMessage());
 		}
-		$message->decryptNameId($key);
+
+		$lastException = NULL;
+		foreach ($keys as $i => $key) {
+			try {
+				$ret = $assertion->getAssertion($key);
+				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;
 	}
 
 	$nameId = $message->getNameId();
diff --git a/www/saml2/idp/metadata.php b/www/saml2/idp/metadata.php
index 216b3e7a3..2d3360b75 100644
--- a/www/saml2/idp/metadata.php
+++ b/www/saml2/idp/metadata.php
@@ -19,11 +19,37 @@ try {
 	$idpentityid = isset($_GET['idpentityid']) ? $_GET['idpentityid'] : $metadata->getMetaDataCurrentEntityID('saml20-idp-hosted');
 	$idpmeta = $metadata->getMetaDataConfig($idpentityid, 'saml20-idp-hosted');
 
+	$keys = array();
+	$certInfo = SimpleSAML_Utilities::loadPublicKey($idpmeta, FALSE, 'new_');
+	if ($certInfo !== NULL) {
+		$keys[] = array(
+			'type' => 'X509Certificate',
+			'signing' => TRUE,
+			'encryption' => TRUE,
+			'X509Certificate' => $certInfo['certData'],
+		);
+		$hasNewCert = TRUE;
+	} else {
+		$hasNewCert = FALSE;
+	}
+
 	$certInfo = SimpleSAML_Utilities::loadPublicKey($idpmeta, TRUE);
-	$certFingerprint = $certInfo['certFingerprint'];
-	if (count($certFingerprint) === 1) {
-		/* Only one valid certificate. */
-		$certFingerprint = $certFingerprint[0];
+	$keys[] = array(
+		'type' => 'X509Certificate',
+		'signing' => TRUE,
+		'encryption' => ($hasNewCert ? FALSE : TRUE),
+		'X509Certificate' => $certInfo['certData'],
+	);
+
+	if ($idpmeta->hasValue('https.certificate')) {
+		$httpsCert = SimpleSAML_Utilities::loadPublicKey($idpmeta, TRUE, 'https.');
+		assert('isset($httpsCert["certData"])');
+		$keys[] = array(
+			'type' => 'X509Certificate',
+			'signing' => TRUE,
+			'encryption' => FALSE,
+			'X509Certificate' => $httpsCert['certData'],
+		);
 	}
 
 	$metaArray = array(
@@ -31,9 +57,14 @@ try {
 		'entityid' => $idpentityid,
 		'SingleSignOnService' => $metadata->getGenerated('SingleSignOnService', 'saml20-idp-hosted'),
 		'SingleLogoutService' => $metadata->getGenerated('SingleLogoutService', 'saml20-idp-hosted'),
-		'certFingerprint' => $certFingerprint,
 	);
 
+	if (count($keys) === 1) {
+		$metaArray['certData'] = $keys[0]['X509Certificate'];
+	} else {
+		$metaArray['keys'] = $keys;
+	}
+
 	if ($idpmeta->getBoolean('saml20.sendartifact', FALSE)) {
 		/* Artifact sending enabled. */
 		$metaArray['ArtifactResolutionService'][] = array(
@@ -59,16 +90,8 @@ try {
 		$metaArray['scope'] = $idpmeta->getArray('scope');
 	}
 
-	if ($idpmeta->hasValue('https.certificate')) {
-		$httpsCert = SimpleSAML_Utilities::loadPublicKey($idpmeta, TRUE, 'https.');
-		assert('isset($httpsCert["certData"])');
-		$metaArray['https.certData'] = $httpsCert['certData'];
-	}
-
-
 	$metaflat = '$metadata[' . var_export($idpentityid, TRUE) . '] = ' . var_export($metaArray, TRUE) . ';';
 
-	$metaArray['certData'] = $certInfo['certData'];
 	$metaBuilder = new SimpleSAML_Metadata_SAMLBuilder($idpentityid);
 	$metaBuilder->addMetadataIdP20($metaArray);
 	$metaBuilder->addOrganizationInfo($metaArray);
diff --git a/www/shib13/idp/metadata.php b/www/shib13/idp/metadata.php
index 27a97b427..40bf49619 100644
--- a/www/shib13/idp/metadata.php
+++ b/www/shib13/idp/metadata.php
@@ -20,20 +20,37 @@ try {
 	$idpentityid = isset($_GET['idpentityid']) ? $_GET['idpentityid'] : $metadata->getMetaDataCurrentEntityID('shib13-idp-hosted');
 	$idpmeta = $metadata->getMetaDataConfig($idpentityid, 'shib13-idp-hosted');
 
-	$certInfo = SimpleSAML_Utilities::loadPublicKey($idpmeta, TRUE);
-	$certFingerprint = $certInfo['certFingerprint'];
-	if (count($certFingerprint) === 1) {
-		/* Only one valid certificate. */
-		$certFingerprint = $certFingerprint[0];
+	$keys = array();
+	$certInfo = SimpleSAML_Utilities::loadPublicKey($idpmeta, FALSE, 'new_');
+	if ($certInfo !== NULL) {
+		$keys[] = array(
+			'type' => 'X509Certificate',
+			'signing' => TRUE,
+			'encryption' => FALSE,
+			'X509Certificate' => $certInfo['certData'],
+		);
 	}
 
+	$certInfo = SimpleSAML_Utilities::loadPublicKey($idpmeta, TRUE);
+	$keys[] = array(
+		'type' => 'X509Certificate',
+		'signing' => TRUE,
+		'encryption' => FALSE,
+		'X509Certificate' => $certInfo['certData'],
+	);
+
 	$metaArray = array(
 		'metadata-set' => 'shib13-idp-remote',
 		'entityid' => $idpentityid,
 		'SingleSignOnService' => $metadata->getGenerated('SingleSignOnService', 'shib13-idp-hosted'),
-		'certFingerprint' => $certFingerprint,
 	);
 
+	if (count($keys) === 1) {
+		$metaArray['certData'] = $keys[0]['X509Certificate'];
+	} else {
+		$metaArray['keys'] = $keys;
+	}
+
 	$metaArray['NameIDFormat'] = $idpmeta->getString('NameIDFormat', 'urn:mace:shibboleth:1.0:nameIdentifier');
 
 	if ($idpmeta->hasValue('OrganizationName')) {
@@ -49,7 +66,6 @@ try {
 
 	$metaflat = '$metadata[' . var_export($idpentityid, TRUE) . '] = ' . var_export($metaArray, TRUE) . ';';
 	
-	$metaArray['certData'] = $certInfo['certData'];
 	$metaBuilder = new SimpleSAML_Metadata_SAMLBuilder($idpentityid);
 	$metaBuilder->addMetadataIdP11($metaArray);
 	$metaBuilder->addOrganizationInfo($metaArray);
-- 
GitLab