mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Matthew Swift
09.19.2016 416f8e9da4a1f99064706c23707ab2241b1bf61c
OPENDJ-2877: implement LDAP security provider and key store

Implement an LDAP/LDIF based key store capable of storing private keys,
secret keys, and trusted certificates. The package-info.java contains
detailed documentation regarding usage and schema.
20 files added
1 files modified
3141 ■■■■■ changed files
opendj-core/pom.xml 1 ●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/security/ExternalKeyWrappingStrategy.java 49 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/security/KeyProtector.java 331 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/security/KeyStoreImpl.java 315 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/security/KeyStoreObject.java 315 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/security/KeyStoreObjectCache.java 57 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/security/KeyStoreParameters.java 142 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/security/LocalizedKeyStoreException.java 43 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/security/OpenDJProvider.java 437 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/security/OpenDJProviderSchema.java 129 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/security/package-info.java 111 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/resources/com/forgerock/opendj/security/keystore.properties 38 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/resources/org/forgerock/opendj/security/03-keystore.ldif 81 ●●●●● patch | view | raw | blame | history
opendj-core/src/test/java/org/forgerock/opendj/security/KeyProtectorTest.java 202 ●●●●● patch | view | raw | blame | history
opendj-core/src/test/java/org/forgerock/opendj/security/KeyStoreImplTest.java 340 ●●●●● patch | view | raw | blame | history
opendj-core/src/test/java/org/forgerock/opendj/security/KeyStoreObjectTest.java 143 ●●●●● patch | view | raw | blame | history
opendj-core/src/test/java/org/forgerock/opendj/security/KeyStoreTestUtils.java 145 ●●●●● patch | view | raw | blame | history
opendj-core/src/test/java/org/forgerock/opendj/security/OpenDJProviderSchemaTest.java 53 ●●●●● patch | view | raw | blame | history
opendj-core/src/test/java/org/forgerock/opendj/security/OpenDJProviderTest.java 122 ●●●●● patch | view | raw | blame | history
opendj-core/src/test/resources/org/forgerock/opendj/security/opendj-provider.conf 6 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/resource/schema/03-keystore.ldif 81 ●●●●● patch | view | raw | blame | history
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>
opendj-core/src/main/java/org/forgerock/opendj/security/ExternalKeyWrappingStrategy.java
New file
@@ -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;
}
opendj-core/src/main/java/org/forgerock/opendj/security/KeyProtector.java
New file
@@ -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, ' ');
        }
    }
}
opendj-core/src/main/java/org/forgerock/opendj/security/KeyStoreImpl.java
New file
@@ -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);
    }
}
opendj-core/src/main/java/org/forgerock/opendj/security/KeyStoreObject.java
New file
@@ -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);
    }
}
opendj-core/src/main/java/org/forgerock/opendj/security/KeyStoreObjectCache.java
New file
@@ -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);
}
opendj-core/src/main/java/org/forgerock/opendj/security/KeyStoreParameters.java
New file
@@ -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;
    }
}
opendj-core/src/main/java/org/forgerock/opendj/security/LocalizedKeyStoreException.java
New file
@@ -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;
    }
}
opendj-core/src/main/java/org/forgerock/opendj/security/OpenDJProvider.java
New file
@@ -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);
        }
    }
}
opendj-core/src/main/java/org/forgerock/opendj/security/OpenDJProviderSchema.java
New file
@@ -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.
    }
}
opendj-core/src/main/java/org/forgerock/opendj/security/package-info.java
New file
@@ -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;
opendj-core/src/main/resources/com/forgerock/opendj/security/keystore.properties
New file
@@ -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'
opendj-core/src/main/resources/org/forgerock/opendj/security/03-keystore.ldif
New file
@@ -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' )
opendj-core/src/test/java/org/forgerock/opendj/security/KeyProtectorTest.java
New file
@@ -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");
    }
}
opendj-core/src/test/java/org/forgerock/opendj/security/KeyStoreImplTest.java
New file
@@ -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);
    }
}
opendj-core/src/test/java/org/forgerock/opendj/security/KeyStoreObjectTest.java
New file
@@ -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);
    }
}
opendj-core/src/test/java/org/forgerock/opendj/security/KeyStoreTestUtils.java
New file
@@ -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.
    }
}
opendj-core/src/test/java/org/forgerock/opendj/security/OpenDJProviderSchemaTest.java
New file
@@ -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();
        }
    }
}
opendj-core/src/test/java/org/forgerock/opendj/security/OpenDJProviderTest.java
New file
@@ -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);
        }
    }
}
opendj-core/src/test/resources/org/forgerock/opendj/security/opendj-provider.conf
New file
@@ -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
opendj-server-legacy/resource/schema/03-keystore.ldif
New file
@@ -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' )