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