Skip to content

Commit

Permalink
Support filtering of KeyStore entries
Browse files Browse the repository at this point in the history
  • Loading branch information
tvernum committed Jul 16, 2021
1 parent 7064ae5 commit 4e208a7
Show file tree
Hide file tree
Showing 8 changed files with 269 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import java.util.List;
import java.util.Locale;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand Down Expand Up @@ -89,6 +90,17 @@ public static KeyStore buildKeyStore(Collection<Certificate> certificateChain, P
return keyStore;
}

/**
* Filters a keystore using a predicate.
* The provided keystore is modified in place.
*/
public static KeyStore filter(KeyStore store, Predicate<KeyStoreEntry> filter) {
stream(store, e -> new SslConfigException("Failed to apply filter to existing keystore", e))
.filter(filter.negate())
.forEach(e -> e.delete());
return store;
}

/**
* Construct an in-memory keystore with multiple trusted cert entries.
*
Expand Down Expand Up @@ -170,7 +182,7 @@ public static X509ExtendedTrustManager createTrustManager(Collection<Certificate
return createTrustManager(store, TrustManagerFactory.getDefaultAlgorithm());
}

static Stream<KeyStoreEntry> stream(KeyStore keyStore,
public static Stream<KeyStoreEntry> stream(KeyStore keyStore,
Function<GeneralSecurityException, ? extends RuntimeException> exceptionHandler) {
try {
return Collections.list(keyStore.aliases()).stream().map(a -> new KeyStoreEntry(keyStore, a, exceptionHandler));
Expand All @@ -179,7 +191,7 @@ static Stream<KeyStoreEntry> stream(KeyStore keyStore,
}
}

static class KeyStoreEntry {
public static class KeyStoreEntry {
private final KeyStore store;
private final String alias;
private final Function<GeneralSecurityException, ? extends RuntimeException> exceptionHandler;
Expand Down Expand Up @@ -270,6 +282,26 @@ public List<? extends X509Certificate> getX509CertificateChain() {
}
}

public void delete() {
try {
store.deleteEntry(alias);
} catch (KeyStoreException e) {
throw exceptionHandler.apply(e);
}
}

public void copyTo(KeyStore otherStore, char[] keyPassword) {
try {
if (store.isKeyEntry(alias)) {
final Key key = store.getKey(alias, keyPassword);
otherStore.setKeyEntry(alias, key, keyPassword, store.getCertificateChain(alias));
} else if (store.isCertificateEntry(alias)) {
otherStore.setCertificateEntry(alias, store.getCertificate(alias));
}
} catch (GeneralSecurityException e) {
throw exceptionHandler.apply(e);
}
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import java.nio.file.Path;
import java.security.KeyStore;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -166,6 +167,8 @@ public abstract class SslConfigurationLoader {
private List<String> defaultCiphers;
private List<String> defaultProtocols;

private Function<KeyStore, KeyStore> keyStoreFilter;

/**
* Construct a new loader with the "standard" default values.
*
Expand Down Expand Up @@ -235,6 +238,15 @@ public void setDefaultProtocols(List<String> defaultProtocols) {
this.defaultProtocols = defaultProtocols;
}


/**
* Apply a filter function to any keystore that is loaded.
* @see StoreKeyConfig
*/
public void setKeyStoreFilter(Function<KeyStore, KeyStore> keyStoreFilter) {
this.keyStoreFilter = keyStoreFilter;
}

