/*
|
* CDDL HEADER START
|
*
|
* The contents of this file are subject to the terms of the
|
* Common Development and Distribution License, Version 1.0 only
|
* (the "License"). You may not use this file except in compliance
|
* with the License.
|
*
|
* You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
|
* or http://forgerock.org/license/CDDLv1.0.html.
|
* See the License for the specific language governing permissions
|
* and limitations under the License.
|
*
|
* When distributing Covered Code, include this CDDL HEADER in each
|
* file and include the License file at legal-notices/CDDLv1_0.txt.
|
* If applicable, add the following below this CDDL HEADER, with the
|
* fields enclosed by brackets "[]" replaced with your own identifying
|
* information:
|
* Portions Copyright [yyyy] [name of copyright owner]
|
*
|
* CDDL HEADER END
|
*
|
*
|
* Copyright 2008 Sun Microsystems, Inc.
|
* Portions Copyright 2013-2015 ForgeRock AS
|
*/
|
package org.opends.server.crypto;
|
|
import java.io.File;
|
import java.io.FileInputStream;
|
import java.io.FileOutputStream;
|
import java.io.InputStream;
|
import java.io.OutputStream;
|
import java.security.MessageDigest;
|
import java.util.Arrays;
|
|
import javax.crypto.Mac;
|
import javax.naming.directory.SearchControls;
|
import javax.naming.directory.SearchResult;
|
import javax.naming.ldap.InitialLdapContext;
|
import javax.naming.ldap.LdapName;
|
|
import org.forgerock.i18n.LocalizableMessage;
|
import org.forgerock.opendj.ldap.SearchScope;
|
import org.opends.admin.ads.ADSContext;
|
import org.opends.admin.ads.util.ConnectionUtils;
|
import org.opends.server.TestCaseUtils;
|
import org.opends.server.core.DirectoryServer;
|
import org.opends.server.protocols.internal.InternalSearchOperation;
|
import org.opends.server.protocols.internal.SearchRequest;
|
import org.opends.server.types.CryptoManager;
|
import org.opends.server.types.CryptoManagerException;
|
import org.opends.server.types.DN;
|
import org.opends.server.types.Entry;
|
import org.opends.server.util.EmbeddedUtils;
|
import org.opends.server.util.StaticUtils;
|
import org.opends.server.util.TimeThread;
|
import org.testng.annotations.AfterClass;
|
import org.testng.annotations.BeforeClass;
|
import org.testng.annotations.DataProvider;
|
import org.testng.annotations.Test;
|
|
import com.forgerock.opendj.cli.CliConstants;
|
|
import static org.opends.server.config.ConfigConstants.*;
|
import static org.opends.server.protocols.internal.InternalClientConnection.*;
|
import static org.opends.server.protocols.internal.Requests.*;
|
import static org.testng.Assert.*;
|
|
/**
|
This class tests the CryptoManager.
|
*/
|
@SuppressWarnings("javadoc")
|
public class CryptoManagerTestCase extends CryptoTestCase {
|
|
@BeforeClass
|
public void setUp()
|
throws Exception {
|
TestCaseUtils.startServer();
|
}
|
|
@AfterClass
|
public void CleanUp() throws Exception {
|
// Removes at least secret keys added in this test case.
|
TestCaseUtils.restartServer();
|
}
|
|
|
@Test
|
public void testGetInstanceKeyCertificate()
|
throws Exception {
|
final CryptoManagerImpl cm = DirectoryServer.getCryptoManager();
|
final byte[] cert
|
= CryptoManagerImpl.getInstanceKeyCertificateFromLocalTruststore();
|
assertNotNull(cert);
|
|
// The certificate should now be accessible in the truststore backend via LDAP.
|
final InitialLdapContext ctx = ConnectionUtils.createLdapsContext(
|
"ldaps://" + "127.0.0.1" + ":"
|
+ String.valueOf(TestCaseUtils.getServerAdminPort()),
|
"cn=Directory Manager", "password",
|
CliConstants.DEFAULT_LDAP_CONNECT_TIMEOUT, null, null, null);
|
// TODO: should the below dn be in ConfigConstants?
|
final String dnStr = "ds-cfg-key-id=ads-certificate,cn=ads-truststore";
|
final LdapName dn = new LdapName(dnStr);
|
final SearchControls searchControls = new SearchControls();
|
searchControls.setSearchScope(SearchControls.OBJECT_SCOPE);
|
final String attrIDs[] = { "ds-cfg-public-key-certificate;binary" };
|
searchControls.setReturningAttributes(attrIDs);
|
final SearchResult certEntry = ctx.search(dn,
|
"(objectclass=ds-cfg-instance-key)", searchControls).next();
|
final javax.naming.directory.Attribute certAttr
|
= certEntry.getAttributes().get(attrIDs[0]);
|
/* attribute ds-cfg-public-key-certificate is a MUST in the schema */
|
assertNotNull(certAttr);
|
byte[] ldapCert = (byte[])certAttr.get();
|
// Compare the certificate values.
|
assertTrue(Arrays.equals(ldapCert, cert));
|
|
// Compare the MD5 hash of the LDAP attribute with the one
|
// retrieved from the CryptoManager.
|
MessageDigest md = MessageDigest.getInstance("MD5");
|
String actual = StaticUtils.bytesToHexNoSpace(md.digest(ldapCert));
|
assertEquals(actual, cm.getInstanceKeyID());
|
|
// Call twice to ensure idempotent.
|
CryptoManagerImpl.publishInstanceKeyEntryInADS();
|
CryptoManagerImpl.publishInstanceKeyEntryInADS();
|
}
|
|
@Test
|
public void testMacSuccess()
|
throws Exception {
|
final CryptoManager cm = DirectoryServer.getCryptoManager();
|
final String text = "1234";
|
|
final String macKeyID = cm.getMacEngineKeyEntryID();
|
|
final Mac signingMac = cm.getMacEngine(macKeyID);
|
final byte[] signedHash = signingMac.doFinal(text.getBytes());
|
|
final Mac validatingMac = cm.getMacEngine(macKeyID);
|
final byte[] calculatedSignature = validatingMac.doFinal(text.getBytes());
|
|
assertTrue(Arrays.equals(calculatedSignature, signedHash));
|
}
|
|
// TODO: other-than-default MAC
|
|
private class CipherParameters {
|
private final String fAlgorithm;
|
private final String fMode;
|
private final String fPadding;
|
private final int fKeyLength;
|
|
public CipherParameters(final String algorithm, final String mode,
|
final String padding, final int keyLength) {
|
fAlgorithm = algorithm;
|
fMode = mode;
|
fPadding = padding;
|
fKeyLength = keyLength;
|
}
|
|
public String getTransformation() {
|
if (null != fAlgorithm)
|
{
|
return fMode != null
|
? fAlgorithm + "/" + fMode + "/" + fPadding
|
: fAlgorithm;
|
}
|
return null;
|
}
|
|
public int getKeyLength() {
|
return fKeyLength;
|
}
|
}
|
|
|
/**
|
Cipher parameter data set.
|
|
@return The set of Cipher parameters with which to test.
|
*/
|
@DataProvider(name = "cipherParametersData")
|
public Object[][] cipherParametersData() {
|
return new Object[][] {
|
// default (preferred) AES/CBC/PKCS5Padding 128bit key.
|
{ new CipherParameters(null, null, null, 128) },
|
// custom
|
// TODO: https://opends.dev.java.net/issues/show_bug.cgi?id=2448
|
// TODO: { new CipherParameters("Blowfish", "CFB", "NoPadding", 448) },
|
// TODO: { new CipherParameters("AES", "CBC", "PKCS5Padding", 256) },
|
{ new CipherParameters("AES", "CFB", "NoPadding", 128) },
|
{ new CipherParameters("Blowfish", "CFB", "NoPadding", 128) },
|
{ new CipherParameters("RC4", null, null, 104) },
|
{ new CipherParameters("RC4", "NONE", "NoPadding", 128) },
|
{ new CipherParameters("DES", "CFB", "NoPadding", 56) },
|
{ new CipherParameters("DESede", "ECB", "PKCS5Padding", 168) },
|
};
|
}
|
|
|
/**
|
Tests a simple encryption-decryption cycle using the supplied cipher
|
parameters.
|
|
@param cp Cipher parameters to use for this test iteration.
|
|
@throws Exception If an exceptional condition arises.
|
*/
|
@Test(dataProvider="cipherParametersData")
|
public void testEncryptDecryptSuccess(CipherParameters cp)
|
throws Exception {
|
final CryptoManager cm = DirectoryServer.getCryptoManager();
|
final String secretMessage = "1234";
|
|
final byte[] cipherText = (null == cp.getTransformation())
|
? cm.encrypt(secretMessage.getBytes()) // default
|
: cm.encrypt(cp.getTransformation(), cp.getKeyLength(),
|
secretMessage.getBytes());
|
assertEquals(-1, new String(cipherText).indexOf(secretMessage));
|
|
final byte[] plainText = cm.decrypt(cipherText);
|
assertEquals(new String(plainText), secretMessage);
|
}
|
|
|
/**
|
Tests a simple cipher stream encryption-decryption cycle using the supplied
|
cipher parameters.
|
|
@param cp Cipher parameters to use for this test iteration.
|
|
@throws Exception If an exceptional condition arises.
|
*/
|
@Test(dataProvider="cipherParametersData")
|
public void testStreamEncryptDecryptSuccess(CipherParameters cp)
|
throws Exception {
|
final CryptoManagerImpl cm = DirectoryServer.getCryptoManager();
|
final String secretMessage = "56789";
|
|
final File tempFile
|
= File.createTempFile(cm.getClass().getName(), null);
|
tempFile.deleteOnExit();
|
|
OutputStream os = new FileOutputStream(tempFile);
|
os = (null == cp.getTransformation())
|
? cm.getCipherOutputStream(os) // default
|
: cm.getCipherOutputStream(cp.getTransformation(), cp.getKeyLength(), os);
|
os.write(secretMessage.getBytes());
|
os.close();
|
|
// TODO: check tempfile for plaintext.
|
|
InputStream is = new FileInputStream(tempFile);
|
is = cm.getCipherInputStream(is);
|
byte[] plainText = new byte[secretMessage.getBytes().length];
|
assertEquals(is.read(plainText), secretMessage.getBytes().length);
|
assertEquals(is.read(), -1);
|
is.close();
|
assertEquals(new String(plainText), secretMessage);
|
}
|
|
/**
|
Tests to ensure the same key identifier (and hence, key) is used for
|
successive encryptions specifying the same algorithm and key length.
|
<p>
|
The default encryption cipher requires an initialization vector. Confirm
|
successive uses of a key produces distinct ciphertext.
|
|
@throws Exception In case an error occurs in the encryption routine.
|
*/
|
@Test
|
public void testKeyEntryReuse()
|
throws Exception {
|
|
final CryptoManager cm = DirectoryServer.getCryptoManager();
|
final String secretMessage = "zyxwvutsrqponmlkjihgfedcba";
|
|
final byte[] cipherText = cm.encrypt(secretMessage.getBytes());
|
final byte[] cipherText2 = cm.encrypt(secretMessage.getBytes());
|
|
// test cycle
|
final byte[] plainText = cm.decrypt(cipherText2);
|
assertEquals(new String(plainText), secretMessage);
|
|
// test for identical keys
|
final byte[] keyID = Arrays.copyOfRange(cipherText, 1, 16);
|
final byte[] keyID2 = Arrays.copyOfRange(cipherText2, 1, 16);
|
assertTrue(Arrays.equals(keyID, keyID2));
|
|
// test for distinct ciphertext
|
assertFalse(Arrays.equals(cipherText, cipherText2));
|
}
|
|
|
/**
|
Test that secret keys are persisted: Encrypt some data using a
|
variety of transformations, restart the instance, and decrypt the
|
retained ciphertext.
|
|
@throws Exception In case an error occurs in the encryption routine.
|
*/
|
@Test
|
public void testKeyPersistence()
|
throws Exception {
|
final CryptoManager cm = DirectoryServer.getCryptoManager();
|
final String secretMessage = "zyxwvutsrqponmlkjihgfedcba";
|
|
final byte[] cipherText = cm.encrypt("Blowfish/CFB/NoPadding", 128,
|
secretMessage.getBytes());
|
final byte[] cipherText2 = cm.encrypt("RC4", 104,
|
secretMessage.getBytes());
|
|
EmbeddedUtils.restartServer(
|
this.getClass().getName(),
|
LocalizableMessage.raw("CryptoManager: testing persistent secret keys."),
|
DirectoryServer.getEnvironmentConfig());
|
|
byte[] plainText = cm.decrypt(cipherText);
|
assertEquals(new String(plainText), secretMessage);
|
plainText = cm.decrypt(cipherText2);
|
assertEquals(new String(plainText), secretMessage);
|
}
|
|
|
/**
|
Mark a key compromised; ensure 1) subsequent encryption requests use a
|
new key; 2) ciphertext produced using the compromised key can still be
|
decrypted; 3) once the compromised key entry is removed, confirm ciphertext
|
produced using the compromised key can no longer be decrypted.
|
|
@throws Exception In case something exceptional happens.
|
*/
|
@Test
|
public void testCompromisedKey() throws Exception {
|
final CryptoManager cm = DirectoryServer.getCryptoManager();
|
final String secretMessage = "zyxwvutsrqponmlkjihgfedcba";
|
final String cipherTransformationName = "AES/CBC/PKCS5Padding";
|
final int cipherKeyLength = 128;
|
|
// Initial encryption ensures a cipher key entry is in ADS.
|
final byte[] cipherText = cm.encrypt(cipherTransformationName,
|
cipherKeyLength, secretMessage.getBytes());
|
|
// Retrieve all uncompromised cipher key entries corresponding to the
|
// specified transformation and key length. Mark each entry compromised.
|
final String baseDNStr // TODO: is this DN defined elsewhere as a constant?
|
= "cn=secret keys," + ADSContext.getAdministrationSuffixDN();
|
final DN baseDN = DN.valueOf(baseDNStr);
|
final String FILTER_OC_INSTANCE_KEY = "(objectclass=" + OC_CRYPTO_CIPHER_KEY + ")";
|
final String FILTER_NOT_COMPROMISED =
|
"(!(" + ATTR_CRYPTO_KEY_COMPROMISED_TIME + "=*))";
|
final String FILTER_CIPHER_TRANSFORMATION_NAME =
|
"(" + ATTR_CRYPTO_CIPHER_TRANSFORMATION_NAME + "=" + cipherTransformationName + ")";
|
final String FILTER_CIPHER_KEY_LENGTH =
|
"(" + ATTR_CRYPTO_KEY_LENGTH_BITS + "=" + cipherKeyLength + ")";
|
final String searchFilter =
|
"(&" + FILTER_OC_INSTANCE_KEY + FILTER_NOT_COMPROMISED + FILTER_CIPHER_TRANSFORMATION_NAME + FILTER_CIPHER_KEY_LENGTH + ")";
|
final SearchRequest request = newSearchRequest(baseDN, SearchScope.SINGLE_LEVEL, searchFilter).addAttribute("dn");
|
InternalSearchOperation searchOp = getRootConnection().processSearch(request);
|
assertFalse(searchOp.getSearchEntries().isEmpty());
|
|
String compromisedTime = TimeThread.getGeneralizedTime();
|
for (Entry e : searchOp.getSearchEntries()) {
|
TestCaseUtils.applyModifications(true,
|
"dn: " + e.getName(),
|
"changetype: modify",
|
"replace: " + ATTR_CRYPTO_KEY_COMPROMISED_TIME,
|
ATTR_CRYPTO_KEY_COMPROMISED_TIME + ": " + compromisedTime);
|
}
|
//Wait so the above asynchronous modification can be applied. The crypto
|
//manager's cipherKeyEntryCache needs to be updated before the encrypt()
|
//method is called below.
|
Thread.sleep(1000);
|
// Use the transformation and key length again. A new cipher key
|
// should be produced.
|
final byte[] cipherText2 = cm.encrypt(cipherTransformationName,
|
cipherKeyLength, secretMessage.getBytes());
|
|
// 1. Test for distinct keys.
|
final byte[] keyID = new byte[16];
|
final byte[] keyID2 = new byte[16];
|
System.arraycopy(cipherText, 1, keyID, 0, 16);
|
System.arraycopy(cipherText2, 1, keyID2, 0, 16);
|
assertFalse(Arrays.equals(keyID, keyID2));
|
|
// 2. Confirm ciphertext produced using the compromised key can still be decrypted.
|
final byte[] plainText = cm.decrypt(cipherText);
|
assertEquals(new String(plainText), secretMessage);
|
|
// 3. Delete the compromised entry(ies) and ensure ciphertext produced
|
// using a compromised key can no longer be decrypted.
|
for (Entry e : searchOp.getSearchEntries()) {
|
TestCaseUtils.applyModifications(true, "dn: " + e.getName(), "changetype: delete");
|
}
|
Thread.sleep(1000); // Clearing the cache is asynchronous.
|
try {
|
cm.decrypt(cipherText);
|
}
|
catch (CryptoManagerException ex) {
|
// TODO: if reasons are added to CryptoManagerException, check for
|
// expected cause.
|
}
|
}
|
|
/**
|
TODO: Test shared secret key wrapping (various wrapping ciphers, if configurable).
|
*/
|
|
|
/**
|
TODO: Test the secret key synchronization protocol.
|
|
1. Create the first instance; add reversible password storage scheme
|
to password policy; add entry using explicit password policy; confirm
|
secret key entry has been produced.
|
|
2. Create and initialize the second instance into the existing ADS domain.
|
The secret key entries should be propagated to the second instance via
|
replication. Then the new instance should detect that the secret key
|
entries are missing ds-cfg-symmetric-key attribute values for that
|
instance, inducing the key synchronization protocol.
|
|
3. Confirm the second instance can decrypt the password of the entry
|
added in step 1; e.g., bind as that user.
|
|
4. Stop the second instance. At the first instance, enable a different
|
reversible password storage scheme (different cipher transformation,
|
and hence secret key entry); add another entry using that password
|
storage scheme; start the second instance; ensure the password can
|
be decrypted at the second instance.
|
*/
|
}
|