From 42027ea9806274d7ad48f8bafe9667c4ec57a18e Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Mon, 5 Jul 2021 11:10:36 +1000 Subject: [PATCH 1/5] Update "ssl-config" to support X-Pack features This commit upgrades the existing SSPL licensed "ssl-config" library to include additional features that are supported by the X-Pack SSL library. This commit does not make any changes to X-Pack to use these new features - it introduces them in preparation for their future use by X-Pack. The reindex module is updated to reflect API changes in ssl-config --- .../common/ssl/CompositeTrustConfig.java | 85 ++++++++ .../common/ssl/DefaultJdkTrustConfig.java | 18 +- .../elasticsearch/common/ssl/DerParser.java | 8 +- .../common/ssl/EmptyKeyConfig.java | 30 ++- .../common/ssl/KeyStoreUtil.java | 138 ++++++++++-- .../common/ssl/PemKeyConfig.java | 114 +++++++--- .../common/ssl/PemTrustConfig.java | 72 ++++--- .../elasticsearch/common/ssl/PemUtils.java | 36 +++- .../common/ssl/Pkcs11KeyConfig.java | 53 +++++ .../common/ssl/SslConfiguration.java | 24 ++- .../common/ssl/SslConfigurationLoader.java | 64 ++++-- .../elasticsearch/common/ssl/SslFileUtil.java | 126 +++++++++++ .../common/ssl/SslKeyConfig.java | 27 +++ .../common/ssl/SslKeystoreConfig.java | 200 ++++++++++++++++++ .../common/ssl/SslTrustConfig.java | 12 ++ .../common/ssl/StoreKeyConfig.java | 107 +++++----- .../common/ssl/StoreTrustConfig.java | 133 ++++++++++-- .../common/ssl/StoredCertificate.java | 80 +++++++ .../common/ssl/TrustEverythingConfig.java | 13 +- .../common/ssl/PemKeyConfigTests.java | 116 +++++++--- .../common/ssl/PemTrustConfigTests.java | 69 +++--- .../common/ssl/PemUtilsTests.java | 46 ++-- .../common/ssl/Pkcs11KeyConfigTests.java | 34 +++ .../ssl/SslConfigurationLoaderTests.java | 5 + .../common/ssl/SslConfigurationTests.java | 18 +- .../common/ssl/StoreKeyConfigTests.java | 63 ++++-- .../common/ssl/StoreTrustConfigTests.java | 86 +++++--- .../index/reindex/ReindexSslConfig.java | 4 + .../reindex/ReindexRestClientSslTests.java | 7 +- 29 files changed, 1458 insertions(+), 330 deletions(-) create mode 100644 libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/CompositeTrustConfig.java create mode 100644 libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/Pkcs11KeyConfig.java create mode 100644 libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslFileUtil.java create mode 100644 libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslKeystoreConfig.java create mode 100644 libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoredCertificate.java create mode 100644 libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/Pkcs11KeyConfigTests.java diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/CompositeTrustConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/CompositeTrustConfig.java new file mode 100644 index 0000000000000..f27fca593a111 --- /dev/null +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/CompositeTrustConfig.java @@ -0,0 +1,85 @@ +/* + * 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 javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedTrustManager; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * A TrustConfiguration that merges trust anchors from a number of other trust configs to produce a single {@link X509ExtendedTrustManager}. + */ +public class CompositeTrustConfig implements SslTrustConfig { + private final List configs; + + CompositeTrustConfig(List configs) { + this.configs = List.copyOf(configs); + } + + @Override + public Collection getDependentFiles() { + return configs.stream().map(SslTrustConfig::getDependentFiles).flatMap(Collection::stream).collect(Collectors.toUnmodifiableSet()); + } + + @Override + public boolean isSystemDefault() { + return configs.stream().allMatch(SslTrustConfig::isSystemDefault); + } + + @Override + public X509ExtendedTrustManager createTrustManager() { + try { + Collection trustedIssuers = configs.stream() + .map(c -> c.createTrustManager()) + .map(tm -> tm.getAcceptedIssuers()) + .flatMap(Arrays::stream) + .collect(Collectors.toSet()); + final KeyStore store = KeyStoreUtil.buildTrustStore(trustedIssuers); + return KeyStoreUtil.createTrustManager(store, TrustManagerFactory.getDefaultAlgorithm()); + } catch (GeneralSecurityException e) { + throw new SslConfigException("Cannot combine trust configurations [" + + configs.stream().map(SslTrustConfig::toString).collect(Collectors.joining(",")) + + "]", + e); + } + } + + @Override + public Collection getConfiguredCertificates() { + return configs.stream().map(SslTrustConfig::getConfiguredCertificates) + .flatMap(Collection::stream) + .collect(Collectors.toUnmodifiableList()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CompositeTrustConfig that = (CompositeTrustConfig) o; + return configs.equals(that.configs); + } + + @Override + public int hashCode() { + return Objects.hash(configs); + } + + @Override + public String toString() { + return "Composite-Trust{" + configs.stream().map(SslTrustConfig::toString).collect(Collectors.joining(",")) + '}'; + } +} diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/DefaultJdkTrustConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/DefaultJdkTrustConfig.java index 36b086c500df5..7ea8f1240c95c 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/DefaultJdkTrustConfig.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/DefaultJdkTrustConfig.java @@ -18,13 +18,15 @@ import java.security.KeyStore; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; +import java.util.List; import java.util.function.BiFunction; /** * This class represents a trust configuration that corresponds to the default trusted CAs of the JDK */ -final class DefaultJdkTrustConfig implements SslTrustConfig { +public final class DefaultJdkTrustConfig implements SslTrustConfig { + + public static final DefaultJdkTrustConfig DEFAULT_INSTANCE = new DefaultJdkTrustConfig(); private final BiFunction systemProperties; private final char[] trustStorePassword; @@ -51,6 +53,11 @@ final class DefaultJdkTrustConfig implements SslTrustConfig { this.trustStorePassword = trustStorePassword; } + @Override + public boolean isSystemDefault() { + return true; + } + @Override public X509ExtendedTrustManager createTrustManager() { try { @@ -90,7 +97,12 @@ private static char[] getSystemTrustStorePassword(BiFunction getDependentFiles() { - return Collections.emptyList(); + return List.of(); + } + + @Override + public Collection getConfiguredCertificates() { + return List.of(); } @Override diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/DerParser.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/DerParser.java index dd4f92f21920f..a188636b1c9fa 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/DerParser.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/DerParser.java @@ -31,7 +31,7 @@ * Based on https://github.com/groovenauts/jmeter_oauth_plugin/blob/master/jmeter/src/ * main/java/org/apache/jmeter/protocol/oauth/sampler/PrivateKeyReader.java */ -final class DerParser { +public final class DerParser { // Constructed Flag private static final int CONSTRUCTED = 0x20; @@ -55,12 +55,12 @@ final class DerParser { private InputStream derInputStream; private int maxAsnObjectLength; - DerParser(byte[] bytes) { + public DerParser(byte[] bytes) { this.derInputStream = new ByteArrayInputStream(bytes); this.maxAsnObjectLength = bytes.length; } - Asn1Object readAsn1Object() throws IOException { + public Asn1Object readAsn1Object() throws IOException { int tag = derInputStream.read(); if (tag == -1) { throw new IOException("Invalid DER: stream too short, missing tag"); @@ -133,7 +133,7 @@ private int getLength() throws IOException { * * @author zhang */ - static class Asn1Object { + public static class Asn1Object { protected final int type; protected final int length; diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/EmptyKeyConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/EmptyKeyConfig.java index cbd355b555abb..9a951ede2746c 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/EmptyKeyConfig.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/EmptyKeyConfig.java @@ -8,17 +8,22 @@ package org.elasticsearch.common.ssl; -import javax.net.ssl.X509ExtendedKeyManager; +import org.elasticsearch.core.Tuple; + import java.nio.file.Path; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; import java.util.Collection; -import java.util.Collections; +import java.util.List; + +import javax.net.ssl.X509ExtendedKeyManager; /** * A {@link SslKeyConfig} that does nothing (provides a null key manager) */ -final class EmptyKeyConfig implements SslKeyConfig { +public final class EmptyKeyConfig implements SslKeyConfig { - static final EmptyKeyConfig INSTANCE = new EmptyKeyConfig(); + public static final EmptyKeyConfig INSTANCE = new EmptyKeyConfig(); private EmptyKeyConfig() { // Enforce a single instance @@ -26,7 +31,22 @@ private EmptyKeyConfig() { @Override public Collection getDependentFiles() { - return Collections.emptyList(); + return List.of(); + } + + @Override + public List> getKeys() { + return List.of(); + } + + @Override + public Collection getConfiguredCertificates() { + return List.of(); + } + + @Override + public boolean hasKeyMaterial() { + return false; } @Override diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/KeyStoreUtil.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/KeyStoreUtil.java index 1be2c94985209..f2500938a9eab 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/KeyStoreUtil.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/KeyStoreUtil.java @@ -21,18 +21,25 @@ import java.nio.file.Files; import java.nio.file.Path; import java.security.GeneralSecurityException; +import java.security.Key; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.cert.Certificate; +import java.security.cert.X509Certificate; import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Locale; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * A variety of utility methods for working with or constructing {@link KeyStore} instances. */ -final class KeyStoreUtil { +public final class KeyStoreUtil { private KeyStoreUtil() { throw new IllegalStateException("Utility class should not be instantiated"); @@ -42,8 +49,8 @@ private KeyStoreUtil() { * Make a best guess about the "type" (see {@link KeyStore#getType()}) of the keystore file located at the given {@code Path}. * This method only references the file name of the keystore, it does not look at its contents. */ - static String inferKeyStoreType(Path path) { - String name = path == null ? "" : path.toString().toLowerCase(Locale.ROOT); + public static String inferKeyStoreType(String path) { + String name = path == null ? "" : path.toLowerCase(Locale.ROOT); if (name.endsWith(".p12") || name.endsWith(".pfx") || name.endsWith(".pkcs12")) { return "PKCS12"; } else { @@ -57,32 +64,25 @@ static String inferKeyStoreType(Path path) { * @throws SslConfigException If there is a problem reading from the provided path * @throws GeneralSecurityException If there is a problem with the keystore contents */ - static KeyStore readKeyStore(Path path, String type, char[] password) throws GeneralSecurityException { - if (Files.notExists(path)) { - throw new SslConfigException("cannot read a [" + type + "] keystore from [" + path.toAbsolutePath() - + "] because the file does not exist"); - } - try { - KeyStore keyStore = KeyStore.getInstance(type); + public static KeyStore readKeyStore(Path path, String ksType, char[] password) throws GeneralSecurityException, IOException { + KeyStore keyStore = KeyStore.getInstance(ksType); + if (path != null) { try (InputStream in = Files.newInputStream(path)) { keyStore.load(in, password); } - return keyStore; - } catch (IOException e) { - throw new SslConfigException("cannot read a [" + type + "] keystore from [" + path.toAbsolutePath() + "] - " + e.getMessage(), - e); } + return keyStore; } /** * Construct an in-memory keystore with a single key entry. - * @param certificateChain A certificate chain (ordered from subject to issuer) - * @param privateKey The private key that corresponds to the subject certificate (index 0 of {@code certificateChain}) - * @param password The password for the private key * + * @param certificateChain A certificate chain (ordered from subject to issuer) + * @param privateKey The private key that corresponds to the subject certificate (index 0 of {@code certificateChain}) + * @param password The password for the private key * @throws GeneralSecurityException If there is a problem with the provided certificates/key */ - static KeyStore buildKeyStore(Collection certificateChain, PrivateKey privateKey, char[] password) + public static KeyStore buildKeyStore(Collection certificateChain, PrivateKey privateKey, char[] password) throws GeneralSecurityException { KeyStore keyStore = buildNewKeyStore(); keyStore.setKeyEntry("key", privateKey, password, certificateChain.toArray(new Certificate[0])); @@ -91,9 +91,10 @@ static KeyStore buildKeyStore(Collection certificateChain, PrivateK /** * Construct an in-memory keystore with multiple trusted cert entries. + * * @param certificates The root certificates to trust */ - static KeyStore buildTrustStore(Iterable certificates) throws GeneralSecurityException { + public static KeyStore buildTrustStore(Iterable certificates) throws GeneralSecurityException { assert certificates != null : "Cannot create keystore with null certificates"; KeyStore store = buildNewKeyStore(); int counter = 0; @@ -115,10 +116,20 @@ private static KeyStore buildNewKeyStore() throws GeneralSecurityException { return keyStore; } + /** + * Returns a {@link X509ExtendedKeyManager} that is built from the provided private key and certificate chain + */ + public static X509ExtendedKeyManager createKeyManager(Certificate[] certificateChain, PrivateKey privateKey, char[] password) + throws GeneralSecurityException, IOException { + KeyStore keyStore = buildKeyStore(List.of(certificateChain), privateKey, password); + return createKeyManager(keyStore, password, KeyManagerFactory.getDefaultAlgorithm()); + } + /** * Creates a {@link X509ExtendedKeyManager} based on the key material in the provided {@link KeyStore} */ - static X509ExtendedKeyManager createKeyManager(KeyStore keyStore, char[] password, String algorithm) throws GeneralSecurityException { + public static X509ExtendedKeyManager createKeyManager(KeyStore keyStore, char[] password, + String algorithm) throws GeneralSecurityException { KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm); kmf.init(keyStore, password); KeyManager[] keyManagers = kmf.getKeyManagers(); @@ -134,7 +145,7 @@ static X509ExtendedKeyManager createKeyManager(KeyStore keyStore, char[] passwor /** * Creates a {@link X509ExtendedTrustManager} based on the trust material in the provided {@link KeyStore} */ - static X509ExtendedTrustManager createTrustManager(@Nullable KeyStore trustStore, String algorithm) + public static X509ExtendedTrustManager createTrustManager(@Nullable KeyStore trustStore, String algorithm) throws NoSuchAlgorithmException, KeyStoreException { TrustManagerFactory tmf = TrustManagerFactory.getInstance(algorithm); tmf.init(trustStore); @@ -148,5 +159,90 @@ static X509ExtendedTrustManager createTrustManager(@Nullable KeyStore trustStore + "] and truststore [" + trustStore + "]"); } + /** + * Creates a {@link X509ExtendedTrustManager} based on the provided certificates + * + * @param certificates the certificates to trust + * @return a trust manager that trusts the provided certificates + */ + public static X509ExtendedTrustManager createTrustManager(Collection certificates) throws GeneralSecurityException { + KeyStore store = buildTrustStore(certificates); + return createTrustManager(store, TrustManagerFactory.getDefaultAlgorithm()); + } + + static Stream stream(KeyStore keyStore, + Function exceptionHandler) { + try { + return Collections.list(keyStore.aliases()).stream().map(a -> new KeyStoreEntry(keyStore, a, exceptionHandler)); + } catch (KeyStoreException e) { + throw exceptionHandler.apply(e); + } + } + + static class KeyStoreEntry { + private final KeyStore store; + private final String alias; + private final Function exceptionHandler; + + KeyStoreEntry(KeyStore store, String alias, Function exceptionHandler) { + this.store = store; + this.alias = alias; + this.exceptionHandler = exceptionHandler; + } + + public String getAlias() { + return alias; + } + + public X509Certificate getX509Certificate() { + try { + final Certificate c = store.getCertificate(alias); + if (c instanceof X509Certificate) { + return (X509Certificate) c; + } else { + return null; + } + } catch (KeyStoreException e) { + throw exceptionHandler.apply(e); + } + } + + public boolean isKeyEntry() { + try { + return store.isKeyEntry(alias); + } catch (KeyStoreException e) { + throw exceptionHandler.apply(e); + } + } + + public PrivateKey getKey(char[] password) { + try { + final Key key = store.getKey(alias, password); + if (key instanceof PrivateKey) { + return (PrivateKey) key; + } + return null; + } catch (GeneralSecurityException e) { + throw exceptionHandler.apply(e); + } + } + + public List getX509CertificateChain() { + try { + final Certificate[] certificates = store.getCertificateChain(alias); + if (certificates == null || certificates.length == 0) { + return List.of(); + } + return Stream.of(certificates) + .filter(c -> c instanceof X509Certificate) + .map(X509Certificate.class::cast) + .collect(Collectors.toUnmodifiableList()); + } catch (KeyStoreException e) { + throw exceptionHandler.apply(e); + } + } + + } + } diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemKeyConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemKeyConfig.java index 6081c6191b7ac..7a64996a0129d 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemKeyConfig.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemKeyConfig.java @@ -8,84 +8,148 @@ package org.elasticsearch.common.ssl; -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.X509ExtendedKeyManager; -import java.io.FileNotFoundException; +import org.elasticsearch.core.Tuple; + import java.io.IOException; -import java.nio.file.NoSuchFileException; import java.nio.file.Path; +import java.security.AccessControlException; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.PrivateKey; import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.X509ExtendedKeyManager; + + /** * A {@link SslKeyConfig} that reads from PEM formatted paths. */ public final class PemKeyConfig implements SslKeyConfig { - private final Path certificate; - private final Path key; + + private static final String KEY_FILE_TYPE = "PEM private key"; + private static final String CERT_FILE_TYPE = "PEM certificate"; + + private final String certificate; + private final String key; private final char[] keyPassword; + private final Path configBasePath; - public PemKeyConfig(Path certificate, Path key, char[] keyPassword) { + /** + * @param certificate Path to the PEM formatted certificate + * @param key Path to the PEM formatted private key for {@code certificate} + * @param keyPassword Password for the private key (or empty is the key is not encrypted) + * @param configBasePath The base directory from which config files should be read (used for diagnostic exceptions) + */ + public PemKeyConfig(String certificate, String key, char[] keyPassword, Path configBasePath) { this.certificate = Objects.requireNonNull(certificate, "Certificate cannot be null"); this.key = Objects.requireNonNull(key, "Key cannot be null"); this.keyPassword = Objects.requireNonNull(keyPassword, "Key password cannot be null (but may be empty)"); + this.configBasePath = Objects.requireNonNull(configBasePath, "Config base path cannot be null"); + } + + @Override + public boolean hasKeyMaterial() { + return true; } @Override public Collection getDependentFiles() { - return Arrays.asList(certificate, key); + return Arrays.asList(resolve(certificate), resolve(key)); + } + + private Path resolve(String fileName) { + return configBasePath.resolve(fileName); + } + + @Override + public Collection getConfiguredCertificates() { + final List certificates = getCertificates(resolve(this.certificate)); + final List info = new ArrayList<>(certificates.size()); + boolean first = true; + for (Certificate cert : certificates) { + if (cert instanceof X509Certificate) { + info.add(new StoredCertificate((X509Certificate) cert, this.certificate, "PEM", null, first)); + } + first = false; + } + return info; } @Override public X509ExtendedKeyManager createKeyManager() { - PrivateKey privateKey = getPrivateKey(); - List certificates = getCertificates(); + final Path keyPath = resolve(key); + final PrivateKey privateKey = getPrivateKey(keyPath); + final Path certPath = resolve(this.certificate); + final List certificates = getCertificates(certPath); try { final KeyStore keyStore = KeyStoreUtil.buildKeyStore(certificates, privateKey, keyPassword); return KeyStoreUtil.createKeyManager(keyStore, keyPassword, KeyManagerFactory.getDefaultAlgorithm()); } catch (GeneralSecurityException e) { - throw new SslConfigException("failed to load a KeyManager for certificate/key pair [" + certificate + "], [" + key + "]", e); + throw new SslConfigException( + "failed to load a KeyManager for certificate/key pair [" + certPath + "], [" + keyPath + "]", e); } } - private PrivateKey getPrivateKey() { + @Override + public List> getKeys() { + final Path keyPath = resolve(key); + final Path certPath = resolve(this.certificate); + final List certificates = getCertificates(certPath); + if (certificates.isEmpty()) { + return List.of(); + } + final Certificate certificate = certificates.get(0); + if (certificate instanceof X509Certificate) { + return List.of(Tuple.tuple(getPrivateKey(keyPath), (X509Certificate) certificate)); + } else { + return List.of(); + } + } + + @Override + public SslTrustConfig asTrustConfig() { + return new PemTrustConfig(List.of(certificate), configBasePath); + } + + private PrivateKey getPrivateKey(Path path) { try { - final PrivateKey privateKey = PemUtils.readPrivateKey(key, () -> keyPassword); + final PrivateKey privateKey = PemUtils.parsePrivateKey(path, () -> keyPassword); if (privateKey == null) { - throw new SslConfigException("could not load ssl private key file [" + key + "]"); + throw new SslConfigException("could not load ssl private key file [" + path + "]"); } return privateKey; - } catch (FileNotFoundException | NoSuchFileException e) { - throw new SslConfigException("the configured ssl private key file [" + key.toAbsolutePath() + "] does not exist", e); + } catch (AccessControlException e) { + throw SslFileUtil.accessControlFailure(KEY_FILE_TYPE, List.of(path), e, configBasePath); } catch (IOException e) { - throw new SslConfigException("the configured ssl private key file [" + key.toAbsolutePath() + "] cannot be read", e); + throw SslFileUtil.ioException(KEY_FILE_TYPE, List.of(path), e); } catch (GeneralSecurityException e) { - throw new SslConfigException("cannot load ssl private key file [" + key.toAbsolutePath() + "]", e); + throw SslFileUtil.securityException(KEY_FILE_TYPE, List.of(path), e); } } - private List getCertificates() { + private List getCertificates(Path path) { try { - return PemUtils.readCertificates(Collections.singleton(certificate)); - } catch (FileNotFoundException | NoSuchFileException e) { - throw new SslConfigException("the configured ssl certificate file [" + certificate.toAbsolutePath() + "] does not exist", e); + return PemUtils.readCertificates(Collections.singleton(path)); + } catch (AccessControlException e) { + throw SslFileUtil.accessControlFailure(CERT_FILE_TYPE, List.of(path), e, configBasePath); } catch (IOException e) { - throw new SslConfigException("the configured ssl certificate file [" + certificate .toAbsolutePath()+ "] cannot be read", e); + throw SslFileUtil.ioException(CERT_FILE_TYPE, List.of(path), e); } catch (GeneralSecurityException e) { - throw new SslConfigException("cannot load ssl certificate from [" + certificate.toAbsolutePath() + "]", e); + throw SslFileUtil.securityException(CERT_FILE_TYPE, List.of(path), e); } } @Override public String toString() { - return "PEM-key-config{cert=" + certificate.toAbsolutePath() + " key=" + key.toAbsolutePath() + "}"; + return "PEM-key-config{cert=" + certificate + " key=" + key + "}"; } @Override diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemTrustConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemTrustConfig.java index 044ef433748b7..d34bea5130225 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemTrustConfig.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemTrustConfig.java @@ -10,15 +10,15 @@ import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509ExtendedTrustManager; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.nio.file.NoSuchFileException; import java.nio.file.Path; +import java.security.AccessControlException; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.cert.Certificate; -import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -32,10 +32,13 @@ * {@link java.security.cert.CertificateFactory#generateCertificate(InputStream)}. */ public final class PemTrustConfig implements SslTrustConfig { - private final List certificateAuthorities; + + private static final String CA_FILE_TYPE = "PEM " + SslConfigurationKeys.CERTIFICATE_AUTHORITIES; + private final List certificateAuthorities; + private final Path basePath; /** - * Construct a new trust config for the provided paths. + * Construct a new trust config for the provided paths (which will be resolved relative to the basePath). * The paths are stored as-is, and are not read until {@link #createTrustManager()} is called. * This means that *
    @@ -47,41 +50,67 @@ public final class PemTrustConfig implements SslTrustConfig { * *
*/ - public PemTrustConfig(List certificateAuthorities) { + public PemTrustConfig(List certificateAuthorities, Path basePath) { this.certificateAuthorities = Collections.unmodifiableList(certificateAuthorities); + this.basePath = basePath; } @Override public Collection getDependentFiles() { - return certificateAuthorities; + return resolveFiles(); + } + + @Override + public Collection getConfiguredCertificates() { + final List info = new ArrayList<>(certificateAuthorities.size()); + for (String caPath : certificateAuthorities) { + for (Certificate cert : readCertificates(List.of(resolveFile(caPath)))) { + if (cert instanceof X509Certificate) { + info.add(new StoredCertificate((X509Certificate) cert, caPath, "PEM", null, false)); + } + } + } + return info; } @Override public X509ExtendedTrustManager createTrustManager() { + final List paths = resolveFiles(); try { - final List certificates = loadCertificates(); - KeyStore store = KeyStoreUtil.buildTrustStore(certificates); + final List certificates = readCertificates(paths); + final KeyStore store = KeyStoreUtil.buildTrustStore(certificates); return KeyStoreUtil.createTrustManager(store, TrustManagerFactory.getDefaultAlgorithm()); } catch (GeneralSecurityException e) { - throw new SslConfigException("cannot create trust using PEM certificates [" + caPathsAsString() + "]", e); + throw new SslConfigException( + "cannot create trust using PEM certificates [" + SslFileUtil.pathsToString(paths) + "]", e); } } - private List loadCertificates() throws CertificateException { + private List resolveFiles() { + return this.certificateAuthorities.stream().map(this::resolveFile).collect(Collectors.toUnmodifiableList()); + } + + private Path resolveFile(String other) { + return basePath.resolve(other); + } + + private List readCertificates(List paths) { try { - return PemUtils.readCertificates(this.certificateAuthorities); - } catch (FileNotFoundException | NoSuchFileException e) { - throw new SslConfigException("cannot configure trust using PEM certificates [" + caPathsAsString() - + "] because one or more files do not exist", e); + return PemUtils.readCertificates(paths); + } catch (AccessControlException e) { + throw SslFileUtil.accessControlFailure(CA_FILE_TYPE, paths, e, basePath); } catch (IOException e) { - throw new SslConfigException("cannot configure trust using PEM certificates [" + caPathsAsString() - + "] because one or more files cannot be read", e); + throw SslFileUtil.ioException(CA_FILE_TYPE, paths, e); + } catch (GeneralSecurityException e) { + throw SslFileUtil.securityException(CA_FILE_TYPE, paths, e); + } catch (SslConfigException e) { + throw SslFileUtil.configException(CA_FILE_TYPE, paths, e); } } @Override public String toString() { - return "PEM-trust{" + caPathsAsString() + "}"; + return "PEM-trust{" + SslFileUtil.pathsToString(resolveFiles()) + "}"; } @Override @@ -101,11 +130,4 @@ public int hashCode() { return Objects.hash(certificateAuthorities); } - private String caPathsAsString() { - return certificateAuthorities.stream() - .map(Path::toAbsolutePath) - .map(Object::toString) - .collect(Collectors.joining(",")); - } - } diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemUtils.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemUtils.java index 46deade3b3d22..eaccf729cc13b 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemUtils.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemUtils.java @@ -18,14 +18,13 @@ import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; import java.io.BufferedReader; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.nio.file.NoSuchFileException; import java.nio.file.Path; +import java.security.AccessControlException; import java.security.GeneralSecurityException; import java.security.KeyFactory; import java.security.KeyPairGenerator; @@ -73,6 +72,29 @@ private PemUtils() { throw new IllegalStateException("Utility class should not be instantiated"); } + /** + * Creates a {@link PrivateKey} from the contents of a file and handles any exceptions + * + * @param path the path for the key file + * @param passwordSupplier A password supplier for the potentially encrypted (password protected) key + * @return a private key from the contents of the file + */ + public static PrivateKey readPrivateKey(Path path, Supplier passwordSupplier) throws IOException, GeneralSecurityException { + try { + final PrivateKey privateKey = PemUtils.parsePrivateKey(path, passwordSupplier); + if (privateKey == null) { + throw new SslConfigException("could not load ssl private key file [" + path + "]"); + } + return privateKey; + } catch (AccessControlException e) { + throw SslFileUtil.accessControlFailure("PEM private key", List.of(path), e, null); + } catch (IOException e) { + throw SslFileUtil.ioException("PEM private key", List.of(path), e); + } catch (GeneralSecurityException e) { + throw SslFileUtil.securityException("PEM private key", List.of(path), e); + } + } + /** * Creates a {@link PrivateKey} from the contents of a file. Supports PKCS#1, PKCS#8 * encoded formats of encrypted and plaintext RSA, DSA and EC(secp256r1) keys @@ -81,7 +103,7 @@ private PemUtils() { * @param passwordSupplier A password supplier for the potentially encrypted (password protected) key * @return a private key from the contents of the file */ - public static PrivateKey readPrivateKey(Path keyPath, Supplier passwordSupplier) throws IOException, GeneralSecurityException { + static PrivateKey parsePrivateKey(Path keyPath, Supplier passwordSupplier) throws IOException, GeneralSecurityException { try (BufferedReader bReader = Files.newBufferedReader(keyPath, StandardCharsets.UTF_8)) { String line = bReader.readLine(); while (null != line && line.startsWith(HEADER) == false) { @@ -109,13 +131,9 @@ public static PrivateKey readPrivateKey(Path keyPath, Supplier passwordS } else if (OPENSSL_EC_PARAMS_HEADER.equals(line.trim())) { return parseOpenSslEC(removeECHeaders(bReader), passwordSupplier); } else { - throw new SslConfigException("error parsing Private Key [" + keyPath.toAbsolutePath() - + "], file does not contain a supported key format"); + throw new SslConfigException("cannot read PEM private key [" + keyPath.toAbsolutePath() + + "] because the file does not contain a supported key format"); } - } catch (FileNotFoundException | NoSuchFileException e) { - throw new SslConfigException("private key file [" + keyPath.toAbsolutePath() + "] does not exist", e); - } catch (IOException | GeneralSecurityException e) { - throw new SslConfigException("private key file [" + keyPath.toAbsolutePath() + "] cannot be parsed", e); } } diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/Pkcs11KeyConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/Pkcs11KeyConfig.java new file mode 100644 index 0000000000000..af6326f54d869 --- /dev/null +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/Pkcs11KeyConfig.java @@ -0,0 +1,53 @@ +/* + * 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 java.nio.file.Path; +import java.util.Collection; +import java.util.List; + +/** + * A {@link SslKeyConfig} that builds a Key Manager from a keystore file. + */ +public class Pkcs11KeyConfig extends SslKeystoreConfig { + + public Pkcs11KeyConfig(char[] storePassword, char[] keyPassword, String algorithm, Path configBasePath) { + super(storePassword, keyPassword, algorithm, configBasePath); + } + + @Override + public SslTrustConfig asTrustConfig() { + return null; + } + + @Override + public Collection getDependentFiles() { + return List.of(); + } + + @Override + public String getKeystorePath() { + return null; + } + + @Override + public String getKeystoreType() { + return "PKCS11"; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Pkcs11KeyConfig{"); + sb.append(", storePassword=").append(getKeystorePassword().length == 0 ? "" : ""); + sb.append(", keyPassword=").append(hasKeyPassword() ? "" : ""); + sb.append(", algorithm=").append(getKeystoreAlgorithm()); + sb.append('}'); + return sb.toString(); + } +} diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfiguration.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfiguration.java index 63dea86bb6d74..ec06f629edd65 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfiguration.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfiguration.java @@ -14,6 +14,7 @@ import java.nio.file.Path; import java.security.GeneralSecurityException; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -56,6 +57,7 @@ public class SslConfiguration { ORDERED_PROTOCOL_ALGORITHM_MAP = Collections.unmodifiableMap(protocolAlgorithmMap); } + private final boolean explicitlyConfigured; private final SslTrustConfig trustConfig; private final SslKeyConfig keyConfig; private final SslVerificationMode verificationMode; @@ -63,8 +65,10 @@ public class SslConfiguration { private final List ciphers; private final List supportedProtocols; - public SslConfiguration(SslTrustConfig trustConfig, SslKeyConfig keyConfig, SslVerificationMode verificationMode, - SslClientAuthenticationMode clientAuth, List ciphers, List supportedProtocols) { + public SslConfiguration(boolean explicitlyConfigured, SslTrustConfig trustConfig, SslKeyConfig keyConfig, + SslVerificationMode verificationMode, SslClientAuthenticationMode clientAuth, + List ciphers, List supportedProtocols) { + this.explicitlyConfigured = explicitlyConfigured; if (ciphers == null || ciphers.isEmpty()) { throw new SslConfigException("cannot configure SSL/TLS without any supported cipher suites"); } @@ -114,6 +118,18 @@ public Collection getDependentFiles() { return paths; } + /** + * @return A collection of {@link StoredCertificate certificates} that are used by this SSL configuration. + * This includes certificates used for identity (with a private key) and those used for trust, but excludes + * certificates that are provided by the JRE. + */ + public Collection getConfiguredCertificates() { + List certificates = new ArrayList<>(); + certificates.addAll(keyConfig.getConfiguredCertificates()); + certificates.addAll(trustConfig.getConfiguredCertificates()); + return certificates; + } + /** * Dynamically create a new SSL context based on the current state of the configuration. * Because the {@link #getKeyConfig() key config} and {@link #getTrustConfig() trust config} may change based on the @@ -178,4 +194,8 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(trustConfig, keyConfig, verificationMode, clientAuth, ciphers, supportedProtocols); } + + public boolean isExplicitlyConfigured() { + return explicitlyConfigured; + } } diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationLoader.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationLoader.java index 9a1021d0df0be..f904a152b09c0 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationLoader.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationLoader.java @@ -235,6 +235,12 @@ public void setDefaultProtocols(List defaultProtocols) { this.defaultProtocols = defaultProtocols; } + /** + * 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()}. + */ + protected abstract boolean hasSettings(String prefix); + /** * Clients of this class should implement this method to load a fully-qualified key from the preferred settings source. * This method will be called for basic string settings (see {@link SslConfigurationKeys#getStringKeys()}). @@ -281,8 +287,8 @@ public SslConfiguration load(Path basePath) { final SslVerificationMode verificationMode = resolveSetting(VERIFICATION_MODE, SslVerificationMode::parse, defaultVerificationMode); final SslClientAuthenticationMode clientAuth = resolveSetting(CLIENT_AUTH, SslClientAuthenticationMode::parse, defaultClientAuth); - final SslTrustConfig trustConfig = buildTrustConfig(basePath, verificationMode); final SslKeyConfig keyConfig = buildKeyConfig(basePath); + final SslTrustConfig trustConfig = buildTrustConfig(basePath, verificationMode, keyConfig); if (protocols == null || protocols.isEmpty()) { throw new SslConfigException("no protocols configured in [" + settingPrefix + PROTOCOLS + "]"); @@ -290,12 +296,13 @@ public SslConfiguration load(Path basePath) { if (ciphers == null || ciphers.isEmpty()) { throw new SslConfigException("no cipher suites configured in [" + settingPrefix + CIPHERS + "]"); } - return new SslConfiguration(trustConfig, keyConfig, verificationMode, clientAuth, ciphers, protocols); + final boolean isExplicitlyConfigured = hasSettings(settingPrefix); + return new SslConfiguration(isExplicitlyConfigured, trustConfig, keyConfig, verificationMode, clientAuth, ciphers, protocols); } - private SslTrustConfig buildTrustConfig(Path basePath, SslVerificationMode verificationMode) { - final List certificateAuthorities = resolveListSetting(CERTIFICATE_AUTHORITIES, basePath::resolve, null); - final Path trustStorePath = resolveSetting(TRUSTSTORE_PATH, basePath::resolve, null); + protected SslTrustConfig buildTrustConfig(Path basePath, SslVerificationMode verificationMode, SslKeyConfig keyConfig) { + final List certificateAuthorities = resolveListSetting(CERTIFICATE_AUTHORITIES, Function.identity(), null); + final String trustStorePath = resolveSetting(TRUSTSTORE_PATH, Function.identity(), null); if (certificateAuthorities != null && trustStorePath != null) { throw new SslConfigException("cannot specify both [" + settingPrefix + CERTIFICATE_AUTHORITIES + "] and [" + @@ -305,21 +312,30 @@ private SslTrustConfig buildTrustConfig(Path basePath, SslVerificationMode verif return TrustEverythingConfig.TRUST_EVERYTHING; } if (certificateAuthorities != null) { - return new PemTrustConfig(certificateAuthorities); + return new PemTrustConfig(certificateAuthorities, basePath); } if (trustStorePath != null) { final char[] password = resolvePasswordSetting(TRUSTSTORE_SECURE_PASSWORD, TRUSTSTORE_LEGACY_PASSWORD); final String storeType = resolveSetting(TRUSTSTORE_TYPE, Function.identity(), inferKeyStoreType(trustStorePath)); final String algorithm = resolveSetting(TRUSTSTORE_ALGORITHM, Function.identity(), TrustManagerFactory.getDefaultAlgorithm()); - return new StoreTrustConfig(trustStorePath, password, storeType, algorithm); + return new StoreTrustConfig(trustStorePath, password, storeType, algorithm, true, basePath); + } + return buildDefaultTrustConfig(defaultTrustConfig, keyConfig); + } + + protected SslTrustConfig buildDefaultTrustConfig(SslTrustConfig defaultTrustConfig, SslKeyConfig keyConfig) { + final SslTrustConfig trust = keyConfig.asTrustConfig(); + if (trust == null) { + return defaultTrustConfig; + } else { + return new CompositeTrustConfig(List.of(defaultTrustConfig, trust)); } - return defaultTrustConfig; } - private SslKeyConfig buildKeyConfig(Path basePath) { - final Path certificatePath = resolveSetting(CERTIFICATE, basePath::resolve, null); - final Path keyPath = resolveSetting(KEY, basePath::resolve, null); - final Path keyStorePath = resolveSetting(KEYSTORE_PATH, basePath::resolve, null); + public SslKeyConfig buildKeyConfig(Path basePath) { + final String certificatePath = stringSetting(CERTIFICATE); + final String keyPath = stringSetting(KEY); + final String keyStorePath = stringSetting(KEYSTORE_PATH); if (certificatePath != null && keyStorePath != null) { throw new SslConfigException("cannot specify both [" + settingPrefix + CERTIFICATE + "] and [" + @@ -336,7 +352,7 @@ private SslKeyConfig buildKeyConfig(Path basePath) { settingPrefix + CERTIFICATE + "]"); } final char[] password = resolvePasswordSetting(KEY_SECURE_PASSPHRASE, KEY_LEGACY_PASSPHRASE); - return new PemKeyConfig(certificatePath, keyPath, password); + return new PemKeyConfig(certificatePath, keyPath, password, basePath); } if (keyStorePath != null) { @@ -347,15 +363,23 @@ private 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); + return new StoreKeyConfig(keyStorePath, storePassword, storeType, keyPassword, algorithm, basePath); } return defaultKeyConfig; } + protected Path resolvePath(String settingKey, Path basePath) { + return resolveSetting(settingKey, basePath::resolve, null); + } + + private String expandSettingKey(String key) { + return settingPrefix + key; + } + private char[] resolvePasswordSetting(String secureSettingKey, String legacySettingKey) { final char[] securePassword = resolveSecureSetting(secureSettingKey, null); - final String legacyPassword = resolveSetting(legacySettingKey, Function.identity(), null); + final String legacyPassword = stringSetting(legacySettingKey); if (securePassword == null) { if (legacyPassword == null) { return EMPTY_PASSWORD; @@ -372,9 +396,13 @@ private char[] resolvePasswordSetting(String secureSettingKey, String legacySett } } + private String stringSetting(String key) { + return resolveSetting(key, Function.identity(), null); + } + private V resolveSetting(String key, Function parser, V defaultValue) { try { - String setting = getSettingAsString(settingPrefix + key); + String setting = getSettingAsString(expandSettingKey(key)); if (setting == null || setting.isEmpty()) { return defaultValue; } @@ -388,7 +416,7 @@ private V resolveSetting(String key, Function parser, V defaultVa private char[] resolveSecureSetting(String key, char[] defaultValue) { try { - char[] setting = getSecureSetting(settingPrefix + key); + char[] setting = getSecureSetting(expandSettingKey(key)); if (setting == null || setting.length == 0) { return defaultValue; } @@ -403,7 +431,7 @@ private char[] resolveSecureSetting(String key, char[] defaultValue) { private List resolveListSetting(String key, Function parser, List defaultValue) { try { - final List list = getSettingAsList(settingPrefix + key); + final List list = getSettingAsList(expandSettingKey(key)); if (list == null || list.isEmpty()) { return defaultValue; } diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslFileUtil.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslFileUtil.java new file mode 100644 index 0000000000000..ae4820ac680ab --- /dev/null +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslFileUtil.java @@ -0,0 +1,126 @@ +/* + * 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 java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.AccessDeniedException; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.security.AccessControlException; +import java.security.GeneralSecurityException; +import java.security.UnrecoverableKeyException; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Utility methods for common file handling in SSL configuration + */ +final class SslFileUtil { + + static String pathsToString(List paths) { + return paths.stream() + .map(Path::toAbsolutePath) + .map(Object::toString) + .collect(Collectors.joining(",")); + } + + static SslConfigException ioException(String fileType, List paths, IOException cause) { + return ioException(fileType, paths, cause, null); + } + + static SslConfigException ioException(String fileType, List paths, IOException cause, String detail) { + if (cause instanceof FileNotFoundException || cause instanceof NoSuchFileException) { + return fileNotFound(fileType, paths, cause); + } + if (cause instanceof AccessDeniedException) { + return accessDenied(fileType, paths, (AccessDeniedException) cause); + } + String message = "cannot read configured " + fileType; + if (paths.isEmpty() == false) { + message += " [" + pathsToString(paths) + "]"; + } + + if (cause.getCause() instanceof UnrecoverableKeyException) { + message += " - this is usually caused by an incorrect password"; + } else if (cause != null && cause.getMessage() != null) { + message += " - " + cause.getMessage(); + } + + if (detail != null) { + message = message + "; " + detail; + } + return new SslConfigException(message, cause); + } + + static SslConfigException fileNotFound(String fileType, List paths, IOException cause) { + String message = "cannot read configured " + fileType + " [" + pathsToString(paths) + "] because "; + if (paths.size() == 1) { + message += "the file does not exist"; + } else { + message += "one or more files do not exist"; + } + return new SslConfigException(message, cause); + } + + static SslConfigException accessDenied(String fileType, List paths, AccessDeniedException cause) { + String message = "not permitted to read "; + if (paths.size() == 1) { + message += "the " + fileType + " file"; + } else { + message += "one of more " + fileType + "files"; + } + message += " [" + pathsToString(paths) + "]"; + return new SslConfigException(message, cause); + } + + static SslConfigException accessControlFailure(String fileType, List paths, AccessControlException cause, Path basePath) { + String message = "cannot read configured " + fileType + " [" + pathsToString(paths) + "] because "; + if (paths.size() == 1) { + message += "access to read the file is blocked"; + } else { + message += "access to read one or more files is blocked"; + } + message += "; SSL resources should be placed in the " ; + if (basePath == null) { + message += "Elasticsearch config directory"; + } else { + message += "[" + basePath + "] directory"; + } + return new SslConfigException(message, cause); + } + + public static SslConfigException securityException(String fileType, List paths, GeneralSecurityException cause) { + return securityException(fileType, paths, cause, null); + } + + public static SslConfigException securityException(String fileType, List paths, GeneralSecurityException cause, String detail) { + String message = "cannot load " + fileType; + if (paths.isEmpty() == false) { + message += " from [" + pathsToString(paths) + "]"; + } + message += " due to " + cause.getClass().getSimpleName(); + if (cause.getMessage() != null) { + message += " (" + cause.getMessage() + ")"; + } + if (detail != null) { + message = message + "; " + detail; + } + return new SslConfigException(message, cause); + } + + public static SslConfigException configException(String fileType, List paths, SslConfigException cause) { + String message = "cannot load " + fileType; + if (paths.isEmpty() == false) { + message += " from [" + pathsToString(paths) + "]"; + } + message += " - " + cause.getMessage(); + return new SslConfigException(message, cause); + } +} diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslKeyConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslKeyConfig.java index 100f11b80e62b..df3dcdb0c382b 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslKeyConfig.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslKeyConfig.java @@ -8,9 +8,14 @@ package org.elasticsearch.common.ssl; +import org.elasticsearch.core.Tuple; + import javax.net.ssl.X509ExtendedKeyManager; import java.nio.file.Path; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; import java.util.Collection; +import java.util.List; /** * An interface for building a key manager at runtime. @@ -31,5 +36,27 @@ public interface SslKeyConfig { */ X509ExtendedKeyManager createKeyManager(); + /** + * @return A list of private keys and their associated certificates + */ + List> getKeys(); + + /** + * @return A collection of {@link X509Certificate certificates} used by this config. + */ + Collection getConfiguredCertificates(); + + default boolean hasKeyMaterial() { + return getKeys().isEmpty() == false; + } + + /** + * Create a {@link SslTrustConfig} based on the underlying file store that backs this key config + * @return + */ + default SslTrustConfig asTrustConfig() { + return null; + } + } diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslKeystoreConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslKeystoreConfig.java new file mode 100644 index 0000000000000..fdade4f08be1b --- /dev/null +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslKeystoreConfig.java @@ -0,0 +1,200 @@ +/* + * 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.core.Tuple; + +import javax.net.ssl.X509ExtendedKeyManager; +import java.io.IOException; +import java.nio.file.Path; +import java.security.AccessControlException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.PrivateKey; +import java.security.UnrecoverableKeyException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Enumeration; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public abstract class SslKeystoreConfig implements SslKeyConfig { + private final char[] storePassword; + private final char[] keyPassword; + private final String algorithm; + private final Path configBasePath; + + public SslKeystoreConfig(char[] storePassword, char[] keyPassword, String algorithm, Path configBasePath) { + this.storePassword = Objects.requireNonNull(storePassword, "Keystore password cannot be null (but may be empty)"); + 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"); + } + + @Override + public boolean hasKeyMaterial() { + return true; + } + + protected Path resolvePath() { + final String path = getKeystorePath(); + if (path == null) { + return null; + } else { + return configBasePath.resolve(path); + } + } + + public abstract String getKeystorePath(); + + public abstract String getKeystoreType(); + + public char[] getKeystorePassword() { + return storePassword; + } + + public char[] getKeyPassword() { + return keyPassword; + } + + public boolean hasKeyPassword() { + return Arrays.equals(storePassword, keyPassword) == false; + } + + public String getKeystoreAlgorithm() { + return algorithm; + } + + protected Path getConfigBasePath() { + return configBasePath; + } + + @Override + public List> getKeys() { + final Path path = resolvePath(); + final KeyStore keyStore = readKeyStore(path); + return KeyStoreUtil.stream(keyStore, ex -> keystoreException(path, ex)) + .filter(KeyStoreUtil.KeyStoreEntry::isKeyEntry) + .map(entry -> { + final X509Certificate certificate = entry.getX509Certificate(); + if (certificate != null) { + return new Tuple<>(entry.getKey(keyPassword), certificate); + } + return null; + }) + .filter(Objects::nonNull) + .collect(Collectors.toUnmodifiableList()); + } + + @Override + public Collection getConfiguredCertificates() { + final Path path = resolvePath(); + final KeyStore keyStore = readKeyStore(path); + return KeyStoreUtil.stream(keyStore, ex -> keystoreException(path, ex)) + .flatMap(entry -> { + final List certificates = new ArrayList<>(); + boolean firstElement = true; + for (X509Certificate certificate : entry.getX509CertificateChain()) { + certificates.add(new StoredCertificate( + certificate, + getKeystorePath(), + getKeystoreType(), + entry.getAlias(), + firstElement + )); + firstElement = false; + } + return certificates.stream(); + }) + .collect(Collectors.toUnmodifiableList()); + } + + @Override + public X509ExtendedKeyManager createKeyManager() { + final Path path = resolvePath(); + return createKeyManager(path); + } + + private X509ExtendedKeyManager createKeyManager(Path path) { + try { + final KeyStore keyStore = readKeyStore(path); + checkKeyStore(keyStore, path); + return KeyStoreUtil.createKeyManager(keyStore, keyPassword, algorithm); + } catch (GeneralSecurityException e) { + throw keystoreException(path, e); + } + } + + private KeyStore readKeyStore(Path path) { + try { + return KeyStoreUtil.readKeyStore(path, getKeystoreType(), storePassword); + } catch (AccessControlException e) { + throw SslFileUtil.accessControlFailure("[" + getKeystoreType() + "] keystore", List.of(path), e, configBasePath); + } catch (IOException e) { + throw SslFileUtil.ioException("[" + getKeystoreType() + "] keystore", List.of(path), e); + } catch (GeneralSecurityException e) { + throw keystoreException(path, e); + } + } + + private SslConfigException keystoreException(Path path, GeneralSecurityException e) { + String extra = null; + if (e instanceof UnrecoverableKeyException) { + extra = "this is usually caused by an incorrect key-password"; + if (keyPassword.length == 0) { + extra += " (no key-password was provided)"; + } else if (Arrays.equals(storePassword, keyPassword)) { + extra += " (we tried to access the key using the same password as the keystore)"; + } + } + return SslFileUtil.securityException("[" + getKeystoreType() + "] keystore", path == null ? List.of() : List.of(path), e, extra); + } + + + /** + * Verifies that the keystore contains at least 1 private key entry. + */ + private void checkKeyStore(KeyStore keyStore, Path path) throws KeyStoreException { + Enumeration aliases = keyStore.aliases(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + if (keyStore.isKeyEntry(alias)) { + return; + } + } + String message = "the " + keyStore.getType() + " keystore"; + if (path != null) { + message += " [" + path + "]"; + } + message += "does not contain a private key entry"; + throw new SslConfigException(message); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SslKeystoreConfig that = (SslKeystoreConfig) o; + return Arrays.equals(storePassword, that.storePassword) + && Arrays.equals(keyPassword, that.keyPassword) + && algorithm.equals(that.algorithm); + } + + @Override + public int hashCode() { + int result = Objects.hash(algorithm); + result = 31 * result + Arrays.hashCode(storePassword); + result = 31 * result + Arrays.hashCode(keyPassword); + return result; + } +} diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslTrustConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslTrustConfig.java index 6cdc488db78d4..e6b73583f09e4 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslTrustConfig.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslTrustConfig.java @@ -10,6 +10,7 @@ import javax.net.ssl.X509ExtendedTrustManager; import java.nio.file.Path; +import java.security.cert.Certificate; import java.util.Collection; /** @@ -31,5 +32,16 @@ public interface SslTrustConfig { */ X509ExtendedTrustManager createTrustManager(); + /** + * @return A collection of {@link Certificate certificates} used by this config, excluding those shipped with the JDK + */ + Collection getConfiguredCertificates(); + + /** + * @return {@code true} if this trust config is based on the system default truststore + */ + default boolean isSystemDefault() { + return false; + } } diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreKeyConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreKeyConfig.java index fbacc8edc986c..8705119ad93f4 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreKeyConfig.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreKeyConfig.java @@ -9,87 +9,84 @@ package org.elasticsearch.common.ssl; import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.TrustManagerFactory; import java.nio.file.Path; -import java.security.GeneralSecurityException; import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.UnrecoverableKeyException; -import java.util.Arrays; import java.util.Collection; -import java.util.Collections; -import java.util.Enumeration; +import java.util.List; +import java.util.Objects; /** * A {@link SslKeyConfig} that builds a Key Manager from a keystore file. */ -public class StoreKeyConfig implements SslKeyConfig { - private final Path path; - private final char[] storePassword; +public class StoreKeyConfig extends SslKeystoreConfig { + private final String keystorePath; private final String type; - private final char[] keyPassword; - private final String algorithm; /** * @param path The path to the keystore file * @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(Path)}. + * See {@link KeyStoreUtil#inferKeyStoreType}. * @param keyPassword The password for the key(s) within the keystore * (see {@link javax.net.ssl.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) */ - StoreKeyConfig(Path path, char[] storePassword, String type, char[] keyPassword, String algorithm) { - this.path = path; - this.storePassword = storePassword; - this.type = type; - this.keyPassword = keyPassword; - this.algorithm = algorithm; + public StoreKeyConfig(String path, char[] storePassword, String type, char[] keyPassword, String algorithm, Path configBasePath) { + super(storePassword, keyPassword, algorithm, configBasePath); + this.keystorePath = Objects.requireNonNull(path, "Keystore path cannot be null"); + this.type = Objects.requireNonNull(type, "Keystore type cannot be null"); + } + + @Override + public SslTrustConfig asTrustConfig() { + final String trustStoreAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); + return new StoreTrustConfig(keystorePath, getKeystorePassword(), type, trustStoreAlgorithm, false, getConfigBasePath()); } @Override public Collection getDependentFiles() { - return Collections.singleton(path); + return List.of(resolvePath()); } @Override - public X509ExtendedKeyManager createKeyManager() { - try { - final KeyStore keyStore = KeyStoreUtil.readKeyStore(path, type, storePassword); - checkKeyStore(keyStore); - return KeyStoreUtil.createKeyManager(keyStore, keyPassword, algorithm); - } catch (UnrecoverableKeyException e) { - String message = "failed to load a KeyManager for keystore [" + path.toAbsolutePath() - + "], this is usually caused by an incorrect key-password"; - if (keyPassword.length == 0) { - message += " (no key-password was provided)"; - } else if (Arrays.equals(storePassword, keyPassword)) { - message += " (we tried to access the key using the same password as the keystore)"; - } - throw new SslConfigException(message, e); - } catch (GeneralSecurityException e) { - throw new SslConfigException("failed to load a KeyManager for keystore [" + path + "] of type [" + type + "]", e); - } + public String getKeystorePath() { + return keystorePath; } - /** - * Verifies that the keystore contains at least 1 private key entry. - */ - private void checkKeyStore(KeyStore keyStore) throws KeyStoreException { - Enumeration aliases = keyStore.aliases(); - while (aliases.hasMoreElements()) { - String alias = aliases.nextElement(); - if (keyStore.isKeyEntry(alias)) { - return; - } - } - final String message; - if (path != null) { - message = "the keystore [" + path + "] does not contain a private key entry"; - } else { - message = "the configured PKCS#11 token does not contain a private key entry"; - } - throw new SslConfigException(message); + @Override + public String getKeystoreType() { + return type; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + StoreKeyConfig that = (StoreKeyConfig) o; + return super.equals(that) && + keystorePath.equals(that.keystorePath) + && type.equals(that.type); + } + + @Override + public int hashCode() { + int result = Objects.hash(keystorePath, type); + result = 31 * result + super.hashCode(); + return result; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("StoreKeyConfig{"); + sb.append("path=").append(keystorePath); + sb.append(", storePassword=").append(getKeystorePassword().length == 0 ? "" : ""); + sb.append(", type=").append(type); + sb.append(", keyPassword=").append(hasKeyPassword() ? "" : ""); + sb.append(", algorithm=").append(getKeystoreAlgorithm()); + sb.append('}'); + return sb.toString(); } } diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreTrustConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreTrustConfig.java index 4edc3b5999c0e..47c0c31218e1c 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreTrustConfig.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreTrustConfig.java @@ -9,57 +9,124 @@ package org.elasticsearch.common.ssl; import javax.net.ssl.X509ExtendedTrustManager; +import java.io.IOException; import java.nio.file.Path; +import java.security.AccessControlException; import java.security.GeneralSecurityException; import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.Enumeration; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; /** * A {@link SslTrustConfig} that builds a Trust Manager from a keystore file. */ -final class StoreTrustConfig implements SslTrustConfig { - private final Path path; +public final class StoreTrustConfig implements SslTrustConfig { + private final String truststorePath; private final char[] password; private final String type; private final String algorithm; + private final boolean requireTrustAnchors; + private final Path configBasePath; /** * @param path The path to the keystore file * @param password The password for the keystore * @param type The {@link KeyStore#getType() type} of the keystore (typically "PKCS12" or "jks"). - * See {@link KeyStoreUtil#inferKeyStoreType(Path)}. + * See {@link KeyStoreUtil#inferKeyStoreType}. * @param algorithm The algorithm to use for the Trust Manager (see {@link javax.net.ssl.TrustManagerFactory#getAlgorithm()}). + * @param requireTrustAnchors If true, the truststore will be checked to ensure that it contains at least one valid trust anchor. + * @param configBasePath The base path for the configuration directory */ - StoreTrustConfig(Path path, char[] password, String type, String algorithm) { - this.path = path; - this.type = type; - this.algorithm = algorithm; - this.password = password; + public StoreTrustConfig(String path, char[] password, String type, String algorithm, boolean requireTrustAnchors, Path configBasePath) { + this.truststorePath = Objects.requireNonNull(path, "Truststore path cannot be null"); + this.type = Objects.requireNonNull(type, "Truststore type cannot be null"); + this.algorithm = Objects.requireNonNull(algorithm, "Truststore algorithm cannot be null"); + this.password = Objects.requireNonNull(password, "Truststore password cannot be null (but may be empty)"); + this.requireTrustAnchors = requireTrustAnchors; + this.configBasePath = configBasePath; } @Override public Collection getDependentFiles() { - return Collections.singleton(path); + return List.of(resolvePath()); + } + + private Path resolvePath() { + return configBasePath.resolve(this.truststorePath); + } + + @Override + public Collection getConfiguredCertificates() { + final Path path = resolvePath(); + final KeyStore trustStore = readKeyStore(path); + return KeyStoreUtil.stream(trustStore, ex -> keystoreException(path, ex)) + .map(entry -> { + final X509Certificate certificate = entry.getX509Certificate(); + if (certificate != null) { + final boolean hasKey = entry.isKeyEntry(); + return new StoredCertificate(certificate, this.truststorePath, this.type, entry.getAlias(), hasKey); + } else { + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toUnmodifiableList()); } @Override public X509ExtendedTrustManager createTrustManager() { + final Path path = resolvePath(); try { - final KeyStore store = KeyStoreUtil.readKeyStore(path, type, password); - checkTrustStore(store); + final KeyStore store = readKeyStore(path); + if (requireTrustAnchors) { + checkTrustStore(store, path); + } return KeyStoreUtil.createTrustManager(store, algorithm); } catch (GeneralSecurityException e) { - throw new SslConfigException("cannot create trust manager for path=[" + (path == null ? null : path.toAbsolutePath()) - + "] type=[" + type + "] password=[" + (password.length == 0 ? "" : "") + "]", e); + throw keystoreException(path, e); } } + private KeyStore readKeyStore(Path path) { + try { + return KeyStoreUtil.readKeyStore(path, type, password); + } catch (AccessControlException e) { + throw SslFileUtil.accessControlFailure(fileTypeForException(), List.of(path), e, configBasePath); + } catch (IOException e) { + throw SslFileUtil.ioException(fileTypeForException(), List.of(path), e, getAdditionalErrorDetails()); + } catch (GeneralSecurityException e) { + throw keystoreException(path, e); + } + } + + private SslConfigException keystoreException(Path path, GeneralSecurityException e) { + final String extra = getAdditionalErrorDetails(); + return SslFileUtil.securityException(fileTypeForException(), List.of(path), e, extra); + } + + private String getAdditionalErrorDetails() { + final String extra; + if (password.length == 0) { + extra = "(no password was provided)"; + } else { + extra = "(a keystore password was provided)"; + } + return extra; + } + + private String fileTypeForException() { + return "[" + type + "] keystore (as a truststore)"; + } + /** * Verifies that the keystore contains at least 1 trusted certificate entry. */ - private void checkTrustStore(KeyStore store) throws GeneralSecurityException { + private void checkTrustStore(KeyStore store, Path path) throws GeneralSecurityException { Enumeration aliases = store.aliases(); while (aliases.hasMoreElements()) { String alias = aliases.nextElement(); @@ -67,13 +134,35 @@ private void checkTrustStore(KeyStore store) throws GeneralSecurityException { return; } } - final String message; - if (path != null) { - message = "the truststore [" + path + "] does not contain any trusted certificate entries"; - } else { - message = "the configured PKCS#11 token does not contain any trusted certificate entries"; - } - throw new SslConfigException(message); + throw new SslConfigException("the truststore [" + path + "] does not contain any trusted certificate entries"); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + StoreTrustConfig that = (StoreTrustConfig) o; + return truststorePath.equals(that.truststorePath) + && Arrays.equals(password, that.password) + && type.equals(that.type) + && algorithm.equals(that.algorithm); + } + + @Override + public int hashCode() { + int result = Objects.hash(truststorePath, type, algorithm); + result = 31 * result + Arrays.hashCode(password); + return result; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("StoreTrustConfig{"); + sb.append("path=").append(truststorePath); + sb.append(", password=").append(password.length == 0 ? "" : ""); + sb.append(", type=").append(type); + sb.append(", algorithm=").append(algorithm); + sb.append('}'); + return sb.toString(); + } } diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoredCertificate.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoredCertificate.java new file mode 100644 index 0000000000000..49f7ec0f7853d --- /dev/null +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoredCertificate.java @@ -0,0 +1,80 @@ +/* + * 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.core.Nullable; + +import java.security.cert.X509Certificate; +import java.util.Objects; + +/** + * Information about a certificate that is locally stored.It includes a reference to the {@link X509Certificate} itself, + * as well as information about where it was loaded from. + */ +public class StoredCertificate { + + private final X509Certificate certificate; + + @Nullable + // Will be null in PKCS#11 + private final String path; + + private final String format; + + @Nullable + // Will be null in PEM + private final String alias; + + private final boolean hasPrivateKey; + + public StoredCertificate(X509Certificate certificate, String path, String format, String alias, boolean hasPrivateKey) { + this.certificate = Objects.requireNonNull(certificate, "Certificate may not be null"); + this.path = path; + this.format = Objects.requireNonNull(format, "Format may not be null"); + this.alias = alias; + this.hasPrivateKey = hasPrivateKey; + } + + public X509Certificate getCertificate() { + return certificate; + } + + public String getPath() { + return path; + } + + public String getFormat() { + return format; + } + + public String getAlias() { + return alias; + } + + public boolean hasPrivateKey() { + return hasPrivateKey; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + StoredCertificate that = (StoredCertificate) o; + return hasPrivateKey == that.hasPrivateKey + && certificate.equals(that.certificate) + && Objects.equals(path, that.path) + && format.equals(that.format) + && Objects.equals(alias, that.alias); + } + + @Override + public int hashCode() { + return Objects.hash(certificate, path, format, alias, hasPrivateKey); + } +} diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/TrustEverythingConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/TrustEverythingConfig.java index 27a770890f028..4936b3f78e8bb 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/TrustEverythingConfig.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/TrustEverythingConfig.java @@ -14,16 +14,16 @@ import java.nio.file.Path; import java.security.cert.X509Certificate; import java.util.Collection; -import java.util.Collections; +import java.util.List; /** * A {@link SslTrustConfig} that trusts all certificates. Used when {@link SslVerificationMode#isCertificateVerificationEnabled()} is * {@code false}. * This class cannot be used on FIPS-140 JVM as it has its own trust manager implementation. */ -final class TrustEverythingConfig implements SslTrustConfig { +public final class TrustEverythingConfig implements SslTrustConfig { - static final TrustEverythingConfig TRUST_EVERYTHING = new TrustEverythingConfig(); + public static final TrustEverythingConfig TRUST_EVERYTHING = new TrustEverythingConfig(); private TrustEverythingConfig() { // single instances @@ -66,7 +66,12 @@ public X509Certificate[] getAcceptedIssuers() { @Override public Collection getDependentFiles() { - return Collections.emptyList(); + return List.of(); + } + + @Override + public Collection getConfiguredCertificates() { + return List.of(); } @Override diff --git a/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/PemKeyConfigTests.java b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/PemKeyConfigTests.java index c132642dccb58..26248872ba2dd 100644 --- a/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/PemKeyConfigTests.java +++ b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/PemKeyConfigTests.java @@ -8,69 +8,124 @@ package org.elasticsearch.common.ssl; +import org.elasticsearch.core.Tuple; import org.elasticsearch.test.ESTestCase; import org.hamcrest.Matchers; +import org.junit.Before; -import javax.net.ssl.X509ExtendedKeyManager; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; import java.security.GeneralSecurityException; import java.security.PrivateKey; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Stream; + +import javax.net.ssl.X509ExtendedKeyManager; import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.iterableWithSize; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; public class PemKeyConfigTests extends ESTestCase { private static final int IP_NAME = 7; private static final int DNS_NAME = 2; + private Path configBasePath; + + @Before + public void setupPath(){ + configBasePath = getDataPath("/certs"); + } + public void testBuildKeyConfigFromPkcs1PemFilesWithoutPassword() throws Exception { - final Path cert = getDataPath("/certs/cert1/cert1.crt"); - final Path key = getDataPath("/certs/cert1/cert1.key"); - final PemKeyConfig keyConfig = new PemKeyConfig(cert, key, new char[0]); - assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(cert, key)); + final String cert = "cert1/cert1.crt"; + final String key = "cert1/cert1.key"; + final PemKeyConfig keyConfig = new PemKeyConfig(cert, key, new char[0], configBasePath); + assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(resolve(cert, key))); assertCertificateAndKey(keyConfig, "CN=cert1"); } public void testBuildKeyConfigFromPkcs1PemFilesWithPassword() throws Exception { - final Path cert = getDataPath("/certs/cert2/cert2.crt"); - final Path key = getDataPath("/certs/cert2/cert2.key"); - final PemKeyConfig keyConfig = new PemKeyConfig(cert, key, "c2-pass".toCharArray()); - assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(cert, key)); + final String cert = "cert2/cert2.crt"; + final String key = "cert2/cert2.key"; + final PemKeyConfig keyConfig = new PemKeyConfig(cert, key, "c2-pass".toCharArray(), configBasePath); + assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(resolve(cert, key))); assertCertificateAndKey(keyConfig, "CN=cert2"); } public void testBuildKeyConfigFromPkcs8PemFilesWithoutPassword() throws Exception { - final Path cert = getDataPath("/certs/cert1/cert1.crt"); - final Path key = getDataPath("/certs/cert1/cert1-pkcs8.key"); - final PemKeyConfig keyConfig = new PemKeyConfig(cert, key, new char[0]); - assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(cert, key)); + final String cert = "cert1/cert1.crt"; + final String key = "cert1/cert1-pkcs8.key"; + final PemKeyConfig keyConfig = new PemKeyConfig(cert, key, new char[0], configBasePath); + assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(resolve(cert, key))); assertCertificateAndKey(keyConfig, "CN=cert1"); } public void testBuildKeyConfigFromPkcs8PemFilesWithPassword() throws Exception { assumeFalse("Can't run in a FIPS JVM, PBE KeySpec is not available", inFipsJvm()); - final Path cert = getDataPath("/certs/cert2/cert2.crt"); - final Path key = getDataPath("/certs/cert2/cert2-pkcs8.key"); - final PemKeyConfig keyConfig = new PemKeyConfig(cert, key, "c2-pass".toCharArray()); - assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(cert, key)); + final String cert = "cert2/cert2.crt"; + final String key = "cert2/cert2-pkcs8.key"; + final PemKeyConfig keyConfig = new PemKeyConfig(cert, key, "c2-pass".toCharArray(), configBasePath); + assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(resolve(cert, key))); assertCertificateAndKey(keyConfig, "CN=cert2"); } + public void testBuildKeyConfigUsingCertificateChain() throws Exception { + final String ca = "ca1/ca.crt"; + final String cert = "cert1/cert1.crt"; + final String key = "cert1/cert1.key"; + + final Path chain = createTempFile("chain", ".crt"); + Files.write(chain, Files.readAllBytes(configBasePath.resolve(cert)), StandardOpenOption.APPEND); + Files.write(chain, Files.readAllBytes(configBasePath.resolve(ca)), StandardOpenOption.APPEND); + + final PemKeyConfig keyConfig = new PemKeyConfig(chain.toString(), key, new char[0], configBasePath); + assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(chain, configBasePath.resolve(key))); + assertCertificateAndKey(keyConfig, "CN=cert1", "CN=Test CA 1"); + final Collection certificates = keyConfig.getConfiguredCertificates(); + assertThat(certificates, Matchers.hasSize(2)); + final Iterator iterator = certificates.iterator(); + StoredCertificate c1 = iterator.next(); + StoredCertificate c2 = iterator.next(); + + assertThat(c1.getCertificate().getSubjectDN().toString(), equalTo("CN=cert1")); + assertThat(c1.hasPrivateKey(), equalTo(true)); + assertThat(c1.getAlias(), nullValue()); + assertThat(c1.getFormat(), equalTo("PEM")); + assertThat(c1.getPath(), equalTo(chain.toString())); + + assertThat(c2.getCertificate().getSubjectDN().toString(), equalTo("CN=Test CA 1")); + assertThat(c2.hasPrivateKey(), equalTo(false)); + assertThat(c2.getAlias(), nullValue()); + assertThat(c2.getFormat(), equalTo("PEM")); + assertThat(c2.getPath(), equalTo(chain.toString())); + + final List> keys = keyConfig.getKeys(); + assertThat(keys, iterableWithSize(1)); + assertThat(keys.get(0).v1(), notNullValue()); + assertThat(keys.get(0).v1().getAlgorithm(), equalTo("RSA")); + assertThat(keys.get(0).v2(), notNullValue()); + assertThat(keys.get(0).v2().getSubjectDN().toString(), equalTo("CN=cert1")); + } + public void testKeyManagerFailsWithIncorrectPassword() throws Exception { final Path cert = getDataPath("/certs/cert2/cert2.crt"); final Path key = getDataPath("/certs/cert2/cert2.key"); - final PemKeyConfig keyConfig = new PemKeyConfig(cert, key, "wrong-password".toCharArray()); + final PemKeyConfig keyConfig = new PemKeyConfig(cert.toString(), key.toString(), "wrong-password".toCharArray(), configBasePath); assertPasswordIsIncorrect(keyConfig, key); } @@ -78,7 +133,7 @@ public void testMissingCertificateFailsWithMeaningfulMessage() throws Exception final Path key = getDataPath("/certs/cert1/cert1.key"); final Path cert = key.getParent().resolve("dne.crt"); - final PemKeyConfig keyConfig = new PemKeyConfig(cert, key, new char[0]); + final PemKeyConfig keyConfig = new PemKeyConfig(cert.toString(), key.toString(), new char[0], configBasePath); assertFileNotFound(keyConfig, "certificate", cert); } @@ -86,7 +141,7 @@ public void testMissingKeyFailsWithMeaningfulMessage() throws Exception { final Path cert = getDataPath("/certs/cert1/cert1.crt"); final Path key = cert.getParent().resolve("dne.key"); - final PemKeyConfig keyConfig = new PemKeyConfig(cert, key, new char[0]); + final PemKeyConfig keyConfig = new PemKeyConfig(cert.toString(), key.toString(), new char[0], configBasePath); assertFileNotFound(keyConfig, "private key", key); } @@ -98,7 +153,7 @@ public void testKeyConfigReloadsFileContents() throws Exception { final Path cert = createTempFile("cert", ".crt"); final Path key = createTempFile("cert", ".key"); - final PemKeyConfig keyConfig = new PemKeyConfig(cert, key, new char[0]); + final PemKeyConfig keyConfig = new PemKeyConfig(cert.toString(), key.toString(), new char[0], configBasePath); Files.copy(cert1, cert, StandardCopyOption.REPLACE_EXISTING); Files.copy(key1, key, StandardCopyOption.REPLACE_EXISTING); @@ -116,7 +171,11 @@ public void testKeyConfigReloadsFileContents() throws Exception { assertFileNotFound(keyConfig, "certificate", cert); } - private void assertCertificateAndKey(PemKeyConfig keyConfig, String expectedDN) throws CertificateParsingException { + private Path[] resolve(String ... names) { + return Stream.of(names).map(configBasePath::resolve).toArray(Path[]::new); + } + + private void assertCertificateAndKey(PemKeyConfig keyConfig, String certDN, String... caDN) throws CertificateParsingException { final X509ExtendedKeyManager keyManager = keyConfig.createKeyManager(); assertThat(keyManager, notNullValue()); @@ -126,27 +185,32 @@ private void assertCertificateAndKey(PemKeyConfig keyConfig, String expectedDN) final X509Certificate[] chain = keyManager.getCertificateChain("key"); assertThat(chain, notNullValue()); - assertThat(chain, arrayWithSize(1)); + assertThat(chain, arrayWithSize(1 + caDN.length)); final X509Certificate certificate = chain[0]; assertThat(certificate.getIssuerDN().getName(), is("CN=Test CA 1")); - assertThat(certificate.getSubjectDN().getName(), is(expectedDN)); + assertThat(certificate.getSubjectDN().getName(), is(certDN)); assertThat(certificate.getSubjectAlternativeNames(), iterableWithSize(2)); assertThat(certificate.getSubjectAlternativeNames(), containsInAnyOrder( Arrays.asList(DNS_NAME, "localhost"), Arrays.asList(IP_NAME, "127.0.0.1") )); + + for (int i = 0; i < caDN.length; i++) { + final X509Certificate ca = chain[i + 1]; + assertThat(ca.getSubjectDN().getName(), is(caDN[i])); + } } private void assertPasswordIsIncorrect(PemKeyConfig keyConfig, Path key) { final SslConfigException exception = expectThrows(SslConfigException.class, keyConfig::createKeyManager); - assertThat(exception.getMessage(), containsString("private key file")); + assertThat(exception.getMessage(), containsString("PEM private key")); assertThat(exception.getMessage(), containsString(key.toAbsolutePath().toString())); assertThat(exception.getCause(), instanceOf(GeneralSecurityException.class)); } private void assertFileNotFound(PemKeyConfig keyConfig, String type, Path file) { final SslConfigException exception = expectThrows(SslConfigException.class, keyConfig::createKeyManager); - assertThat(exception.getMessage(), containsString(type + " file")); + assertThat(exception.getMessage(), containsString(type + " [")); assertThat(exception.getMessage(), containsString(file.toAbsolutePath().toString())); assertThat(exception.getMessage(), containsString("does not exist")); assertThat(exception.getCause(), instanceOf(NoSuchFileException.class)); diff --git a/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/PemTrustConfigTests.java b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/PemTrustConfigTests.java index a69bc0cba2a35..eaaa89ac6a568 100644 --- a/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/PemTrustConfigTests.java +++ b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/PemTrustConfigTests.java @@ -10,7 +10,9 @@ import org.elasticsearch.test.ESTestCase; import org.hamcrest.Matchers; +import org.junit.Before; +import javax.net.ssl.X509ExtendedTrustManager; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; @@ -21,59 +23,66 @@ import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; -import javax.net.ssl.X509ExtendedTrustManager; - public class PemTrustConfigTests extends ESTestCase { + private static final String CERTS_DIR = "/certs"; + private Path basePath; + + @Before + public void setupPath() { + basePath = getDataPath(CERTS_DIR); + } + public void testBuildTrustConfigFromSinglePemFile() throws Exception { - final Path cert = getDataPath("/certs/ca1/ca.crt"); - final PemTrustConfig trustConfig = new PemTrustConfig(Collections.singletonList(cert)); - assertThat(trustConfig.getDependentFiles(), Matchers.containsInAnyOrder(cert)); + final String cert = "ca1/ca.crt"; + final PemTrustConfig trustConfig = new PemTrustConfig(Collections.singletonList(cert), basePath); + assertThat(trustConfig.getDependentFiles(), Matchers.containsInAnyOrder(resolvePath(cert))); assertCertificateChain(trustConfig, "CN=Test CA 1"); } public void testBuildTrustConfigFromMultiplePemFiles() throws Exception { - final Path cert1 = getDataPath("/certs/ca1/ca.crt"); - final Path cert2 = getDataPath("/certs/ca2/ca.crt"); - final Path cert3 = getDataPath("/certs/ca3/ca.crt"); - final PemTrustConfig trustConfig = new PemTrustConfig(Arrays.asList(cert1, cert2, cert3)); - assertThat(trustConfig.getDependentFiles(), Matchers.containsInAnyOrder(cert1, cert2, cert3)); + final String cert1 = "ca1/ca.crt"; + final String cert2 = "ca2/ca.crt"; + final String cert3 = "ca3/ca.crt"; + final PemTrustConfig trustConfig = new PemTrustConfig(Arrays.asList(cert1, cert2, cert3), basePath); + assertThat(trustConfig.getDependentFiles(), Matchers.containsInAnyOrder(resolvePaths(cert1, cert2, cert3))); assertCertificateChain(trustConfig, "CN=Test CA 1", "CN=Test CA 2", "CN=Test CA 3"); } public void testBadFileFormatFails() throws Exception { final Path ca = createTempFile("ca", ".crt"); Files.write(ca, generateRandomByteArrayOfLength(128), StandardOpenOption.APPEND); - final PemTrustConfig trustConfig = new PemTrustConfig(Collections.singletonList(ca)); + final PemTrustConfig trustConfig = new PemTrustConfig(List.of(ca.toString()), basePath); assertThat(trustConfig.getDependentFiles(), Matchers.containsInAnyOrder(ca)); assertInvalidFileFormat(trustConfig, ca); } public void testEmptyFileFails() throws Exception { final Path ca = createTempFile("ca", ".crt"); - final PemTrustConfig trustConfig = new PemTrustConfig(Collections.singletonList(ca)); + final PemTrustConfig trustConfig = new PemTrustConfig(List.of(ca.toString()), basePath); assertThat(trustConfig.getDependentFiles(), Matchers.containsInAnyOrder(ca)); assertEmptyFile(trustConfig, ca); } public void testMissingFileFailsWithMeaningfulMessage() throws Exception { - final Path cert = getDataPath("/certs/ca1/ca.crt").getParent().resolve("dne.crt"); - final PemTrustConfig trustConfig = new PemTrustConfig(Collections.singletonList(cert)); - assertThat(trustConfig.getDependentFiles(), Matchers.containsInAnyOrder(cert)); - assertFileNotFound(trustConfig, cert); + final PemTrustConfig trustConfig = new PemTrustConfig(Collections.singletonList("dne.crt"), basePath); + final Path path = resolvePath("dne.crt"); + assertThat(trustConfig.getDependentFiles(), Matchers.containsInAnyOrder(path)); + assertFileNotFound(trustConfig, path); } public void testOneMissingFileFailsWithMeaningfulMessageEvenIfOtherFileExist() throws Exception { - final Path cert1 = getDataPath("/certs/ca1/ca.crt"); - final Path cert2 = getDataPath("/certs/ca2/ca.crt").getParent().resolve("dne.crt"); - final Path cert3 = getDataPath("/certs/ca3/ca.crt"); - final PemTrustConfig trustConfig = new PemTrustConfig(Arrays.asList(cert1, cert2, cert3)); - assertThat(trustConfig.getDependentFiles(), Matchers.containsInAnyOrder(cert1, cert2, cert3)); - assertFileNotFound(trustConfig, cert2); + final String cert1 = "ca1/ca.crt"; + final String cert2 = "ca2/dne.crt"; + final String cert3 = "ca3/ca.crt"; + final PemTrustConfig trustConfig = new PemTrustConfig(Arrays.asList(cert1, cert2, cert3), basePath); + assertThat(trustConfig.getDependentFiles(), Matchers.containsInAnyOrder(resolvePaths(cert1, cert2, cert3))); + assertFileNotFound(trustConfig, resolvePath(cert2)); } public void testTrustConfigReloadsFileContents() throws Exception { @@ -84,7 +93,7 @@ public void testTrustConfigReloadsFileContents() throws Exception { final Path ca1 = createTempFile("ca1", ".crt"); final Path ca2 = createTempFile("ca2", ".crt"); - final PemTrustConfig trustConfig = new PemTrustConfig(Arrays.asList(ca1, ca2)); + final PemTrustConfig trustConfig = new PemTrustConfig(Arrays.asList(ca1.toString(), ca2.toString()), basePath); Files.copy(cert1, ca1, StandardCopyOption.REPLACE_EXISTING); Files.copy(cert2, ca2, StandardCopyOption.REPLACE_EXISTING); @@ -126,19 +135,27 @@ private void assertInvalidFileFormat(PemTrustConfig trustConfig, Path file) { if (inFipsJvm() && exception.getMessage().contains("failed to parse any certificates")) { return; } - assertThat(exception.getMessage(), Matchers.containsString("cannot create trust")); - assertThat(exception.getMessage(), Matchers.containsString("PEM")); + assertThat(exception.getMessage(), Matchers.containsString("cannot load")); + assertThat(exception.getMessage(), Matchers.containsString("PEM certificate")); assertThat(exception.getCause(), Matchers.instanceOf(GeneralSecurityException.class)); } private void assertFileNotFound(PemTrustConfig trustConfig, Path file) { final SslConfigException exception = expectThrows(SslConfigException.class, trustConfig::createTrustManager); - assertThat(exception.getMessage(), Matchers.containsString("files do not exist")); + assertThat(exception.getMessage(), Matchers.containsString("not exist")); assertThat(exception.getMessage(), Matchers.containsString("PEM")); assertThat(exception.getMessage(), Matchers.containsString(file.toAbsolutePath().toString())); assertThat(exception.getCause(), Matchers.instanceOf(NoSuchFileException.class)); } + private Path resolvePath(String relativeName) { + return getDataPath(CERTS_DIR).resolve(relativeName); + } + + private Path[] resolvePaths(String... names) { + return Stream.of(names).map(this::resolvePath).toArray(Path[]::new); + } + private byte[] generateRandomByteArrayOfLength(int length) { byte[] bytes = randomByteArrayOfLength(length); /* diff --git a/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/PemUtilsTests.java b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/PemUtilsTests.java index cf2b20ff529d0..72f456daaa557 100644 --- a/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/PemUtilsTests.java +++ b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/PemUtilsTests.java @@ -10,6 +10,7 @@ import org.elasticsearch.test.ESTestCase; +import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; @@ -36,7 +37,7 @@ public void testReadPKCS8RsaKey() throws Exception { Key key = getKeyFromKeystore("RSA"); assertThat(key, notNullValue()); assertThat(key, instanceOf(PrivateKey.class)); - PrivateKey privateKey = PemUtils.readPrivateKey(getDataPath("/certs/pem-utils/rsa_key_pkcs8_plain.pem"), EMPTY_PASSWORD); + PrivateKey privateKey = PemUtils.parsePrivateKey(getDataPath("/certs/pem-utils/rsa_key_pkcs8_plain.pem"), EMPTY_PASSWORD); assertThat(privateKey, notNullValue()); assertThat(privateKey, equalTo(key)); } @@ -45,7 +46,7 @@ public void testReadPKCS8RsaKeyWithBagAttrs() throws Exception { Key key = getKeyFromKeystore("RSA"); assertThat(key, notNullValue()); assertThat(key, instanceOf(PrivateKey.class)); - PrivateKey privateKey = PemUtils.readPrivateKey(getDataPath("/certs/pem-utils/testnode_with_bagattrs.pem"), EMPTY_PASSWORD); + PrivateKey privateKey = PemUtils.parsePrivateKey(getDataPath("/certs/pem-utils/testnode_with_bagattrs.pem"), EMPTY_PASSWORD); assertThat(privateKey, equalTo(key)); } @@ -53,14 +54,14 @@ public void testReadPKCS8DsaKey() throws Exception { Key key = getKeyFromKeystore("DSA"); assertThat(key, notNullValue()); assertThat(key, instanceOf(PrivateKey.class)); - PrivateKey privateKey = PemUtils.readPrivateKey(getDataPath("/certs/pem-utils/dsa_key_pkcs8_plain.pem"), EMPTY_PASSWORD); + PrivateKey privateKey = PemUtils.parsePrivateKey(getDataPath("/certs/pem-utils/dsa_key_pkcs8_plain.pem"), EMPTY_PASSWORD); assertThat(privateKey, notNullValue()); assertThat(privateKey, equalTo(key)); } public void testReadEcKeyCurves() throws Exception { String curve = randomFrom("secp256r1", "secp384r1", "secp521r1"); - PrivateKey privateKey = PemUtils.readPrivateKey(getDataPath("/certs/pem-utils/private_" + curve + ".pem"), ""::toCharArray); + PrivateKey privateKey = PemUtils.parsePrivateKey(getDataPath("/certs/pem-utils/private_" + curve + ".pem"), ""::toCharArray); assertThat(privateKey, instanceOf(ECPrivateKey.class)); ECParameterSpec parameterSpec = ((ECPrivateKey) privateKey).getParams(); ECGenParameterSpec algorithmParameterSpec = new ECGenParameterSpec(curve); @@ -73,7 +74,7 @@ public void testReadPKCS8EcKey() throws Exception { Key key = getKeyFromKeystore("EC"); assertThat(key, notNullValue()); assertThat(key, instanceOf(PrivateKey.class)); - PrivateKey privateKey = PemUtils.readPrivateKey(getDataPath("/certs/pem-utils/ec_key_pkcs8_plain.pem"), EMPTY_PASSWORD); + PrivateKey privateKey = PemUtils.parsePrivateKey(getDataPath("/certs/pem-utils/ec_key_pkcs8_plain.pem"), EMPTY_PASSWORD); assertThat(privateKey, notNullValue()); assertThat(privateKey, equalTo(key)); } @@ -83,7 +84,7 @@ public void testReadEncryptedPKCS8Key() throws Exception { Key key = getKeyFromKeystore("RSA"); assertThat(key, notNullValue()); assertThat(key, instanceOf(PrivateKey.class)); - PrivateKey privateKey = PemUtils.readPrivateKey(getDataPath + PrivateKey privateKey = PemUtils.parsePrivateKey(getDataPath ("/certs/pem-utils/key_pkcs8_encrypted.pem"), TESTNODE_PASSWORD); assertThat(privateKey, notNullValue()); assertThat(privateKey, equalTo(key)); @@ -93,7 +94,7 @@ public void testReadDESEncryptedPKCS1Key() throws Exception { Key key = getKeyFromKeystore("RSA"); assertThat(key, notNullValue()); assertThat(key, instanceOf(PrivateKey.class)); - PrivateKey privateKey = PemUtils.readPrivateKey(getDataPath("/certs/pem-utils/testnode.pem"), TESTNODE_PASSWORD); + PrivateKey privateKey = PemUtils.parsePrivateKey(getDataPath("/certs/pem-utils/testnode.pem"), TESTNODE_PASSWORD); assertThat(privateKey, notNullValue()); assertThat(privateKey, equalTo(key)); } @@ -103,7 +104,7 @@ public void testReadAESEncryptedPKCS1Key() throws Exception { assertThat(key, notNullValue()); assertThat(key, instanceOf(PrivateKey.class)); String bits = randomFrom("128", "192", "256"); - PrivateKey privateKey = PemUtils.readPrivateKey(getDataPath("/certs/pem-utils/testnode-aes" + bits + ".pem"), TESTNODE_PASSWORD); + PrivateKey privateKey = PemUtils.parsePrivateKey(getDataPath("/certs/pem-utils/testnode-aes" + bits + ".pem"), TESTNODE_PASSWORD); assertThat(privateKey, notNullValue()); assertThat(privateKey, equalTo(key)); @@ -113,7 +114,7 @@ public void testReadPKCS1RsaKey() throws Exception { Key key = getKeyFromKeystore("RSA"); assertThat(key, notNullValue()); assertThat(key, instanceOf(PrivateKey.class)); - PrivateKey privateKey = PemUtils.readPrivateKey(getDataPath("/certs/pem-utils/testnode-unprotected.pem"), TESTNODE_PASSWORD); + PrivateKey privateKey = PemUtils.parsePrivateKey(getDataPath("/certs/pem-utils/testnode-unprotected.pem"), TESTNODE_PASSWORD); assertThat(privateKey, notNullValue()); assertThat(privateKey, equalTo(key)); @@ -123,7 +124,7 @@ public void testReadOpenSslDsaKey() throws Exception { Key key = getKeyFromKeystore("DSA"); assertThat(key, notNullValue()); assertThat(key, instanceOf(PrivateKey.class)); - PrivateKey privateKey = PemUtils.readPrivateKey(getDataPath("/certs/pem-utils/dsa_key_openssl_plain.pem"), EMPTY_PASSWORD); + PrivateKey privateKey = PemUtils.parsePrivateKey(getDataPath("/certs/pem-utils/dsa_key_openssl_plain.pem"), EMPTY_PASSWORD); assertThat(privateKey, notNullValue()); assertThat(privateKey, equalTo(key)); @@ -133,7 +134,7 @@ public void testReadOpenSslDsaKeyWithParams() throws Exception { Key key = getKeyFromKeystore("DSA"); assertThat(key, notNullValue()); assertThat(key, instanceOf(PrivateKey.class)); - PrivateKey privateKey = PemUtils.readPrivateKey(getDataPath("/certs/pem-utils/dsa_key_openssl_plain_with_params.pem"), + PrivateKey privateKey = PemUtils.parsePrivateKey(getDataPath("/certs/pem-utils/dsa_key_openssl_plain_with_params.pem"), EMPTY_PASSWORD); assertThat(privateKey, notNullValue()); @@ -144,7 +145,7 @@ public void testReadEncryptedOpenSslDsaKey() throws Exception { Key key = getKeyFromKeystore("DSA"); assertThat(key, notNullValue()); assertThat(key, instanceOf(PrivateKey.class)); - PrivateKey privateKey = PemUtils.readPrivateKey(getDataPath("/certs/pem-utils/dsa_key_openssl_encrypted.pem"), TESTNODE_PASSWORD); + PrivateKey privateKey = PemUtils.parsePrivateKey(getDataPath("/certs/pem-utils/dsa_key_openssl_encrypted.pem"), TESTNODE_PASSWORD); assertThat(privateKey, notNullValue()); assertThat(privateKey, equalTo(key)); @@ -154,7 +155,7 @@ public void testReadOpenSslEcKey() throws Exception { Key key = getKeyFromKeystore("EC"); assertThat(key, notNullValue()); assertThat(key, instanceOf(PrivateKey.class)); - PrivateKey privateKey = PemUtils.readPrivateKey(getDataPath("/certs/pem-utils/ec_key_openssl_plain.pem"), EMPTY_PASSWORD); + PrivateKey privateKey = PemUtils.parsePrivateKey(getDataPath("/certs/pem-utils/ec_key_openssl_plain.pem"), EMPTY_PASSWORD); assertThat(privateKey, notNullValue()); assertThat(privateKey, equalTo(key)); @@ -164,7 +165,7 @@ public void testReadOpenSslEcKeyWithParams() throws Exception { Key key = getKeyFromKeystore("EC"); assertThat(key, notNullValue()); assertThat(key, instanceOf(PrivateKey.class)); - PrivateKey privateKey = PemUtils.readPrivateKey(getDataPath("/certs/pem-utils/ec_key_openssl_plain_with_params.pem"), + PrivateKey privateKey = PemUtils.parsePrivateKey(getDataPath("/certs/pem-utils/ec_key_openssl_plain_with_params.pem"), EMPTY_PASSWORD); assertThat(privateKey, notNullValue()); @@ -175,7 +176,7 @@ public void testReadEncryptedOpenSslEcKey() throws Exception { Key key = getKeyFromKeystore("EC"); assertThat(key, notNullValue()); assertThat(key, instanceOf(PrivateKey.class)); - PrivateKey privateKey = PemUtils.readPrivateKey(getDataPath("/certs/pem-utils/ec_key_openssl_encrypted.pem"), TESTNODE_PASSWORD); + PrivateKey privateKey = PemUtils.parsePrivateKey(getDataPath("/certs/pem-utils/ec_key_openssl_encrypted.pem"), TESTNODE_PASSWORD); assertThat(privateKey, notNullValue()); assertThat(privateKey, equalTo(key)); @@ -183,30 +184,27 @@ public void testReadEncryptedOpenSslEcKey() throws Exception { public void testReadUnsupportedKey() { final Path path = getDataPath("/certs/pem-utils/key_unsupported.pem"); - SslConfigException e = expectThrows(SslConfigException.class, () -> PemUtils.readPrivateKey(path, TESTNODE_PASSWORD)); + SslConfigException e = expectThrows(SslConfigException.class, () -> PemUtils.parsePrivateKey(path, TESTNODE_PASSWORD)); assertThat(e.getMessage(), containsString("file does not contain a supported key format")); assertThat(e.getMessage(), containsString(path.toAbsolutePath().toString())); } - public void testReadPemCertificateAsKey() { + public void testErrorWhenReadingPemCertificateAsKey() { final Path path = getDataPath("/certs/pem-utils/testnode.crt"); - SslConfigException e = expectThrows(SslConfigException.class, () -> PemUtils.readPrivateKey(path, TESTNODE_PASSWORD)); + SslConfigException e = expectThrows(SslConfigException.class, () -> PemUtils.parsePrivateKey(path, TESTNODE_PASSWORD)); assertThat(e.getMessage(), containsString("file does not contain a supported key format")); assertThat(e.getMessage(), containsString(path.toAbsolutePath().toString())); } public void testReadCorruptedKey() { final Path path = getDataPath("/certs/pem-utils/corrupted_key_pkcs8_plain.pem"); - SslConfigException e = expectThrows(SslConfigException.class, () -> PemUtils.readPrivateKey(path, TESTNODE_PASSWORD)); - assertThat(e.getMessage(), containsString("private key")); - assertThat(e.getMessage(), containsString("cannot be parsed")); - assertThat(e.getMessage(), containsString(path.toAbsolutePath().toString())); - assertThat(e.getCause().getMessage(), containsString("PEM footer is invalid or missing")); + IOException e = expectThrows(IOException.class, () -> PemUtils.parsePrivateKey(path, TESTNODE_PASSWORD)); + assertThat(e.getMessage(), containsString("PEM footer is invalid or missing")); } public void testReadEmptyFile() { final Path path = getDataPath("/certs/pem-utils/empty.pem"); - SslConfigException e = expectThrows(SslConfigException.class, () -> PemUtils.readPrivateKey(path, TESTNODE_PASSWORD)); + SslConfigException e = expectThrows(SslConfigException.class, () -> PemUtils.parsePrivateKey(path, TESTNODE_PASSWORD)); assertThat(e.getMessage(), containsString("file is empty")); assertThat(e.getMessage(), containsString(path.toAbsolutePath().toString())); } diff --git a/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/Pkcs11KeyConfigTests.java b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/Pkcs11KeyConfigTests.java new file mode 100644 index 0000000000000..9d7f2323da97b --- /dev/null +++ b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/Pkcs11KeyConfigTests.java @@ -0,0 +1,34 @@ +/* + * 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 javax.net.ssl.KeyManagerFactory; +import java.nio.file.Path; +import java.security.KeyStoreException; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; + +public class Pkcs11KeyConfigTests extends ESTestCase { + + public void testTryLoadPkcs11Keystore() throws Exception { + assumeFalse("Can't run in a FIPS JVM", inFipsJvm()); + char[] password = "password".toCharArray(); + final Path configPath = getDataPath("."); + final String algorithm = KeyManagerFactory.getDefaultAlgorithm(); + final Pkcs11KeyConfig pkcs11KeyConfig = new Pkcs11KeyConfig(password, password, algorithm, configPath); + final SslConfigException ee = expectThrows(SslConfigException.class, pkcs11KeyConfig::createKeyManager); + assertThat(ee.getMessage(), containsString("cannot load [PKCS11] keystore")); + assertThat(ee.getCause(), instanceOf(KeyStoreException.class)); + assertThat(ee.getCause().getMessage(), containsString("PKCS11 not found")); + } + +} diff --git a/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/SslConfigurationLoaderTests.java b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/SslConfigurationLoaderTests.java index b14b67a9a5b9d..19ffb029974b6 100644 --- a/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/SslConfigurationLoaderTests.java +++ b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/SslConfigurationLoaderTests.java @@ -36,6 +36,11 @@ public class SslConfigurationLoaderTests extends ESTestCase { private Settings settings; private MockSecureSettings secureSettings = new MockSecureSettings(); private SslConfigurationLoader loader = new SslConfigurationLoader("test.ssl.") { + @Override + protected boolean hasSettings(String prefix) { + return true; + } + @Override protected String getSettingAsString(String key) throws Exception { return settings.get(key); diff --git a/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/SslConfigurationTests.java b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/SslConfigurationTests.java index d42b55b1eb1a3..bb6c4de02fcf3 100644 --- a/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/SslConfigurationTests.java +++ b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/SslConfigurationTests.java @@ -38,7 +38,7 @@ public void testBasicConstruction() { final List ciphers = randomSubsetOf(randomIntBetween(1, DEFAULT_CIPHERS.size()), DEFAULT_CIPHERS); final List protocols = randomSubsetOf(randomIntBetween(1, 4), VALID_PROTOCOLS); final SslConfiguration configuration = - new SslConfiguration(trustConfig, keyConfig, verificationMode, clientAuth, ciphers, protocols); + new SslConfiguration(true, trustConfig, keyConfig, verificationMode, clientAuth, ciphers, protocols); assertThat(configuration.getTrustConfig(), is(trustConfig)); assertThat(configuration.getKeyConfig(), is(keyConfig)); @@ -63,27 +63,27 @@ public void testEqualsAndHashCode() { final List ciphers = randomSubsetOf(randomIntBetween(1, DEFAULT_CIPHERS.size() - 1), DEFAULT_CIPHERS); final List protocols = randomSubsetOf(randomIntBetween(1, VALID_PROTOCOLS.length - 1), VALID_PROTOCOLS); final SslConfiguration configuration = - new SslConfiguration(trustConfig, keyConfig, verificationMode, clientAuth, ciphers, protocols); + new SslConfiguration(true, trustConfig, keyConfig, verificationMode, clientAuth, ciphers, protocols); EqualsHashCodeTestUtils.checkEqualsAndHashCode(configuration, - orig -> new SslConfiguration(orig.getTrustConfig(), orig.getKeyConfig(), orig.getVerificationMode(), orig.getClientAuth(), + orig -> new SslConfiguration(true, orig.getTrustConfig(), orig.getKeyConfig(), orig.getVerificationMode(), orig.getClientAuth(), orig.getCipherSuites(), orig.getSupportedProtocols()), orig -> { switch (randomIntBetween(1, 4)) { case 1: - return new SslConfiguration(orig.getTrustConfig(), orig.getKeyConfig(), + return new SslConfiguration(true, orig.getTrustConfig(), orig.getKeyConfig(), randomValueOtherThan(orig.getVerificationMode(), () -> randomFrom(SslVerificationMode.values())), orig.getClientAuth(), orig.getCipherSuites(), orig.getSupportedProtocols()); case 2: - return new SslConfiguration(orig.getTrustConfig(), orig.getKeyConfig(), orig.getVerificationMode(), + return new SslConfiguration(true, orig.getTrustConfig(), orig.getKeyConfig(), orig.getVerificationMode(), randomValueOtherThan(orig.getClientAuth(), () -> randomFrom(SslClientAuthenticationMode.values())), orig.getCipherSuites(), orig.getSupportedProtocols()); case 3: - return new SslConfiguration(orig.getTrustConfig(), orig.getKeyConfig(), + return new SslConfiguration(true, orig.getTrustConfig(), orig.getKeyConfig(), orig.getVerificationMode(), orig.getClientAuth(), DEFAULT_CIPHERS, orig.getSupportedProtocols()); case 4: default: - return new SslConfiguration(orig.getTrustConfig(), orig.getKeyConfig(), orig.getVerificationMode(), + return new SslConfiguration(true, orig.getTrustConfig(), orig.getKeyConfig(), orig.getVerificationMode(), orig.getClientAuth(), orig.getCipherSuites(), Arrays.asList(VALID_PROTOCOLS)); } }); @@ -92,7 +92,7 @@ public void testEqualsAndHashCode() { public void testDependentFiles() { final SslTrustConfig trustConfig = Mockito.mock(SslTrustConfig.class); final SslKeyConfig keyConfig = Mockito.mock(SslKeyConfig.class); - final SslConfiguration configuration = new SslConfiguration(trustConfig, keyConfig, + final SslConfiguration configuration = new SslConfiguration(true, trustConfig, keyConfig, randomFrom(SslVerificationMode.values()), randomFrom(SslClientAuthenticationMode.values()), DEFAULT_CIPHERS, SslConfigurationLoader.DEFAULT_PROTOCOLS); @@ -112,7 +112,7 @@ public void testBuildSslContext() { final SslTrustConfig trustConfig = Mockito.mock(SslTrustConfig.class); final SslKeyConfig keyConfig = Mockito.mock(SslKeyConfig.class); final String protocol = randomFrom(SslConfigurationLoader.DEFAULT_PROTOCOLS); - final SslConfiguration configuration = new SslConfiguration(trustConfig, keyConfig, + final SslConfiguration configuration = new SslConfiguration(true, trustConfig, keyConfig, randomFrom(SslVerificationMode.values()), randomFrom(SslClientAuthenticationMode.values()), DEFAULT_CIPHERS, Collections.singletonList(protocol)); diff --git a/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/StoreKeyConfigTests.java b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/StoreKeyConfigTests.java index 6b74ce57f5b7f..cffb0a95ec4c9 100644 --- a/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/StoreKeyConfigTests.java +++ b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/StoreKeyConfigTests.java @@ -8,13 +8,14 @@ package org.elasticsearch.common.ssl; +import org.elasticsearch.core.Tuple; import org.elasticsearch.test.ESTestCase; import org.hamcrest.Matchers; +import org.junit.Before; -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.X509ExtendedKeyManager; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.security.GeneralSecurityException; @@ -22,6 +23,10 @@ import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.util.Arrays; +import java.util.List; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.X509ExtendedKeyManager; import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -40,10 +45,17 @@ public class StoreKeyConfigTests extends ESTestCase { private static final char[] P12_PASS = "p12-pass".toCharArray(); private static final char[] JKS_PASS = "jks-pass".toCharArray(); + private Path configBasePath; + + @Before + public void setupPath() { + configBasePath = getDataPath("/certs"); + } + public void testLoadSingleKeyPKCS12() throws Exception { assumeFalse("Can't use JKS/PKCS12 keystores in a FIPS JVM", inFipsJvm()); final Path p12 = getDataPath("/certs/cert1/cert1.p12"); - final StoreKeyConfig keyConfig = new StoreKeyConfig(p12, P12_PASS, "PKCS12", P12_PASS, KeyManagerFactory.getDefaultAlgorithm()); + final StoreKeyConfig keyConfig = config(p12, P12_PASS, "PKCS12"); assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(p12)); assertKeysLoaded(keyConfig, "cert1"); } @@ -51,33 +63,34 @@ public void testLoadSingleKeyPKCS12() throws Exception { public void testLoadMultipleKeyPKCS12() 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 = new StoreKeyConfig(p12, P12_PASS, "PKCS12", P12_PASS, KeyManagerFactory.getDefaultAlgorithm()); + final StoreKeyConfig keyConfig = config(p12, P12_PASS, "PKCS12"); assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(p12)); assertKeysLoaded(keyConfig, "cert1", "cert2"); } public void testLoadMultipleKeyJksWithSeparateKeyPassword() throws Exception { assumeFalse("Can't use JKS/PKCS12 keystores in a FIPS JVM", inFipsJvm()); - final Path jks = getDataPath("/certs/cert-all/certs.jks"); + final String jks = "cert-all/certs.jks"; final StoreKeyConfig keyConfig = new StoreKeyConfig(jks, JKS_PASS, "jks", "key-pass".toCharArray(), - KeyManagerFactory.getDefaultAlgorithm()); - assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(jks)); + KeyManagerFactory.getDefaultAlgorithm(), 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 Path jks = getDataPath("/certs/cert-all/certs.jks"); + final String jks = "cert-all/certs.jks"; final StoreKeyConfig keyConfig = new StoreKeyConfig(jks, P12_PASS, "jks", "key-pass".toCharArray(), - KeyManagerFactory.getDefaultAlgorithm()); - assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(jks)); - assertPasswordIsIncorrect(keyConfig, jks); + KeyManagerFactory.getDefaultAlgorithm(), configBasePath); + final Path path = configBasePath.resolve(jks); + assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(path)); + assertPasswordIsIncorrect(keyConfig, path); } public void testKeyManagerFailsWithIncorrectKeyPassword() throws Exception { assumeFalse("Can't use JKS/PKCS12 keystores in a FIPS JVM", inFipsJvm()); final Path jks = getDataPath("/certs/cert-all/certs.jks"); - final StoreKeyConfig keyConfig = new StoreKeyConfig(jks, JKS_PASS, "jks", JKS_PASS, KeyManagerFactory.getDefaultAlgorithm()); + final StoreKeyConfig keyConfig = config(jks, JKS_PASS, "jks"); assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(jks)); assertPasswordIsIncorrect(keyConfig, jks); } @@ -85,7 +98,7 @@ public void testKeyManagerFailsWithIncorrectKeyPassword() throws Exception { public void testKeyManagerFailsWithMissingKeystoreFile() throws Exception { assumeFalse("Can't use JKS/PKCS12 keystores in a FIPS JVM", inFipsJvm()); final Path path = getDataPath("/certs/cert-all/certs.jks").getParent().resolve("dne.jks"); - final StoreKeyConfig keyConfig = new StoreKeyConfig(path, JKS_PASS, "jks", JKS_PASS, KeyManagerFactory.getDefaultAlgorithm()); + final StoreKeyConfig keyConfig = config(path, JKS_PASS, "jks"); assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(path)); assertFileNotFound(keyConfig, path); } @@ -104,7 +117,7 @@ public void testMissingKeyEntriesFailsWithMeaningfulMessage() throws Exception { ks = getDataPath("/certs/ca-all/ca.jks"); password = JKS_PASS; } - final StoreKeyConfig keyConfig = new StoreKeyConfig(ks, password, type, password, KeyManagerFactory.getDefaultAlgorithm()); + final StoreKeyConfig keyConfig = config(ks, password, type); assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(ks)); assertNoPrivateKeyEntries(keyConfig, ks); } @@ -117,7 +130,7 @@ public void testKeyConfigReloadsFileContents() throws Exception { final Path p12 = createTempFile("cert", ".p12"); - final StoreKeyConfig keyConfig = new StoreKeyConfig(p12, P12_PASS, "PKCS12", P12_PASS, KeyManagerFactory.getDefaultAlgorithm()); + final StoreKeyConfig keyConfig = config(p12, P12_PASS, "PKCS12"); Files.copy(cert1, p12, StandardCopyOption.REPLACE_EXISTING); assertKeysLoaded(keyConfig, "cert1"); @@ -135,6 +148,11 @@ public void testKeyConfigReloadsFileContents() throws Exception { assertFileNotFound(keyConfig, p12); } + private StoreKeyConfig config(Path path, char[] password, String type) { + final String pathName = path == null ? null : path.toString(); + return new StoreKeyConfig(pathName, password, type, password, KeyManagerFactory.getDefaultAlgorithm(), configBasePath); + } + private void assertKeysLoaded(StoreKeyConfig keyConfig, String... names) throws CertificateParsingException { final X509ExtendedKeyManager keyManager = keyConfig.createKeyManager(); assertThat(keyManager, notNullValue()); @@ -156,8 +174,19 @@ private void assertKeysLoaded(StoreKeyConfig keyConfig, String... names) throws Arrays.asList(IP_NAME, "127.0.0.1") )); } - } + final List> keys = keyConfig.getKeys(); + assertThat(keys, iterableWithSize(names.length)); + for (Tuple tup : keys) { + PrivateKey privateKey = tup.v1(); + assertThat(privateKey, notNullValue()); + assertThat(privateKey.getAlgorithm(), is("RSA")); + + final X509Certificate certificate = tup.v2(); + 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()); @@ -192,7 +221,7 @@ private void assertFileNotFound(StoreKeyConfig keyConfig, Path file) { assertThat(exception.getMessage(), containsString("keystore")); assertThat(exception.getMessage(), containsString(file.toAbsolutePath().toString())); assertThat(exception.getMessage(), containsString("does not exist")); - assertThat(exception.getCause(), nullValue()); + assertThat(exception.getCause(), instanceOf(NoSuchFileException.class)); } private void assertNoPrivateKeyEntries(StoreKeyConfig keyConfig, Path file) { diff --git a/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/StoreTrustConfigTests.java b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/StoreTrustConfigTests.java index bbe55688f35d5..91ab353758c9a 100644 --- a/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/StoreTrustConfigTests.java +++ b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/StoreTrustConfigTests.java @@ -10,11 +10,13 @@ import org.elasticsearch.test.ESTestCase; import org.hamcrest.Matchers; +import org.junit.Before; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509ExtendedTrustManager; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; @@ -25,7 +27,7 @@ import java.util.stream.Stream; import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.instanceOf; public class StoreTrustConfigTests extends ESTestCase { @@ -33,19 +35,26 @@ public class StoreTrustConfigTests extends ESTestCase { private static final char[] JKS_PASS = "jks-pass".toCharArray(); private static final String DEFAULT_ALGORITHM = TrustManagerFactory.getDefaultAlgorithm(); + private Path configBasePath; + + @Before + public void setupPath() { + configBasePath = getDataPath("/certs"); + } + public void testBuildTrustConfigFromPKCS12() throws Exception { assumeFalse("Can't use JKS/PKCS12 keystores in a FIPS JVM", inFipsJvm()); - final Path ks = getDataPath("/certs/ca1/ca.p12"); - final StoreTrustConfig trustConfig = new StoreTrustConfig(ks, P12_PASS, "PKCS12", DEFAULT_ALGORITHM); - assertThat(trustConfig.getDependentFiles(), Matchers.containsInAnyOrder(ks)); + final String ks = "ca1/ca.p12"; + final StoreTrustConfig trustConfig = new StoreTrustConfig(ks, P12_PASS, "PKCS12", DEFAULT_ALGORITHM, true, configBasePath); + assertThat(trustConfig.getDependentFiles(), Matchers.containsInAnyOrder(resolve(ks))); assertCertificateChain(trustConfig, "CN=Test CA 1"); } public void testBuildTrustConfigFromJKS() throws Exception { assumeFalse("Can't use JKS/PKCS12 keystores in a FIPS JVM", inFipsJvm()); - final Path ks = getDataPath("/certs/ca-all/ca.jks"); - final StoreTrustConfig trustConfig = new StoreTrustConfig(ks, JKS_PASS, "jks", DEFAULT_ALGORITHM); - assertThat(trustConfig.getDependentFiles(), Matchers.containsInAnyOrder(ks)); + final String ks = "ca-all/ca.jks"; + final StoreTrustConfig trustConfig = new StoreTrustConfig(ks, JKS_PASS, "jks", DEFAULT_ALGORITHM, true, configBasePath); + assertThat(trustConfig.getDependentFiles(), Matchers.containsInAnyOrder(resolve(ks))); assertCertificateChain(trustConfig, "CN=Test CA 1", "CN=Test CA 2", "CN=Test CA 3"); } @@ -53,44 +62,50 @@ public void testBadKeyStoreFormatFails() throws Exception { assumeFalse("Can't use JKS/PKCS12 keystores in a FIPS JVM", inFipsJvm()); final Path ks = createTempFile("ca", ".p12"); Files.write(ks, randomByteArrayOfLength(128), StandardOpenOption.APPEND); - final StoreTrustConfig trustConfig = new StoreTrustConfig(ks, new char[0], randomFrom("PKCS12", "jks"), DEFAULT_ALGORITHM); + final String type = randomFrom("PKCS12", "jks"); + final String fileName = ks.toString(); + final StoreTrustConfig trustConfig = new StoreTrustConfig(fileName, new char[0], type, DEFAULT_ALGORITHM, true, configBasePath); assertThat(trustConfig.getDependentFiles(), Matchers.containsInAnyOrder(ks)); assertInvalidFileFormat(trustConfig, ks); } public void testMissingKeyStoreFailsWithMeaningfulMessage() throws Exception { assumeFalse("Can't use JKS/PKCS12 keystores in a FIPS JVM", inFipsJvm()); - final Path ks = getDataPath("/certs/ca-all/ca.p12").getParent().resolve("keystore.dne"); - final StoreTrustConfig trustConfig = new StoreTrustConfig(ks, new char[0], randomFrom("PKCS12", "jks"), DEFAULT_ALGORITHM); - assertThat(trustConfig.getDependentFiles(), Matchers.containsInAnyOrder(ks)); - assertFileNotFound(trustConfig, ks); + final String ks = "ca-all/keystore.dne"; + final String type = randomFrom("PKCS12", "jks"); + final StoreTrustConfig trustConfig = new StoreTrustConfig(ks, new char[0], type, DEFAULT_ALGORITHM, true, configBasePath); + final Path path = resolve(ks); + assertThat(trustConfig.getDependentFiles(), Matchers.containsInAnyOrder(path)); + assertFileNotFound(trustConfig, path); } public void testIncorrectPasswordFailsWithMeaningfulMessage() throws Exception { assumeFalse("Can't use JKS/PKCS12 keystores in a FIPS JVM", inFipsJvm()); - final Path ks = getDataPath("/certs/ca1/ca.p12"); - final StoreTrustConfig trustConfig = new StoreTrustConfig(ks, new char[0], "PKCS12", DEFAULT_ALGORITHM); - assertThat(trustConfig.getDependentFiles(), Matchers.containsInAnyOrder(ks)); - assertPasswordIsIncorrect(trustConfig, ks); + final String ks = "ca1/ca.p12"; + final StoreTrustConfig trustConfig = new StoreTrustConfig(ks, new char[0], "PKCS12", DEFAULT_ALGORITHM, true, configBasePath); + final Path path = resolve(ks); + assertThat(trustConfig.getDependentFiles(), Matchers.containsInAnyOrder(path)); + assertPasswordIsIncorrect(trustConfig, path); } public void testMissingTrustEntriesFailsWithMeaningfulMessage() throws Exception { assumeFalse("Can't use JKS/PKCS12 keystores in a FIPS JVM", inFipsJvm()); - final Path ks; + final String ks; final char[] password; final String type; if (randomBoolean()) { type = "PKCS12"; - ks = getDataPath("/certs/cert-all/certs.p12"); + ks = "cert-all/certs.p12"; password = P12_PASS; } else { type = "jks"; - ks = getDataPath("/certs/cert-all/certs.jks"); + ks = "cert-all/certs.jks"; password = JKS_PASS; } - final StoreTrustConfig trustConfig = new StoreTrustConfig(ks, password, type, DEFAULT_ALGORITHM); - assertThat(trustConfig.getDependentFiles(), Matchers.containsInAnyOrder(ks)); - assertNoCertificateEntries(trustConfig, ks); + final StoreTrustConfig trustConfig = new StoreTrustConfig(ks, password, type, DEFAULT_ALGORITHM, true, configBasePath); + final Path path = resolve(ks); + assertThat(trustConfig.getDependentFiles(), Matchers.containsInAnyOrder(path)); + assertNoCertificateEntries(trustConfig, path); } public void testTrustConfigReloadsKeysStoreContents() throws Exception { @@ -100,7 +115,8 @@ public void testTrustConfigReloadsKeysStoreContents() throws Exception { final Path ks = createTempFile("ca", "p12"); - final StoreTrustConfig trustConfig = new StoreTrustConfig(ks, P12_PASS, "PKCS12", DEFAULT_ALGORITHM); + final String fileName = ks.toString(); + final StoreTrustConfig trustConfig = new StoreTrustConfig(fileName, P12_PASS, "PKCS12", DEFAULT_ALGORITHM, true, configBasePath); Files.copy(ks1, ks, StandardCopyOption.REPLACE_EXISTING); assertCertificateChain(trustConfig, "CN=Test CA 1"); @@ -115,6 +131,10 @@ public void testTrustConfigReloadsKeysStoreContents() throws Exception { assertCertificateChain(trustConfig, "CN=Test CA 1", "CN=Test CA 2", "CN=Test CA 3"); } + private Path resolve(String name) { + return configBasePath.resolve(name); + } + private void assertCertificateChain(StoreTrustConfig trustConfig, String... caNames) { final X509ExtendedTrustManager trustManager = trustConfig.createTrustManager(); final X509Certificate[] issuers = trustManager.getAcceptedIssuers(); @@ -128,18 +148,18 @@ private void assertCertificateChain(StoreTrustConfig trustConfig, String... caNa private void assertInvalidFileFormat(StoreTrustConfig trustConfig, Path file) { final SslConfigException exception = expectThrows(SslConfigException.class, trustConfig::createTrustManager); - assertThat(exception.getMessage(), Matchers.containsString("cannot read")); - assertThat(exception.getMessage(), Matchers.containsString("keystore")); - assertThat(exception.getMessage(), Matchers.containsString(file.toAbsolutePath().toString())); + assertThat(exception.getMessage(), containsString("cannot read")); + assertThat(exception.getMessage(), containsString("keystore")); + assertThat(exception.getMessage(), containsString(file.toAbsolutePath().toString())); assertThat(exception.getCause(), Matchers.instanceOf(IOException.class)); } private void assertFileNotFound(StoreTrustConfig trustConfig, Path file) { final SslConfigException exception = expectThrows(SslConfigException.class, trustConfig::createTrustManager); - assertThat(exception.getMessage(), Matchers.containsString("file does not exist")); - assertThat(exception.getMessage(), Matchers.containsString("keystore")); - assertThat(exception.getMessage(), Matchers.containsString(file.toAbsolutePath().toString())); - assertThat(exception.getCause(), nullValue()); + assertThat(exception.getMessage(), containsString("file does not exist")); + assertThat(exception.getMessage(), containsString("keystore")); + assertThat(exception.getMessage(), containsString(file.toAbsolutePath().toString())); + assertThat(exception.getCause(), instanceOf(NoSuchFileException.class)); } private void assertPasswordIsIncorrect(StoreTrustConfig trustConfig, Path key) { @@ -151,9 +171,9 @@ private void assertPasswordIsIncorrect(StoreTrustConfig trustConfig, Path key) { private void assertNoCertificateEntries(StoreTrustConfig trustConfig, Path file) { final SslConfigException exception = expectThrows(SslConfigException.class, trustConfig::createTrustManager); - assertThat(exception.getMessage(), Matchers.containsString("does not contain any trusted certificate entries")); - assertThat(exception.getMessage(), Matchers.containsString("truststore")); - assertThat(exception.getMessage(), Matchers.containsString(file.toAbsolutePath().toString())); + assertThat(exception.getMessage(), containsString("does not contain any trusted certificate entries")); + assertThat(exception.getMessage(), containsString("truststore")); + assertThat(exception.getMessage(), containsString(file.toAbsolutePath().toString())); } } diff --git a/modules/reindex/src/main/java/org/elasticsearch/index/reindex/ReindexSslConfig.java b/modules/reindex/src/main/java/org/elasticsearch/index/reindex/ReindexSslConfig.java index f6ceb039aae8c..71fef44de4666 100644 --- a/modules/reindex/src/main/java/org/elasticsearch/index/reindex/ReindexSslConfig.java +++ b/modules/reindex/src/main/java/org/elasticsearch/index/reindex/ReindexSslConfig.java @@ -80,6 +80,10 @@ public static List> getSettings() { public ReindexSslConfig(Settings settings, Environment environment, ResourceWatcherService resourceWatcher) { final SslConfigurationLoader loader = new SslConfigurationLoader("reindex.ssl.") { + @Override + protected boolean hasSettings(String prefix) { + return settings.getAsSettings(prefix).isEmpty() == false; + } @Override protected String getSettingAsString(String key) { diff --git a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexRestClientSslTests.java b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexRestClientSslTests.java index 2d93a460d17e9..383534718454e 100644 --- a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexRestClientSslTests.java +++ b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexRestClientSslTests.java @@ -97,10 +97,13 @@ private static SSLContext buildServerSslContext() throws Exception { final Path cert = PathUtils.get(ReindexRestClientSslTests.class.getResource("http/http.crt").toURI()); final Path key = PathUtils.get(ReindexRestClientSslTests.class.getResource("http/http.key").toURI()); - final X509ExtendedKeyManager keyManager = new PemKeyConfig(cert, key, password).createKeyManager(); + final Path configPath = cert.getParent().getParent(); + final PemKeyConfig keyConfig = new PemKeyConfig(cert.toString(), key.toString(), password, configPath); + final X509ExtendedKeyManager keyManager = keyConfig.createKeyManager(); final Path ca = PathUtils.get(ReindexRestClientSslTests.class.getResource("ca.pem").toURI()); - final X509ExtendedTrustManager trustManager = new PemTrustConfig(Collections.singletonList(ca)).createTrustManager(); + final List caList = Collections.singletonList(ca.toString()); + final X509ExtendedTrustManager trustManager = new PemTrustConfig(caList, configPath).createTrustManager(); sslContext.init(new KeyManager[] { keyManager }, new TrustManager[] { trustManager }, null); return sslContext; From 83c53cc46273b799bd0defe63b7db7cf92375a69 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Wed, 14 Jul 2021 11:39:36 +1000 Subject: [PATCH 2/5] Improve toString on SslKeystoreConfig --- .../common/ssl/Pkcs11KeyConfig.java | 9 ----- .../common/ssl/SslKeystoreConfig.java | 33 +++++++++++++++---- .../common/ssl/StoreKeyConfig.java | 12 ------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/Pkcs11KeyConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/Pkcs11KeyConfig.java index af6326f54d869..1e70b55e032bd 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/Pkcs11KeyConfig.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/Pkcs11KeyConfig.java @@ -41,13 +41,4 @@ public String getKeystoreType() { return "PKCS11"; } - @Override - public String toString() { - final StringBuilder sb = new StringBuilder("Pkcs11KeyConfig{"); - sb.append(", storePassword=").append(getKeystorePassword().length == 0 ? "" : ""); - sb.append(", keyPassword=").append(hasKeyPassword() ? "" : ""); - sb.append(", algorithm=").append(getKeystoreAlgorithm()); - sb.append('}'); - return sb.toString(); - } } diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslKeystoreConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslKeystoreConfig.java index fdade4f08be1b..2e74ae535adfd 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslKeystoreConfig.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslKeystoreConfig.java @@ -28,13 +28,13 @@ import java.util.Objects; import java.util.stream.Collectors; -public abstract class SslKeystoreConfig implements SslKeyConfig { +abstract class SslKeystoreConfig implements SslKeyConfig { private final char[] storePassword; private final char[] keyPassword; private final String algorithm; private final Path configBasePath; - public SslKeystoreConfig(char[] storePassword, char[] keyPassword, String algorithm, Path configBasePath) { + protected SslKeystoreConfig(char[] storePassword, char[] keyPassword, String algorithm, Path configBasePath) { this.storePassword = Objects.requireNonNull(storePassword, "Keystore password cannot be null (but may be empty)"); this.keyPassword = Objects.requireNonNull(keyPassword, "Key password cannot be null (but may be empty)"); this.algorithm = Objects.requireNonNull(algorithm, "Keystore algorithm cannot be null"); @@ -67,10 +67,6 @@ public char[] getKeyPassword() { return keyPassword; } - public boolean hasKeyPassword() { - return Arrays.equals(storePassword, keyPassword) == false; - } - public String getKeystoreAlgorithm() { return algorithm; } @@ -197,4 +193,29 @@ public int hashCode() { result = 31 * result + Arrays.hashCode(keyPassword); return result; } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(getClass().getSimpleName()); + sb.append('{'); + + String path = getKeystorePath(); + if (path != null) { + sb.append("path=").append(path).append(", "); + } + sb.append("type=").append(getKeystoreType()); + sb.append(", storePassword=").append(getKeystorePassword().length == 0 ? "" : ""); + sb.append(", keyPassword="); + if (keyPassword.length == 0) { + sb.append(""); + } else if (Arrays.equals(storePassword, keyPassword)) { + sb.append(""); + } else { + sb.append(""); + } + sb.append(", algorithm=").append(algorithm); + sb.append('}'); + return sb.toString(); + } + } diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreKeyConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreKeyConfig.java index 8705119ad93f4..0329aec54f35f 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreKeyConfig.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreKeyConfig.java @@ -77,16 +77,4 @@ public int hashCode() { return result; } - @Override - public String toString() { - final StringBuilder sb = new StringBuilder("StoreKeyConfig{"); - sb.append("path=").append(keystorePath); - sb.append(", storePassword=").append(getKeystorePassword().length == 0 ? "" : ""); - sb.append(", type=").append(type); - sb.append(", keyPassword=").append(hasKeyPassword() ? "" : ""); - sb.append(", algorithm=").append(getKeystoreAlgorithm()); - sb.append('}'); - return sb.toString(); - } - } From 03420fde053cae2e349e95d43220ccd31dc2f6d3 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Thu, 15 Jul 2021 15:45:47 +1000 Subject: [PATCH 3/5] Address Feedback - Remove PKCS#11 - Improve exception messages - Simplify return types --- .../common/ssl/EmptyKeyConfig.java | 2 +- .../common/ssl/PemKeyConfig.java | 22 +- .../common/ssl/Pkcs11KeyConfig.java | 44 ---- .../elasticsearch/common/ssl/SslFileUtil.java | 20 +- .../common/ssl/SslKeyConfig.java | 5 +- .../common/ssl/SslKeystoreConfig.java | 221 ------------------ .../common/ssl/StoreKeyConfig.java | 166 +++++++++++-- .../common/ssl/StoredCertificate.java | 2 +- .../common/ssl/Pkcs11KeyConfigTests.java | 34 --- 9 files changed, 187 insertions(+), 329 deletions(-) delete mode 100644 libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/Pkcs11KeyConfig.java delete mode 100644 libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslKeystoreConfig.java delete mode 100644 libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/Pkcs11KeyConfigTests.java diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/EmptyKeyConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/EmptyKeyConfig.java index 9a951ede2746c..363b47dbdc4bd 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/EmptyKeyConfig.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/EmptyKeyConfig.java @@ -40,7 +40,7 @@ public List> getKeys() { } @Override - public Collection getConfiguredCertificates() { + public Collection getConfiguredCertificates() { return List.of(); } diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemKeyConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemKeyConfig.java index 7a64996a0129d..1a6608f78bd40 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemKeyConfig.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemKeyConfig.java @@ -43,14 +43,14 @@ public final class PemKeyConfig implements SslKeyConfig { private final Path configBasePath; /** - * @param certificate Path to the PEM formatted certificate - * @param key Path to the PEM formatted private key for {@code certificate} - * @param keyPassword Password for the private key (or empty is the key is not encrypted) - * @param configBasePath The base directory from which config files should be read (used for diagnostic exceptions) + * @param certificatePath Path to the PEM formatted certificate + * @param keyPath Path to the PEM formatted private key for {@code certificate} + * @param keyPassword Password for the private key (or empty is the key is not encrypted) + * @param configBasePath The base directory from which config files should be read (used for diagnostic exceptions) */ - public PemKeyConfig(String certificate, String key, char[] keyPassword, Path configBasePath) { - this.certificate = Objects.requireNonNull(certificate, "Certificate cannot be null"); - this.key = Objects.requireNonNull(key, "Key cannot be null"); + public PemKeyConfig(String certificatePath, String keyPath, char[] keyPassword, Path configBasePath) { + this.certificate = Objects.requireNonNull(certificatePath, "Certificate path cannot be null"); + this.key = Objects.requireNonNull(keyPath, "Key path cannot be null"); this.keyPassword = Objects.requireNonNull(keyPassword, "Key password cannot be null (but may be empty)"); this.configBasePath = Objects.requireNonNull(configBasePath, "Config base path cannot be null"); } @@ -70,7 +70,7 @@ private Path resolve(String fileName) { } @Override - public Collection getConfiguredCertificates() { + public Collection getConfiguredCertificates() { final List certificates = getCertificates(resolve(this.certificate)); final List info = new ArrayList<>(certificates.size()); boolean first = true; @@ -106,9 +106,9 @@ public List> getKeys() { if (certificates.isEmpty()) { return List.of(); } - final Certificate certificate = certificates.get(0); - if (certificate instanceof X509Certificate) { - return List.of(Tuple.tuple(getPrivateKey(keyPath), (X509Certificate) certificate)); + final Certificate leafCertificate = certificates.get(0); + if (leafCertificate instanceof X509Certificate) { + return List.of(Tuple.tuple(getPrivateKey(keyPath), (X509Certificate) leafCertificate)); } else { return List.of(); } diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/Pkcs11KeyConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/Pkcs11KeyConfig.java deleted file mode 100644 index 1e70b55e032bd..0000000000000 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/Pkcs11KeyConfig.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 java.nio.file.Path; -import java.util.Collection; -import java.util.List; - -/** - * A {@link SslKeyConfig} that builds a Key Manager from a keystore file. - */ -public class Pkcs11KeyConfig extends SslKeystoreConfig { - - public Pkcs11KeyConfig(char[] storePassword, char[] keyPassword, String algorithm, Path configBasePath) { - super(storePassword, keyPassword, algorithm, configBasePath); - } - - @Override - public SslTrustConfig asTrustConfig() { - return null; - } - - @Override - public Collection getDependentFiles() { - return List.of(); - } - - @Override - public String getKeystorePath() { - return null; - } - - @Override - public String getKeystoreType() { - return "PKCS11"; - } - -} diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslFileUtil.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslFileUtil.java index ae4820ac680ab..2adf5ddc0974d 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslFileUtil.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslFileUtil.java @@ -47,7 +47,7 @@ static SslConfigException ioException(String fileType, List paths, IOExcep message += " [" + pathsToString(paths) + "]"; } - if (cause.getCause() instanceof UnrecoverableKeyException) { + if (hasCause(UnrecoverableKeyException.class, cause)) { message += " - this is usually caused by an incorrect password"; } else if (cause != null && cause.getMessage() != null) { message += " - " + cause.getMessage(); @@ -111,10 +111,28 @@ public static SslConfigException securityException(String fileType, List p } if (detail != null) { message = message + "; " + detail; + } else if (hasCause(UnrecoverableKeyException.class, cause)) { + message += "; this is usually caused by an incorrect password"; } + return new SslConfigException(message, cause); } + private static boolean hasCause(Class exceptionType, Throwable exception) { + if (exception == null) { + return false; + } + if (exceptionType.isInstance(exception)) { + return true; + } + + final Throwable cause = exception.getCause(); + if (cause == null || cause == exception) { + return false; + } + return hasCause(exceptionType, cause); + } + public static SslConfigException configException(String fileType, List paths, SslConfigException cause) { String message = "cannot load " + fileType; if (paths.isEmpty() == false) { diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslKeyConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslKeyConfig.java index df3dcdb0c382b..210f2221089d9 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslKeyConfig.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslKeyConfig.java @@ -42,9 +42,9 @@ public interface SslKeyConfig { List> getKeys(); /** - * @return A collection of {@link X509Certificate certificates} used by this config. + * @return A collection of {@link StoredCertificate certificates} used by this config. */ - Collection getConfiguredCertificates(); + Collection getConfiguredCertificates(); default boolean hasKeyMaterial() { return getKeys().isEmpty() == false; @@ -52,7 +52,6 @@ default boolean hasKeyMaterial() { /** * Create a {@link SslTrustConfig} based on the underlying file store that backs this key config - * @return */ default SslTrustConfig asTrustConfig() { return null; diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslKeystoreConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslKeystoreConfig.java deleted file mode 100644 index 2e74ae535adfd..0000000000000 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslKeystoreConfig.java +++ /dev/null @@ -1,221 +0,0 @@ -/* - * 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.core.Tuple; - -import javax.net.ssl.X509ExtendedKeyManager; -import java.io.IOException; -import java.nio.file.Path; -import java.security.AccessControlException; -import java.security.GeneralSecurityException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.PrivateKey; -import java.security.UnrecoverableKeyException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Enumeration; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; - -abstract class SslKeystoreConfig implements SslKeyConfig { - private final char[] storePassword; - private final char[] keyPassword; - private final String algorithm; - private final Path configBasePath; - - protected SslKeystoreConfig(char[] storePassword, char[] keyPassword, String algorithm, Path configBasePath) { - this.storePassword = Objects.requireNonNull(storePassword, "Keystore password cannot be null (but may be empty)"); - 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"); - } - - @Override - public boolean hasKeyMaterial() { - return true; - } - - protected Path resolvePath() { - final String path = getKeystorePath(); - if (path == null) { - return null; - } else { - return configBasePath.resolve(path); - } - } - - public abstract String getKeystorePath(); - - public abstract String getKeystoreType(); - - public char[] getKeystorePassword() { - return storePassword; - } - - public char[] getKeyPassword() { - return keyPassword; - } - - public String getKeystoreAlgorithm() { - return algorithm; - } - - protected Path getConfigBasePath() { - return configBasePath; - } - - @Override - public List> getKeys() { - final Path path = resolvePath(); - final KeyStore keyStore = readKeyStore(path); - return KeyStoreUtil.stream(keyStore, ex -> keystoreException(path, ex)) - .filter(KeyStoreUtil.KeyStoreEntry::isKeyEntry) - .map(entry -> { - final X509Certificate certificate = entry.getX509Certificate(); - if (certificate != null) { - return new Tuple<>(entry.getKey(keyPassword), certificate); - } - return null; - }) - .filter(Objects::nonNull) - .collect(Collectors.toUnmodifiableList()); - } - - @Override - public Collection getConfiguredCertificates() { - final Path path = resolvePath(); - final KeyStore keyStore = readKeyStore(path); - return KeyStoreUtil.stream(keyStore, ex -> keystoreException(path, ex)) - .flatMap(entry -> { - final List certificates = new ArrayList<>(); - boolean firstElement = true; - for (X509Certificate certificate : entry.getX509CertificateChain()) { - certificates.add(new StoredCertificate( - certificate, - getKeystorePath(), - getKeystoreType(), - entry.getAlias(), - firstElement - )); - firstElement = false; - } - return certificates.stream(); - }) - .collect(Collectors.toUnmodifiableList()); - } - - @Override - public X509ExtendedKeyManager createKeyManager() { - final Path path = resolvePath(); - return createKeyManager(path); - } - - private X509ExtendedKeyManager createKeyManager(Path path) { - try { - final KeyStore keyStore = readKeyStore(path); - checkKeyStore(keyStore, path); - return KeyStoreUtil.createKeyManager(keyStore, keyPassword, algorithm); - } catch (GeneralSecurityException e) { - throw keystoreException(path, e); - } - } - - private KeyStore readKeyStore(Path path) { - try { - return KeyStoreUtil.readKeyStore(path, getKeystoreType(), storePassword); - } catch (AccessControlException e) { - throw SslFileUtil.accessControlFailure("[" + getKeystoreType() + "] keystore", List.of(path), e, configBasePath); - } catch (IOException e) { - throw SslFileUtil.ioException("[" + getKeystoreType() + "] keystore", List.of(path), e); - } catch (GeneralSecurityException e) { - throw keystoreException(path, e); - } - } - - private SslConfigException keystoreException(Path path, GeneralSecurityException e) { - String extra = null; - if (e instanceof UnrecoverableKeyException) { - extra = "this is usually caused by an incorrect key-password"; - if (keyPassword.length == 0) { - extra += " (no key-password was provided)"; - } else if (Arrays.equals(storePassword, keyPassword)) { - extra += " (we tried to access the key using the same password as the keystore)"; - } - } - return SslFileUtil.securityException("[" + getKeystoreType() + "] keystore", path == null ? List.of() : List.of(path), e, extra); - } - - - /** - * Verifies that the keystore contains at least 1 private key entry. - */ - private void checkKeyStore(KeyStore keyStore, Path path) throws KeyStoreException { - Enumeration aliases = keyStore.aliases(); - while (aliases.hasMoreElements()) { - String alias = aliases.nextElement(); - if (keyStore.isKeyEntry(alias)) { - return; - } - } - String message = "the " + keyStore.getType() + " keystore"; - if (path != null) { - message += " [" + path + "]"; - } - message += "does not contain a private key entry"; - throw new SslConfigException(message); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - SslKeystoreConfig that = (SslKeystoreConfig) o; - return Arrays.equals(storePassword, that.storePassword) - && Arrays.equals(keyPassword, that.keyPassword) - && algorithm.equals(that.algorithm); - } - - @Override - public int hashCode() { - int result = Objects.hash(algorithm); - result = 31 * result + Arrays.hashCode(storePassword); - result = 31 * result + Arrays.hashCode(keyPassword); - return result; - } - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder(getClass().getSimpleName()); - sb.append('{'); - - String path = getKeystorePath(); - if (path != null) { - sb.append("path=").append(path).append(", "); - } - sb.append("type=").append(getKeystoreType()); - sb.append(", storePassword=").append(getKeystorePassword().length == 0 ? "" : ""); - sb.append(", keyPassword="); - if (keyPassword.length == 0) { - sb.append(""); - } else if (Arrays.equals(storePassword, keyPassword)) { - sb.append(""); - } else { - sb.append(""); - } - sb.append(", algorithm=").append(algorithm); - sb.append('}'); - return sb.toString(); - } - -} diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreKeyConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreKeyConfig.java index 0329aec54f35f..15f61a61cd28e 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreKeyConfig.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreKeyConfig.java @@ -8,20 +8,38 @@ package org.elasticsearch.common.ssl; +import org.elasticsearch.core.Tuple; + import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedKeyManager; +import java.io.IOException; import java.nio.file.Path; +import java.security.AccessControlException; +import java.security.GeneralSecurityException; import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.PrivateKey; +import java.security.UnrecoverableKeyException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.Enumeration; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; /** * A {@link SslKeyConfig} that builds a Key Manager from a keystore file. */ -public class StoreKeyConfig extends SslKeystoreConfig { +public class StoreKeyConfig implements SslKeyConfig { private final String keystorePath; private final String type; + private final char[] storePassword; + private final char[] keyPassword; + private final String algorithm; + private final Path configBasePath; /** * @param path The path to the keystore file @@ -34,7 +52,10 @@ public class StoreKeyConfig extends SslKeystoreConfig { * @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) { - super(storePassword, keyPassword, algorithm, configBasePath); + this.storePassword = Objects.requireNonNull(storePassword, "Keystore password cannot be null (but may be empty)"); + 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"); } @@ -42,7 +63,7 @@ public StoreKeyConfig(String path, char[] storePassword, String type, char[] key @Override public SslTrustConfig asTrustConfig() { final String trustStoreAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); - return new StoreTrustConfig(keystorePath, getKeystorePassword(), type, trustStoreAlgorithm, false, getConfigBasePath()); + return new StoreTrustConfig(keystorePath, storePassword, type, trustStoreAlgorithm, false, configBasePath); } @Override @@ -51,13 +72,130 @@ public Collection getDependentFiles() { } @Override - public String getKeystorePath() { - return keystorePath; + public boolean hasKeyMaterial() { + return true; + } + + private Path resolvePath() { + return configBasePath.resolve(keystorePath); + } + + @Override + public List> getKeys() { + final Path path = resolvePath(); + final KeyStore keyStore = readKeyStore(path); + return KeyStoreUtil.stream(keyStore, ex -> keystoreException(path, ex)) + .filter(KeyStoreUtil.KeyStoreEntry::isKeyEntry) + .map(entry -> { + final X509Certificate certificate = entry.getX509Certificate(); + if (certificate != null) { + return new Tuple<>(entry.getKey(keyPassword), certificate); + } + return null; + }) + .filter(Objects::nonNull) + .collect(Collectors.toUnmodifiableList()); + } + + @Override + public Collection getConfiguredCertificates() { + final Path path = resolvePath(); + final KeyStore keyStore = readKeyStore(path); + return KeyStoreUtil.stream(keyStore, ex -> keystoreException(path, ex)) + .flatMap(entry -> { + final List certificates = new ArrayList<>(); + boolean firstElement = true; + for (X509Certificate certificate : entry.getX509CertificateChain()) { + certificates.add(new StoredCertificate(certificate, keystorePath, type, entry.getAlias(), firstElement)); + firstElement = false; + } + return certificates.stream(); + }) + .collect(Collectors.toUnmodifiableList()); + } + + @Override + public X509ExtendedKeyManager createKeyManager() { + final Path path = resolvePath(); + return createKeyManager(path); + } + + private X509ExtendedKeyManager createKeyManager(Path path) { + try { + final KeyStore keyStore = readKeyStore(path); + checkKeyStore(keyStore, path); + return KeyStoreUtil.createKeyManager(keyStore, keyPassword, algorithm); + } catch (GeneralSecurityException e) { + throw keystoreException(path, e); + } + } + + private KeyStore readKeyStore(Path path) { + try { + return KeyStoreUtil.readKeyStore(path, type, storePassword); + } catch (AccessControlException e) { + throw SslFileUtil.accessControlFailure("[" + type + "] keystore", List.of(path), e, configBasePath); + } catch (IOException e) { + throw SslFileUtil.ioException("[" + type + "] keystore", List.of(path), e); + } catch (GeneralSecurityException e) { + throw keystoreException(path, e); + } + } + + private SslConfigException keystoreException(Path path, GeneralSecurityException e) { + String extra = null; + if (e instanceof UnrecoverableKeyException) { + extra = "this is usually caused by an incorrect key-password"; + if (keyPassword.length == 0) { + extra += " (no key-password was provided)"; + } else if (Arrays.equals(storePassword, keyPassword)) { + extra += " (we tried to access the key using the same password as the keystore)"; + } + } + return SslFileUtil.securityException("[" + type + "] keystore", path == null ? List.of() : List.of(path), e, extra); + } + + /** + * Verifies that the keystore contains at least 1 private key entry. + */ + private void checkKeyStore(KeyStore keyStore, Path path) throws KeyStoreException { + Enumeration aliases = keyStore.aliases(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + if (keyStore.isKeyEntry(alias)) { + return; + } + } + String message = "the " + keyStore.getType() + " keystore"; + if (path != null) { + message += " [" + path + "]"; + } + message += "does not contain a private key entry"; + throw new SslConfigException(message); } @Override - public String getKeystoreType() { - return type; + public String toString() { + final StringBuilder sb = new StringBuilder(getClass().getSimpleName()); + sb.append('{'); + + String path = keystorePath; + if (path != null) { + sb.append("path=").append(path).append(", "); + } + sb.append("type=").append(type); + sb.append(", storePassword=").append(storePassword.length == 0 ? "" : ""); + sb.append(", keyPassword="); + if (keyPassword.length == 0) { + sb.append(""); + } else if (Arrays.equals(storePassword, keyPassword)) { + sb.append(""); + } else { + sb.append(""); + } + sb.append(", algorithm=").append(algorithm); + sb.append('}'); + return sb.toString(); } @Override @@ -65,16 +203,18 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; StoreKeyConfig that = (StoreKeyConfig) o; - return super.equals(that) && - keystorePath.equals(that.keystorePath) - && type.equals(that.type); + return this.keystorePath.equals(that.keystorePath) + && this.type.equals(that.type) + && this.algorithm.equals(that.algorithm) + && Arrays.equals(this.storePassword, that.storePassword) + && Arrays.equals(this.keyPassword, that.keyPassword); } @Override public int hashCode() { - int result = Objects.hash(keystorePath, type); - result = 31 * result + super.hashCode(); + int result = Objects.hash(keystorePath, type, algorithm); + result = 31 * result + Arrays.hashCode(storePassword); + result = 31 * result + Arrays.hashCode(keyPassword); return result; } - } diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoredCertificate.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoredCertificate.java index 49f7ec0f7853d..24f33a1118b5e 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoredCertificate.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoredCertificate.java @@ -17,7 +17,7 @@ * Information about a certificate that is locally stored.It includes a reference to the {@link X509Certificate} itself, * as well as information about where it was loaded from. */ -public class StoredCertificate { +public final class StoredCertificate { private final X509Certificate certificate; diff --git a/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/Pkcs11KeyConfigTests.java b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/Pkcs11KeyConfigTests.java deleted file mode 100644 index 9d7f2323da97b..0000000000000 --- a/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/Pkcs11KeyConfigTests.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 javax.net.ssl.KeyManagerFactory; -import java.nio.file.Path; -import java.security.KeyStoreException; - -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.instanceOf; - -public class Pkcs11KeyConfigTests extends ESTestCase { - - public void testTryLoadPkcs11Keystore() throws Exception { - assumeFalse("Can't run in a FIPS JVM", inFipsJvm()); - char[] password = "password".toCharArray(); - final Path configPath = getDataPath("."); - final String algorithm = KeyManagerFactory.getDefaultAlgorithm(); - final Pkcs11KeyConfig pkcs11KeyConfig = new Pkcs11KeyConfig(password, password, algorithm, configPath); - final SslConfigException ee = expectThrows(SslConfigException.class, pkcs11KeyConfig::createKeyManager); - assertThat(ee.getMessage(), containsString("cannot load [PKCS11] keystore")); - assertThat(ee.getCause(), instanceOf(KeyStoreException.class)); - assertThat(ee.getCause().getMessage(), containsString("PKCS11 not found")); - } - -} From 0e856925c83f065e09e667eb52c30aa1b46ef324 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Thu, 15 Jul 2021 15:56:42 +1000 Subject: [PATCH 4/5] Add test for unordered PEM cert chain PemKeyConfig.getConfiguredCertificates relies on the certificate file containing a chain from leaf -> issuer -> .. -> root This commit adds a test to ensure that createKeyManager will fail if that order is not correct, and therefore the assumption in getConfiguredCertificates is acceptable --- .../common/ssl/PemKeyConfigTests.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/PemKeyConfigTests.java b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/PemKeyConfigTests.java index 26248872ba2dd..dbb37e4521794 100644 --- a/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/PemKeyConfigTests.java +++ b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/PemKeyConfigTests.java @@ -122,6 +122,31 @@ public void testBuildKeyConfigUsingCertificateChain() throws Exception { assertThat(keys.get(0).v2().getSubjectDN().toString(), equalTo("CN=cert1")); } + public void testInvertedCertificateChainFailsToCreateKeyManager() throws Exception { + final String ca = "ca1/ca.crt"; + final String cert = "cert1/cert1.crt"; + final String key = "cert1/cert1.key"; + + final Path chain = createTempFile("chain", ".crt"); + // This is (intentionally) the wrong order. It should be cert + ca. + Files.write(chain, Files.readAllBytes(configBasePath.resolve(ca)), StandardOpenOption.APPEND); + Files.write(chain, Files.readAllBytes(configBasePath.resolve(cert)), StandardOpenOption.APPEND); + + final PemKeyConfig keyConfig = new PemKeyConfig(chain.toString(), key, new char[0], configBasePath); + final SslConfigException exception = expectThrows(SslConfigException.class, keyConfig::createKeyManager); + + assertThat(exception.getMessage(), containsString("failed to load a KeyManager")); + final Throwable cause = exception.getCause(); + assertThat(cause, notNullValue()); + if (inFipsJvm()) { + // BC FKS first checks that the key & cert match (they don't because the key is for 'cert1' not 'ca') + assertThat(cause.getMessage(), containsString("RSA keys do not have the same modulus")); + } else { + // SUN PKCS#12 first checks that the chain is correctly structured (it's not, due to the order) + assertThat(cause.getMessage(), containsString("Certificate chain is not valid")); + } + } + public void testKeyManagerFailsWithIncorrectPassword() throws Exception { final Path cert = getDataPath("/certs/cert2/cert2.crt"); final Path key = getDataPath("/certs/cert2/cert2.key"); From 75bcd1b2ac7e5dab3a79b910acc64cae79adf141 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Thu, 15 Jul 2021 16:11:01 +1000 Subject: [PATCH 5/5] Add javadoc on KeyStoreEntry --- .../common/ssl/KeyStoreUtil.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/KeyStoreUtil.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/KeyStoreUtil.java index f2500938a9eab..2a29e931dffa2 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/KeyStoreUtil.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/KeyStoreUtil.java @@ -194,6 +194,20 @@ public String getAlias() { return alias; } + /** + * If this entry is a private key entry (see {@link #isKeyEntry()}), + * and the entry includes a certificate chain, + * and the leaf (first) element of that chain is an X.509 certificate, + * then that leaf certificate is returned. + * + * If this entry is a trusted certificate entry + * and the trusted certificate is an X.509 certificate, + * then the trusted certificate is returned. + * + * In all other cases, returns {@code null}. + * + * @see KeyStore#getCertificate(String) + */ public X509Certificate getX509Certificate() { try { final Certificate c = store.getCertificate(alias); @@ -207,6 +221,9 @@ public X509Certificate getX509Certificate() { } } + /** + * @see KeyStore#isKeyEntry(String) + */ public boolean isKeyEntry() { try { return store.isKeyEntry(alias); @@ -215,6 +232,12 @@ public boolean isKeyEntry() { } } + /** + * If the current entry stores a private key, returns that key. + * Otherwise returns {@code null}. + * + * @see KeyStore#getKey(String, char[]) + */ public PrivateKey getKey(char[] password) { try { final Key key = store.getKey(alias, password); @@ -227,6 +250,11 @@ public PrivateKey getKey(char[] password) { } } + /** + * If this entry is a private key entry (see {@link #isKeyEntry()}), returns the certificate chain that is stored in the entry. + * If the entry contains any certificates that are not X.509 certificates, they are ignored. + * If the entry is not a private key entry, or it does not contain any X.509 certificates, then an empty list is returned. + */ public List getX509CertificateChain() { try { final Certificate[] certificates = store.getCertificateChain(alias);