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