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
| | |
| | | <configuration> |
| | | <messageFiles> |
| | | <messageFile>com/forgerock/opendj/ldap/core.properties</messageFile> |
| | | <messageFile>com/forgerock/opendj/security/keystore.properties</messageFile> |
| | | </messageFiles> |
| | | </configuration> |
| | | </execution> |
| New file |
| | |
| | | /* |
| | | * 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; |
| | | } |
| New file |
| | |
| | | /* |
| | | * 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, ' '); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 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); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 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); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 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); |
| | | } |
| New file |
| | |
| | | /* |
| | | * 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; |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 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; |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 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); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 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. |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 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; |
| | | |
| New file |
| | |
| | | # |
| | | # 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' |
| New file |
| | |
| | | # 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' ) |
| New file |
| | |
| | | /* |
| | | * 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"); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 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); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 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); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 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. |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 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(); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 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); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | 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 |
| | | |
| New file |
| | |
| | | # 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' ) |