From 416f8e9da4a1f99064706c23707ab2241b1bf61c Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Mon, 10 Oct 2016 00:36:08 +0000
Subject: [PATCH] OPENDJ-2877: implement LDAP security provider and key store
---
opendj-core/src/test/java/org/forgerock/opendj/security/KeyStoreImplTest.java | 340 ++++++
opendj-core/src/test/java/org/forgerock/opendj/security/KeyProtectorTest.java | 202 +++
opendj-core/src/main/java/org/forgerock/opendj/security/package-info.java | 111 ++
opendj-core/src/test/java/org/forgerock/opendj/security/KeyStoreObjectTest.java | 143 ++
opendj-core/src/main/java/org/forgerock/opendj/security/KeyStoreObject.java | 315 +++++
opendj-core/src/main/java/org/forgerock/opendj/security/KeyStoreImpl.java | 315 +++++
opendj-core/src/main/java/org/forgerock/opendj/security/OpenDJProviderSchema.java | 129 ++
opendj-core/src/main/resources/com/forgerock/opendj/security/keystore.properties | 38
opendj-core/src/test/java/org/forgerock/opendj/security/KeyStoreTestUtils.java | 145 ++
opendj-core/src/test/java/org/forgerock/opendj/security/OpenDJProviderSchemaTest.java | 53 +
opendj-core/src/main/java/org/forgerock/opendj/security/KeyStoreParameters.java | 142 ++
opendj-core/src/main/java/org/forgerock/opendj/security/KeyProtector.java | 331 ++++++
opendj-core/src/main/resources/org/forgerock/opendj/security/03-keystore.ldif | 81 +
opendj-core/src/main/java/org/forgerock/opendj/security/LocalizedKeyStoreException.java | 43
opendj-core/src/main/java/org/forgerock/opendj/security/ExternalKeyWrappingStrategy.java | 49
opendj-core/src/main/java/org/forgerock/opendj/security/KeyStoreObjectCache.java | 57 +
opendj-server-legacy/resource/schema/03-keystore.ldif | 81 +
opendj-core/src/test/resources/org/forgerock/opendj/security/opendj-provider.conf | 6
opendj-core/src/test/java/org/forgerock/opendj/security/OpenDJProviderTest.java | 122 ++
opendj-core/pom.xml | 1
opendj-core/src/main/java/org/forgerock/opendj/security/OpenDJProvider.java | 437 ++++++++
21 files changed, 3,141 insertions(+), 0 deletions(-)
diff --git a/opendj-core/pom.xml b/opendj-core/pom.xml
index 5f24bfc..ba438c5 100644
--- a/opendj-core/pom.xml
+++ b/opendj-core/pom.xml
@@ -89,6 +89,7 @@
<configuration>
<messageFiles>
<messageFile>com/forgerock/opendj/ldap/core.properties</messageFile>
+ <messageFile>com/forgerock/opendj/security/keystore.properties</messageFile>
</messageFiles>
</configuration>
</execution>
diff --git a/opendj-core/src/main/java/org/forgerock/opendj/security/ExternalKeyWrappingStrategy.java b/opendj-core/src/main/java/org/forgerock/opendj/security/ExternalKeyWrappingStrategy.java
new file mode 100644
index 0000000..a2253f7
--- /dev/null
+++ b/opendj-core/src/main/java/org/forgerock/opendj/security/ExternalKeyWrappingStrategy.java
@@ -0,0 +1,49 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.security;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * A service provider interface for externalizing the strategy used for wrapping individual private/secret keys.
+ * Applications can configure an LDAP keystore to delegate key wrapping responsibilities by setting the
+ * {@link KeyStoreParameters#EXTERNAL_KEY_WRAPPING_STRATEGY} option.
+ */
+public interface ExternalKeyWrappingStrategy {
+ /**
+ * Wraps the provided encoded key.
+ *
+ * @param unwrappedKey
+ * The non-{@code null} key to be wrapped. The format of the unwrapped key is unspecified.
+ * @return The non-{@code null} protected key. The format of the returned wrapped key is implementation defined.
+ * @throws LocalizedKeyStoreException
+ * If an unexpected problem occurred when wrapping the key.
+ */
+ ByteSequence wrapKey(ByteSequence unwrappedKey) throws LocalizedKeyStoreException;
+
+ /**
+ * Unwraps the provided {@link #wrapKey(ByteSequence) wrapped} key.
+ *
+ * @param wrappedKey
+ * The non-{@code null} key to be unwrapped. The format of the wrapped key is implementation
+ * defined and must have been produced via a call to {@link #wrapKey(ByteSequence)}.
+ * @return The non-{@code null} unwrapped key which must contain exactly the same content passed to {@link
+ * #wrapKey(ByteSequence)}.
+ * @throws LocalizedKeyStoreException
+ * If an unexpected problem occurred when unwrapping the key.
+ */
+ ByteSequence unwrapKey(ByteSequence wrappedKey) throws LocalizedKeyStoreException;
+}
diff --git a/opendj-core/src/main/java/org/forgerock/opendj/security/KeyProtector.java b/opendj-core/src/main/java/org/forgerock/opendj/security/KeyProtector.java
new file mode 100644
index 0000000..36ccedb
--- /dev/null
+++ b/opendj-core/src/main/java/org/forgerock/opendj/security/KeyProtector.java
@@ -0,0 +1,331 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.security;
+
+import static com.forgerock.opendj.security.KeystoreMessages.*;
+import static javax.crypto.Cipher.SECRET_KEY;
+import static javax.crypto.Cipher.UNWRAP_MODE;
+import static javax.crypto.Cipher.WRAP_MODE;
+import static org.forgerock.opendj.security.KeyStoreParameters.EXTERNAL_KEY_WRAPPING_STRATEGY;
+import static org.forgerock.opendj.security.KeyStoreParameters.GLOBAL_PASSWORD;
+import static org.forgerock.opendj.security.KeyStoreParameters.PBKDF2_ITERATIONS;
+import static org.forgerock.opendj.security.KeyStoreParameters.PBKDF2_SALT_SIZE;
+
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Arrays;
+
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.util.Options;
+
+/**
+ * Converts an {@link Key#getEncoded() encoded key} to or from its ASN.1 representation. An instance of this class must
+ * be created for each encoding or decoding attempt. Keys are encoded using the following ASN.1 format:
+ * <pre>
+ * -- An encoded private or secret key.
+ * Key ::= SEQUENCE {
+ * -- Encoding version.
+ * version INTEGER,
+ * key CHOICE {
+ * -- A clear-text key which has not been wrapped.
+ * plainKey [0] OCTET STRING,
+ *
+ * -- A key which has been wrapped by the key store's default mechanism using a combination of the key
+ * -- store's global password and/or the key's individual password.
+ * keyStoreWrappedKey [1] SEQUENCE {
+ * salt OCTET STRING,
+ * wrappedKey OCTET STRING
+ * },
+ *
+ * -- A key which has been optionally wrapped by the key store's default mechanism using the key's
+ * -- individual password, if provided, and then re-wrapped by an external mechanism. The octet string format
+ * -- is defined by the external mechanism.
+ * externallyWrappedKey [2] OCTET STRING
+ * }
+ * }
+ * </pre>
+ */
+final class KeyProtector {
+ // ASN.1 encoding constants.
+ private static final int ENCODING_VERSION_V1 = 1;
+ private static final byte PLAIN_KEY = (byte) 0xA0;
+ private static final byte KEYSTORE_WRAPPED_KEY = (byte) 0xA1;
+ private static final byte EXTERNALLY_WRAPPED_KEY = (byte) 0xA2;
+ // Crypto parameters.
+ private static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1";
+ private static final int PBKDF2_KEY_SIZE = 128;
+ private static final String CIPHER_ALGORITHM = "AESWrap";
+ private static final String DUMMY_KEY_ALGORITHM = "PADDED";
+ /** PRNG for encoding keys. */
+ private final SecureRandom prng = new SecureRandom();
+ /** The key protection options. */
+ private final Options options;
+
+ KeyProtector(final Options options) {
+ this.options = options;
+ }
+
+ ByteString encodeKey(final Key key, final char[] keyPassword) throws LocalizedKeyStoreException {
+ final char[] keyStorePassword = options.get(GLOBAL_PASSWORD).newInstance();
+ final char[] concatenatedPasswords = concatenate(keyStorePassword, keyPassword);
+ final ByteStringBuilder builder = new ByteStringBuilder();
+ try (final ASN1Writer asn1Writer = ASN1.getWriter(builder)) {
+ asn1Writer.writeStartSequence();
+ asn1Writer.writeInteger(ENCODING_VERSION_V1);
+ final ExternalKeyWrappingStrategy strategy = options.get(EXTERNAL_KEY_WRAPPING_STRATEGY);
+ if (strategy == null) {
+ encodePlainOrWrappedKey(key, concatenatedPasswords, asn1Writer);
+ } else {
+ final ByteStringBuilder externalBuilder = new ByteStringBuilder();
+ try (final ASN1Writer externalAsn1Writer = ASN1.getWriter(externalBuilder)) {
+ encodePlainOrWrappedKey(key, concatenatedPasswords, externalAsn1Writer);
+ }
+ final ByteSequence externallyWrappedKey = strategy.wrapKey(externalBuilder.toByteString());
+ asn1Writer.writeOctetString(EXTERNALLY_WRAPPED_KEY, externallyWrappedKey);
+ }
+ asn1Writer.writeEndSequence();
+ } catch (final IOException e) {
+ // IO exceptions should not occur during encoding because we are writing to a byte array.
+ throw new IllegalStateException(e);
+ } finally {
+ destroyCharArray(concatenatedPasswords);
+ destroyCharArray(keyStorePassword);
+ }
+ return builder.toByteString();
+ }
+
+ private void encodePlainOrWrappedKey(final Key key, final char[] concatenatedPasswords, final ASN1Writer asn1Writer)
+ throws IOException, LocalizedKeyStoreException {
+ if (concatenatedPasswords == null) {
+ asn1Writer.writeOctetString(PLAIN_KEY, key.getEncoded());
+ } else {
+ asn1Writer.writeStartSequence(KEYSTORE_WRAPPED_KEY);
+
+ final byte[] salt = new byte[options.get(PBKDF2_SALT_SIZE)];
+ prng.nextBytes(salt);
+ asn1Writer.writeOctetString(salt);
+
+ final Integer iterations = options.get(PBKDF2_ITERATIONS);
+ final SecretKey aesKey = createAESSecretKey(concatenatedPasswords, salt, iterations);
+ final Cipher cipher = getCipher(WRAP_MODE, aesKey);
+ try {
+ final byte[] wrappedKey = cipher.wrap(pad(key));
+ asn1Writer.writeOctetString(wrappedKey);
+ } catch (final IllegalBlockSizeException | InvalidKeyException e) {
+ throw new IllegalStateException(e); // Should not happen because padding is ok.
+ }
+
+ asn1Writer.writeEndSequence();
+ }
+ }
+
+ Key decodeSecretKey(final ByteSequence encodedKey, final String algorithm, final char[] keyPassword)
+ throws LocalizedKeyStoreException {
+ return decodeKey(encodedKey, algorithm, keyPassword, false);
+ }
+
+ Key decodePrivateKey(final ByteSequence encodedKey, final String algorithm, final char[] keyPassword)
+ throws LocalizedKeyStoreException {
+ return decodeKey(encodedKey, algorithm, keyPassword, true);
+ }
+
+ private Key decodeKey(final ByteSequence encodedKey, final String algorithm, final char[] keyPassword,
+ final boolean isPrivateKey) throws LocalizedKeyStoreException {
+ try (final ASN1Reader asn1Reader = ASN1.getReader(encodedKey)) {
+ asn1Reader.readStartSequence();
+ final int version = (int) asn1Reader.readInteger();
+ final Key key;
+ switch (version) {
+ case ENCODING_VERSION_V1:
+ key = decodeKeyV1(asn1Reader, algorithm, keyPassword, isPrivateKey);
+ break;
+ default:
+ throw new LocalizedKeyStoreException(KEYSTORE_DECODE_UNSUPPORTED_VERSION.get(version));
+ }
+ asn1Reader.readEndSequence();
+ return key;
+ } catch (final IOException e) {
+ throw new LocalizedKeyStoreException(KEYSTORE_DECODE_MALFORMED.get(), e);
+ }
+ }
+
+ private Key decodeKeyV1(final ASN1Reader asn1Reader, final String algorithm, final char[] keyPassword,
+ final boolean isPrivateKey) throws IOException, LocalizedKeyStoreException {
+ switch (asn1Reader.peekType()) {
+ case PLAIN_KEY:
+ final byte[] plainKey = asn1Reader.readOctetString(PLAIN_KEY).toByteArray();
+ return newKeyFromBytes(plainKey, algorithm, isPrivateKey);
+ case KEYSTORE_WRAPPED_KEY:
+ final char[] keyStorePassword = options.get(GLOBAL_PASSWORD).newInstance();
+ final char[] concatenatedPasswords = concatenate(keyStorePassword, keyPassword);
+ if (concatenatedPasswords == null) {
+ throw new LocalizedKeyStoreException(KEYSTORE_DECODE_KEY_MISSING_PWD.get());
+ }
+ asn1Reader.readStartSequence(KEYSTORE_WRAPPED_KEY);
+ try {
+ final byte[] salt = asn1Reader.readOctetString().toByteArray();
+ final byte[] wrappedKey = asn1Reader.readOctetString().toByteArray();
+ final Integer iterations = options.get(PBKDF2_ITERATIONS);
+ final SecretKey aesKey = createAESSecretKey(concatenatedPasswords, salt, iterations);
+ final Cipher cipher = getCipher(UNWRAP_MODE, aesKey);
+ final Key paddedKey = cipher.unwrap(wrappedKey, DUMMY_KEY_ALGORITHM, SECRET_KEY);
+ return unpad(paddedKey, algorithm, isPrivateKey);
+ } catch (final NoSuchAlgorithmException e) {
+ throw new IllegalStateException(e); // Should not happen because it's a pseudo secret key.
+ } catch (final InvalidKeyException e) {
+ throw new LocalizedKeyStoreException(KEYSTORE_DECODE_KEYSTORE_DECRYPT_FAILURE.get(), e);
+ } finally {
+ destroyCharArray(concatenatedPasswords);
+ destroyCharArray(keyStorePassword);
+ asn1Reader.readEndSequence();
+ }
+ case EXTERNALLY_WRAPPED_KEY:
+ final ExternalKeyWrappingStrategy strategy = options.get(EXTERNAL_KEY_WRAPPING_STRATEGY);
+ if (strategy == null) {
+ throw new LocalizedKeyStoreException(KEYSTORE_DECODE_KEY_MISSING_KEYSTORE_EXT.get());
+ }
+ final ByteString externallyWrappedKey = asn1Reader.readOctetString(EXTERNALLY_WRAPPED_KEY);
+ try (final ASN1Reader externalAsn1Reader = ASN1.getReader(strategy.unwrapKey(externallyWrappedKey))) {
+ return decodeKeyV1(externalAsn1Reader, algorithm, keyPassword, isPrivateKey);
+ }
+ default:
+ throw new LocalizedKeyStoreException(KEYSTORE_DECODE_MALFORMED.get());
+ }
+ }
+
+ private static char[] concatenate(final char[] keyStorePassword, final char[] keyPassword) {
+ if (keyStorePassword == null && keyPassword == null) {
+ return null;
+ } else if (keyStorePassword == null) {
+ return keyPassword.clone();
+ } else if (keyPassword == null) {
+ return keyStorePassword.clone();
+ } else {
+ final char[] concat = new char[keyStorePassword.length + keyPassword.length];
+ System.arraycopy(keyStorePassword, 0, concat, 0, keyStorePassword.length);
+ System.arraycopy(keyPassword, 0, concat, keyStorePassword.length, keyPassword.length);
+ return concat;
+ }
+ }
+
+ private static Cipher getCipher(final int cipherMode, final SecretKey aesKey) throws LocalizedKeyStoreException {
+ try {
+ final Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+ cipher.init(cipherMode, aesKey);
+ return cipher;
+ } catch (final NoSuchAlgorithmException | NoSuchPaddingException e) {
+ throw new LocalizedKeyStoreException(KEYSTORE_UNSUPPORTED_CIPHER.get(CIPHER_ALGORITHM), e);
+ } catch (final InvalidKeyException e) {
+ // Should not happen.
+ throw new IllegalStateException("key is incompatible with the cipher", e);
+ }
+ }
+
+ private static SecretKey createAESSecretKey(final char[] password, final byte[] salt, final Integer iterations)
+ throws LocalizedKeyStoreException {
+ try {
+ final SecretKeyFactory factory = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);
+ final PBEKeySpec pbeKeySpec = new PBEKeySpec(password, salt, iterations, PBKDF2_KEY_SIZE);
+ try {
+ final SecretKey pbeKey = factory.generateSecret(pbeKeySpec);
+ return new SecretKeySpec(pbeKey.getEncoded(), "AES");
+ } finally {
+ pbeKeySpec.clearPassword();
+ }
+ } catch (final NoSuchAlgorithmException e) {
+ throw new LocalizedKeyStoreException(KEYSTORE_UNSUPPORTED_KF.get(PBKDF2_ALGORITHM), e);
+ } catch (final InvalidKeySpecException e) {
+ throw new LocalizedKeyStoreException(KEYSTORE_UNSUPPORTED_KF_ARGS.get(PBKDF2_ALGORITHM,
+ iterations,
+ PBKDF2_KEY_SIZE), e);
+ }
+ }
+
+ /**
+ * AESWrap implementation only supports encoded keys that are multiple of 8 bytes. This method always adds
+ * padding, even if it's not needed, in order to avoid ambiguity.
+ */
+ private static Key pad(final Key key) {
+ final byte[] keyBytes = key.getEncoded();
+ final int keySize = keyBytes.length;
+ final int paddingSize = 8 - (keySize % 8);
+ final byte[] paddedKeyBytes = Arrays.copyOf(keyBytes, keySize + paddingSize);
+ for (int i = 0; i < paddingSize; i++) {
+ paddedKeyBytes[keySize + i] = (byte) (i + 1);
+ }
+ return new SecretKeySpec(paddedKeyBytes, DUMMY_KEY_ALGORITHM);
+ }
+
+ private static Key unpad(final Key paddedKey, final String algorithm, final boolean isPrivateKey)
+ throws LocalizedKeyStoreException {
+ final byte[] paddedKeyBytes = paddedKey.getEncoded();
+ final int paddedKeySize = paddedKeyBytes.length;
+ if (paddedKeySize < 8) {
+ throw new LocalizedKeyStoreException(KEYSTORE_DECODE_BAD_PADDING.get());
+ }
+ final byte paddingSize = paddedKeyBytes[paddedKeySize - 1];
+ if (paddingSize < 1 || paddingSize > 8) {
+ throw new LocalizedKeyStoreException(KEYSTORE_DECODE_BAD_PADDING.get());
+ }
+ final int keySize = paddedKeySize - paddingSize;
+ for (int i = 0; i < paddingSize; i++) {
+ if (paddedKeyBytes[keySize + i] != (i + 1)) {
+ throw new LocalizedKeyStoreException(KEYSTORE_DECODE_BAD_PADDING.get());
+ }
+ }
+ final byte[] keyBytes = Arrays.copyOf(paddedKeyBytes, keySize);
+ return newKeyFromBytes(keyBytes, algorithm, isPrivateKey);
+ }
+
+ private static Key newKeyFromBytes(final byte[] plainKey, final String algorithm, final boolean isPrivateKey)
+ throws LocalizedKeyStoreException {
+ if (isPrivateKey) {
+ try {
+ final KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
+ return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(plainKey));
+ } catch (final InvalidKeySpecException | NoSuchAlgorithmException e) {
+ throw new LocalizedKeyStoreException(KEYSTORE_UNSUPPORTED_KF.get(algorithm), e);
+ }
+ } else {
+ return new SecretKeySpec(plainKey, algorithm);
+ }
+ }
+
+ private static void destroyCharArray(final char[] chars) {
+ if (chars != null) {
+ Arrays.fill(chars, ' ');
+ }
+ }
+}
diff --git a/opendj-core/src/main/java/org/forgerock/opendj/security/KeyStoreImpl.java b/opendj-core/src/main/java/org/forgerock/opendj/security/KeyStoreImpl.java
new file mode 100644
index 0000000..f8e67ec
--- /dev/null
+++ b/opendj-core/src/main/java/org/forgerock/opendj/security/KeyStoreImpl.java
@@ -0,0 +1,315 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.security;
+
+import static com.forgerock.opendj.security.KeystoreMessages.*;
+import static org.forgerock.opendj.ldap.Entries.diffEntries;
+import static org.forgerock.opendj.ldap.Filter.and;
+import static org.forgerock.opendj.ldap.Filter.equality;
+import static org.forgerock.opendj.ldap.SearchScope.SINGLE_LEVEL;
+import static org.forgerock.opendj.ldap.requests.Requests.newDeleteRequest;
+import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
+import static org.forgerock.opendj.security.KeyStoreObject.*;
+import static org.forgerock.opendj.security.KeyStoreParameters.CACHE;
+import static org.forgerock.opendj.security.KeyStoreParameters.GLOBAL_PASSWORD;
+import static org.forgerock.opendj.security.KeyStoreParameters.newKeyStoreParameters;
+import static org.forgerock.opendj.security.OpenDJProvider.newClearTextPasswordFactory;
+import static org.forgerock.opendj.security.OpenDJProviderSchema.ATTR_CERTIFICATE_BINARY;
+import static org.forgerock.util.Options.copyOf;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.math.BigInteger;
+import java.security.Key;
+import java.security.KeyStore.LoadStoreParameter;
+import java.security.KeyStoreException;
+import java.security.KeyStoreSpi;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.List;
+
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.ConstraintViolationException;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.EntryNotFoundException;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldif.ConnectionEntryReader;
+import org.forgerock.util.Options;
+
+/** LDAP Key Store implementation. This class should not be used directly. */
+final class KeyStoreImpl extends KeyStoreSpi {
+ private static final LocalizedLogger logger = LocalizedLogger.getLocalizedLogger(KeyStoreImpl.class);
+
+ private static final String[] SEARCH_ATTR_LIST = { "*", "createTimeStamp", "modifyTimeStamp" };
+ private static final Filter FILTER_KEYSTORE_OBJECT = Filter.valueOf("(objectClass=ds-keystore-object)");
+
+ private final OpenDJProvider provider;
+ private KeyStoreParameters config;
+ private KeyStoreObjectCache cache;
+ private KeyProtector keyProtector;
+
+ KeyStoreImpl(final OpenDJProvider provider) {
+ this.provider = provider;
+ }
+
+ @Override
+ public Key engineGetKey(final String alias, final char[] password)
+ throws NoSuchAlgorithmException, UnrecoverableKeyException {
+ final KeyStoreObject object = readKeyStoreObject(alias);
+ return object != null ? object.getKey(keyProtector, password) : null;
+ }
+
+ @Override
+ public Certificate[] engineGetCertificateChain(final String alias) {
+ final KeyStoreObject object = readKeyStoreObject(alias);
+ return object != null ? object.getCertificateChain() : null;
+ }
+
+ @Override
+ public Certificate engineGetCertificate(final String alias) {
+ final KeyStoreObject object = readKeyStoreObject(alias);
+ return object != null ? object.getCertificate() : null;
+ }
+
+ @Override
+ public Date engineGetCreationDate(final String alias) {
+ final KeyStoreObject object = readKeyStoreObject(alias);
+ return object != null ? object.getCreationDate() : null;
+ }
+
+ @Override
+ public void engineSetKeyEntry(final String alias, final Key key, final char[] password, final Certificate[] chain)
+ throws KeyStoreException {
+ writeKeyStoreObject(newKeyObject(alias, key, chain, keyProtector, password));
+ }
+
+ @Override
+ public void engineSetKeyEntry(final String alias, final byte[] key, final Certificate[] chain)
+ throws KeyStoreException {
+ // TODO: It's unclear how this method should be implemented as well as who or what calls it. This method does
+ // not seem to be called from JDK code.
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void engineSetCertificateEntry(final String alias, final Certificate cert) throws KeyStoreException {
+ final KeyStoreObject object = readKeyStoreObject(alias);
+ if (object != null && !object.isTrustedCertificate()) {
+ throw new LocalizedKeyStoreException(KEYSTORE_KEY_ENTRY_ALREADY_EXISTS.get(alias));
+ }
+ writeKeyStoreObject(newTrustedCertificateObject(alias, cert));
+ }
+
+ @Override
+ public void engineDeleteEntry(final String alias) throws KeyStoreException {
+ try (final Connection connection = config.getConnection()) {
+ connection.delete(newDeleteRequest(dnOf(config.getBaseDN(), alias)));
+ } catch (final EntryNotFoundException ignored) {
+ // Ignore: this is what other key store implementations seem to do.
+ } catch (final LdapException e) {
+ throw new LocalizedKeyStoreException(KEYSTORE_DELETE_FAILURE.get(alias), e);
+ }
+ }
+
+ @Override
+ public Enumeration<String> engineAliases() {
+ final SearchRequest searchRequest = newSearchRequest(config.getBaseDN(),
+ SINGLE_LEVEL,
+ FILTER_KEYSTORE_OBJECT,
+ "1.1");
+ try (final Connection connection = config.getConnection();
+ final ConnectionEntryReader reader = connection.search(searchRequest)) {
+ final List<String> aliases = new ArrayList<>();
+ while (reader.hasNext()) {
+ if (reader.isEntry()) {
+ aliases.add(aliasOf(reader.readEntry()));
+ }
+ }
+ return Collections.enumeration(aliases);
+ } catch (final IOException e) {
+ // There's not much we can do here except log a warning and return an empty list of aliases.
+ logger.warn(KEYSTORE_READ_FAILURE.get(), e);
+ return Collections.emptyEnumeration();
+ }
+ }
+
+ private String aliasOf(final Entry entry) {
+ return entry.getName().rdn().getFirstAVA().getAttributeValue().toString();
+ }
+
+ @Override
+ public boolean engineContainsAlias(final String alias) {
+ return readKeyStoreObject(alias) != null;
+ }
+
+ @Override
+ public int engineSize() {
+ try (final Connection connection = config.getConnection()) {
+ final Entry baseEntry = connection.readEntry(config.getBaseDN(), "numSubordinates");
+ return baseEntry.parseAttribute("numSubordinates").asInteger(0);
+ } catch (final LdapException e) {
+ // There's not much we can do here except log a warning and return 0.
+ logger.warn(KEYSTORE_READ_FAILURE.get(), e);
+ }
+ return 0;
+ }
+
+ @Override
+ public boolean engineIsKeyEntry(final String alias) {
+ final KeyStoreObject object = readKeyStoreObject(alias);
+ return object != null && !object.isTrustedCertificate();
+ }
+
+ @Override
+ public boolean engineIsCertificateEntry(final String alias) {
+ final KeyStoreObject object = readKeyStoreObject(alias);
+ return object != null && object.isTrustedCertificate();
+ }
+
+ @Override
+ public String engineGetCertificateAlias(final Certificate cert) {
+ final Filter filter = and(FILTER_KEYSTORE_OBJECT,
+ equality(ATTR_CERTIFICATE_BINARY, getCertificateAssertion(cert)));
+ final SearchRequest searchRequest = newSearchRequest(config.getBaseDN(), SINGLE_LEVEL, filter, "1.1");
+ try (final Connection connection = config.getConnection();
+ final ConnectionEntryReader reader = connection.search(searchRequest)) {
+ while (reader.hasNext()) {
+ if (reader.isEntry()) {
+ return aliasOf(reader.readEntry());
+ }
+ }
+ } catch (IOException e) {
+ // There's not much we can do here except log a warning and return null.
+ logger.warn(KEYSTORE_READ_FAILURE.get(), e);
+ }
+ return null;
+ }
+
+ private String getCertificateAssertion(final Certificate cert) {
+ final X509Certificate x509Certificate = (X509Certificate) cert;
+ final BigInteger serialNumber = x509Certificate.getSerialNumber();
+ final String issuerDn = x509Certificate.getIssuerX500Principal().getName().replaceAll("\"", "\"\"");
+ return String.format("{serialNumber %s,issuer rdnSequence:\"%s\"}", serialNumber, issuerDn);
+ }
+
+ /** No op: the LDAP key store performs updates immediately against the backend LDAP server. */
+ @Override
+ public void engineStore(final OutputStream stream, final char[] password) {
+ if (stream != null) {
+ throw new IllegalArgumentException("the LDAP key store is not file based");
+ }
+ engineStore(null);
+ }
+
+ @Override
+ public void engineStore(final LoadStoreParameter param) {
+ // Do nothing - data is written immediately.
+ }
+
+ /**
+ * The LDAP key store cannot be loaded from an input stream, so this method can only be called if the provider
+ * has been configured.
+ */
+ @Override
+ public void engineLoad(final InputStream stream, final char[] password) {
+ if (stream != null) {
+ throw new IllegalArgumentException("the LDAP key store is not file based");
+ } else if (provider.getDefaultConfig() == null || password == null || password.length == 0) {
+ engineLoad(null);
+ } else {
+ // Use the default options except for the provided password.
+ final KeyStoreParameters defaultConfig = provider.getDefaultConfig();
+ final Options options = copyOf(defaultConfig.getOptions())
+ .set(GLOBAL_PASSWORD, newClearTextPasswordFactory(password));
+ engineLoad(newKeyStoreParameters(defaultConfig.getConnectionFactory(), defaultConfig.getBaseDN(), options));
+ }
+ }
+
+ @Override
+ public void engineLoad(final LoadStoreParameter param) {
+ if (param != null) {
+ try {
+ config = (KeyStoreParameters) param;
+ } catch (final ClassCastException e) {
+ throw new IllegalArgumentException("load must be called with KeyStoreParameters class");
+ }
+ } else if (provider.getDefaultConfig() != null) {
+ config = provider.getDefaultConfig();
+ } else {
+ throw new IllegalArgumentException("the LDAP key store must be configured using KeyStoreParameters "
+ + "or using the security provider's configuration file");
+ }
+ keyProtector = new KeyProtector(config.getOptions());
+ cache = config.getOptions().get(CACHE);
+ }
+
+ private KeyStoreObject readKeyStoreObject(final String alias) {
+ // See if it is in cache first.
+ final KeyStoreObject cachedKeyStoreObject = readCache(alias);
+ if (cachedKeyStoreObject != null) {
+ return cachedKeyStoreObject;
+ }
+ try (final Connection connection = config.getConnection()) {
+ final Entry ldapEntry = connection.readEntry(dnOf(config.getBaseDN(), alias), SEARCH_ATTR_LIST);
+ return writeCache(KeyStoreObject.valueOf(ldapEntry));
+ } catch (EntryNotFoundException ignored) {
+ // The requested key does not exist - fall through.
+ } catch (LocalizedKeyStoreException | IOException e) {
+ // There's not much we can do here except log a warning and assume the key does not exist.
+ logger.warn(KEYSTORE_READ_ALIAS_FAILURE.get(alias), e);
+ }
+ return null;
+ }
+
+
+ private void writeKeyStoreObject(final KeyStoreObject keyStoreObject) throws LocalizedKeyStoreException {
+ try (final Connection connection = config.getConnection()) {
+ final Entry newLdapEntry = keyStoreObject.toLDAPEntry(config.getBaseDN());
+ try {
+ connection.add(newLdapEntry);
+ } catch (ConstraintViolationException e) {
+ if (e.getResult().getResultCode() != ResultCode.ENTRY_ALREADY_EXISTS) {
+ throw e; // Give up.
+ }
+ final Entry oldLdapEntry = connection.readEntry(newLdapEntry.getName());
+ connection.modify(diffEntries(oldLdapEntry, newLdapEntry));
+ }
+ writeCache(keyStoreObject);
+ } catch (final IOException e) {
+ throw new LocalizedKeyStoreException(KEYSTORE_UPDATE_ALIAS_FAILURE.get(keyStoreObject.getAlias()), e);
+ }
+ }
+
+ private KeyStoreObject writeCache(final KeyStoreObject keyStoreObject) {
+ cache.put(keyStoreObject);
+ return keyStoreObject;
+ }
+
+ private KeyStoreObject readCache(final String alias) {
+ return cache.get(alias);
+ }
+}
diff --git a/opendj-core/src/main/java/org/forgerock/opendj/security/KeyStoreObject.java b/opendj-core/src/main/java/org/forgerock/opendj/security/KeyStoreObject.java
new file mode 100644
index 0000000..4b077cd
--- /dev/null
+++ b/opendj-core/src/main/java/org/forgerock/opendj/security/KeyStoreObject.java
@@ -0,0 +1,315 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.security;
+
+import static com.forgerock.opendj.security.KeystoreMessages.KEYSTORE_ENTRY_MALFORMED;
+import static com.forgerock.opendj.security.KeystoreMessages.KEYSTORE_UNRECOGNIZED_OBJECT_CLASS;
+import static org.forgerock.opendj.ldap.Functions.byteStringToCertificate;
+import static org.forgerock.opendj.security.OpenDJProviderSchema.*;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.GeneralizedTime;
+import org.forgerock.opendj.ldap.LinkedHashMapEntry;
+
+/** An in memory representation of a LDAP key store object. */
+public final class KeyStoreObject {
+ private interface Impl {
+ void addAttributes(Entry entry);
+
+ Certificate[] getCertificateChain();
+
+ Certificate getCertificate();
+
+ Key toKey(KeyProtector protector, char[] keyPassword) throws GeneralSecurityException;
+ }
+
+ private static final class TrustedCertificateImpl implements Impl {
+ private final Certificate trustedCertificate;
+
+ private TrustedCertificateImpl(Certificate trustedCertificate) {
+ this.trustedCertificate = trustedCertificate;
+ }
+
+ @Override
+ public void addAttributes(final Entry entry) {
+ entry.addAttribute(ATTR_OBJECT_CLASS, "top", OC_KEY_STORE_OBJECT, OC_TRUSTED_CERTIFICATE);
+ entry.addAttribute(ATTR_CERTIFICATE_BINARY, encodeCertificate(trustedCertificate));
+ }
+
+ @Override
+ public Certificate[] getCertificateChain() {
+ return null;
+ }
+
+ @Override
+ public Certificate getCertificate() {
+ return trustedCertificate;
+ }
+
+ @Override
+ public Key toKey(final KeyProtector protector, final char[] keyPassword) {
+ return null;
+ }
+ }
+
+ private static final class PrivateKeyImpl implements Impl {
+ private final String algorithm;
+ private final ByteString protectedKey;
+ private final Certificate[] certificateChain;
+
+ private PrivateKeyImpl(String algorithm, ByteString protectedKey, Certificate[] chain) {
+ this.algorithm = algorithm;
+ this.protectedKey = protectedKey;
+ this.certificateChain = chain.clone();
+ }
+
+ @Override
+ public void addAttributes(final Entry entry) {
+ entry.addAttribute(ATTR_OBJECT_CLASS, "top", OC_KEY_STORE_OBJECT, OC_PRIVATE_KEY);
+ entry.addAttribute(ATTR_KEY_ALGORITHM, algorithm);
+ entry.addAttribute(ATTR_KEY, protectedKey);
+ // The KeyStore method "getCertificateAlias" is best implemented using an LDAP search whose filter is a
+ // certificateExactMatch assertion against an attribute whose value conforms to the LDAP "certificate"
+ // syntax. To facilitate this we split out the first certificate from the rest of the certificate chain.
+ entry.addAttribute(ATTR_CERTIFICATE_BINARY, encodeCertificate(certificateChain[0]));
+ if (certificateChain.length > 1) {
+ entry.addAttribute(ATTR_CERTIFICATE_CHAIN, encodeCertificateChain());
+ }
+ }
+
+ // Encode all certificates except the first which is stored separately.
+ private ByteString encodeCertificateChain() {
+ final ByteStringBuilder builder = new ByteStringBuilder();
+ final ASN1Writer writer = ASN1.getWriter(builder);
+ try {
+ writer.writeStartSequence();
+ for (int i = 1; i < certificateChain.length; i++) {
+ writer.writeOctetString(encodeCertificate(certificateChain[i]));
+ }
+ writer.writeEndSequence();
+ } catch (IOException e) {
+ // Should not happen.
+ throw new RuntimeException(e);
+ }
+ return builder.toByteString();
+ }
+
+ @Override
+ public Certificate[] getCertificateChain() {
+ return certificateChain.clone();
+ }
+
+ @Override
+ public Certificate getCertificate() {
+ return certificateChain.length > 0 ? certificateChain[0] : null;
+ }
+
+ @Override
+ public Key toKey(final KeyProtector protector, final char[] keyPassword) throws GeneralSecurityException {
+ return protector.decodePrivateKey(protectedKey, algorithm, keyPassword);
+ }
+ }
+
+ private static final class SecretKeyImpl implements Impl {
+ private final String algorithm;
+ private final ByteString protectedKey;
+
+ private SecretKeyImpl(String algorithm, ByteString protectedKey) {
+ this.algorithm = algorithm;
+ this.protectedKey = protectedKey;
+ }
+
+ @Override
+ public void addAttributes(final Entry entry) {
+ entry.addAttribute(ATTR_OBJECT_CLASS, "top", OC_KEY_STORE_OBJECT, OC_SECRET_KEY);
+ entry.addAttribute(ATTR_KEY_ALGORITHM, algorithm);
+ entry.addAttribute(ATTR_KEY, protectedKey);
+ }
+
+ @Override
+ public Certificate[] getCertificateChain() {
+ return null;
+ }
+
+ @Override
+ public Certificate getCertificate() {
+ return null;
+ }
+
+ @Override
+ public Key toKey(final KeyProtector protector, final char[] keyPassword) throws GeneralSecurityException {
+ return protector.decodeSecretKey(protectedKey, algorithm, keyPassword);
+ }
+ }
+
+ static KeyStoreObject newTrustedCertificateObject(String alias, Certificate certificate) {
+ return new KeyStoreObject(alias, new Date(), new TrustedCertificateImpl(certificate));
+ }
+
+ static KeyStoreObject newKeyObject(String alias, Key key, Certificate[] chain, KeyProtector protector,
+ char[] keyPassword) throws LocalizedKeyStoreException {
+ final ByteString protectedKey = protector.encodeKey(key, keyPassword);
+ final Impl impl = key instanceof PrivateKey
+ ? new PrivateKeyImpl(key.getAlgorithm(), protectedKey, chain)
+ : new SecretKeyImpl(key.getAlgorithm(), protectedKey);
+ return new KeyStoreObject(alias, new Date(), impl);
+ }
+
+ static KeyStoreObject valueOf(final Entry ldapEntry) throws LocalizedKeyStoreException {
+ try {
+ final String alias = ldapEntry.parseAttribute(ATTR_ALIAS).requireValue().asString();
+
+ GeneralizedTime timeStamp = ldapEntry.parseAttribute(ATTR_MODIFY_TIME_STAMP).asGeneralizedTime();
+ if (timeStamp == null) {
+ timeStamp = ldapEntry.parseAttribute(ATTR_CREATE_TIME_STAMP).asGeneralizedTime();
+ }
+ final Date creationDate = timeStamp != null ? timeStamp.toDate() : new Date();
+
+ final Impl impl;
+ if (ldapEntry.containsAttribute(ATTR_OBJECT_CLASS, OC_TRUSTED_CERTIFICATE)) {
+ impl = valueOfTrustedCertificate(ldapEntry);
+ } else if (ldapEntry.containsAttribute(ATTR_OBJECT_CLASS, OC_PRIVATE_KEY)) {
+ impl = valueOfPrivateKey(ldapEntry);
+ } else if (ldapEntry.containsAttribute(ATTR_OBJECT_CLASS, OC_SECRET_KEY)) {
+ impl = valueOfSecretKey(ldapEntry);
+ } else {
+ throw new LocalizedKeyStoreException(KEYSTORE_UNRECOGNIZED_OBJECT_CLASS.get(ldapEntry.getName()));
+ }
+ return new KeyStoreObject(alias, creationDate, impl);
+ } catch (LocalizedIllegalArgumentException | IOException e) {
+ throw new LocalizedKeyStoreException(KEYSTORE_ENTRY_MALFORMED.get(ldapEntry.getName()), e);
+ }
+ }
+
+ private static Impl valueOfSecretKey(final Entry ldapEntry) {
+ final String algorithm = ldapEntry.parseAttribute(ATTR_KEY_ALGORITHM).requireValue().asString();
+ final ByteString protectedKey = ldapEntry.parseAttribute(ATTR_KEY).requireValue().asByteString();
+ return new SecretKeyImpl(algorithm, protectedKey);
+ }
+
+ private static Impl valueOfPrivateKey(final Entry ldapEntry) throws IOException {
+ final String algorithm = ldapEntry.parseAttribute(ATTR_KEY_ALGORITHM).requireValue().asString();
+ final ByteString protectedKey = ldapEntry.parseAttribute(ATTR_KEY).requireValue().asByteString();
+ final List<Certificate> certificateChainList = new ArrayList<>();
+ final X509Certificate publicKeyCertificate =
+ ldapEntry.parseAttribute(ATTR_CERTIFICATE_BINARY).requireValue().asCertificate();
+ certificateChainList.add(publicKeyCertificate);
+ final ByteString encodedCertificateChain = ldapEntry.parseAttribute(ATTR_CERTIFICATE_CHAIN).asByteString();
+ if (encodedCertificateChain != null) {
+ final ASN1Reader reader = ASN1.getReader(encodedCertificateChain);
+ reader.readStartSequence();
+ while (reader.hasNextElement()) {
+ final ByteString certificate = reader.readOctetString();
+ certificateChainList.add(byteStringToCertificate().apply(certificate));
+ }
+ reader.readEndSequence();
+ }
+ final Certificate[] certificateChain = certificateChainList.toArray(new Certificate[0]);
+ return new PrivateKeyImpl(algorithm, protectedKey, certificateChain);
+ }
+
+ private static Impl valueOfTrustedCertificate(final Entry ldapEntry) {
+ final X509Certificate trustedCertificate =
+ ldapEntry.parseAttribute(ATTR_CERTIFICATE_BINARY).requireValue().asCertificate();
+ return new TrustedCertificateImpl(trustedCertificate);
+ }
+
+ private static ByteString encodeCertificate(final Certificate certificate) {
+ try {
+ return ByteString.wrap(certificate.getEncoded());
+ } catch (CertificateEncodingException e) {
+ // Should not happen.
+ throw new RuntimeException(e);
+ }
+ }
+
+ private final String alias;
+ private final Date creationDate;
+ private final Impl impl;
+
+ private KeyStoreObject(final String alias, final Date creationDate, final Impl impl) {
+ this.alias = alias;
+ this.creationDate = creationDate;
+ this.impl = impl;
+ }
+
+ Date getCreationDate() {
+ return creationDate;
+ }
+
+ /**
+ * Returns the alias associated with this key store object.
+ *
+ * @return The alias associated with this key store object.
+ */
+ public String getAlias() {
+ return alias;
+ }
+
+ Certificate[] getCertificateChain() {
+ return impl.getCertificateChain();
+ }
+
+ boolean isTrustedCertificate() {
+ return impl instanceof TrustedCertificateImpl;
+ }
+
+ Entry toLDAPEntry(final DN baseDN) {
+ final LinkedHashMapEntry entry = new LinkedHashMapEntry(dnOf(baseDN, alias));
+ entry.addAttribute(ATTR_ALIAS, alias);
+ impl.addAttributes(entry);
+ return entry;
+ }
+
+ Certificate getCertificate() {
+ return impl.getCertificate();
+ }
+
+ Key getKey(final KeyProtector protector, final char[] keyPassword)
+ throws NoSuchAlgorithmException, UnrecoverableKeyException {
+ try {
+ return impl.toKey(protector, keyPassword);
+ } catch (NoSuchAlgorithmException | UnrecoverableKeyException e) {
+ throw e;
+ } catch (GeneralSecurityException e) {
+ throw new UnrecoverableKeyException(e.getMessage());
+ }
+ }
+
+ static DN dnOf(DN baseDN, String alias) {
+ return baseDN.child(ATTR_ALIAS, alias);
+ }
+}
diff --git a/opendj-core/src/main/java/org/forgerock/opendj/security/KeyStoreObjectCache.java b/opendj-core/src/main/java/org/forgerock/opendj/security/KeyStoreObjectCache.java
new file mode 100644
index 0000000..0c2f6f2
--- /dev/null
+++ b/opendj-core/src/main/java/org/forgerock/opendj/security/KeyStoreObjectCache.java
@@ -0,0 +1,57 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.security;
+
+/**
+ * A service provider interface for implementing key store caches. The key store cache can be configured using the
+ * {@link KeyStoreParameters#CACHE} option. It is strongly recommended that cache implementations evict key store
+ * objects after a period of time in order to avoid returning stale objects.
+ *
+ * @see OpenDJProvider#newKeyStoreObjectCacheFromMap(java.util.Map)
+ */
+public interface KeyStoreObjectCache {
+ /** A cache implementation that does not cache anything. */
+ KeyStoreObjectCache NONE = new KeyStoreObjectCache() {
+ @Override
+ public void put(final KeyStoreObject keyStoreObject) {
+ // Do nothing.
+ }
+
+ @Override
+ public KeyStoreObject get(final String alias) {
+ // Always miss.
+ return null;
+ }
+ };
+
+ /**
+ * Puts a key store object in the cache replacing any existing key store object with the same alias.
+ *
+ * @param keyStoreObject
+ * The key store object.
+ */
+ void put(KeyStoreObject keyStoreObject);
+
+ /**
+ * Returns the named key store object from the cache if present, or {@code null} if the object is not present or
+ * has been removed.
+ *
+ * @param alias
+ * The alias of the key store object to be retrieved.
+ * @return The key store object or {@code null} if the object is not present.
+ */
+ KeyStoreObject get(String alias);
+}
diff --git a/opendj-core/src/main/java/org/forgerock/opendj/security/KeyStoreParameters.java b/opendj-core/src/main/java/org/forgerock/opendj/security/KeyStoreParameters.java
new file mode 100644
index 0000000..afa0fc9
--- /dev/null
+++ b/opendj-core/src/main/java/org/forgerock/opendj/security/KeyStoreParameters.java
@@ -0,0 +1,142 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.security;
+
+import static org.forgerock.opendj.security.KeyStoreObjectCache.NONE;
+import static org.forgerock.opendj.security.OpenDJProvider.newClearTextPasswordFactory;
+import static org.forgerock.util.Options.defaultOptions;
+
+import java.security.KeyStore.LoadStoreParameter;
+import java.security.KeyStore.ProtectionParameter;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.util.Factory;
+import org.forgerock.util.Option;
+import org.forgerock.util.Options;
+
+/**
+ * The parameters which configure how the LDAP key store will be accessed. The connection factory should be configured
+ * to return connections which are already authenticated as a user having sufficient privileges to read and update LDAP
+ * key store entries. In addition, the factory should use connection pooling in order to avoid excessive reconnection
+ * when the key store is accessed frequently.
+ */
+public final class KeyStoreParameters implements LoadStoreParameter {
+ /**
+ * The optional password which is used to protect all private and secret keys. Note that individual keys may be
+ * protected by a separate password. The default value for this option is a password factory which always
+ * returns {@code null}, indicating that there is no global password and that separate passwords should be used
+ * instead.
+ * <p/>
+ * Applications should provide a factory which always returns a new instance of the same password. The LDAP key
+ * store will destroy the contents of the returned password after each use. It is the responsibility of the
+ * factory to protect the in memory representation of the password between successive calls.
+ *
+ * @see OpenDJProvider#newClearTextPasswordFactory(char[])
+ */
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ public static final Option<Factory<char[]>> GLOBAL_PASSWORD =
+ (Option) Option.of(Factory.class, newClearTextPasswordFactory(null));
+ /**
+ * The caching mechanism that the key store will use. Caching can significantly increase performance by reducing
+ * interactions with the backend LDAP server(s), at the risk of returning stale key store objects for a period of
+ * time. By default caching is disabled.
+ *
+ * @see OpenDJProvider#newKeyStoreObjectCacheFromMap(java.util.Map)
+ */
+ public static final Option<KeyStoreObjectCache> CACHE = Option.withDefault(NONE);
+ /**
+ * The number of iterations to use when deriving encryption keys from passwords using PBKDF2. The default is 10000
+ * as recommended by NIST.
+ */
+ public static final Option<Integer> PBKDF2_ITERATIONS = Option.withDefault(10000);
+ /**
+ * The number of random bytes to use as the salt when deriving encryption keys from passwords using PBKDF2. The
+ * default is 16.
+ */
+ public static final Option<Integer> PBKDF2_SALT_SIZE = Option.withDefault(16);
+ /**
+ * An alternative external mechanism for wrapping private and secret keys in the key store. By default, the key
+ * store will use its own mechanism based on PBKDF2 and a global {@link #GLOBAL_PASSWORD password} if provided.
+ */
+ public static final Option<ExternalKeyWrappingStrategy> EXTERNAL_KEY_WRAPPING_STRATEGY =
+ Option.of(ExternalKeyWrappingStrategy.class, null);
+
+ private final ConnectionFactory factory;
+ private final DN baseDN;
+ private final Options options;
+
+ /**
+ * Creates a set of LDAP key store parameters with default options. See the class Javadoc for more information
+ * about the parameters.
+ *
+ * @param factory
+ * The LDAP connection factory.
+ * @param baseDN
+ * The DN of the subtree containing the LDAP key store.
+ * @return The key store parameters.
+ */
+ public static KeyStoreParameters newKeyStoreParameters(final ConnectionFactory factory, final DN baseDN) {
+ return newKeyStoreParameters(factory, baseDN, defaultOptions());
+ }
+
+ /**
+ * Creates a set of LDAP key store parameters with custom options. See the class Javadoc for more information about
+ * the parameters.
+ *
+ * @param factory
+ * The LDAP connection factory.
+ * @param baseDN
+ * The DN of the subtree containing the LDAP key store.
+ * @param options
+ * The optional key store parameters, including the cache configuration, key store password, and crypto
+ * parameters. The supported options are defined in this class.
+ * @return The key store parameters.
+ */
+ public static KeyStoreParameters newKeyStoreParameters(final ConnectionFactory factory, final DN baseDN,
+ final Options options) {
+ return new KeyStoreParameters(factory, baseDN, options);
+ }
+
+ private KeyStoreParameters(final ConnectionFactory factory, final DN baseDN, final Options options) {
+ this.factory = factory;
+ this.baseDN = baseDN;
+ this.options = options;
+ }
+
+ @Override
+ public ProtectionParameter getProtectionParameter() {
+ throw new IllegalStateException(); // LDAP key store does not use this method.
+ }
+
+ Options getOptions() {
+ return options;
+ }
+
+ Connection getConnection() throws LdapException {
+ return getConnectionFactory().getConnection();
+ }
+
+ ConnectionFactory getConnectionFactory() {
+ return factory;
+ }
+
+ DN getBaseDN() {
+ return baseDN;
+ }
+}
diff --git a/opendj-core/src/main/java/org/forgerock/opendj/security/LocalizedKeyStoreException.java b/opendj-core/src/main/java/org/forgerock/opendj/security/LocalizedKeyStoreException.java
new file mode 100644
index 0000000..f7496b9
--- /dev/null
+++ b/opendj-core/src/main/java/org/forgerock/opendj/security/LocalizedKeyStoreException.java
@@ -0,0 +1,43 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.security;
+
+import java.security.KeyStoreException;
+
+import org.forgerock.i18n.LocalizableException;
+import org.forgerock.i18n.LocalizableMessage;
+
+/** A localized {@link KeyStoreException}. */
+@SuppressWarnings("serial")
+final class LocalizedKeyStoreException extends KeyStoreException implements LocalizableException {
+ /** The I18N message associated with this exception. */
+ private final LocalizableMessage message;
+
+ LocalizedKeyStoreException(final LocalizableMessage message) {
+ super(String.valueOf(message));
+ this.message = message;
+ }
+
+ LocalizedKeyStoreException(final LocalizableMessage message, final Throwable cause) {
+ super(String.valueOf(message), cause);
+ this.message = message;
+ }
+
+ @Override
+ public LocalizableMessage getMessageObject() {
+ return this.message;
+ }
+}
diff --git a/opendj-core/src/main/java/org/forgerock/opendj/security/OpenDJProvider.java b/opendj-core/src/main/java/org/forgerock/opendj/security/OpenDJProvider.java
new file mode 100644
index 0000000..b5cba5a
--- /dev/null
+++ b/opendj-core/src/main/java/org/forgerock/opendj/security/OpenDJProvider.java
@@ -0,0 +1,437 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.security;
+
+import static java.util.Collections.singletonList;
+import static java.util.Collections.synchronizedMap;
+import static org.forgerock.opendj.ldap.Connections.newCachedConnectionPool;
+import static org.forgerock.opendj.ldap.Connections.newInternalConnectionFactory;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.AUTHN_BIND_REQUEST;
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+import static org.forgerock.opendj.ldap.requests.Requests.newSimpleBindRequest;
+import static org.forgerock.opendj.ldif.LDIF.copyTo;
+import static org.forgerock.opendj.ldif.LDIF.newEntryIteratorReader;
+import static org.forgerock.opendj.security.KeyStoreParameters.GLOBAL_PASSWORD;
+import static org.forgerock.opendj.security.KeyStoreParameters.newKeyStoreParameters;
+import static org.forgerock.opendj.security.OpenDJProviderSchema.SCHEMA;
+import static org.forgerock.util.Options.defaultOptions;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.URI;
+import java.security.AccessController;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.PrivilegedAction;
+import java.security.Provider;
+import java.security.ProviderException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.IntermediateResponseHandler;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LdapResultHandler;
+import org.forgerock.opendj.ldap.MemoryBackend;
+import org.forgerock.opendj.ldap.RequestContext;
+import org.forgerock.opendj.ldap.RequestHandler;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SearchResultHandler;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldif.LDIFEntryReader;
+import org.forgerock.opendj.ldif.LDIFEntryWriter;
+import org.forgerock.util.Factory;
+import org.forgerock.util.Options;
+
+/**
+ * The OpenDJ LDAP security provider which exposes an LDAP/LDIF based {@link java.security.KeyStore KeyStore}
+ * service, as well as providing utility methods facilitating construction of LDAP/LDIF based key stores. See the
+ * package documentation for more information.
+ */
+public final class OpenDJProvider extends Provider {
+ private static final long serialVersionUID = -1;
+ // Security provider configuration property names.
+ private static final String PREFIX = "org.forgerock.opendj.security.";
+ private static final String LDIF_PROPERTY = PREFIX + "ldif";
+ private static final String HOST_PROPERTY = PREFIX + "host";
+ private static final String PORT_PROPERTY = PREFIX + "port";
+ private static final String BIND_DN_PROPERTY = PREFIX + "bindDN";
+ private static final String BIND_PASSWORD_PROPERTY = PREFIX + "bindPassword";
+ private static final String KEYSTORE_BASE_DN_PROPERTY = PREFIX + "keyStoreBaseDN";
+ private static final String KEYSTORE_PASSWORD_PROPERTY = PREFIX + "keyStorePassword";
+ // Default key store configuration or null if key stores need explicit configuration.
+ private final KeyStoreParameters defaultConfig;
+
+ /** Creates a default LDAP security provider with no default key store configuration. */
+ public OpenDJProvider() {
+ this((KeyStoreParameters) null);
+ }
+
+ /**
+ * Creates a LDAP security provider with provided default key store configuration.
+ *
+ * @param configFile
+ * The configuration file, which may be {@code null} indicating that key stores will be configured when they
+ * are instantiated.
+ */
+ public OpenDJProvider(final String configFile) {
+ this(new File(configFile).toURI());
+ }
+
+ /**
+ * Creates a LDAP security provider with provided default key store configuration.
+ *
+ * @param configFile
+ * The configuration file, which may be {@code null} indicating that key stores will be configured when they
+ * are instantiated.
+ */
+ public OpenDJProvider(final URI configFile) {
+ this(configFile != null ? parseConfig(configFile) : null);
+ }
+
+ OpenDJProvider(final KeyStoreParameters defaultConfig) {
+ super("OpenDJ", 1.0D, "OpenDJ LDAP security provider");
+ this.defaultConfig = defaultConfig;
+ AccessController.doPrivileged(new PrivilegedAction<Void>() {
+ public Void run() {
+ putService(new KeyStoreService());
+ return null;
+ }
+ });
+ }
+
+ /**
+ * Creates a new LDAP key store with default options. The returned key store will already have been
+ * {@link KeyStore#load(KeyStore.LoadStoreParameter) loaded}.
+ *
+ * @param factory
+ * The LDAP connection factory.
+ * @param baseDN
+ * The DN of the subtree containing the LDAP key store.
+ * @return The LDAP key store.
+ */
+ public static KeyStore newLDAPKeyStore(final ConnectionFactory factory, final DN baseDN) {
+ return newLDAPKeyStore(factory, baseDN, defaultOptions());
+ }
+
+ /**
+ * Creates a new LDAP key store with custom options. The returned key store will already have been
+ * {@link KeyStore#load(KeyStore.LoadStoreParameter) loaded}.
+ *
+ * @param factory
+ * The LDAP connection factory.
+ * @param baseDN
+ * The DN of the subtree containing the LDAP key store.
+ * @param options
+ * The optional key store parameters, including the cache configuration, key store password, and crypto
+ * parameters.
+ * @return The LDAP key store.
+ * @see KeyStoreParameters For the list of available key store options.
+ */
+ public static KeyStore newLDAPKeyStore(final ConnectionFactory factory, final DN baseDN, final Options options) {
+ try {
+ final KeyStore keyStore = KeyStore.getInstance("LDAP", new OpenDJProvider());
+ keyStore.load(newKeyStoreParameters(factory, baseDN, options));
+ return keyStore;
+ } catch (GeneralSecurityException | IOException e) {
+ // Should not happen.
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Creates a new LDIF based key store which will read and write key store objects to the provided key store file.
+ * The LDIF file will be read during construction and re-written after each update. The returned key store will
+ * already have been {@link KeyStore#load(KeyStore.LoadStoreParameter) loaded}.
+ *
+ * @param ldifFile
+ * The name of the LDIF file containing the key store objects.
+ * @param baseDN
+ * The DN of the subtree containing the LDAP key store.
+ * @return The LDIF key store.
+ * @throws IOException
+ * If an error occurred while reading the LDIF file.
+ */
+ public static KeyStore newLDIFKeyStore(final File ldifFile, final DN baseDN) throws IOException {
+ return newLDIFKeyStore(ldifFile, baseDN, defaultOptions());
+ }
+
+ /**
+ * Creates a new LDIF based key store which will read and write key store objects to the provided key store file.
+ * The LDIF file will be read during construction and re-written after each update. The returned key store will
+ * already have been {@link KeyStore#load(KeyStore.LoadStoreParameter) loaded}.
+ *
+ * @param ldifFile
+ * The name of the LDIF file containing the key store objects.
+ * @param baseDN
+ * The DN of the subtree containing the LDAP key store.
+ * @param options
+ * The optional key store parameters, including the cache configuration, key store password, and crypto
+ * parameters.
+ * @return The LDIF key store.
+ * @throws IOException
+ * If an error occurred while reading the LDIF file.
+ */
+ public static KeyStore newLDIFKeyStore(final File ldifFile, final DN baseDN, final Options options)
+ throws IOException {
+ return newLDAPKeyStore(newLDIFConnectionFactory(ldifFile), baseDN, options);
+ }
+
+ private static ConnectionFactory newLDIFConnectionFactory(final File ldifFile) throws IOException {
+ try (LDIFEntryReader reader = new LDIFEntryReader(new FileReader(ldifFile)).setSchema(SCHEMA)) {
+ final MemoryBackend backend = new MemoryBackend(SCHEMA, reader).enableVirtualAttributes(true);
+ return newInternalConnectionFactory(new WriteLDIFOnUpdateRequestHandler(backend, ldifFile));
+ }
+ }
+
+ /**
+ * Creates a new key store object cache which will delegate to the provided {@link Map}. It is the responsibility
+ * of the map implementation to perform cache eviction if needed. The provided map MUST be thread-safe.
+ *
+ * @param map
+ * The thread-safe {@link Map} implementation in which key store objects will be stored.
+ * @return The new key store object cache.
+ */
+ public static KeyStoreObjectCache newKeyStoreObjectCacheFromMap(final Map<String, KeyStoreObject> map) {
+ return new KeyStoreObjectCache() {
+ @Override
+ public void put(final KeyStoreObject keyStoreObject) {
+ map.put(keyStoreObject.getAlias(), keyStoreObject);
+ }
+
+ @Override
+ public KeyStoreObject get(final String alias) {
+ return map.get(alias);
+ }
+ };
+ }
+
+ /**
+ * Creates a new fixed capacity key store object cache which will evict objects once it reaches the
+ * provided capacity. This implementation is only intended for simple use cases and is not particularly scalable.
+ *
+ * @param capacity
+ * The maximum number of key store objects that will be cached before eviction occurs.
+ * @return The new key store object cache.
+ */
+ public static KeyStoreObjectCache newCapacityBasedKeyStoreObjectCache(final int capacity) {
+ return newKeyStoreObjectCacheFromMap(synchronizedMap(new LinkedHashMap<String, KeyStoreObject>() {
+ private static final long serialVersionUID = -1;
+
+ @Override
+ protected boolean removeEldestEntry(final Map.Entry<String, KeyStoreObject> eldest) {
+ return size() > capacity;
+ }
+ }));
+ }
+
+ /**
+ * Returns a password factory which will return a copy of the provided password for each invocation of
+ * {@link Factory#newInstance()}, and which does not provide any protection of the in memory representation of
+ * the password.
+ *
+ * @param password
+ * The password or {@code null} if no password should ever be returned.
+ * @return A password factory which will return a copy of the provided password.
+ */
+ public static Factory<char[]> newClearTextPasswordFactory(final char[] password) {
+ return new Factory<char[]>() {
+ private final char[] clonedPassword = password != null ? password.clone() : null;
+
+ @Override
+ public char[] newInstance() {
+ return clonedPassword != null ? clonedPassword.clone() : null;
+ }
+ };
+ }
+
+ KeyStoreParameters getDefaultConfig() {
+ return defaultConfig;
+ }
+
+ private static KeyStoreParameters parseConfig(final URI configFile) {
+ try (final Reader configFileReader = new InputStreamReader(configFile.toURL().openStream())) {
+ final Properties properties = new Properties();
+ properties.load(configFileReader);
+
+ final String keyStoreBaseDNProperty = properties.getProperty(KEYSTORE_BASE_DN_PROPERTY);
+ if (keyStoreBaseDNProperty == null) {
+ throw new IllegalArgumentException("missing key store base DN");
+ }
+ final DN keyStoreBaseDN = DN.valueOf(keyStoreBaseDNProperty);
+
+ final Options keystoreOptions = defaultOptions();
+ final String keystorePassword = properties.getProperty(KEYSTORE_PASSWORD_PROPERTY);
+ if (keystorePassword != null) {
+ keystoreOptions.set(GLOBAL_PASSWORD, newClearTextPasswordFactory(keystorePassword.toCharArray()));
+ }
+
+ final ConnectionFactory factory;
+ final String ldif = properties.getProperty(LDIF_PROPERTY);
+ if (ldif != null) {
+ factory = newLDIFConnectionFactory(new File(ldif));
+ } else {
+ final String host = properties.getProperty(HOST_PROPERTY, "localhost");
+ final int port = Integer.parseInt(properties.getProperty(PORT_PROPERTY, "389"));
+ final DN bindDN = DN.valueOf(properties.getProperty(BIND_DN_PROPERTY, ""));
+ final String bindPassword = properties.getProperty(BIND_PASSWORD_PROPERTY);
+
+ final Options factoryOptions = defaultOptions();
+ if (!bindDN.isRootDN()) {
+ factoryOptions.set(AUTHN_BIND_REQUEST,
+ newSimpleBindRequest(bindDN.toString(), bindPassword.toCharArray()));
+ }
+ factory = newCachedConnectionPool(new LDAPConnectionFactory(host, port, factoryOptions));
+ }
+
+ return newKeyStoreParameters(factory, keyStoreBaseDN, keystoreOptions);
+ } catch (Exception e) {
+ throw new ProviderException("Error parsing configuration in file '" + configFile + "'", e);
+ }
+ }
+
+ private static final class WriteLDIFOnUpdateRequestHandler implements RequestHandler<RequestContext> {
+ private final MemoryBackend backend;
+ private final File ldifFile;
+
+ private WriteLDIFOnUpdateRequestHandler(final MemoryBackend backend, final File ldifFile) {
+ this.backend = backend;
+ this.ldifFile = ldifFile;
+ }
+
+ @Override
+ public void handleAdd(final RequestContext requestContext, final AddRequest request,
+ final IntermediateResponseHandler intermediateResponseHandler,
+ final LdapResultHandler<Result> resultHandler) {
+ backend.handleAdd(requestContext, request, intermediateResponseHandler, saveAndForwardTo(resultHandler));
+ }
+
+ @Override
+ public void handleBind(final RequestContext requestContext, final int version, final BindRequest request,
+ final IntermediateResponseHandler intermediateResponseHandler,
+ final LdapResultHandler<BindResult> resultHandler) {
+ backend.handleBind(requestContext, version, request, intermediateResponseHandler, resultHandler);
+ }
+
+ @Override
+ public void handleCompare(final RequestContext requestContext, final CompareRequest request,
+ final IntermediateResponseHandler intermediateResponseHandler,
+ final LdapResultHandler<CompareResult> resultHandler) {
+ backend.handleCompare(requestContext, request, intermediateResponseHandler, resultHandler);
+ }
+
+ @Override
+ public void handleDelete(final RequestContext requestContext, final DeleteRequest request,
+ final IntermediateResponseHandler intermediateResponseHandler,
+ final LdapResultHandler<Result> resultHandler) {
+ backend.handleDelete(requestContext, request, intermediateResponseHandler, saveAndForwardTo(resultHandler));
+ }
+
+ @Override
+ public <R extends ExtendedResult> void handleExtendedRequest(final RequestContext requestContext,
+ final ExtendedRequest<R> request,
+ final IntermediateResponseHandler
+ intermediateResponseHandler,
+ final LdapResultHandler<R> resultHandler) {
+ backend.handleExtendedRequest(requestContext, request, intermediateResponseHandler, resultHandler);
+ }
+
+ @Override
+ public void handleModify(final RequestContext requestContext, final ModifyRequest request,
+ final IntermediateResponseHandler intermediateResponseHandler,
+ final LdapResultHandler<Result> resultHandler) {
+ backend.handleModify(requestContext, request, intermediateResponseHandler, saveAndForwardTo(resultHandler));
+ }
+
+ @Override
+ public void handleModifyDN(final RequestContext requestContext, final ModifyDNRequest request,
+ final IntermediateResponseHandler intermediateResponseHandler,
+ final LdapResultHandler<Result> resultHandler) {
+ backend.handleModifyDN(requestContext,
+ request,
+ intermediateResponseHandler,
+ saveAndForwardTo(resultHandler));
+ }
+
+ @Override
+ public void handleSearch(final RequestContext requestContext, final SearchRequest request,
+ final IntermediateResponseHandler intermediateResponseHandler,
+ final SearchResultHandler entryHandler,
+ final LdapResultHandler<Result> resultHandler) {
+ backend.handleSearch(requestContext, request, intermediateResponseHandler, entryHandler, resultHandler);
+ }
+
+ private LdapResultHandler<Result> saveAndForwardTo(final LdapResultHandler<Result> resultHandler) {
+ return new LdapResultHandler<Result>() {
+ @Override
+ public void handleException(final LdapException exception) {
+ resultHandler.handleException(exception);
+ }
+
+ @Override
+ public void handleResult(final Result result) {
+ try {
+ writeLDIF(backend, ldifFile);
+ resultHandler.handleResult(result);
+ } catch (IOException e) {
+ final LdapException ldapException =
+ newLdapException(ResultCode.OTHER, "Unable to write LDIF file " + ldifFile, e);
+ resultHandler.handleException(ldapException);
+ }
+ }
+ };
+ }
+
+ private static void writeLDIF(final MemoryBackend backend, final File ldifFile) throws IOException {
+ try (final FileWriter fileWriter = new FileWriter(ldifFile);
+ final BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
+ final LDIFEntryWriter entryWriter = new LDIFEntryWriter(bufferedWriter)) {
+ copyTo(newEntryIteratorReader(backend.getAll().iterator()), entryWriter);
+ }
+ }
+ }
+
+ private final class KeyStoreService extends Service {
+ private KeyStoreService() {
+ super(OpenDJProvider.this, "KeyStore", "LDAP", KeyStoreImpl.class.getName(), singletonList("OpenDJ"), null);
+ }
+
+ // Override the default constructor so that we can pass in this provider and any file based configuration.
+ @Override
+ public Object newInstance(final Object constructorParameter) {
+ return new KeyStoreImpl(OpenDJProvider.this);
+ }
+ }
+}
diff --git a/opendj-core/src/main/java/org/forgerock/opendj/security/OpenDJProviderSchema.java b/opendj-core/src/main/java/org/forgerock/opendj/security/OpenDJProviderSchema.java
new file mode 100644
index 0000000..6112684
--- /dev/null
+++ b/opendj-core/src/main/java/org/forgerock/opendj/security/OpenDJProviderSchema.java
@@ -0,0 +1,129 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.security;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.unmodifiableSet;
+import static org.forgerock.opendj.ldap.schema.Schema.getCoreSchema;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import org.forgerock.opendj.ldap.schema.AttributeType;
+import org.forgerock.opendj.ldap.schema.ObjectClass;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldap.schema.SchemaBuilder;
+import org.forgerock.opendj.ldif.LDIFEntryReader;
+
+/** Utility methods for accessing the LDAP schema elements required in order to support the OpenDJ security provider. */
+public final class OpenDJProviderSchema {
+ // Minimal schema for required for interacting with the LDAP key store.
+ private static final URL SCHEMA_LDIF_URL = OpenDJProviderSchema.class.getResource("03-keystore.ldif");
+ static final Schema SCHEMA;
+ private static final Set<ObjectClass> OBJECT_CLASSES;
+ private static final Set<AttributeType> ATTRIBUTE_TYPES;
+ // Object classes.
+ static final String OC_KEY_STORE_OBJECT = "ds-keystore-object";
+ static final String OC_TRUSTED_CERTIFICATE = "ds-keystore-trusted-certificate";
+ static final String OC_PRIVATE_KEY = "ds-keystore-private-key";
+ static final String OC_SECRET_KEY = "ds-keystore-secret-key";
+ // Attribute types.
+ static final String ATTR_ALIAS = "ds-keystore-alias";
+ static final String ATTR_KEY_ALGORITHM = "ds-keystore-key-algorithm";
+ static final String ATTR_KEY = "ds-keystore-key";
+ static final String ATTR_CERTIFICATE_CHAIN = "ds-keystore-certificate-chain";
+ private static final String ATTR_CERTIFICATE = "ds-keystore-certificate";
+ static final String ATTR_CERTIFICATE_BINARY = ATTR_CERTIFICATE + ";binary";
+ // Standard attribute types.
+ static final String ATTR_OBJECT_CLASS = "objectClass";
+ static final String ATTR_MODIFY_TIME_STAMP = "modifyTimeStamp";
+ static final String ATTR_CREATE_TIME_STAMP = "createTimeStamp";
+
+ static {
+ try (final InputStream inputStream = SCHEMA_LDIF_URL.openStream();
+ final LDIFEntryReader reader = new LDIFEntryReader(inputStream)) {
+ SCHEMA = new SchemaBuilder(getCoreSchema())
+ .addSchema(reader.readEntry(), false)
+ .toSchema()
+ .asNonStrictSchema();
+ } catch (final IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ OBJECT_CLASSES = unmodifiableSet(new LinkedHashSet<>(asList(SCHEMA.getObjectClass(OC_KEY_STORE_OBJECT),
+ SCHEMA.getObjectClass(OC_TRUSTED_CERTIFICATE),
+ SCHEMA.getObjectClass(OC_SECRET_KEY),
+ SCHEMA.getObjectClass(OC_PRIVATE_KEY))));
+
+ ATTRIBUTE_TYPES = unmodifiableSet(new LinkedHashSet<>(asList(SCHEMA.getAttributeType(ATTR_ALIAS),
+ SCHEMA.getAttributeType(ATTR_KEY_ALGORITHM),
+ SCHEMA.getAttributeType(ATTR_KEY),
+ SCHEMA.getAttributeType(ATTR_CERTIFICATE_CHAIN),
+ SCHEMA.getAttributeType(ATTR_CERTIFICATE))));
+ }
+
+ /**
+ * Returns the set of LDAP object classes required in order to support the OpenDJ security provider.
+ *
+ * @return The set of LDAP object classes required in order to support the OpenDJ security provider.
+ */
+ public static Set<ObjectClass> getObjectClasses() {
+ return OBJECT_CLASSES;
+ }
+
+ /**
+ * Returns the set of LDAP attribute types required in order to support the OpenDJ security provider.
+ *
+ * @return The set of LDAP attribute types required in order to support the OpenDJ security provider.
+ */
+ public static Set<AttributeType> getAttributeTypes() {
+ return ATTRIBUTE_TYPES;
+ }
+
+ /**
+ * Returns a URL referencing a resource containing the LDIF schema that is required in order to support the
+ * OpenDJ security provider.
+ *
+ * @return The URL referencing the LDIF schema.
+ */
+ public static URL getSchemaLDIFResource() {
+ return SCHEMA_LDIF_URL;
+ }
+
+ /**
+ * Adds the schema elements required by the OpenDJ security provider to the provided schema builder.
+ *
+ * @param builder
+ * The schema builder to which the schema elements should be added.
+ * @return The schema builder.
+ */
+ public static SchemaBuilder addOpenDJProviderSchema(final SchemaBuilder builder) {
+ for (final AttributeType attributeType : ATTRIBUTE_TYPES) {
+ builder.buildAttributeType(attributeType).addToSchema();
+ }
+ for (final ObjectClass objectClass : OBJECT_CLASSES) {
+ builder.buildObjectClass(objectClass).addToSchema();
+ }
+ return builder;
+ }
+
+ private OpenDJProviderSchema() {
+ // Prevent instantiation.
+ }
+}
diff --git a/opendj-core/src/main/java/org/forgerock/opendj/security/package-info.java b/opendj-core/src/main/java/org/forgerock/opendj/security/package-info.java
new file mode 100644
index 0000000..11ed6e7
--- /dev/null
+++ b/opendj-core/src/main/java/org/forgerock/opendj/security/package-info.java
@@ -0,0 +1,111 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+
+/**
+ * An LDAP based security provider having the name "OpenDJ" and exposing an LDAP/LDIF based {@link
+ * java.security.KeyStore KeyStore} service. The key store has the type "LDAP" and alias "OPENDJ" and can be created
+ * using a number of approaches. Firstly, by directly calling one of the factory methods in {@link
+ * org.forgerock.opendj.security.OpenDJProvider}:
+ * <p>
+ * <pre>
+ * ConnectionFactory ldapServer = ...;
+ * DN keyStoreBaseDN = DN.valueOf("ou=key store,dc=example,dc=com");
+ * Options options = Options.defaultOptions();
+ *
+ * KeyStore ldapKeyStore = OpenDJProvider.newLDAPKeyStore(ldapServer, keyStoreBaseDN, options);
+ * </pre>
+ * <p>
+ * Alternatively, if the OpenDJ security provider is registered with the JVM's JCA framework together with a suitable
+ * configuration file, then an LDAP key store can be created like this:
+ * <p>
+ * <pre>
+ * KeyStore ldapKeyStore = KeyStore.getInstance("LDAP");
+ * ldapKeyStore.load(null);
+ * </pre>
+ * <p>
+ * The configuration file should be specified as the provider argument in the JVM's security configuration. It supports
+ * the following options:
+ * <pre>
+ * # If this option is set then the LDAP key store will be LDIF file based. This is useful for testing.
+ * org.forgerock.opendj.security.ldif=/path/to/keystore.ldif
+ *
+ * # Otherwise use LDAP. Note that only a simple single-server configuration is supported for now since applications
+ * # are expected to configure directly using KeyStore.load(KeyStoreParameters).
+ * org.forgerock.opendj.security.host=localhost
+ * org.forgerock.opendj.security.port=1389
+ * org.forgerock.opendj.security.bindDN=cn=directory manager
+ * org.forgerock.opendj.security.bindPassword=password
+ *
+ * # The base DN beneath which key store entries will be located.
+ * org.forgerock.opendj.security.keyStoreBaseDN=ou=key store,dc=example,dc=com
+ * </pre>
+ * <p>
+ * Interacting with an LDAP/LDIF key store using Java's "keytool" command is a little complicated if the OpenDJ provider
+ * is not configured in the JVM due to the need to specify the class-path:
+ * <p>
+ * <pre>
+ * # Generate an RSA private key entry:
+ * keytool -J-cp -J/path/to/opendj/server/lib/bootstrap-client.jar \
+ * -providerName OpenDJ -providerClass org.forgerock.opendj.security.OpenDJProvider \
+ * -providerArg /path/to/keystore.conf \
+ * -storetype LDAP -keystore NONE -storepass changeit -keypass changeit \
+ * -genkey -alias "private-key" -keyalg rsa \
+ * -ext "san=dns:localhost.example.com" \
+ * -dname "CN=localhost.example.com,O=Example Corp,C=FR"
+ *
+ * # Generate an AES secret key entry:
+ * keytool -J-cp -J/path/to/opendj/server/lib/bootstrap-client.jar \
+ * -providerName OpenDJ -providerClass org.forgerock.opendj.security.OpenDJProvider \
+ * -providerArg /path/to/keystore.conf \
+ * -storetype LDAP -keystore NONE -storepass changeit -keypass changeit \
+ * -genseckey -alias "secret-key" -keyalg AES -keysize 128
+ *
+ * # Import a trusted certificate from raw ASN1 content:
+ * keytool -J-cp -J/path/to/opendj/server/lib/bootstrap-client.jar \
+ * -providerName OpenDJ -providerClass org.forgerock.opendj.security.OpenDJProvider \
+ * -providerArg /path/to/keystore.conf \
+ * -storetype LDAP -keystore NONE -storepass changeit -keypass changeit \
+ * -importcert -alias "trusted-cert" -file /path/to/cert.crt
+ *
+ * # Import a trusted certificate from PEM file:
+ * keytool -J-cp -J/path/to/opendj/server/lib/bootstrap-client.jar \
+ * -providerName OpenDJ -providerClass org.forgerock.opendj.security.OpenDJProvider \
+ * -providerArg /path/to/keystore.conf \
+ * -storetype LDAP -keystore NONE -storepass changeit -keypass changeit \
+ * -importcert -alias "trusted-cert" -file /path/to/cert.pem
+ *
+ * # List the contents of the key store:
+ * keytool -J-cp -J/path/to/opendj/server/lib/bootstrap-client.jar \
+ * -providerName OpenDJ -providerClass org.forgerock.opendj.security.OpenDJProvider \
+ * -providerArg /path/to/keystore.conf \
+ * -storetype LDAP -keystore NONE -storepass changeit -keypass changeit \
+ * -list -v
+ * </pre>
+ * <p>
+ * The LDAP key store will store objects in entries directly beneath the key store's base DN. The base DN entry is
+ * expected to already exist. Private key and secret key entries are protected by 128-bit AES symmetric key derived
+ * using PBKDF2 from the key's password, if provided, and the key store's global password, if provided. If both
+ * passwords are provided then the keys will be encrypted twice. This does not provide additional protection but does
+ * provide more control over access to a single key store. For example, multiple applications may be able to access a
+ * single key store, with each application protecting their sensitive data using their individual password.
+ * <p>
+ * The LDAP schema used for the key store is contained in this JAR as resource and can be obtained using {@link
+ * org.forgerock.opendj.security.OpenDJProviderSchema#getSchemaLDIFResource()}. Alternatively, clients may build a
+ * {@link org.forgerock.opendj.ldap.schema.Schema Schema} using the method
+ * {@link org.forgerock.opendj.security.OpenDJProviderSchema#addOpenDJProviderSchema}.
+ */
+package org.forgerock.opendj.security;
+
diff --git a/opendj-core/src/main/resources/com/forgerock/opendj/security/keystore.properties b/opendj-core/src/main/resources/com/forgerock/opendj/security/keystore.properties
new file mode 100644
index 0000000..810197d
--- /dev/null
+++ b/opendj-core/src/main/resources/com/forgerock/opendj/security/keystore.properties
@@ -0,0 +1,38 @@
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2016 ForgeRock AS.
+
+KEYSTORE_DECODE_KEY_MISSING_PWD=The key cannot be decoded because it has been protected with a global key \
+ store password and/or key password, but neither have been provided
+KEYSTORE_DECODE_KEY_MISSING_KEYSTORE_EXT=The key cannot be decoded because it has been protected with an external \
+ global key store protection mechanism, but no mechanism has been configured
+KEYSTORE_DECODE_KEYSTORE_DECRYPT_FAILURE=The key cannot be decrypted using the provided global key \
+ store password and/or key password. The password(s) are probably incorrect
+KEYSTORE_DECODE_UNSUPPORTED_VERSION=The key cannot be decoded because its encoding version %d is not supported
+KEYSTORE_DECODE_MALFORMED=The key cannot be decoded because it is malformed
+KEYSTORE_DECODE_BAD_PADDING=The key cannot be decoded because it contained invalid padding after being decrypted
+KEYSTORE_UNSUPPORTED_CIPHER=The key could not be encrypted or decrypted because the JVM does not support the '%s' cipher
+KEYSTORE_UNSUPPORTED_KF=The key could not be encrypted or decrypted because the JVM does not support the '%s' algorithm
+KEYSTORE_UNSUPPORTED_KF_ARGS=The key could not be encrypted or decrypted because the JVM does not support '%s' with \
+ %d iterations and %d bits
+KEYSTORE_UNRECOGNIZED_OBJECT_CLASS=The key store entry '%s' could not be parsed because it does not contain a \
+ recognized object class
+KEYSTORE_ENTRY_MALFORMED=The key store entry '%s' could not be parsed because it contains missing or malformed \
+ attributes
+KEYSTORE_KEY_ENTRY_ALREADY_EXISTS=The trusted certificate '%s' could not be added to the key store because there \
+ is already a private or secret key entry with the same alias
+KEYSTORE_DELETE_FAILURE=An unexpected error occurred while attempting to remove the key store entry '%s'
+KEYSTORE_READ_FAILURE=An unexpected error occurred while accessing the key store
+KEYSTORE_READ_ALIAS_FAILURE=An unexpected error occurred while attempting to read the key store entry '%s'
+KEYSTORE_UPDATE_ALIAS_FAILURE=An unexpected error occurred while attempting to update the key store entry '%s'
diff --git a/opendj-core/src/main/resources/org/forgerock/opendj/security/03-keystore.ldif b/opendj-core/src/main/resources/org/forgerock/opendj/security/03-keystore.ldif
new file mode 100644
index 0000000..9036bab
--- /dev/null
+++ b/opendj-core/src/main/resources/org/forgerock/opendj/security/03-keystore.ldif
@@ -0,0 +1,81 @@
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2016 ForgeRock AS.
+
+# This file contains the attribute type and object class definitions for use
+# with LDAP based key stores.
+#
+# WARNING: this file MUST exists in both the SDK and the server. The two copies must be synchronized.
+#
+dn: cn=schema
+objectClass: top
+objectClass: ldapSubentry
+objectClass: subschema
+attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.190
+ NAME 'ds-keystore-alias'
+ EQUALITY caseExactMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+ SINGLE-VALUE
+ X-ORIGIN 'OpenDJ Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.191
+ NAME 'ds-keystore-certificate'
+ EQUALITY certificateExactMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.8
+ SINGLE-VALUE
+ X-ORIGIN 'OpenDJ Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.193
+ NAME 'ds-keystore-key-algorithm'
+ EQUALITY caseIgnoreMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+ SINGLE-VALUE
+ X-ORIGIN 'OpenDJ Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.194
+ NAME 'ds-keystore-key'
+ EQUALITY octetStringMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.40
+ SINGLE-VALUE
+ X-ORIGIN 'OpenDJ Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.195
+ NAME 'ds-keystore-certificate-chain'
+ EQUALITY octetStringMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.40
+ SINGLE-VALUE
+ X-ORIGIN 'OpenDJ Directory Server' )
+objectClasses: ( 1.3.6.1.4.1.36733.2.1.2.47
+ NAME 'ds-keystore-object'
+ SUP top
+ ABSTRACT
+ MUST ds-keystore-alias
+ X-ORIGIN 'OpenDJ Directory Server' )
+objectClasses: ( 1.3.6.1.4.1.36733.2.1.2.48
+ NAME 'ds-keystore-trusted-certificate'
+ SUP ds-keystore-object
+ STRUCTURAL
+ MUST ds-keystore-certificate
+ X-ORIGIN 'OpenDJ Directory Server' )
+objectClasses: ( 1.3.6.1.4.1.36733.2.1.2.49
+ NAME 'ds-keystore-private-key'
+ SUP ds-keystore-object
+ STRUCTURAL
+ MUST ( ds-keystore-key $
+ ds-keystore-key-algorithm $
+ ds-keystore-certificate )
+ MAY ds-keystore-certificate-chain
+ X-ORIGIN 'OpenDJ Directory Server' )
+objectClasses: ( 1.3.6.1.4.1.36733.2.1.2.50
+ NAME 'ds-keystore-secret-key'
+ SUP ds-keystore-object
+ STRUCTURAL
+ MUST ( ds-keystore-key $
+ ds-keystore-key-algorithm )
+ X-ORIGIN 'OpenDJ Directory Server' )
diff --git a/opendj-core/src/test/java/org/forgerock/opendj/security/KeyProtectorTest.java b/opendj-core/src/test/java/org/forgerock/opendj/security/KeyProtectorTest.java
new file mode 100644
index 0000000..6549f06
--- /dev/null
+++ b/opendj-core/src/test/java/org/forgerock/opendj/security/KeyProtectorTest.java
@@ -0,0 +1,202 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.security;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.forgerock.opendj.security.KeyStoreParameters.EXTERNAL_KEY_WRAPPING_STRATEGY;
+import static org.forgerock.opendj.security.KeyStoreParameters.GLOBAL_PASSWORD;
+import static org.forgerock.opendj.security.KeyStoreTestUtils.PRIVATE_KEY;
+import static org.forgerock.opendj.security.OpenDJProvider.newClearTextPasswordFactory;
+import static org.forgerock.util.Options.defaultOptions;
+
+import java.security.Key;
+
+import javax.crypto.spec.SecretKeySpec;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.SdkTestCase;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+@SuppressWarnings("javadoc")
+public class KeyProtectorTest extends SdkTestCase {
+ // Not multiple of 8 bytes - needs padding for AESWrap.
+ private static final ByteString PLAIN_KEY_NEEDS_PADDING = ByteString.valueOfUtf8("123456781234");
+ // Multiple of 8 bytes - doesn't need padding for AESWrap.
+ private static final ByteString PLAIN_KEY = ByteString.valueOfUtf8("1234567812345678");
+ private static final char[] KEYSTORE_PASSWORD = "keystore password".toCharArray();
+ private static final char[] KEY_PASSWORD = "key password".toCharArray();
+ private static final char[] BAD_PASSWORD = "bad password".toCharArray();
+ // Fake external protection mechanism which just performs base 64 encoding/decoding.
+ private static final ExternalKeyWrappingStrategy TEST_STRATEGY = new ExternalKeyWrappingStrategy() {
+ @Override
+ public ByteSequence wrapKey(final ByteSequence unwrappedKey) throws LocalizedKeyStoreException {
+ return ByteString.valueOfUtf8(unwrappedKey.toBase64String());
+ }
+
+ @Override
+ public ByteSequence unwrapKey(final ByteSequence wrappedKey) throws LocalizedKeyStoreException {
+ return ByteString.valueOfBase64(wrappedKey.toString());
+ }
+ };
+
+ @BeforeClass
+ public void sanityCheckTestFramework() throws Exception {
+ assertThat(encodedKeyIsEncrypted(PLAIN_KEY)).isFalse();
+ assertThat(encodedKeyIsEncrypted(new ByteStringBuilder().appendBytes(PLAIN_KEY)
+ .appendUtf8("tail")
+ .toByteString())).isFalse();
+ assertThat(encodedKeyIsEncrypted(new ByteStringBuilder().appendUtf8("head")
+ .appendBytes(PLAIN_KEY)
+ .toByteString())).isFalse();
+ assertThat(encodedKeyIsEncrypted(new ByteStringBuilder().appendUtf8("head")
+ .appendBytes(PLAIN_KEY)
+ .appendUtf8("tail")
+ .toByteString())).isFalse();
+ assertThat(encodedKeyIsEncrypted(ByteString.valueOfUtf8("different"))).isTrue();
+ }
+
+ @Test
+ public void shouldEncodeAndDecodeWithNoProtection() throws Exception {
+ final KeyProtector kp = new KeyProtector(defaultOptions());
+ final ByteString encodedKey = kp.encodeKey(secretKey(PLAIN_KEY), null);
+ assertThat(encodedKey).isNotEqualTo(PLAIN_KEY);
+ assertThat(encodedKeyIsEncrypted(encodedKey)).isFalse();
+ assertThat(kp.decodeSecretKey(encodedKey, "RAW", null).getEncoded()).isEqualTo(PLAIN_KEY.toByteArray());
+ }
+
+ @Test
+ public void shouldEncodeAndDecodeWithIndividualPassword() throws Exception {
+ shouldEncodeAndDecodeAndBeEncrypted(new KeyProtector(defaultOptions()), KEY_PASSWORD);
+ }
+
+ @Test
+ public void shouldEncodeAndDecodeWithKeyStorePasswordOnly() throws Exception {
+ shouldEncodeAndDecodeAndBeEncrypted(forPasswordProtectedKeyStore(KEYSTORE_PASSWORD), null);
+ }
+
+ private static KeyProtector forPasswordProtectedKeyStore(final char[] keystorePassword) {
+ return new KeyProtector(defaultOptions().set(GLOBAL_PASSWORD, newClearTextPasswordFactory(keystorePassword)));
+ }
+
+ @Test
+ public void shouldEncodeAndDecodeWithKeyStorePasswordAndIndividualPassword() throws Exception {
+ shouldEncodeAndDecodeAndBeEncrypted(forPasswordProtectedKeyStore(KEYSTORE_PASSWORD), KEY_PASSWORD);
+ }
+
+ private static KeyProtector forExternallyProtectedKeyStore() {
+ return new KeyProtector(defaultOptions().set(EXTERNAL_KEY_WRAPPING_STRATEGY, TEST_STRATEGY));
+ }
+
+ @Test
+ public void shouldEncodeAndDecodeWithExternalMechanismOnly() throws Exception {
+ shouldEncodeAndDecodeAndBeEncrypted(forExternallyProtectedKeyStore(), null);
+ }
+
+ @Test
+ public void shouldEncodeAndDecodeWithExternalMechanismAndIndividualPassword() throws Exception {
+ shouldEncodeAndDecodeAndBeEncrypted(forExternallyProtectedKeyStore(), KEY_PASSWORD);
+ }
+
+ @Test(expectedExceptions = LocalizedKeyStoreException.class)
+ public void shouldFailWhenDecodeWithWrongIndividualPassword() throws Exception {
+ final KeyProtector kpEncode = forPasswordProtectedKeyStore(KEYSTORE_PASSWORD);
+ final ByteString encodedKey = kpEncode.encodeKey(secretKey(PLAIN_KEY), KEY_PASSWORD);
+ final KeyProtector kpDecode = forPasswordProtectedKeyStore(KEYSTORE_PASSWORD);
+ kpDecode.decodeSecretKey(encodedKey, "RAW", BAD_PASSWORD);
+ }
+
+ @Test(expectedExceptions = LocalizedKeyStoreException.class)
+ public void shouldFailWhenDecodeWithWrongKeyStorePassword() throws Exception {
+ final KeyProtector kpEncode = forPasswordProtectedKeyStore(KEYSTORE_PASSWORD);
+ final ByteString encodedKey = kpEncode.encodeKey(secretKey(PLAIN_KEY), KEY_PASSWORD);
+ final KeyProtector kpDecode = forPasswordProtectedKeyStore(BAD_PASSWORD);
+ kpDecode.decodeSecretKey(encodedKey, "RAW", KEY_PASSWORD);
+ }
+
+ @Test(expectedExceptions = LocalizedKeyStoreException.class)
+ public void shouldFailWhenDecodeWithMissingKeyStorePassword() throws Exception {
+ final KeyProtector kpEncode = forPasswordProtectedKeyStore(KEYSTORE_PASSWORD);
+ final ByteString encodedKey = kpEncode.encodeKey(secretKey(PLAIN_KEY), KEY_PASSWORD);
+ final KeyProtector kpDecode = forPasswordProtectedKeyStore(null);
+ kpDecode.decodeSecretKey(encodedKey, "RAW", KEY_PASSWORD);
+ }
+
+ @Test(expectedExceptions = LocalizedKeyStoreException.class)
+ public void shouldFailWhenDecodeWithMissingIndividualPassword() throws Exception {
+ final KeyProtector kpEncode = forPasswordProtectedKeyStore(KEYSTORE_PASSWORD);
+ final ByteString encodedKey = kpEncode.encodeKey(secretKey(PLAIN_KEY), KEY_PASSWORD);
+ final KeyProtector kpDecode = forPasswordProtectedKeyStore(KEYSTORE_PASSWORD);
+ kpDecode.decodeSecretKey(encodedKey, "RAW", null);
+ }
+
+ @Test(expectedExceptions = LocalizedKeyStoreException.class)
+ public void shouldFailWhenDecodeWithMissingPasswords() throws Exception {
+ final KeyProtector kpEncode = forPasswordProtectedKeyStore(KEYSTORE_PASSWORD);
+ final ByteString encodedKey = kpEncode.encodeKey(secretKey(PLAIN_KEY), KEY_PASSWORD);
+ final KeyProtector kpDecode = forPasswordProtectedKeyStore(null);
+ kpDecode.decodeSecretKey(encodedKey, "RAW", null);
+ }
+
+ @Test(expectedExceptions = LocalizedKeyStoreException.class)
+ public void shouldFailWhenDecodeWithMalformedEncodedKey() throws Exception {
+ final KeyProtector kp = forPasswordProtectedKeyStore(KEYSTORE_PASSWORD);
+ kp.decodeSecretKey(ByteString.valueOfUtf8("malformed encoded key"), "RAW", KEY_PASSWORD);
+ }
+
+ @Test
+ public void shouldEncodeAndDecodeKeysThatNeedPadding() throws Exception {
+ final KeyProtector kp = forPasswordProtectedKeyStore(KEYSTORE_PASSWORD);
+ final ByteString encodedKey = kp.encodeKey(secretKey(PLAIN_KEY_NEEDS_PADDING), KEY_PASSWORD);
+ assertThat(encodedKey).isNotEqualTo(PLAIN_KEY_NEEDS_PADDING);
+ final byte[] decodedKey = kp.decodeSecretKey(encodedKey, "RAW", KEY_PASSWORD).getEncoded();
+ assertThat(decodedKey).isEqualTo(PLAIN_KEY_NEEDS_PADDING.toByteArray());
+ }
+
+ @Test
+ public void shouldEncodeAndDecodePrivateKeys() throws Exception {
+ final KeyProtector kp = forPasswordProtectedKeyStore(KEYSTORE_PASSWORD);
+ final ByteString encodedKey = kp.encodeKey(PRIVATE_KEY, KEY_PASSWORD);
+ assertThat(encodedKey).isNotEqualTo(ByteString.wrap(PRIVATE_KEY.getEncoded()));
+ final byte[] decodedKey = kp.decodePrivateKey(encodedKey, "RSA", KEY_PASSWORD).getEncoded();
+ assertThat(decodedKey).isEqualTo(PRIVATE_KEY.getEncoded());
+ }
+
+ private void shouldEncodeAndDecodeAndBeEncrypted(final KeyProtector kp, final char[] password) throws Exception {
+ final ByteString encodedKey = kp.encodeKey(secretKey(PLAIN_KEY), password);
+ assertThat(encodedKey).isNotEqualTo(PLAIN_KEY);
+ assertThat(encodedKeyIsEncrypted(encodedKey)).isTrue();
+ assertThat(kp.decodeSecretKey(encodedKey, "RAW", password).getEncoded()).isEqualTo(PLAIN_KEY.toByteArray());
+ }
+
+ // Best effort check to ensure that the encoded content does not contain the clear text representation of the key.
+ private boolean encodedKeyIsEncrypted(final ByteString encodedKey) {
+ final int end = encodedKey.length() - PLAIN_KEY.length();
+ for (int i = 0; i <= end; i++) {
+ final ByteString subSequence = encodedKey.subSequence(i, i + PLAIN_KEY.length());
+ if (subSequence.equals(PLAIN_KEY)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static Key secretKey(final ByteString rawKey) {
+ return new SecretKeySpec(rawKey.toByteArray(), "RAW");
+ }
+}
diff --git a/opendj-core/src/test/java/org/forgerock/opendj/security/KeyStoreImplTest.java b/opendj-core/src/test/java/org/forgerock/opendj/security/KeyStoreImplTest.java
new file mode 100644
index 0000000..3350fbe
--- /dev/null
+++ b/opendj-core/src/test/java/org/forgerock/opendj/security/KeyStoreImplTest.java
@@ -0,0 +1,340 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.security;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.forgerock.opendj.ldap.Connections.newInternalConnectionFactory;
+import static org.forgerock.opendj.security.KeyStoreParameters.GLOBAL_PASSWORD;
+import static org.forgerock.opendj.security.KeyStoreParameters.newKeyStoreParameters;
+import static org.forgerock.opendj.security.KeyStoreTestUtils.*;
+import static org.forgerock.opendj.security.OpenDJProvider.newClearTextPasswordFactory;
+import static org.forgerock.util.Options.defaultOptions;
+import static org.mockito.Mockito.mock;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.Key;
+import java.security.KeyStore;
+import java.security.KeyStore.LoadStoreParameter;
+import java.security.KeyStore.PasswordProtection;
+import java.security.KeyStore.PrivateKeyEntry;
+import java.security.KeyStore.SecretKeyEntry;
+import java.security.KeyStore.TrustedCertificateEntry;
+import java.security.PrivateKey;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+
+import javax.crypto.SecretKey;
+
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.MemoryBackend;
+import org.forgerock.opendj.ldap.SdkTestCase;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.util.Options;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+@SuppressWarnings("javadoc")
+public class KeyStoreImplTest extends SdkTestCase {
+ private MemoryBackend backend;
+ private KeyStore keyStore;
+
+ @BeforeClass
+ public void beforeClass() {
+ Schema.setDefaultSchema(OpenDJProviderSchema.SCHEMA);
+ }
+
+ @BeforeMethod
+ public void beforeMethod() throws Exception {
+ backend = createKeyStoreMemoryBackend();
+ keyStore = createKeyStore(backend);
+ }
+
+ // Test key store operations.
+
+ @Test
+ public void getProviderShouldReturnOpenDJProvider() {
+ assertThat(keyStore.getProvider()).isInstanceOf(OpenDJProvider.class);
+ }
+
+ @Test
+ public void getTypeShouldReturnLDAP() {
+ assertThat(keyStore.getType()).isEqualTo("LDAP");
+ }
+
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void storeShouldThrowWhenOutputStreamIsNotNull() throws Exception {
+ keyStore.store(mock(OutputStream.class), null);
+ }
+
+ @Test
+ public void storeShouldBeNoOpWhenOutputStreamIsNull() throws Exception {
+ keyStore.store(null, null);
+ }
+
+ @Test
+ public void storeShouldBeNoOp() throws Exception {
+ keyStore.store(null);
+ }
+
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void loadWithNonNullInputStreamShouldThrow() throws Exception {
+ final KeyStore keyStore = KeyStore.getInstance("LDAP", new OpenDJProvider());
+ keyStore.load(mock(InputStream.class), null);
+ }
+
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void loadWithNullInputStreamShouldThrowWhenNoProviderConfig() throws Exception {
+ final KeyStore keyStore = KeyStore.getInstance("LDAP", new OpenDJProvider());
+ keyStore.load(null, null);
+ }
+
+ @Test
+ public void loadWithNullInputStreamShouldUseProviderConfig() throws Exception {
+ final ConnectionFactory factory = newInternalConnectionFactory(backend);
+ final Options options = defaultOptions().set(GLOBAL_PASSWORD, newClearTextPasswordFactory(KEYSTORE_PASSWORD));
+ final KeyStoreParameters config = newKeyStoreParameters(factory, KEYSTORE_DN, options);
+ final OpenDJProvider provider = new OpenDJProvider(config);
+ final KeyStore keyStore = KeyStore.getInstance("LDAP", provider);
+
+ keyStore.load(null, null);
+
+ assertThat(keyStore.size()).isEqualTo(0);
+ assertThat(backend.size()).isEqualTo(1);
+ keyStore.setKeyEntry(TEST_ALIAS, createSecretKey(), KEY_PASSWORD, null);
+ assertThat(keyStore.size()).isEqualTo(1);
+ assertThat(backend.size()).isEqualTo(2);
+ }
+
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void loadWithNullLoadStoreParameterShouldThrowWhenNoProviderConfig() throws Exception {
+ final KeyStore keyStore = KeyStore.getInstance("LDAP", new OpenDJProvider());
+ keyStore.load(null);
+ }
+
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void loadWithNullLoadStoreParameterShouldThrowWhenParametersHaveWrongType() throws Exception {
+ final KeyStore keyStore = KeyStore.getInstance("LDAP", new OpenDJProvider());
+ keyStore.load(mock(LoadStoreParameter.class));
+ }
+
+ @Test
+ public void loadWithNullLoadStoreParameterShouldUseProviderConfig() throws Exception {
+ final ConnectionFactory factory = newInternalConnectionFactory(backend);
+ final Options options = defaultOptions().set(GLOBAL_PASSWORD, newClearTextPasswordFactory(KEYSTORE_PASSWORD));
+ final KeyStoreParameters config = newKeyStoreParameters(factory, KEYSTORE_DN, options);
+ final OpenDJProvider provider = new OpenDJProvider(config);
+ final KeyStore keyStore = KeyStore.getInstance("LDAP", provider);
+
+ keyStore.load(null);
+
+ assertThat(keyStore.size()).isEqualTo(0);
+ assertThat(backend.size()).isEqualTo(1);
+ keyStore.setKeyEntry(TEST_ALIAS, createSecretKey(), KEY_PASSWORD, null);
+ assertThat(keyStore.size()).isEqualTo(1);
+ assertThat(backend.size()).isEqualTo(2);
+ }
+
+ @Test
+ public void loadWithNonNullLoadStoreParameterShouldNotUseProviderConfig() throws Exception {
+ final ConnectionFactory factory = newInternalConnectionFactory(backend);
+ final Options options = defaultOptions().set(GLOBAL_PASSWORD, newClearTextPasswordFactory(KEYSTORE_PASSWORD));
+ final KeyStoreParameters config = newKeyStoreParameters(factory, KEYSTORE_DN, options);
+ final KeyStore keyStore = KeyStore.getInstance("LDAP", new OpenDJProvider());
+
+ keyStore.load(config);
+
+ assertThat(keyStore.size()).isEqualTo(0);
+ assertThat(backend.size()).isEqualTo(1);
+ keyStore.setKeyEntry(TEST_ALIAS, createSecretKey(), KEY_PASSWORD, null);
+ assertThat(keyStore.size()).isEqualTo(1);
+ assertThat(backend.size()).isEqualTo(2);
+ }
+
+ // Test keys and certificate management happy paths.
+
+ @Test
+ public void secretKeysCanBeStoredAndRetrieved() throws Exception {
+ final SecretKey key = createSecretKey();
+ keyStore.setKeyEntry(TEST_ALIAS, key, KEY_PASSWORD, null);
+
+ final Key retrievedKey = keyStore.getKey(TEST_ALIAS, KEY_PASSWORD);
+ assertThat(retrievedKey).isNotNull();
+ assertThat(retrievedKey).isInstanceOf(SecretKey.class);
+ assertThat(retrievedKey.getAlgorithm()).isEqualTo(key.getAlgorithm());
+ assertThat(retrievedKey.getFormat()).isEqualTo(key.getFormat());
+ assertThat(retrievedKey.getEncoded()).isEqualTo(key.getEncoded());
+
+ assertThat(keyStore.size()).isEqualTo(1);
+ assertThat(Collections.list(keyStore.aliases())).containsExactly(TEST_ALIAS);
+ assertThat(keyStore.containsAlias(TEST_ALIAS));
+ assertThat(keyStore.getCertificate(TEST_ALIAS)).isNull();
+ assertThat(keyStore.getCertificateChain(TEST_ALIAS)).isNull();
+ assertThat(keyStore.entryInstanceOf(TEST_ALIAS, SecretKeyEntry.class));
+ assertThat(keyStore.getCreationDate(TEST_ALIAS)).isNotNull();
+ assertThat(keyStore.getEntry(TEST_ALIAS, newPasswordProtection())).isInstanceOf(SecretKeyEntry.class);
+ assertThat(keyStore.isCertificateEntry(TEST_ALIAS)).isFalse();
+ assertThat(keyStore.isKeyEntry(TEST_ALIAS)).isTrue();
+ }
+
+ private static PasswordProtection newPasswordProtection() {
+ return new PasswordProtection(KEY_PASSWORD.clone());
+ }
+
+ @Test
+ public void privateKeysCanBeStoredAndRetrieved() throws Exception {
+ keyStore.setKeyEntry(TEST_ALIAS, PRIVATE_KEY, KEY_PASSWORD, CERTIFICATE_CHAIN);
+
+ final Key retrievedKey = keyStore.getKey(TEST_ALIAS, KEY_PASSWORD);
+ assertThat(retrievedKey).isNotNull();
+ assertThat(retrievedKey).isInstanceOf(PrivateKey.class);
+ assertThat(retrievedKey.getAlgorithm()).isEqualTo(PRIVATE_KEY.getAlgorithm());
+ assertThat(retrievedKey.getFormat()).isEqualTo(PRIVATE_KEY.getFormat());
+ assertThat(retrievedKey.getEncoded()).isEqualTo(PRIVATE_KEY.getEncoded());
+
+ assertThat(keyStore.size()).isEqualTo(1);
+ assertThat(Collections.list(keyStore.aliases())).containsExactly(TEST_ALIAS);
+ assertThat(keyStore.containsAlias(TEST_ALIAS));
+ assertThat(keyStore.getCertificate(TEST_ALIAS)).isSameAs(PUBLIC_KEY_CERTIFICATE);
+ assertThat(keyStore.getCertificateChain(TEST_ALIAS)).isNotSameAs(CERTIFICATE_CHAIN); // should defensive copy
+ assertThat(keyStore.getCertificateChain(TEST_ALIAS)).containsExactly(PUBLIC_KEY_CERTIFICATE);
+ assertThat(keyStore.entryInstanceOf(TEST_ALIAS, PrivateKeyEntry.class));
+ assertThat(keyStore.getCreationDate(TEST_ALIAS)).isNotNull();
+ assertThat(keyStore.getEntry(TEST_ALIAS, newPasswordProtection())).isInstanceOf(PrivateKeyEntry.class);
+ assertThat(keyStore.isCertificateEntry(TEST_ALIAS)).isFalse();
+ assertThat(keyStore.isKeyEntry(TEST_ALIAS)).isTrue();
+ }
+
+ @Test
+ public void trustedCertificatesCanBeStoredAndRetrieved() throws Exception {
+ keyStore.setCertificateEntry(TEST_ALIAS, PUBLIC_KEY_CERTIFICATE);
+
+ final Certificate retrievedCertificate = keyStore.getCertificate(TEST_ALIAS);
+ assertThat(retrievedCertificate).isNotNull();
+ assertThat(retrievedCertificate).isInstanceOf(X509Certificate.class);
+ assertThat(retrievedCertificate).isEqualTo(PUBLIC_KEY_CERTIFICATE);
+
+ assertThat(keyStore.size()).isEqualTo(1);
+ assertThat(Collections.list(keyStore.aliases())).containsExactly(TEST_ALIAS);
+ assertThat(keyStore.containsAlias(TEST_ALIAS));
+ assertThat(keyStore.getCertificateChain(TEST_ALIAS)).isNull();
+ assertThat(keyStore.entryInstanceOf(TEST_ALIAS, TrustedCertificateEntry.class));
+ assertThat(keyStore.getCreationDate(TEST_ALIAS)).isNotNull();
+ assertThat(keyStore.getEntry(TEST_ALIAS, null)).isInstanceOf(TrustedCertificateEntry.class);
+ assertThat(keyStore.isCertificateEntry(TEST_ALIAS)).isTrue();
+ assertThat(keyStore.isKeyEntry(TEST_ALIAS)).isFalse();
+ }
+
+ // Test keys and certificate management edge cases.
+
+ @Test
+ public void getKeyShouldReturnNullWhenAliasUnknown() throws Exception {
+ final Key retrievedKey = keyStore.getKey(TEST_ALIAS, KEY_PASSWORD);
+ assertThat(retrievedKey).isNull();
+ }
+
+ @Test(expectedExceptions = UnrecoverableKeyException.class)
+ public void getKeyShouldThrowWhenPasswordIsMissing() throws Exception {
+ keyStore.setKeyEntry(TEST_ALIAS, createSecretKey(), KEY_PASSWORD, null);
+ keyStore.getKey(TEST_ALIAS, null);
+ }
+
+ @Test(expectedExceptions = UnrecoverableKeyException.class)
+ public void getKeyShouldThrowWhenPasswordIsBad() throws Exception {
+ keyStore.setKeyEntry(TEST_ALIAS, createSecretKey(), KEY_PASSWORD, null);
+ keyStore.getKey(TEST_ALIAS, "bad".toCharArray());
+ }
+
+ @Test
+ public void setKeyEntryWithSecretKeyWithCertChainIsAllowed() throws Exception {
+ keyStore.setKeyEntry(TEST_ALIAS, createSecretKey(), KEY_PASSWORD, CERTIFICATE_CHAIN);
+ assertThat(keyStore.isKeyEntry(TEST_ALIAS)).isTrue();
+ assertThat(keyStore.getCertificate(TEST_ALIAS)).isNull();
+ }
+
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void setKeyEntryShouldThrowWhenPrivateKeyWithoutCertChain() throws Exception {
+ keyStore.setKeyEntry(TEST_ALIAS, PRIVATE_KEY, KEY_PASSWORD, null);
+ }
+
+ @Test(expectedExceptions = UnsupportedOperationException.class)
+ public void setKeyEntryWithPreEncodedKeyIsNotSupported() throws Exception {
+ keyStore.setKeyEntry(TEST_ALIAS, createSecretKey().getEncoded(), null);
+ }
+
+ @Test
+ public void keyStoreCanManageMultipleObjects() throws Exception {
+ final String[] aliases = { "cert1", "cert2", "pkey", "skey1", "skey2" };
+
+ keyStore.setCertificateEntry("cert1", PUBLIC_KEY_CERTIFICATE);
+ keyStore.setCertificateEntry("cert2", TRUSTED_CERTIFICATE);
+ keyStore.setKeyEntry("pkey", PRIVATE_KEY, KEY_PASSWORD, CERTIFICATE_CHAIN);
+ keyStore.setKeyEntry("skey1", createSecretKey(), KEY_PASSWORD, null);
+ keyStore.setKeyEntry("skey2", createSecretKey(), KEY_PASSWORD, null);
+
+ assertThat(Collections.list(keyStore.aliases())).containsOnly(aliases);
+ for (int i = 0; i < aliases.length; i++) {
+ final String alias = aliases[i];
+ assertThat(keyStore.size()).isEqualTo(5 - i);
+ assertThat(keyStore.containsAlias(alias)).isTrue();
+ keyStore.deleteEntry(alias);
+ assertThat(keyStore.containsAlias(alias)).isFalse();
+ }
+ assertThat(keyStore.size()).isEqualTo(0);
+ assertThat(Collections.list(keyStore.aliases())).isEmpty();
+ }
+
+ @Test
+ public void deleteEntryShouldIgnoreMissingAliases() throws Exception {
+ keyStore.deleteEntry("unknown");
+ }
+
+ @Test
+ public void getCertificateAliasShouldPerformCertificateMatchSearches() throws Exception {
+ keyStore.setKeyEntry("privateKey", PRIVATE_KEY, KEY_PASSWORD, CERTIFICATE_CHAIN);
+ keyStore.setCertificateEntry("trustedCertificate", TRUSTED_CERTIFICATE);
+
+ assertThat(keyStore.getCertificateAlias(PUBLIC_KEY_CERTIFICATE)).isEqualTo("privateKey");
+ assertThat(keyStore.getCertificateAlias(TRUSTED_CERTIFICATE)).isEqualTo("trustedCertificate");
+
+ keyStore.deleteEntry("privateKey");
+ assertThat(keyStore.getCertificateAlias(PUBLIC_KEY_CERTIFICATE)).isNull();
+
+ keyStore.deleteEntry("trustedCertificate");
+ assertThat(keyStore.getCertificateAlias(TRUSTED_CERTIFICATE)).isNull();
+ }
+
+ @Test
+ public void setKeyShouldReplaceExistingObjects() throws Exception {
+ keyStore.setKeyEntry(TEST_ALIAS, PRIVATE_KEY, KEY_PASSWORD, CERTIFICATE_CHAIN);
+ assertThat(keyStore.getKey(TEST_ALIAS, KEY_PASSWORD)).isInstanceOf(PrivateKey.class);
+
+ keyStore.setKeyEntry(TEST_ALIAS, createSecretKey(), KEY_PASSWORD, null);
+ assertThat(keyStore.getKey(TEST_ALIAS, KEY_PASSWORD)).isInstanceOf(SecretKey.class);
+ }
+
+ @Test
+ public void setCertificateShouldReplaceExistingCertificates() throws Exception {
+ keyStore.setCertificateEntry(TEST_ALIAS, PUBLIC_KEY_CERTIFICATE);
+ assertThat(keyStore.getCertificate(TEST_ALIAS)).isEqualTo(PUBLIC_KEY_CERTIFICATE);
+
+ keyStore.setCertificateEntry(TEST_ALIAS, TRUSTED_CERTIFICATE);
+ assertThat(keyStore.getCertificate(TEST_ALIAS)).isEqualTo(TRUSTED_CERTIFICATE);
+ }
+}
diff --git a/opendj-core/src/test/java/org/forgerock/opendj/security/KeyStoreObjectTest.java b/opendj-core/src/test/java/org/forgerock/opendj/security/KeyStoreObjectTest.java
new file mode 100644
index 0000000..07cca17
--- /dev/null
+++ b/opendj-core/src/test/java/org/forgerock/opendj/security/KeyStoreObjectTest.java
@@ -0,0 +1,143 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.security;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.forgerock.opendj.ldap.ByteString.valueOfBase64;
+import static org.forgerock.opendj.security.KeyStoreObject.dnOf;
+import static org.forgerock.opendj.security.KeyStoreObject.newKeyObject;
+import static org.forgerock.opendj.security.KeyStoreObject.newTrustedCertificateObject;
+import static org.forgerock.opendj.security.KeyStoreParameters.GLOBAL_PASSWORD;
+import static org.forgerock.opendj.security.KeyStoreTestUtils.*;
+import static org.forgerock.opendj.security.OpenDJProvider.newClearTextPasswordFactory;
+import static org.forgerock.util.Options.defaultOptions;
+
+import java.security.Key;
+import java.security.PrivateKey;
+
+import javax.crypto.SecretKey;
+
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.SdkTestCase;
+import org.testng.annotations.Test;
+
+@SuppressWarnings("javadoc")
+public class KeyStoreObjectTest extends SdkTestCase {
+ private static final KeyProtector KEY_PROTECTOR =
+ new KeyProtector(defaultOptions().set(GLOBAL_PASSWORD, newClearTextPasswordFactory(KEYSTORE_PASSWORD)));
+
+ @Test
+ public void testNewTrustedCertificateEntry() throws Exception {
+ // Check constructed trusted certificate.
+ final KeyStoreObject keyStoreObject = newTrustedCertificateObject(TEST_ALIAS, PUBLIC_KEY_CERTIFICATE);
+ validateTrustedCertificateKeyStoreEntry(keyStoreObject);
+
+ // Check LDAP encoding.
+ final Entry ldapEntry = keyStoreObject.toLDAPEntry(KEYSTORE_DN);
+ assertThat((Object) ldapEntry.getName()).isEqualTo(TEST_DN);
+ assertThat(ldapEntry.parseAttribute("objectClass").asSetOfString())
+ .containsOnly("top", "ds-keystore-object", "ds-keystore-trusted-certificate");
+ assertThat(ldapEntry.parseAttribute("ds-keystore-alias").asString()).isEqualTo(TEST_ALIAS);
+ assertThat(ldapEntry.parseAttribute("ds-keystore-certificate;binary")
+ .asCertificate()).isEqualTo(PUBLIC_KEY_CERTIFICATE);
+ validateTrustedCertificateKeyStoreEntry(KeyStoreObject.valueOf(ldapEntry));
+ }
+
+ private void validateTrustedCertificateKeyStoreEntry(final KeyStoreObject keyStoreObject) throws Exception {
+ assertThat(keyStoreObject.getAlias()).isEqualTo(TEST_ALIAS);
+ assertThat(keyStoreObject.isTrustedCertificate()).isTrue();
+ assertThat(keyStoreObject.getCreationDate()).isNotNull();
+ assertThat(keyStoreObject.getCertificate()).isSameAs(PUBLIC_KEY_CERTIFICATE);
+ assertThat(keyStoreObject.getCertificateChain()).isNull();
+ assertThat(keyStoreObject.getKey(new KeyProtector(defaultOptions()), KEY_PASSWORD)).isNull();
+ }
+
+ @Test
+ public void testNewPrivateKeyEntry() throws Exception {
+ // Check constructed private key.
+ final KeyStoreObject keyStoreObject =
+ newKeyObject(TEST_ALIAS, PRIVATE_KEY, CERTIFICATE_CHAIN, KEY_PROTECTOR, KEY_PASSWORD);
+ validatePrivateKeyStoreEntry(keyStoreObject);
+
+ // Check LDAP encoding.
+ final Entry ldapEntry = keyStoreObject.toLDAPEntry(KEYSTORE_DN);
+ assertThat((Object) ldapEntry.getName()).isEqualTo(TEST_DN);
+ assertThat(ldapEntry.parseAttribute("objectClass").asSetOfString()).containsOnly("top",
+ "ds-keystore-object",
+ "ds-keystore-private-key");
+ assertThat(ldapEntry.parseAttribute("ds-keystore-alias").asString()).isEqualTo(TEST_ALIAS);
+ assertThat(ldapEntry.parseAttribute("ds-keystore-key-algorithm").asString()).isEqualTo("RSA");
+
+ // Just check that these attributes are present for now. Their content will be validated in the next step.
+ assertThat(ldapEntry.containsAttribute("ds-keystore-certificate;binary")).isTrue();
+ assertThat(ldapEntry.containsAttribute("ds-keystore-certificate-chain")).isFalse();
+ assertThat(ldapEntry.containsAttribute("ds-keystore-key")).isTrue();
+ validatePrivateKeyStoreEntry(KeyStoreObject.valueOf(ldapEntry));
+ }
+
+ private void validatePrivateKeyStoreEntry(final KeyStoreObject keyStoreObject) throws Exception {
+ assertThat(keyStoreObject.getAlias()).isEqualTo(TEST_ALIAS);
+ assertThat(keyStoreObject.isTrustedCertificate()).isFalse();
+ assertThat(keyStoreObject.getCreationDate()).isNotNull();
+ assertThat(keyStoreObject.getCertificate()).isEqualTo(PUBLIC_KEY_CERTIFICATE);
+ assertThat(keyStoreObject.getCertificateChain()).containsExactly(CERTIFICATE_CHAIN);
+ final Key privateKey = keyStoreObject.getKey(KEY_PROTECTOR, KEY_PASSWORD);
+ assertThat(privateKey).isInstanceOf(PrivateKey.class);
+ assertThat(privateKey.getAlgorithm()).isEqualTo("RSA");
+ assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
+ assertThat(privateKey.getEncoded()).isEqualTo(valueOfBase64(PRIVATE_KEY_ENCODED_B64).toByteArray());
+ }
+
+ @Test
+ public void testNewSecretKeyEntry() throws Exception {
+ // Check constructed secret key.
+ final SecretKey secretKey = createSecretKey();
+ final KeyStoreObject keyStoreObject =
+ newKeyObject(TEST_ALIAS, secretKey, CERTIFICATE_CHAIN, KEY_PROTECTOR, KEY_PASSWORD);
+ validateSecretKeyStoreEntry(keyStoreObject, secretKey.getEncoded());
+
+ // Check LDAP encoding.
+ final Entry ldapEntry = keyStoreObject.toLDAPEntry(KEYSTORE_DN);
+ assertThat((Object) ldapEntry.getName()).isEqualTo(TEST_DN);
+ assertThat(ldapEntry.parseAttribute("objectClass").asSetOfString()).containsOnly("top",
+ "ds-keystore-object",
+ "ds-keystore-secret-key");
+ assertThat(ldapEntry.parseAttribute("ds-keystore-alias").asString()).isEqualTo(TEST_ALIAS);
+ assertThat(ldapEntry.parseAttribute("ds-keystore-key-algorithm").asString()).isEqualTo("PBKDF2WithHmacSHA1");
+
+ // Just check that these attributes are present for now. Their content will be validated in the next step.
+ assertThat(ldapEntry.containsAttribute("ds-keystore-key")).isTrue();
+ validateSecretKeyStoreEntry(KeyStoreObject.valueOf(ldapEntry), secretKey.getEncoded());
+ }
+
+ private void validateSecretKeyStoreEntry(KeyStoreObject keyStoreObject, byte[] encodedKey) throws Exception {
+ assertThat(keyStoreObject.getAlias()).isEqualTo(TEST_ALIAS);
+ assertThat(keyStoreObject.isTrustedCertificate()).isFalse();
+ assertThat(keyStoreObject.getCreationDate()).isNotNull();
+ assertThat(keyStoreObject.getCertificate()).isNull();
+ assertThat(keyStoreObject.getCertificateChain()).isNull();
+ final Key secretKey = keyStoreObject.getKey(KEY_PROTECTOR, KEY_PASSWORD);
+ assertThat(secretKey).isInstanceOf(SecretKey.class);
+ assertThat(secretKey.getAlgorithm()).isEqualTo("PBKDF2WithHmacSHA1");
+ assertThat(secretKey.getFormat()).isEqualTo("RAW");
+ assertThat(secretKey.getEncoded()).isEqualTo(encodedKey);
+ }
+
+ @Test
+ public void testDnOf() throws Exception {
+ assertThat((Object) dnOf(KEYSTORE_DN, TEST_ALIAS)).isEqualTo(TEST_DN);
+ }
+}
diff --git a/opendj-core/src/test/java/org/forgerock/opendj/security/KeyStoreTestUtils.java b/opendj-core/src/test/java/org/forgerock/opendj/security/KeyStoreTestUtils.java
new file mode 100644
index 0000000..d2d7bb2
--- /dev/null
+++ b/opendj-core/src/test/java/org/forgerock/opendj/security/KeyStoreTestUtils.java
@@ -0,0 +1,145 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.security;
+
+import static org.forgerock.opendj.ldap.ByteString.valueOfBase64;
+import static org.forgerock.opendj.ldap.Connections.newInternalConnectionFactory;
+import static org.forgerock.opendj.ldap.Functions.byteStringToCertificate;
+import static org.forgerock.opendj.security.KeyStoreParameters.GLOBAL_PASSWORD;
+import static org.forgerock.opendj.security.OpenDJProvider.newLDAPKeyStore;
+import static org.forgerock.opendj.security.OpenDJProvider.newClearTextPasswordFactory;
+import static org.forgerock.opendj.security.OpenDJProviderSchema.SCHEMA;
+import static org.forgerock.util.Options.defaultOptions;
+
+import java.io.IOException;
+import java.security.KeyFactory;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+import java.security.cert.Certificate;
+import java.security.spec.KeySpec;
+import java.security.spec.PKCS8EncodedKeySpec;
+
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.MemoryBackend;
+import org.forgerock.opendj.ldif.LDIFEntryReader;
+import org.forgerock.util.Options;
+
+@SuppressWarnings("javadoc")
+final class KeyStoreTestUtils {
+ static final DN KEYSTORE_DN = DN.valueOf("ou=key store,dc=example,dc=com");
+ static final String TEST_ALIAS = "test";
+ static final DN TEST_DN = KEYSTORE_DN.child("ds-keystore-alias", TEST_ALIAS);
+ private static final String TRUSTED_CERTIFICATE_B64 =
+ "MIIENjCCAx6gAwIBAgIDCYLsMA0GCSqGSIb3DQEBCwUAMEcxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMSAwHgYDVQ"
+ + "QDExdSYXBpZFNTTCBTSEEyNTYgQ0EgLSBHMzAeFw0xNjAxMTExNjU3NDFaFw0xNzAxMTIyMzU2MTlaMBoxGDAWBgNVBAMMDy"
+ + "ouZm9yZ2Vyb2NrLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL4349tGBV/t73Dnggfu++adiLCvRd8tm0"
+ + "mWP0l2G6x3lw/oKfwq9qOp57XLmkpVPLhzbaNWL80G9pIPH+db/I8o2+1kwFl/DIcLE/IqVNgCc9ZHEG9Hi0FFPYW18Zi5Sz"
+ + "UaimmTxNGYmKJ/rmUgbX5g34YZ3Pcc8zS+YOCeWFvDa+YKXXHdX1LzDfSzWii6ZYD1xHY4/DFwcg6x9FkNs653U0NJEf/xyb"
+ + "/fvsMbqSwosgLhJ9XBCCxgtOHSjJRKbDajypoFYJfFEuywLiSgx2pfqDl47J6lUKm905nDVQss5uzDgkUAd3VGwc1Ee1+617"
+ + "R6qJ5QYTTKX9YhzTxnv70CAwEAAaOCAVYwggFSMB8GA1UdIwQYMBaAFMOc8/zTRgg0u85Gf6B8W/PiCMtZMFcGCCsGAQUFBw"
+ + "EBBEswSTAfBggrBgEFBQcwAYYTaHR0cDovL2d2LnN5bWNkLmNvbTAmBggrBgEFBQcwAoYaaHR0cDovL2d2LnN5bWNiLmNvbS"
+ + "9ndi5jcnQwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjApBgNVHREEIjAggg8qLmZvcm"
+ + "dlcm9jay5vcmeCDWZvcmdlcm9jay5vcmcwKwYDVR0fBCQwIjAgoB6gHIYaaHR0cDovL2d2LnN5bWNiLmNvbS9ndi5jcmwwDA"
+ + "YDVR0TAQH/BAIwADBBBgNVHSAEOjA4MDYGBmeBDAECATAsMCoGCCsGAQUFBwIBFh5odHRwczovL3d3dy5yYXBpZHNzbC5jb2"
+ + "0vbGVnYWwwDQYJKoZIhvcNAQELBQADggEBAH+gL/akHpj8uRC8KyyNY2NX34OiAskNPr2Z2UhTkYXCWm5B2V0bQaZwF/AbrV"
+ + "Z/EwCSnQYoDg5WrGS6SWhvRAVjJ33EG7jUE4C7q9nyYH8NKzvfdz7w50heRCB5lPpD0gg01VzLSJ7cAY1eP9fhTjFxckDjVp"
+ + "8M/t6cmp3kWpRgamww2SVizoKZtRALdR9Re7acR2EHnzBT1l1R7oNNcyW7jqPzneDZEr/ZWQhVWOAljpgxnGFDO+HAxtiltU"
+ + "E2j4IOwsU7zHsPlZgfYOfyCp/+1QVuIiXLSD9+YWH92wSKi/7z/d4hD8jG8lCUkpmQXkbEw6jMwsRN4bpmyM2c4Gc=";
+ private static final String PUBLIC_KEY_CERTIFICATE_B64 =
+ "MIIDQDCCAiigAwIBAgIEelaEuDANBgkqhkiG9w0BAQsFADBBMQswCQYDVQQGEwJGUjEVMBMGA1UEChMMRXhhbXBsZSBDb3Jw"
+ + "MRswGQYDVQQDExJvcGVuZGouZXhhbXBsZS5jb20wHhcNMTYwOTA1MTU1MDM3WhcNMTYxMjA0MTU1MDM3WjBBMQswCQYDVQQG"
+ + "EwJGUjEVMBMGA1UEChMMRXhhbXBsZSBDb3JwMRswGQYDVQQDExJvcGVuZGouZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEB"
+ + "AQUAA4IBDwAwggEKAoIBAQDh4tTZu1vNvAgDEXpGEvzkl3r4ayGfX7jSqWpjDtSyfbfIW71MiIQ90O64g5hzArHWhOWrgbCA"
+ + "BXHIk9Ad7wn87bWLIoagHQCUQ89QrDKMntvAea66B4RLKJRilNIm07b+mGjEx3FJb2NfoCA2UmLVBKEvYpNHrxv5c//tet+M"
+ + "Vbs7AL74t5ALCTeK99h2m2dmYvraAc7zbneKBdBK+7eIhdjZzrT1ElN8HfCQ4PzZD0cglue8M6V0R993BC8L0h00IHaHKzTM"
+ + "IKEMWUI9ailHON4fYI61BuNcRYyyKUQ1pojadEQ5bqEJ1zf51D6D6dusVKA52EAC+KPa0oHvdxlnAgMBAAGjQDA+MB0GA1Ud"
+ + "EQQWMBSCEm9wZW5kai5leGFtcGxlLmNvbTAdBgNVHQ4EFgQUKtFrvmqMm5M6esHCMR/bI7l0jqMwDQYJKoZIhvcNAQELBQAD"
+ + "ggEBAIw51ZDT3g1V51wgDIKGrtUC1yPxLmBqXg5lUWI8RJurwMnokGWGvDMemLw2gAIgQxrRKsOcPaIxYbjLY+Y5+I2Zof19"
+ + "eXJTRqqo/m4qNRpbzdzEdGv7lcH4zzL1YbLCLgoWyLgC2GFkJRas3pkdEplA6nf/fc5k4DJrMnV/oYjdvs0PH2gsW9drPkck"
+ + "lLTToWEijzyFEnry/O5EpsCuJdjN422rxVRJd1Qr/mLX5yt2kW83oMT3PRNREB+ZcHfVT0NvZ8KqYmMEpuODPx+XUuojuQsN"
+ + "bjlWXd4PSX8jqqvT3uLNWotKLQbDZV6Lp4f1Uf1qXukD2j9o8XKF+6ww03M=";
+ static final Certificate TRUSTED_CERTIFICATE =
+ byteStringToCertificate().apply(valueOfBase64(TRUSTED_CERTIFICATE_B64));
+ static final Certificate PUBLIC_KEY_CERTIFICATE =
+ byteStringToCertificate().apply(valueOfBase64(PUBLIC_KEY_CERTIFICATE_B64));
+ static final Certificate[] CERTIFICATE_CHAIN = new Certificate[] { PUBLIC_KEY_CERTIFICATE };
+ static final String PRIVATE_KEY_ENCODED_B64 =
+ "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDh4tTZu1vNvAgDEXpGEvzkl3r4ayGfX7jSqWpjDtSyfbfI"
+ + "W71MiIQ90O64g5hzArHWhOWrgbCABXHIk9Ad7wn87bWLIoagHQCUQ89QrDKMntvAea66B4RLKJRilNIm07b+mGjEx3FJb2Nf"
+ + "oCA2UmLVBKEvYpNHrxv5c//tet+MVbs7AL74t5ALCTeK99h2m2dmYvraAc7zbneKBdBK+7eIhdjZzrT1ElN8HfCQ4PzZD0cg"
+ + "lue8M6V0R993BC8L0h00IHaHKzTMIKEMWUI9ailHON4fYI61BuNcRYyyKUQ1pojadEQ5bqEJ1zf51D6D6dusVKA52EAC+KPa"
+ + "0oHvdxlnAgMBAAECggEBALEYOpJdvtLkiU+Gg1uvBVBeps1eiKS/0lJu+nahKQarY8wUiKwZF7yzMoW8vmflA/JQjRPSgMNO"
+ + "AXAk2vSs9SK0ZzGnJu8e7dZP95ii+Jqg7V7Qx7kXrZOTRAqp7Lz+HakranBkgR/20W0mSDruiofBsnFJEnkQA5mmZU8Vl3AY"
+ + "SYwKP785N/nO7vZMMOkSrs5BYwDeYHIwncxXlaUBBCHf1I0tAHMe3SlqpPHpjrFiv2IofjGTGWEe6KDiUMWGNOkTLHgxb9rZ"
+ + "TRh2p3vuMfOfkzd0Tgj6DOUVb1SOI8g4nFwfphwU008eV86OV7nxIbc+DXfQYpZ/850Rra/lCmECgYEA+AiIsw8QkkJuws+G"
+ + "oYWuVMH+m/qbWami+g1CDaR8zPTSAMdqNLamrnXkN3HtiMn+qjxBh5f7zw4eu5TA/jyFBTcOvh1gsYJNTGKPrzxyHF3k3BV/"
+ + "SGplIdzos+f7JSLNLZmSFSpikoAhy46sZKw30XjG74BHmeQmcAmLsx6a8lECgYEA6SQwv+i196e3DlfPBuUHuB9Z91FFUZi7"
+ + "A3Q3ziB56xXxOZO+F1L565Ve0z2j1jeVnmrBANkXnJUVWK2g+dCi8/gvocQ3hGr/WbJVXD6T57GkcwtApR1qkQxRVycxqYR0"
+ + "mYjb6jGOMJZ8OeC0UWYDWBlgjmv65C01IdItTJz/6jcCgYEAkk5mZEjkm4G4WA2V+r0iIjj0eQmQjYk0647agbWfMD7RiUgX"
+ + "69Q56fr8jYAUf3W3VK+Kb/NEw9QuaLPMS6tjQ7pAZgBqQwr7ka0p2FItdXIlR3UeyZaI5TqrwUN7r2Ih6V4G/5kq4APY63vT"
+ + "UOcNXfCCWFAw7CPaUIgw8Y2CFKECgYAEEOyEvFNIIXWw21kx/paW4H0aMiGqXaaNVd6PSsO1lOljHq+HCpxvPmir+Hw+BTQn"
+ + "0ibRk/e0dGkt5cFT+g6NgLub76ckORWBA/o3JKRBuzhqBT04Y/3yz6svgPB9y2CZOOjU+c5IDKfX/pJGhSfzxmWHtlxm1F8D"
+ + "2v2NQ4O3GwKBgASHtjgsEed1s+K6EU2S2dRqzrAyrokUZp5dKoYPPE2l4AIb0IGODD3GO3uZ6xAYg7RZkemtm2tLH4sTF6QU"
+ + "tRGputLLoAPp3qTWVCKW668hdNApC/WtqRwY68KOQlhoMgWQWLgX3Lu1gGqJHu89B3UmswvgNCZ7hA49P33jR2wh";
+ static final PrivateKey PRIVATE_KEY;
+
+ static {
+ final KeySpec spec = new PKCS8EncodedKeySpec(valueOfBase64(PRIVATE_KEY_ENCODED_B64).toByteArray());
+ try {
+ PRIVATE_KEY = KeyFactory.getInstance("RSA").generatePrivate(spec);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ static final char[] KEYSTORE_PASSWORD = "changeit".toCharArray();
+ static final char[] KEY_PASSWORD = "changeit".toCharArray();
+
+ static MemoryBackend createKeyStoreMemoryBackend() {
+ try (LDIFEntryReader reader = new LDIFEntryReader("dn: " + KEYSTORE_DN,
+ "objectClass: top",
+ "objectClass: organizationalUnit",
+ "ou: key store").setSchema(SCHEMA)) {
+ return new MemoryBackend(SCHEMA, reader).enableVirtualAttributes(true);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ static KeyStore createKeyStore(final MemoryBackend backend) {
+ final ConnectionFactory factory = newInternalConnectionFactory(backend);
+ final Options options = defaultOptions().set(GLOBAL_PASSWORD, newClearTextPasswordFactory(KEYSTORE_PASSWORD));
+ return newLDAPKeyStore(factory, KEYSTORE_DN, options);
+ }
+
+ static SecretKey createSecretKey() throws Exception {
+ final SecureRandom secureRandom = new SecureRandom();
+ final byte[] salt = new byte[16];
+ secureRandom.nextBytes(salt);
+ final SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
+ return factory.generateSecret(new PBEKeySpec("password".toCharArray(), salt, 65536, 128));
+ }
+
+ private KeyStoreTestUtils() {
+ // Prevent instantiation.
+ }
+}
diff --git a/opendj-core/src/test/java/org/forgerock/opendj/security/OpenDJProviderSchemaTest.java b/opendj-core/src/test/java/org/forgerock/opendj/security/OpenDJProviderSchemaTest.java
new file mode 100644
index 0000000..83ce80a
--- /dev/null
+++ b/opendj-core/src/test/java/org/forgerock/opendj/security/OpenDJProviderSchemaTest.java
@@ -0,0 +1,53 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.security;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.forgerock.opendj.ldap.schema.Schema.getCoreSchema;
+import static org.forgerock.opendj.security.OpenDJProviderSchema.addOpenDJProviderSchema;
+
+import org.forgerock.opendj.ldap.SdkTestCase;
+import org.forgerock.opendj.ldap.schema.AttributeType;
+import org.forgerock.opendj.ldap.schema.ObjectClass;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldap.schema.SchemaBuilder;
+import org.testng.annotations.Test;
+
+@SuppressWarnings("javadoc")
+public class OpenDJProviderSchemaTest extends SdkTestCase {
+ @Test
+ public void testGetObjectClasses() throws Exception {
+ assertThat(OpenDJProviderSchema.getObjectClasses()).isNotEmpty();
+ }
+
+ @Test
+ public void testGetAttributeTypes() throws Exception {
+ assertThat(OpenDJProviderSchema.getAttributeTypes()).isNotEmpty();
+ }
+
+ @Test
+ public void testAddOpenDJProviderSchema() throws Exception {
+ final SchemaBuilder schemaBuilder = new SchemaBuilder(getCoreSchema());
+ final Schema schema = addOpenDJProviderSchema(schemaBuilder).toSchema();
+ assertThat(schema.getWarnings()).isEmpty();
+ for (ObjectClass objectClass : OpenDJProviderSchema.getObjectClasses()) {
+ assertThat(schema.hasObjectClass(objectClass.getNameOrOID())).isTrue();
+ }
+ for (AttributeType attributeType : OpenDJProviderSchema.getAttributeTypes()) {
+ assertThat(schema.hasAttributeType(attributeType.getNameOrOID())).isTrue();
+ }
+ }
+}
diff --git a/opendj-core/src/test/java/org/forgerock/opendj/security/OpenDJProviderTest.java b/opendj-core/src/test/java/org/forgerock/opendj/security/OpenDJProviderTest.java
new file mode 100644
index 0000000..8e8e652
--- /dev/null
+++ b/opendj-core/src/test/java/org/forgerock/opendj/security/OpenDJProviderTest.java
@@ -0,0 +1,122 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.security;
+
+import static java.util.Collections.list;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.forgerock.opendj.security.KeyStoreObject.newTrustedCertificateObject;
+import static org.forgerock.opendj.security.KeyStoreTestUtils.KEYSTORE_DN;
+import static org.forgerock.opendj.security.KeyStoreTestUtils.PUBLIC_KEY_CERTIFICATE;
+import static org.forgerock.opendj.security.KeyStoreTestUtils.createKeyStore;
+import static org.forgerock.opendj.security.KeyStoreTestUtils.createKeyStoreMemoryBackend;
+import static org.forgerock.opendj.security.OpenDJProvider.newCapacityBasedKeyStoreObjectCache;
+import static org.forgerock.opendj.security.OpenDJProvider.newKeyStoreObjectCacheFromMap;
+import static org.forgerock.opendj.security.OpenDJProvider.newClearTextPasswordFactory;
+
+import java.net.URL;
+import java.security.KeyStore;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.forgerock.opendj.ldap.MemoryBackend;
+import org.forgerock.opendj.ldap.SdkTestCase;
+import org.forgerock.util.Factory;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+@SuppressWarnings("javadoc")
+public class OpenDJProviderTest extends SdkTestCase {
+ @Test
+ public void testNewProviderWithoutConfigFile() throws Exception {
+ final OpenDJProvider provider = new OpenDJProvider();
+ assertThat((Object) provider.getDefaultConfig()).isNull();
+ }
+
+ @Test
+ public void testNewProviderFromConfigFile() throws Exception {
+ final URL configUrl = getClass().getResource("opendj-provider.conf");
+ final OpenDJProvider provider = new OpenDJProvider(configUrl.toURI());
+
+ assertThat((Object) provider.getDefaultConfig().getBaseDN()).isEqualTo(KEYSTORE_DN);
+ assertThat(provider.getDefaultConfig().getConnectionFactory()).isNotNull();
+ assertThat(provider.getDefaultConfig().getOptions()).isNotNull();
+ }
+
+ @Test
+ public void testNewLDAPKeyStore() throws Exception {
+ final MemoryBackend backend = createKeyStoreMemoryBackend();
+ final KeyStore keystore = createKeyStore(backend);
+
+ assertThat(keystore.getProvider()).isInstanceOf(OpenDJProvider.class);
+ assertThat(keystore.getType()).isEqualTo("LDAP");
+ assertThat(keystore.size()).isZero();
+ assertThat(list(keystore.aliases())).isEmpty();
+ }
+
+ @Test
+ public void testNewKeyStoreObjectCacheFromMap() throws Exception {
+ final Map<String, KeyStoreObject> map = new HashMap<>();
+ final KeyStoreObjectCache cache = newKeyStoreObjectCacheFromMap(map);
+ final KeyStoreObject keyStoreObject = newTrustedCertificateObject("test", PUBLIC_KEY_CERTIFICATE);
+
+ cache.put(keyStoreObject);
+ assertThat(map).containsEntry("test", keyStoreObject);
+ assertThat(cache.get("test")).isSameAs(keyStoreObject);
+ }
+
+ @Test
+ public void testNewCapacityBasedKeyStoreObjectCache() throws Exception {
+ final KeyStoreObject keyStoreObject1 = newTrustedCertificateObject("test1", PUBLIC_KEY_CERTIFICATE);
+ final KeyStoreObject keyStoreObject2 = newTrustedCertificateObject("test2", PUBLIC_KEY_CERTIFICATE);
+ final KeyStoreObject keyStoreObject3 = newTrustedCertificateObject("test3", PUBLIC_KEY_CERTIFICATE);
+ final KeyStoreObject keyStoreObject4 = newTrustedCertificateObject("test4", PUBLIC_KEY_CERTIFICATE);
+
+ final KeyStoreObjectCache cache = newCapacityBasedKeyStoreObjectCache(3);
+
+ cache.put(keyStoreObject1);
+ cache.put(keyStoreObject2);
+ cache.put(keyStoreObject3);
+ assertThat(cache.get("test1")).isSameAs(keyStoreObject1);
+ assertThat(cache.get("test2")).isSameAs(keyStoreObject2);
+ assertThat(cache.get("test3")).isSameAs(keyStoreObject3);
+ cache.put(keyStoreObject4);
+ assertThat(cache.get("test1")).isNull();
+ assertThat(cache.get("test2")).isSameAs(keyStoreObject2);
+ assertThat(cache.get("test3")).isSameAs(keyStoreObject3);
+ assertThat(cache.get("test4")).isSameAs(keyStoreObject4);
+ }
+
+ @DataProvider
+ public static Object[][] obfuscatedPasswords() {
+ // @formatter:off
+ return new Object[][] {
+ { null },
+ { "".toCharArray() },
+ { "password".toCharArray() },
+ { "\u0000\u007F\u0080\u00FF\uFFFF".toCharArray() },
+ };
+ // @formatter:on
+ }
+
+ @Test(dataProvider = "obfuscatedPasswords")
+ public void testNewObfuscatedPasswordFactory(final char[] password) {
+ final Factory<char[]> factory = newClearTextPasswordFactory(password);
+ assertThat(factory.newInstance()).isEqualTo(password);
+ if (password != null) {
+ assertThat(factory.newInstance()).isNotSameAs(password);
+ }
+ }
+}
diff --git a/opendj-core/src/test/resources/org/forgerock/opendj/security/opendj-provider.conf b/opendj-core/src/test/resources/org/forgerock/opendj/security/opendj-provider.conf
new file mode 100644
index 0000000..2cebca0
--- /dev/null
+++ b/opendj-core/src/test/resources/org/forgerock/opendj/security/opendj-provider.conf
@@ -0,0 +1,6 @@
+org.forgerock.opendj.security.host=localhost
+org.forgerock.opendj.security.port=1389
+org.forgerock.opendj.security.bindDN=cn=directory manager
+org.forgerock.opendj.security.bindPassword=password
+org.forgerock.opendj.security.keyStoreBaseDN=ou=key store,dc=example,dc=com
+
diff --git a/opendj-server-legacy/resource/schema/03-keystore.ldif b/opendj-server-legacy/resource/schema/03-keystore.ldif
new file mode 100644
index 0000000..9036bab
--- /dev/null
+++ b/opendj-server-legacy/resource/schema/03-keystore.ldif
@@ -0,0 +1,81 @@
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2016 ForgeRock AS.
+
+# This file contains the attribute type and object class definitions for use
+# with LDAP based key stores.
+#
+# WARNING: this file MUST exists in both the SDK and the server. The two copies must be synchronized.
+#
+dn: cn=schema
+objectClass: top
+objectClass: ldapSubentry
+objectClass: subschema
+attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.190
+ NAME 'ds-keystore-alias'
+ EQUALITY caseExactMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+ SINGLE-VALUE
+ X-ORIGIN 'OpenDJ Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.191
+ NAME 'ds-keystore-certificate'
+ EQUALITY certificateExactMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.8
+ SINGLE-VALUE
+ X-ORIGIN 'OpenDJ Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.193
+ NAME 'ds-keystore-key-algorithm'
+ EQUALITY caseIgnoreMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+ SINGLE-VALUE
+ X-ORIGIN 'OpenDJ Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.194
+ NAME 'ds-keystore-key'
+ EQUALITY octetStringMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.40
+ SINGLE-VALUE
+ X-ORIGIN 'OpenDJ Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.195
+ NAME 'ds-keystore-certificate-chain'
+ EQUALITY octetStringMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.40
+ SINGLE-VALUE
+ X-ORIGIN 'OpenDJ Directory Server' )
+objectClasses: ( 1.3.6.1.4.1.36733.2.1.2.47
+ NAME 'ds-keystore-object'
+ SUP top
+ ABSTRACT
+ MUST ds-keystore-alias
+ X-ORIGIN 'OpenDJ Directory Server' )
+objectClasses: ( 1.3.6.1.4.1.36733.2.1.2.48
+ NAME 'ds-keystore-trusted-certificate'
+ SUP ds-keystore-object
+ STRUCTURAL
+ MUST ds-keystore-certificate
+ X-ORIGIN 'OpenDJ Directory Server' )
+objectClasses: ( 1.3.6.1.4.1.36733.2.1.2.49
+ NAME 'ds-keystore-private-key'
+ SUP ds-keystore-object
+ STRUCTURAL
+ MUST ( ds-keystore-key $
+ ds-keystore-key-algorithm $
+ ds-keystore-certificate )
+ MAY ds-keystore-certificate-chain
+ X-ORIGIN 'OpenDJ Directory Server' )
+objectClasses: ( 1.3.6.1.4.1.36733.2.1.2.50
+ NAME 'ds-keystore-secret-key'
+ SUP ds-keystore-object
+ STRUCTURAL
+ MUST ( ds-keystore-key $
+ ds-keystore-key-algorithm )
+ X-ORIGIN 'OpenDJ Directory Server' )
--
Gitblit v1.10.0