/**
* Clients of this class should implement this method to determine whether there are any settings for a given prefix.
* This is used to populate {@link SslConfiguration#isExplicitlyConfigured()}.
Expand Down Expand Up @@ -363,7 +375,7 @@ public SslKeyConfig buildKeyConfig(Path basePath) {
}
final String storeType = resolveSetting(KEYSTORE_TYPE, Function.identity(), inferKeyStoreType(keyStorePath));
final String algorithm = resolveSetting(KEYSTORE_ALGORITHM, Function.identity(), KeyManagerFactory.getDefaultAlgorithm());
return new StoreKeyConfig(keyStorePath, storePassword, storeType, keyPassword, algorithm, basePath);
return new StoreKeyConfig(keyStorePath, storePassword, storeType, keyStoreFilter, keyPassword, algorithm, basePath);
}

return defaultKeyConfig;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

package org.elasticsearch.common.ssl;

import org.elasticsearch.core.Nullable;
import org.elasticsearch.core.Tuple;

import javax.net.ssl.KeyManagerFactory;
Expand All @@ -28,6 +29,7 @@
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
Expand All @@ -37,6 +39,7 @@ public class StoreKeyConfig implements SslKeyConfig {
private final String keystorePath;
private final String type;
private final char[] storePassword;
private final Function<KeyStore, KeyStore> filter;
private final char[] keyPassword;
private final String algorithm;
private final Path configBasePath;
Expand All @@ -46,18 +49,21 @@ public class StoreKeyConfig implements SslKeyConfig {
* @param storePassword The password for the keystore
* @param type The {@link KeyStore#getType() type} of the keystore (typically "PKCS12" or "jks").
* See {@link KeyStoreUtil#inferKeyStoreType}.
* @param filter A function to process the keystore after it is loaded. See {@link KeyStoreUtil#filter}
* @param keyPassword The password for the key(s) within the keystore
* (see {@link javax.net.ssl.KeyManagerFactory#init(KeyStore, char[])}).
* (see {@link KeyManagerFactory#init(KeyStore, char[])}).
* @param algorithm The algorithm to use for the Key Manager (see {@link KeyManagerFactory#getAlgorithm()}).
* @param configBasePath The base path for configuration files (used for error handling)
*/
public StoreKeyConfig(String path, char[] storePassword, String type, char[] keyPassword, String algorithm, Path configBasePath) {
public StoreKeyConfig(String path, char[] storePassword, String type, @Nullable Function<KeyStore, KeyStore> filter,
char[] keyPassword, String algorithm, Path configBasePath) {
this.keystorePath = Objects.requireNonNull(path, "Keystore path cannot be null");
this.storePassword = Objects.requireNonNull(storePassword, "Keystore password cannot be null (but may be empty)");
this.type = Objects.requireNonNull(type, "Keystore type cannot be null");
this.filter = filter;
this.keyPassword = Objects.requireNonNull(keyPassword, "Key password cannot be null (but may be empty)");
this.algorithm = Objects.requireNonNull(algorithm, "Keystore algorithm cannot be null");
this.configBasePath = Objects.requireNonNull(configBasePath, "Config path cannot be null");
this.keystorePath = Objects.requireNonNull(path, "Keystore path cannot be null");
this.type = Objects.requireNonNull(type, "Keystore type cannot be null");
}

@Override
Expand All @@ -80,10 +86,23 @@ private Path resolvePath() {
return configBasePath.resolve(keystorePath);
}

/**
* Equivalent to {@link #getKeys(boolean) getKeys(false)}.
*/
@Override
public List<Tuple<PrivateKey, X509Certificate>> getKeys() {
return getKeys(false);
}

/**
* Return the list of keys inside the configured keystore, optionally applying the {@code filter} that was set during construction.
*/
public List<Tuple<PrivateKey, X509Certificate>> getKeys(boolean filterKeystore) {
final Path path = resolvePath();
final KeyStore keyStore = readKeyStore(path);
KeyStore keyStore = readKeyStore(path);
if (filterKeystore) {
keyStore = this.processKeyStore(keyStore);
}
return KeyStoreUtil.stream(keyStore, ex -> keystoreException(path, ex))
.filter(KeyStoreUtil.KeyStoreEntry::isKeyEntry)
.map(entry -> {
Expand Down Expand Up @@ -122,14 +141,26 @@ public X509ExtendedKeyManager createKeyManager() {

private X509ExtendedKeyManager createKeyManager(Path path) {
try {
final KeyStore keyStore = readKeyStore(path);
KeyStore keyStore = readKeyStore(path);
keyStore = processKeyStore(keyStore);
checkKeyStore(keyStore, path);
return KeyStoreUtil.createKeyManager(keyStore, keyPassword, algorithm);
} catch (GeneralSecurityException e) {
throw keystoreException(path, e);
}
}

private KeyStore processKeyStore(KeyStore keyStore) {
if (filter == null) {
return keyStore;
}
final KeyStore processedKeystore = filter.apply(keyStore);
if (processedKeystore != null) {
return processedKeystore;
}
return keyStore;
}

private KeyStore readKeyStore(Path path) {
try {
return KeyStoreUtil.readKeyStore(path, type, storePassword);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

package org.elasticsearch.common.ssl;

import org.elasticsearch.test.ESTestCase;

import java.nio.file.Path;
import java.security.KeyStore;
import java.util.Collections;

import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo;

public class KeyStoreUtilTests extends ESTestCase {
private static final char[] P12_PASS = "p12-pass".toCharArray();

public void testFilter() throws Exception {
assumeFalse("Can't use PKCS#12 keystores in a FIPS JVM", inFipsJvm());

final Path p12 = getDataPath("/certs/cert-all/certs.p12");
final KeyStore original = KeyStoreUtil.readKeyStore(p12, "PKCS12", P12_PASS);

// No-op filter
final KeyStore clone = KeyStoreUtil.filter(KeyStoreUtil.readKeyStore(p12, "PKCS12", P12_PASS), entry -> true);
assertThat(Collections.list(clone.aliases()), containsInAnyOrder("cert1", "cert2"));
assertSameEntry(original, clone, "cert1", P12_PASS);
assertSameEntry(original, clone, "cert2", P12_PASS);

// Filter by alias
final KeyStore cert1 = KeyStoreUtil.filter(
KeyStoreUtil.readKeyStore(p12, "PKCS12", P12_PASS),
entry -> entry.getAlias().equals("cert1")
);
assertThat(Collections.list(cert1.aliases()), containsInAnyOrder("cert1"));
assertSameEntry(original, cert1, "cert1", P12_PASS);

// Filter by cert
final KeyStore cert2 = KeyStoreUtil.filter(
KeyStoreUtil.readKeyStore(p12, "PKCS12", P12_PASS),
entry -> entry.getX509Certificate().getSubjectX500Principal().getName().equals("CN=cert2")
);
assertThat(Collections.list(cert2.aliases()), containsInAnyOrder("cert2"));
assertSameEntry(original, cert2, "cert2", P12_PASS);
}

private void assertSameEntry(KeyStore ks1, KeyStore ks2, String alias, char[] keyPassword) throws Exception {
assertThat(ks1.isKeyEntry(alias), equalTo(ks2.isKeyEntry(alias)));
assertThat(ks1.isCertificateEntry(alias), equalTo(ks2.isCertificateEntry(alias)));
assertThat(ks1.getCertificate(alias), equalTo(ks2.getCertificate(alias)));
assertThat(ks1.getCertificateChain(alias), equalTo(ks2.getCertificateChain(alias)));
assertThat(ks1.getKey(alias, P12_PASS), equalTo(ks2.getKey(alias, keyPassword)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.X509ExtendedKeyManager;
Expand All @@ -44,6 +46,8 @@ public class StoreKeyConfigTests extends ESTestCase {

private static final char[] P12_PASS = "p12-pass".toCharArray();
private static final char[] JKS_PASS = "jks-pass".toCharArray();
private static final String KEY_MGR_ALGORITHM = KeyManagerFactory.getDefaultAlgorithm();
private static final char[] KEY_PASS = "key-pass".toCharArray();

private Path configBasePath;

Expand All @@ -68,20 +72,31 @@ public void testLoadMultipleKeyPKCS12() throws Exception {
assertKeysLoaded(keyConfig, "cert1", "cert2");
}

public void testFilterMultipleKeyPKCS12() throws Exception {
assumeFalse("Can't use JKS/PKCS12 keystores in a FIPS JVM", inFipsJvm());
final Path p12 = getDataPath("/certs/cert-all/certs.p12");
final StoreKeyConfig keyConfig = config(
p12,
P12_PASS,
"PKCS12",
ks -> KeyStoreUtil.filter(ks, entry -> entry.getAlias().equals("cert1"))
);
assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(p12));
assertKeysLoaded(keyConfig, "cert1");
}

public void testLoadMultipleKeyJksWithSeparateKeyPassword() throws Exception {
assumeFalse("Can't use JKS/PKCS12 keystores in a FIPS JVM", inFipsJvm());
final String jks = "cert-all/certs.jks";
final StoreKeyConfig keyConfig = new StoreKeyConfig(jks, JKS_PASS, "jks", "key-pass".toCharArray(),
KeyManagerFactory.getDefaultAlgorithm(), configBasePath);
final StoreKeyConfig keyConfig = new StoreKeyConfig(jks, JKS_PASS, "jks", null, KEY_PASS, KEY_MGR_ALGORITHM, configBasePath);
assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(configBasePath.resolve(jks)));
assertKeysLoaded(keyConfig, "cert1", "cert2");
}

public void testKeyManagerFailsWithIncorrectStorePassword() throws Exception {
assumeFalse("Can't use JKS/PKCS12 keystores in a FIPS JVM", inFipsJvm());
final String jks = "cert-all/certs.jks";
final StoreKeyConfig keyConfig = new StoreKeyConfig(jks, P12_PASS, "jks", "key-pass".toCharArray(),
KeyManagerFactory.getDefaultAlgorithm(), configBasePath);
final StoreKeyConfig keyConfig = new StoreKeyConfig(jks, P12_PASS, "jks", null, KEY_PASS, KEY_MGR_ALGORITHM, configBasePath);
final Path path = configBasePath.resolve(jks);
assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(path));
assertPasswordIsIncorrect(keyConfig, path);
Expand Down Expand Up @@ -149,8 +164,12 @@ public void testKeyConfigReloadsFileContents() throws Exception {
}

private StoreKeyConfig config(Path path, char[] password, String type) {
return config(path, password, type, null);
}

private StoreKeyConfig config(Path path, char[] password, String type, Function<KeyStore, KeyStore> filter) {
final String pathName = path == null ? null : path.toString();
return new StoreKeyConfig(pathName, password, type, password, KeyManagerFactory.getDefaultAlgorithm(), configBasePath);
return new StoreKeyConfig(pathName, password, type, filter, password, KeyManagerFactory.getDefaultAlgorithm(), configBasePath);
}

private void assertKeysLoaded(StoreKeyConfig keyConfig, String... names) throws CertificateParsingException {
Expand All @@ -175,7 +194,7 @@ private void assertKeysLoaded(StoreKeyConfig keyConfig, String... names) throws
));
}

final List<Tuple<PrivateKey, X509Certificate>> keys = keyConfig.getKeys();
final List<Tuple<PrivateKey, X509Certificate>> keys = keyConfig.getKeys(true);
assertThat(keys, iterableWithSize(names.length));
for (Tuple<PrivateKey, X509Certificate> tup : keys) {
PrivateKey privateKey = tup.v1();
Expand All @@ -186,7 +205,7 @@ private void assertKeysLoaded(StoreKeyConfig keyConfig, String... names) throws
assertThat(certificate.getIssuerDN().getName(), is("CN=Test CA 1"));
}
}

private void assertKeysNotLoaded(StoreKeyConfig keyConfig, String... names) throws CertificateParsingException {
final X509ExtendedKeyManager keyManager = keyConfig.createKeyManager();
assertThat(keyManager, notNullValue());
Expand Down
Loading

0 comments on commit 4e208a7

Please sign in to comment.