diff --git a/libs/ssl-config/build.gradle b/libs/ssl-config/build.gradle new file mode 100644 index 0000000000000..8d5b1d18b8c04 --- /dev/null +++ b/libs/ssl-config/build.gradle @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +dependencies { + compile "org.elasticsearch:elasticsearch-core:${version}" + + if (isEclipse == false || project.path == ":libs:ssl-config-tests") { + testCompile("org.elasticsearch.test:framework:${version}") { + exclude group: 'org.elasticsearch', module: 'elasticsearch-ssl-config' + } + } + + testCompile "com.carrotsearch.randomizedtesting:randomizedtesting-runner:${versions.randomizedrunner}" + testCompile "junit:junit:${versions.junit}" + testCompile "org.hamcrest:hamcrest-all:${versions.hamcrest}" +} + +forbiddenApisMain { + replaceSignatureFiles 'jdk-signatures' +} +forbiddenPatterns { + exclude '**/*.key' + exclude '**/*.pem' + exclude '**/*.p12' + exclude '**/*.jks' +} 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 new file mode 100644 index 0000000000000..5a1fbe72c3f49 --- /dev/null +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/DefaultJdkTrustConfig.java @@ -0,0 +1,128 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.ssl; + +import org.elasticsearch.common.Nullable; + +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedTrustManager; +import java.io.IOException; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +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 { + + private final BiFunction systemProperties; + private final char[] trustStorePassword; + + /** + * Create a trust config that uses System properties to determine the TrustStore type, and the relevant password. + */ + DefaultJdkTrustConfig() { + this(System::getProperty); + } + + /** + * Create a trust config that uses supplied {@link BiFunction} to determine the TrustStore type, and the relevant password. + */ + DefaultJdkTrustConfig(BiFunction systemProperties) { + this(systemProperties, isPkcs11Truststore(systemProperties) ? getSystemTrustStorePassword(systemProperties) : null); + } + + /** + * @param trustStorePassword the password for the truststore. It applies only when PKCS#11 tokens are used, is null otherwise + */ + DefaultJdkTrustConfig(BiFunction systemProperties, @Nullable char[] trustStorePassword) { + this.systemProperties = systemProperties; + this.trustStorePassword = trustStorePassword; + } + + @Override + public X509ExtendedTrustManager createTrustManager() { + try { + return KeyStoreUtil.createTrustManager(getSystemTrustStore(), TrustManagerFactory.getDefaultAlgorithm()); + } catch (GeneralSecurityException e) { + throw new SslConfigException("failed to initialize a TrustManager for the system keystore", e); + } + } + + /** + * When a PKCS#11 token is used as the system default keystore/truststore, we need to pass the keystore + * password when loading, even for reading certificates only ( as opposed to i.e. JKS keystores where + * we only need to pass the password for reading Private Key entries ). + * + * @return the KeyStore used as truststore for PKCS#11 initialized with the password, null otherwise + */ + private KeyStore getSystemTrustStore() { + if (isPkcs11Truststore(systemProperties) && trustStorePassword != null) { + try { + KeyStore keyStore = KeyStore.getInstance("PKCS11"); + keyStore.load(null, trustStorePassword); + return keyStore; + } catch (GeneralSecurityException | IOException e) { + throw new SslConfigException("failed to load the system PKCS#11 truststore", e); + } + } + return null; + } + + private static boolean isPkcs11Truststore(BiFunction systemProperties) { + return systemProperties.apply("javax.net.ssl.trustStoreType", "").equalsIgnoreCase("PKCS11"); + } + + private static char[] getSystemTrustStorePassword(BiFunction systemProperties) { + return systemProperties.apply("javax.net.ssl.trustStorePassword", "").toCharArray(); + } + + @Override + public Collection getDependentFiles() { + return Collections.emptyList(); + } + + @Override + public String toString() { + return "JDK-trusted-certs"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final DefaultJdkTrustConfig that = (DefaultJdkTrustConfig) o; + return Arrays.equals(this.trustStorePassword, that.trustStorePassword); + } + + @Override + public int hashCode() { + return Arrays.hashCode(trustStorePassword); + } +} 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 new file mode 100644 index 0000000000000..da650369d508c --- /dev/null +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/DerParser.java @@ -0,0 +1,297 @@ +/* + Copyright (c) 1998-2010 AOL Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +package org.elasticsearch.common.ssl; + + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.util.Objects; + +/** + * A bare-minimum ASN.1 DER decoder, just having enough functions to + * decode PKCS#1 private keys in order to remain JCE/JVM agnostic. + *

+ * 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 { + // Constructed Flag + private static final int CONSTRUCTED = 0x20; + + // Tag and data types + private static final int INTEGER = 0x02; + private static final int OCTET_STRING = 0x04; + private static final int OBJECT_OID = 0x06; + private static final int NUMERIC_STRING = 0x12; + private static final int PRINTABLE_STRING = 0x13; + private static final int VIDEOTEX_STRING = 0x15; + private static final int IA5_STRING = 0x16; + private static final int GRAPHIC_STRING = 0x19; + private static final int ISO646_STRING = 0x1A; + private static final int GENERAL_STRING = 0x1B; + + private static final int UTF8_STRING = 0x0C; + private static final int UNIVERSAL_STRING = 0x1C; + private static final int BMP_STRING = 0x1E; + + + private InputStream derInputStream; + private int maxAsnObjectLength; + + DerParser(byte[] bytes) { + this.derInputStream = new ByteArrayInputStream(bytes); + this.maxAsnObjectLength = bytes.length; + } + + Asn1Object readAsn1Object() throws IOException { + int tag = derInputStream.read(); + if (tag == -1) { + throw new IOException("Invalid DER: stream too short, missing tag"); + } + int length = getLength(); + // getLength() can return any 32 bit integer, so ensure that a corrupted encoding won't + // force us into allocating a very large array + if (length > maxAsnObjectLength) { + throw new IOException("Invalid DER: size of ASN.1 object to be parsed appears to be larger than the size of the key file " + + "itself."); + } + byte[] value = new byte[length]; + int n = derInputStream.read(value); + if (n < length) { + throw new IOException("Invalid DER: stream too short, missing value. " + + "Could only read " + n + " out of " + length + " bytes"); + } + return new Asn1Object(tag, length, value); + + } + + /** + * Decode the length of the field. Can only support length + * encoding up to 4 octets. + *

+ * In BER/DER encoding, length can be encoded in 2 forms: + *

+ *
    + *
  • Short form. One octet. Bit 8 has value "0" and bits 7-1 + * give the length. + *
  • + *
  • Long form. Two to 127 octets (only 4 is supported here). + * Bit 8 of first octet has value "1" and bits 7-1 give the + * number of additional length octets. Second and following + * octets give the length, base 256, most significant digit first. + *
  • + *
+ * + * @return The length as integer + */ + private int getLength() throws IOException { + + int i = derInputStream.read(); + if (i == -1) + throw new IOException("Invalid DER: length missing"); + + // A single byte short length + if ((i & ~0x7F) == 0) + return i; + + int num = i & 0x7F; + + // We can't handle length longer than 4 bytes + if (i >= 0xFF || num > 4) + throw new IOException("Invalid DER: length field too big (" + + i + ")"); //$NON-NLS-1$ + + byte[] bytes = new byte[num]; + int n = derInputStream.read(bytes); + if (n < num) + throw new IOException("Invalid DER: length too short"); + + return new BigInteger(1, bytes).intValue(); + } + + + /** + * An ASN.1 TLV. The object is not parsed. It can + * only handle integers. + * + * @author zhang + */ + static class Asn1Object { + + protected final int type; + protected final int length; + protected final byte[] value; + protected final int tag; + + /** + * Construct a ASN.1 TLV. The TLV could be either a + * constructed or primitive entity. + *

+ * The first byte in DER encoding is made of following fields: + *

+ *
+         * -------------------------------------------------
+         * |Bit 8|Bit 7|Bit 6|Bit 5|Bit 4|Bit 3|Bit 2|Bit 1|
+         * -------------------------------------------------
+         * |  Class    | CF  |     +      Type             |
+         * -------------------------------------------------
+         * 
+ *
    + *
  • Class: Universal, Application, Context or Private + *
  • CF: Constructed flag. If 1, the field is constructed. + *
  • Type: This is actually called tag in ASN.1. It + * indicates data type (Integer, String) or a construct + * (sequence, choice, set). + *
+ * + * @param tag Tag or Identifier + * @param length Length of the field + * @param value Encoded octet string for the field. + */ + Asn1Object(int tag, int length, byte[] value) { + this.tag = tag; + this.type = tag & 0x1F; + this.length = length; + this.value = value; + } + + public int getType() { + return type; + } + + public int getLength() { + return length; + } + + public byte[] getValue() { + return value; + } + + public boolean isConstructed() { + return (tag & DerParser.CONSTRUCTED) == DerParser.CONSTRUCTED; + } + + /** + * For constructed field, return a parser for its content. + * + * @return A parser for the construct. + */ + public DerParser getParser() throws IOException { + if (!isConstructed()) + throw new IOException("Invalid DER: can't parse primitive entity"); //$NON-NLS-1$ + + return new DerParser(value); + } + + /** + * Get the value as integer + * + * @return BigInteger + */ + public BigInteger getInteger() throws IOException { + if (type != DerParser.INTEGER) + throw new IOException("Invalid DER: object is not integer"); //$NON-NLS-1$ + + return new BigInteger(value); + } + + public String getString() throws IOException { + + String encoding; + + switch (type) { + case DerParser.OCTET_STRING: + // octet string is basically a byte array + return toHexString(value); + case DerParser.NUMERIC_STRING: + case DerParser.PRINTABLE_STRING: + case DerParser.VIDEOTEX_STRING: + case DerParser.IA5_STRING: + case DerParser.GRAPHIC_STRING: + case DerParser.ISO646_STRING: + case DerParser.GENERAL_STRING: + encoding = "ISO-8859-1"; //$NON-NLS-1$ + break; + + case DerParser.BMP_STRING: + encoding = "UTF-16BE"; //$NON-NLS-1$ + break; + + case DerParser.UTF8_STRING: + encoding = "UTF-8"; //$NON-NLS-1$ + break; + + case DerParser.UNIVERSAL_STRING: + throw new IOException("Invalid DER: can't handle UCS-4 string"); //$NON-NLS-1$ + + default: + throw new IOException("Invalid DER: object is not a string"); //$NON-NLS-1$ + } + + return new String(value, encoding); + } + + public String getOid() throws IOException { + + if (type != DerParser.OBJECT_OID) { + throw new IOException("Ivalid DER: object is not object OID"); + } + StringBuilder sb = new StringBuilder(64); + switch (value[0] / 40) { + case 0: + sb.append('0'); + break; + case 1: + sb.append('1'); + value[0] -= 40; + break; + default: + sb.append('2'); + value[0] -= 80; + break; + } + int oidPart = 0; + for (int i = 0; i < length; i++) { + oidPart = (oidPart << 7) + (value[i] & 0x7F); + if ((value[i] & 0x80) == 0) { + sb.append('.'); + sb.append(oidPart); + oidPart = 0; + } + } + + return sb.toString(); + } + } + + private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); + private static String toHexString(byte[] bytes) { + Objects.requireNonNull(bytes); + StringBuilder sb = new StringBuilder(2 * bytes.length); + + for (int i = 0; i < bytes.length; i++) { + byte b = bytes[i]; + sb.append(HEX_DIGITS[b >> 4 & 0xf]).append(HEX_DIGITS[b & 0xf]); + } + + return sb.toString(); + } + +} 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 new file mode 100644 index 0000000000000..0844ffb7ee952 --- /dev/null +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/EmptyKeyConfig.java @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.ssl; + +import javax.net.ssl.X509ExtendedKeyManager; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Collections; + +/** + * A {@link SslKeyConfig} that does nothing (provides a null key manager) + */ +final class EmptyKeyConfig implements SslKeyConfig { + + static final EmptyKeyConfig INSTANCE = new EmptyKeyConfig(); + + private EmptyKeyConfig() { + // Enforce a single instance + } + + @Override + public Collection getDependentFiles() { + return Collections.emptyList(); + } + + @Override + public X509ExtendedKeyManager createKeyManager() { + return null; + } + + @Override + public String toString() { + return "empty-key-config"; + } +} 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 new file mode 100644 index 0000000000000..0a2526c7f7cfa --- /dev/null +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/KeyStoreUtil.java @@ -0,0 +1,163 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.ssl; + +import org.elasticsearch.common.Nullable; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509ExtendedTrustManager; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.util.Collection; +import java.util.Locale; + +/** + * A variety of utility methods for working with or constructing {@link KeyStore} instances. + */ +final class KeyStoreUtil { + + private KeyStoreUtil() { + throw new IllegalStateException("Utility class should not be instantiated"); + } + + /** + * 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); + if (name.endsWith(".p12") || name.endsWith(".pfx") || name.endsWith(".pkcs12")) { + return "PKCS12"; + } else { + return "jks"; + } + } + + /** + * Read the given keystore file. + * + * @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); + 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); + } + } + + /** + * 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 + * + * @throws GeneralSecurityException If there is a problem with the provided certificates/key + */ + static KeyStore buildKeyStore(Collection certificateChain, PrivateKey privateKey, char[] password) + throws GeneralSecurityException { + KeyStore keyStore = buildNewKeyStore(); + keyStore.setKeyEntry("key", privateKey, password, certificateChain.toArray(new Certificate[0])); + return keyStore; + } + + /** + * Construct an in-memory keystore with multiple trusted cert entries. + * @param certificates The root certificates to trust + */ + static KeyStore buildTrustStore(Iterable certificates) throws GeneralSecurityException { + assert certificates != null : "Cannot create keystore with null certificates"; + KeyStore store = buildNewKeyStore(); + int counter = 0; + for (Certificate certificate : certificates) { + store.setCertificateEntry("cert-" + counter, certificate); + counter++; + } + return store; + } + + private static KeyStore buildNewKeyStore() throws GeneralSecurityException { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + try { + keyStore.load(null, null); + } catch (IOException e) { + // This should never happen so callers really shouldn't be forced to deal with it themselves. + throw new SslConfigException("Unexpected error initializing a new in-memory keystore", e); + } + return keyStore; + } + + /** + * 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 { + KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm); + kmf.init(keyStore, password); + KeyManager[] keyManagers = kmf.getKeyManagers(); + for (KeyManager keyManager : keyManagers) { + if (keyManager instanceof X509ExtendedKeyManager) { + return (X509ExtendedKeyManager) keyManager; + } + } + throw new SslConfigException("failed to find a X509ExtendedKeyManager in the key manager factory for [" + algorithm + + "] and keystore [" + keyStore + "]"); + } + + /** + * Creates a {@link X509ExtendedTrustManager} based on the trust material in the provided {@link KeyStore} + */ + static X509ExtendedTrustManager createTrustManager(@Nullable KeyStore trustStore, String algorithm) + throws NoSuchAlgorithmException, KeyStoreException { + TrustManagerFactory tmf = TrustManagerFactory.getInstance(algorithm); + tmf.init(trustStore); + TrustManager[] trustManagers = tmf.getTrustManagers(); + for (TrustManager trustManager : trustManagers) { + if (trustManager instanceof X509ExtendedTrustManager) { + return (X509ExtendedTrustManager) trustManager; + } + } + throw new SslConfigException("failed to find a X509ExtendedTrustManager in the trust manager factory for [" + algorithm + + "] and truststore [" + trustStore + "]"); + } + + +} 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 new file mode 100644 index 0000000000000..dd091e0a22218 --- /dev/null +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemKeyConfig.java @@ -0,0 +1,122 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.ssl; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.X509ExtendedKeyManager; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * A {@link SslKeyConfig} that reads from PEM formatted paths. + */ +public final class PemKeyConfig implements SslKeyConfig { + private final Path certificate; + private final Path key; + private final char[] keyPassword; + + public PemKeyConfig(Path certificate, Path key, char[] keyPassword) { + 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)"); + } + + @Override + public Collection getDependentFiles() { + return Arrays.asList(certificate, key); + } + + @Override + public X509ExtendedKeyManager createKeyManager() { + PrivateKey privateKey = getPrivateKey(); + List certificates = getCertificates(); + 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); + } + } + + private PrivateKey getPrivateKey() { + try { + final PrivateKey privateKey = PemUtils.readPrivateKey(key, () -> keyPassword); + if (privateKey == null) { + throw new SslConfigException("could not load ssl private key file [" + key + "]"); + } + return privateKey; + } catch (FileNotFoundException | NoSuchFileException e) { + throw new SslConfigException("the configured ssl private key file [" + key.toAbsolutePath() + "] does not exist", e); + } catch (IOException e) { + throw new SslConfigException("the configured ssl private key file [" + key.toAbsolutePath() + "] cannot be read", e); + } catch (GeneralSecurityException e) { + throw new SslConfigException("cannot load ssl private key file [" + key.toAbsolutePath() + "]", e); + } + } + + private List getCertificates() { + 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); + } catch (IOException e) { + throw new SslConfigException("the configured ssl certificate file [" + certificate .toAbsolutePath()+ "] cannot be read", e); + } catch (GeneralSecurityException e) { + throw new SslConfigException("cannot load ssl certificate from [" + certificate.toAbsolutePath() + "]", e); + } + } + + @Override + public String toString() { + return "PEM-key-config{cert=" + certificate.toAbsolutePath() + " key=" + key.toAbsolutePath() + "}"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final PemKeyConfig that = (PemKeyConfig) o; + return Objects.equals(this.certificate, that.certificate) && + Objects.equals(this.key, that.key) && + Arrays.equals(this.keyPassword, that.keyPassword); + } + + @Override + public int hashCode() { + int result = Objects.hash(certificate, key); + result = 31 * result + Arrays.hashCode(keyPassword); + return result; + } +} 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 new file mode 100644 index 0000000000000..f3cf8cd8bd7aa --- /dev/null +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemTrustConfig.java @@ -0,0 +1,122 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.ssl; + +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.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * A {@link org.elasticsearch.common.ssl.SslTrustConfig} that reads a list of PEM encoded trusted certificates (CAs) from the file + * system. + * Strictly speaking, this class does not require PEM certificates, and will load any file that can be read by + * {@link java.security.cert.CertificateFactory#generateCertificate(InputStream)}. + */ +public final class PemTrustConfig implements SslTrustConfig { + private final List certificateAuthorities; + + /** + * Construct a new trust config for the provided paths. + * The paths are stored as-is, and are not read until {@link #createTrustManager()} is called. + * This means that + *
    + *
  1. validation of the file (contents and accessibility) is deferred, and this constructor will not fail on missing + * of invalid files.
  2. + *
  3. + * if the contents of the files are modified, then subsequent calls {@link #createTrustManager()} will return a new trust + * manager that trust a different set of CAs. + *
  4. + *
+ */ + public PemTrustConfig(List certificateAuthorities) { + this.certificateAuthorities = Collections.unmodifiableList(certificateAuthorities); + } + + @Override + public Collection getDependentFiles() { + return certificateAuthorities; + } + + @Override + public X509ExtendedTrustManager createTrustManager() { + try { + final List certificates = loadCertificates(); + 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); + } + } + + private List loadCertificates() throws CertificateException { + 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); + } catch (IOException e) { + throw new SslConfigException("cannot configure trust using PEM certificates [" + caPathsAsString() + + "] because one or more files cannot be read", e); + } + } + + @Override + public String toString() { + return "PEM-trust{" + caPathsAsString() + "}"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final PemTrustConfig that = (PemTrustConfig) o; + return Objects.equals(this.certificateAuthorities, that.certificateAuthorities); + } + + @Override + 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 new file mode 100644 index 0000000000000..aca7ba56b2ae9 --- /dev/null +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemUtils.java @@ -0,0 +1,613 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.ssl; + +import org.elasticsearch.common.CharArrays; + +import javax.crypto.Cipher; +import javax.crypto.EncryptedPrivateKeyInfo; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +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.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPairGenerator; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.interfaces.ECKey; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.DSAPrivateKeySpec; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPrivateCrtKeySpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +final class PemUtils { + + private static final String PKCS1_HEADER = "-----BEGIN RSA PRIVATE KEY-----"; + private static final String PKCS1_FOOTER = "-----END RSA PRIVATE KEY-----"; + private static final String OPENSSL_DSA_HEADER = "-----BEGIN DSA PRIVATE KEY-----"; + private static final String OPENSSL_DSA_FOOTER = "-----END DSA PRIVATE KEY-----"; + private static final String OPENSSL_DSA_PARAMS_HEADER ="-----BEGIN DSA PARAMETERS-----"; + private static final String OPENSSL_DSA_PARAMS_FOOTER ="-----END DSA PARAMETERS-----"; + private static final String PKCS8_HEADER = "-----BEGIN PRIVATE KEY-----"; + private static final String PKCS8_FOOTER = "-----END PRIVATE KEY-----"; + private static final String PKCS8_ENCRYPTED_HEADER = "-----BEGIN ENCRYPTED PRIVATE KEY-----"; + private static final String PKCS8_ENCRYPTED_FOOTER = "-----END ENCRYPTED PRIVATE KEY-----"; + private static final String OPENSSL_EC_HEADER = "-----BEGIN EC PRIVATE KEY-----"; + private static final String OPENSSL_EC_FOOTER = "-----END EC PRIVATE KEY-----"; + private static final String OPENSSL_EC_PARAMS_HEADER = "-----BEGIN EC PARAMETERS-----"; + private static final String OPENSSL_EC_PARAMS_FOOTER = "-----END EC PARAMETERS-----"; + private static final String HEADER = "-----BEGIN"; + + private PemUtils() { + throw new IllegalStateException("Utility class should not be instantiated"); + } + + /** + * 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 + * + * @param keyPath 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 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) { + line = bReader.readLine(); + } + if (null == line) { + throw new SslConfigException("Error parsing Private Key [" + keyPath.toAbsolutePath() + "], file is empty"); + } + if (PKCS8_ENCRYPTED_HEADER.equals(line.trim())) { + char[] password = passwordSupplier.get(); + if (password == null) { + throw new SslConfigException("cannot read encrypted key [" + keyPath.toAbsolutePath() + "] without a password"); + } + return parsePKCS8Encrypted(bReader, password); + } else if (PKCS8_HEADER.equals(line.trim())) { + return parsePKCS8(bReader); + } else if (PKCS1_HEADER.equals(line.trim())) { + return parsePKCS1Rsa(bReader, passwordSupplier); + } else if (OPENSSL_DSA_HEADER.equals(line.trim())) { + return parseOpenSslDsa(bReader, passwordSupplier); + } else if (OPENSSL_DSA_PARAMS_HEADER.equals(line.trim())) { + return parseOpenSslDsa(removeDsaHeaders(bReader), passwordSupplier); + } else if (OPENSSL_EC_HEADER.equals(line.trim())) { + return parseOpenSslEC(bReader, passwordSupplier); + } 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"); + } + } 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); + } + } + + /** + * Removes the EC Headers that OpenSSL adds to EC private keys as the information in them + * is redundant + * + * @throws IOException if the EC Parameter footer is missing + */ + private static BufferedReader removeECHeaders(BufferedReader bReader) throws IOException { + String line = bReader.readLine(); + while (line != null) { + if (OPENSSL_EC_PARAMS_FOOTER.equals(line.trim())) { + break; + } + line = bReader.readLine(); + } + if (null == line || OPENSSL_EC_PARAMS_FOOTER.equals(line.trim()) == false) { + throw new IOException("Malformed PEM file, EC Parameters footer is missing"); + } + // Verify that the key starts with the correct header before passing it to parseOpenSslEC + if (OPENSSL_EC_HEADER.equals(bReader.readLine()) == false) { + throw new IOException("Malformed PEM file, EC Key header is missing"); + } + return bReader; + } + + /** + * Removes the DSA Params Headers that OpenSSL adds to DSA private keys as the information in them + * is redundant + * + * @throws IOException if the EC Parameter footer is missing + */ + private static BufferedReader removeDsaHeaders(BufferedReader bReader) throws IOException { + String line = bReader.readLine(); + while (line != null) { + if (OPENSSL_DSA_PARAMS_FOOTER.equals(line.trim())) { + break; + } + line = bReader.readLine(); + } + if (null == line || OPENSSL_DSA_PARAMS_FOOTER.equals(line.trim()) == false) { + throw new IOException("Malformed PEM file, DSA Parameters footer is missing"); + } + // Verify that the key starts with the correct header before passing it to parseOpenSslDsa + if (OPENSSL_DSA_HEADER.equals(bReader.readLine()) == false) { + throw new IOException("Malformed PEM file, DSA Key header is missing"); + } + return bReader; + } + + /** + * Creates a {@link PrivateKey} from the contents of {@code bReader} that contains an plaintext private key encoded in + * PKCS#8 + * + * @param bReader the {@link BufferedReader} containing the key file contents + * @return {@link PrivateKey} + * @throws IOException if the file can't be read + * @throws GeneralSecurityException if the private key can't be generated from the {@link PKCS8EncodedKeySpec} + */ + private static PrivateKey parsePKCS8(BufferedReader bReader) throws IOException, GeneralSecurityException { + StringBuilder sb = new StringBuilder(); + String line = bReader.readLine(); + while (line != null) { + if (PKCS8_FOOTER.equals(line.trim())) { + break; + } + sb.append(line.trim()); + line = bReader.readLine(); + } + if (null == line || PKCS8_FOOTER.equals(line.trim()) == false) { + throw new IOException("Malformed PEM file, PEM footer is invalid or missing"); + } + byte[] keyBytes = Base64.getDecoder().decode(sb.toString()); + String keyAlgo = getKeyAlgorithmIdentifier(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(keyAlgo); + return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes)); + } + + /** + * Creates a {@link PrivateKey} from the contents of {@code bReader} that contains an EC private key encoded in + * OpenSSL traditional format. + * + * @param bReader the {@link BufferedReader} containing the key file contents + * @param passwordSupplier A password supplier for the potentially encrypted (password protected) key + * @return {@link PrivateKey} + * @throws IOException if the file can't be read + * @throws GeneralSecurityException if the private key can't be generated from the {@link ECPrivateKeySpec} + */ + private static PrivateKey parseOpenSslEC(BufferedReader bReader, Supplier passwordSupplier) throws IOException, + GeneralSecurityException { + StringBuilder sb = new StringBuilder(); + String line = bReader.readLine(); + Map pemHeaders = new HashMap<>(); + while (line != null) { + if (OPENSSL_EC_FOOTER.equals(line.trim())) { + break; + } + // Parse PEM headers according to https://www.ietf.org/rfc/rfc1421.txt + if (line.contains(":")) { + String[] header = line.split(":"); + pemHeaders.put(header[0].trim(), header[1].trim()); + } else { + sb.append(line.trim()); + } + line = bReader.readLine(); + } + if (null == line || OPENSSL_EC_FOOTER.equals(line.trim()) == false) { + throw new IOException("Malformed PEM file, PEM footer is invalid or missing"); + } + byte[] keyBytes = possiblyDecryptPKCS1Key(pemHeaders, sb.toString(), passwordSupplier); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + ECPrivateKeySpec ecSpec = parseEcDer(keyBytes); + return keyFactory.generatePrivate(ecSpec); + } + + /** + * Creates a {@link PrivateKey} from the contents of {@code bReader} that contains an RSA private key encoded in + * OpenSSL traditional format. + * + * @param bReader the {@link BufferedReader} containing the key file contents + * @param passwordSupplier A password supplier for the potentially encrypted (password protected) key + * @return {@link PrivateKey} + * @throws IOException if the file can't be read + * @throws GeneralSecurityException if the private key can't be generated from the {@link RSAPrivateCrtKeySpec} + */ + private static PrivateKey parsePKCS1Rsa(BufferedReader bReader, Supplier passwordSupplier) throws IOException, + GeneralSecurityException { + StringBuilder sb = new StringBuilder(); + String line = bReader.readLine(); + Map pemHeaders = new HashMap<>(); + + while (line != null) { + if (PKCS1_FOOTER.equals(line.trim())) { + // Unencrypted + break; + } + // Parse PEM headers according to https://www.ietf.org/rfc/rfc1421.txt + if (line.contains(":")) { + String[] header = line.split(":"); + pemHeaders.put(header[0].trim(), header[1].trim()); + } else { + sb.append(line.trim()); + } + line = bReader.readLine(); + } + if (null == line || PKCS1_FOOTER.equals(line.trim()) == false) { + throw new IOException("Malformed PEM file, PEM footer is invalid or missing"); + } + byte[] keyBytes = possiblyDecryptPKCS1Key(pemHeaders, sb.toString(), passwordSupplier); + RSAPrivateCrtKeySpec spec = parseRsaDer(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return keyFactory.generatePrivate(spec); + } + + /** + * Creates a {@link PrivateKey} from the contents of {@code bReader} that contains an DSA private key encoded in + * OpenSSL traditional format. + * + * @param bReader the {@link BufferedReader} containing the key file contents + * @param passwordSupplier A password supplier for the potentially encrypted (password protected) key + * @return {@link PrivateKey} + * @throws IOException if the file can't be read + * @throws GeneralSecurityException if the private key can't be generated from the {@link DSAPrivateKeySpec} + */ + private static PrivateKey parseOpenSslDsa(BufferedReader bReader, Supplier passwordSupplier) throws IOException, + GeneralSecurityException { + StringBuilder sb = new StringBuilder(); + String line = bReader.readLine(); + Map pemHeaders = new HashMap<>(); + + while (line != null) { + if (OPENSSL_DSA_FOOTER.equals(line.trim())) { + // Unencrypted + break; + } + // Parse PEM headers according to https://www.ietf.org/rfc/rfc1421.txt + if (line.contains(":")) { + String[] header = line.split(":"); + pemHeaders.put(header[0].trim(), header[1].trim()); + } else { + sb.append(line.trim()); + } + line = bReader.readLine(); + } + if (null == line || OPENSSL_DSA_FOOTER.equals(line.trim()) == false) { + throw new IOException("Malformed PEM file, PEM footer is invalid or missing"); + } + byte[] keyBytes = possiblyDecryptPKCS1Key(pemHeaders, sb.toString(), passwordSupplier); + DSAPrivateKeySpec spec = parseDsaDer(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("DSA"); + return keyFactory.generatePrivate(spec); + } + + /** + * Creates a {@link PrivateKey} from the contents of {@code bReader} that contains an encrypted private key encoded in + * PKCS#8 + * + * @param bReader the {@link BufferedReader} containing the key file contents + * @param keyPassword The password for the encrypted (password protected) key + * @return {@link PrivateKey} + * @throws IOException if the file can't be read + * @throws GeneralSecurityException if the private key can't be generated from the {@link PKCS8EncodedKeySpec} + */ + private static PrivateKey parsePKCS8Encrypted(BufferedReader bReader, char[] keyPassword) throws IOException, + GeneralSecurityException { + StringBuilder sb = new StringBuilder(); + String line = bReader.readLine(); + while (line != null) { + if (PKCS8_ENCRYPTED_FOOTER.equals(line.trim())) { + break; + } + sb.append(line.trim()); + line = bReader.readLine(); + } + if (null == line || PKCS8_ENCRYPTED_FOOTER.equals(line.trim()) == false) { + throw new IOException("Malformed PEM file, PEM footer is invalid or missing"); + } + byte[] keyBytes = Base64.getDecoder().decode(sb.toString()); + + EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(keyBytes); + SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(encryptedPrivateKeyInfo.getAlgName()); + SecretKey secretKey = secretKeyFactory.generateSecret(new PBEKeySpec(keyPassword)); + Arrays.fill(keyPassword, '\u0000'); + Cipher cipher = Cipher.getInstance(encryptedPrivateKeyInfo.getAlgName()); + cipher.init(Cipher.DECRYPT_MODE, secretKey, encryptedPrivateKeyInfo.getAlgParameters()); + PKCS8EncodedKeySpec keySpec = encryptedPrivateKeyInfo.getKeySpec(cipher); + String keyAlgo = getKeyAlgorithmIdentifier(keySpec.getEncoded()); + KeyFactory keyFactory = KeyFactory.getInstance(keyAlgo); + return keyFactory.generatePrivate(keySpec); + } + + /** + * Decrypts the password protected contents using the algorithm and IV that is specified in the PEM Headers of the file + * + * @param pemHeaders The Proc-Type and DEK-Info PEM headers that have been extracted from the key file + * @param keyContents The key as a base64 encoded String + * @param passwordSupplier A password supplier for the encrypted (password protected) key + * @return the decrypted key bytes + * @throws GeneralSecurityException if the key can't be decrypted + * @throws IOException if the PEM headers are missing or malformed + */ + private static byte[] possiblyDecryptPKCS1Key(Map pemHeaders, String keyContents, Supplier passwordSupplier) + throws GeneralSecurityException, IOException { + byte[] keyBytes = Base64.getDecoder().decode(keyContents); + String procType = pemHeaders.get("Proc-Type"); + if ("4,ENCRYPTED".equals(procType)) { + //We only handle PEM encryption + String encryptionParameters = pemHeaders.get("DEK-Info"); + if (null == encryptionParameters) { + //malformed pem + throw new IOException("Malformed PEM File, DEK-Info header is missing"); + } + char[] password = passwordSupplier.get(); + if (password == null) { + throw new IOException("cannot read encrypted key without a password"); + } + Cipher cipher = getCipherFromParameters(encryptionParameters, password); + byte[] decryptedKeyBytes = cipher.doFinal(keyBytes); + return decryptedKeyBytes; + } + return keyBytes; + } + + /** + * Creates a {@link Cipher} from the contents of the DEK-Info header of a PEM file. RFC 1421 indicates that supported algorithms are + * defined in RFC 1423. RFC 1423 only defines DES-CBS and triple DES (EDE) in CBC mode. AES in CBC mode is also widely used though ( 3 + * different variants of 128, 192, 256 bit keys ) + * + * @param dekHeaderValue The value of the the DEK-Info PEM header + * @param password The password with which the key is encrypted + * @return a cipher of the appropriate algorithm and parameters to be used for decryption + * @throws GeneralSecurityException if the algorithm is not available in the used security provider, or if the key is inappropriate + * for the cipher + * @throws IOException if the DEK-Info PEM header is invalid + */ + private static Cipher getCipherFromParameters(String dekHeaderValue, char[] password) throws + GeneralSecurityException, IOException { + final String padding = "PKCS5Padding"; + final SecretKey encryptionKey; + final String[] valueTokens = dekHeaderValue.split(","); + if (valueTokens.length != 2) { + throw new IOException("Malformed PEM file, DEK-Info PEM header is invalid"); + } + final String algorithm = valueTokens[0]; + final String ivString = valueTokens[1]; + final byte[] iv; + try { + iv = hexStringToByteArray(ivString); + } catch (IllegalArgumentException e) { + throw new IOException("Malformed PEM file, DEK-Info IV is invalid", e); + } + if ("DES-CBC".equals(algorithm)) { + byte[] key = generateOpenSslKey(password, iv, 8); + encryptionKey = new SecretKeySpec(key, "DES"); + } else if ("DES-EDE3-CBC".equals(algorithm)) { + byte[] key = generateOpenSslKey(password, iv, 24); + encryptionKey = new SecretKeySpec(key, "DESede"); + } else if ("AES-128-CBC".equals(algorithm)) { + byte[] key = generateOpenSslKey(password, iv, 16); + encryptionKey = new SecretKeySpec(key, "AES"); + } else if ("AES-192-CBC".equals(algorithm)) { + byte[] key = generateOpenSslKey(password, iv, 24); + encryptionKey = new SecretKeySpec(key, "AES"); + } else if ("AES-256-CBC".equals(algorithm)) { + byte[] key = generateOpenSslKey(password, iv, 32); + encryptionKey = new SecretKeySpec(key, "AES"); + } else { + throw new GeneralSecurityException("Private Key encrypted with unsupported algorithm [" + algorithm + "]"); + } + String transformation = encryptionKey.getAlgorithm() + "/" + "CBC" + "/" + padding; + Cipher cipher = Cipher.getInstance(transformation); + cipher.init(Cipher.DECRYPT_MODE, encryptionKey, new IvParameterSpec(iv)); + return cipher; + } + + /** + * Performs key stretching in the same manner that OpenSSL does. This is basically a KDF + * that uses n rounds of salted MD5 (as many times as needed to get the necessary number of key bytes) + *

+ * https://www.openssl.org/docs/man1.1.0/crypto/PEM_write_bio_PrivateKey_traditional.html + */ + private static byte[] generateOpenSslKey(char[] password, byte[] salt, int keyLength) { + byte[] passwordBytes = CharArrays.toUtf8Bytes(password); + MessageDigest md5 = messageDigest("md5"); + byte[] key = new byte[keyLength]; + int copied = 0; + int remaining; + while (copied < keyLength) { + remaining = keyLength - copied; + md5.update(passwordBytes, 0, passwordBytes.length); + md5.update(salt, 0, 8);// AES IV (salt) is longer but we only need 8 bytes + byte[] tempDigest = md5.digest(); + int bytesToCopy = (remaining > 16) ? 16 : remaining; // MD5 digests are 16 bytes + System.arraycopy(tempDigest, 0, key, copied, bytesToCopy); + copied += bytesToCopy; + if (remaining == 0) { + break; + } + md5.update(tempDigest, 0, 16); // use previous round digest as IV + } + Arrays.fill(passwordBytes, (byte) 0); + return key; + } + + /** + * Converts a hexadecimal string to a byte array + */ + private static byte[] hexStringToByteArray(String hexString) { + int len = hexString.length(); + if (len % 2 == 0) { + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + final int k = Character.digit(hexString.charAt(i), 16); + final int l = Character.digit(hexString.charAt(i + 1), 16); + if (k == -1 || l == -1) { + throw new IllegalStateException("String [" + hexString + "] is not hexadecimal"); + } + data[i / 2] = (byte) ((k << 4) + l); + } + return data; + } else { + throw new IllegalStateException("Hexadecimal string [" + hexString + + "] has odd length and cannot be converted to a byte array"); + } + } + + /** + * Parses a DER encoded EC key to an {@link ECPrivateKeySpec} using a minimal {@link DerParser} + * + * @param keyBytes the private key raw bytes + * @return {@link ECPrivateKeySpec} + * @throws IOException if the DER encoded key can't be parsed + */ + private static ECPrivateKeySpec parseEcDer(byte[] keyBytes) throws IOException, + GeneralSecurityException { + DerParser parser = new DerParser(keyBytes); + DerParser.Asn1Object sequence = parser.readAsn1Object(); + parser = sequence.getParser(); + parser.readAsn1Object().getInteger(); // version + String keyHex = parser.readAsn1Object().getString(); + BigInteger privateKeyInt = new BigInteger(keyHex, 16); + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC"); + AlgorithmParameterSpec prime256v1ParamSpec = new ECGenParameterSpec("secp256r1"); + keyPairGenerator.initialize(prime256v1ParamSpec); + ECParameterSpec parameterSpec = ((ECKey) keyPairGenerator.generateKeyPair().getPrivate()).getParams(); + return new ECPrivateKeySpec(privateKeyInt, parameterSpec); + } + + /** + * Parses a DER encoded RSA key to a {@link RSAPrivateCrtKeySpec} using a minimal {@link DerParser} + * + * @param keyBytes the private key raw bytes + * @return {@link RSAPrivateCrtKeySpec} + * @throws IOException if the DER encoded key can't be parsed + */ + private static RSAPrivateCrtKeySpec parseRsaDer(byte[] keyBytes) throws IOException { + DerParser parser = new DerParser(keyBytes); + DerParser.Asn1Object sequence = parser.readAsn1Object(); + parser = sequence.getParser(); + parser.readAsn1Object().getInteger(); // (version) We don't need it but must read to get to modulus + BigInteger modulus = parser.readAsn1Object().getInteger(); + BigInteger publicExponent = parser.readAsn1Object().getInteger(); + BigInteger privateExponent = parser.readAsn1Object().getInteger(); + BigInteger prime1 = parser.readAsn1Object().getInteger(); + BigInteger prime2 = parser.readAsn1Object().getInteger(); + BigInteger exponent1 = parser.readAsn1Object().getInteger(); + BigInteger exponent2 = parser.readAsn1Object().getInteger(); + BigInteger coefficient = parser.readAsn1Object().getInteger(); + return new RSAPrivateCrtKeySpec(modulus, publicExponent, privateExponent, prime1, prime2, exponent1, exponent2, coefficient); + } + + /** + * Parses a DER encoded DSA key to a {@link DSAPrivateKeySpec} using a minimal {@link DerParser} + * + * @param keyBytes the private key raw bytes + * @return {@link DSAPrivateKeySpec} + * @throws IOException if the DER encoded key can't be parsed + */ + private static DSAPrivateKeySpec parseDsaDer(byte[] keyBytes) throws IOException { + DerParser parser = new DerParser(keyBytes); + DerParser.Asn1Object sequence = parser.readAsn1Object(); + parser = sequence.getParser(); + parser.readAsn1Object().getInteger(); // (version) We don't need it but must read to get to p + BigInteger p = parser.readAsn1Object().getInteger(); + BigInteger q = parser.readAsn1Object().getInteger(); + BigInteger g = parser.readAsn1Object().getInteger(); + parser.readAsn1Object().getInteger(); // we don't need x + BigInteger x = parser.readAsn1Object().getInteger(); + return new DSAPrivateKeySpec(x, p, q, g); + } + + /** + * Parses a DER encoded private key and reads its algorithm identifier Object OID. + * + * @param keyBytes the private key raw bytes + * @return A string identifier for the key algorithm (RSA, DSA, or EC) + * @throws GeneralSecurityException if the algorithm oid that is parsed from ASN.1 is unknown + * @throws IOException if the DER encoded key can't be parsed + */ + private static String getKeyAlgorithmIdentifier(byte[] keyBytes) throws IOException, GeneralSecurityException { + DerParser parser = new DerParser(keyBytes); + DerParser.Asn1Object sequence = parser.readAsn1Object(); + parser = sequence.getParser(); + parser.readAsn1Object().getInteger(); // version + DerParser.Asn1Object algSequence = parser.readAsn1Object(); + parser = algSequence.getParser(); + String oidString = parser.readAsn1Object().getOid(); + switch (oidString) { + case "1.2.840.10040.4.1": + return "DSA"; + case "1.2.840.113549.1.1.1": + return "RSA"; + case "1.2.840.10045.2.1": + return "EC"; + } + throw new GeneralSecurityException("Error parsing key algorithm identifier. Algorithm with OID [" + oidString + + "] is not żsupported"); + } + + static List readCertificates(Collection certPaths) throws CertificateException, IOException { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + List certificates = new ArrayList<>(certPaths.size()); + for (Path path : certPaths) { + try (InputStream input = Files.newInputStream(path)) { + final Collection parsed = certFactory.generateCertificates(input); + if (parsed.isEmpty()) { + throw new SslConfigException("failed to parse any certificates from [" + path.toAbsolutePath() + "]"); + } + certificates.addAll(parsed); + } + } + return certificates; + } + + private static MessageDigest messageDigest(String digestAlgorithm) { + try { + return MessageDigest.getInstance(digestAlgorithm); + } catch (NoSuchAlgorithmException e) { + throw new SslConfigException("unexpected exception creating MessageDigest instance for [" + digestAlgorithm + "]", e); + } + } +} diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslClientAuthenticationMode.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslClientAuthenticationMode.java new file mode 100644 index 0000000000000..8a972b6c78826 --- /dev/null +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslClientAuthenticationMode.java @@ -0,0 +1,101 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.common.ssl; + +import javax.net.ssl.SSLParameters; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * The client authentication mode that is used for SSL servers. + */ +public enum SslClientAuthenticationMode { + + /** + * Never request a client certificate. + */ + NONE() { + public boolean enabled() { + return false; + } + + public void configure(SSLParameters sslParameters) { + // nothing to do here + assert !sslParameters.getWantClientAuth(); + assert !sslParameters.getNeedClientAuth(); + } + }, + /** + * Request a client certificate, but do not enforce that one is provided. + */ + OPTIONAL() { + public boolean enabled() { + return true; + } + + public void configure(SSLParameters sslParameters) { + sslParameters.setWantClientAuth(true); + } + }, + /** + * Request and require a client certificate. + */ + REQUIRED() { + public boolean enabled() { + return true; + } + + public void configure(SSLParameters sslParameters) { + sslParameters.setNeedClientAuth(true); + } + }; + + /** + * @return true if client authentication is enabled + */ + public abstract boolean enabled(); + + /** + * Configure client authentication of the provided {@link SSLParameters} + */ + public abstract void configure(SSLParameters sslParameters); + + private static final Map LOOKUP = Collections.unmodifiableMap(buildLookup()); + + static Map buildLookup() { + final Map map = new LinkedHashMap<>(3); + map.put("none", NONE); + map.put("optional", OPTIONAL); + map.put("required", REQUIRED); + return map; + } + + public static SslClientAuthenticationMode parse(String value) { + final SslClientAuthenticationMode mode = LOOKUP.get(value.toLowerCase(Locale.ROOT)); + if (mode == null) { + final String allowedValues = LOOKUP.keySet().stream().collect(Collectors.joining(",")); + throw new SslConfigException("could not resolve ssl client authentication, unknown value [" + + value + "], recognised values are [" + allowedValues + "]"); + } + return mode; + } +} diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigException.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigException.java new file mode 100644 index 0000000000000..ae5d332e0a1e7 --- /dev/null +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigException.java @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.ssl; + +/** + * A base exception for problems that occur while trying to configure SSL. + */ +public class SslConfigException extends RuntimeException { + public SslConfigException(String message, Exception cause) { + super(message, cause); + } + + public SslConfigException(String message) { + super(message); + } +} 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 new file mode 100644 index 0000000000000..146ba916b6b07 --- /dev/null +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfiguration.java @@ -0,0 +1,164 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.ssl; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509ExtendedTrustManager; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * A object encapsulating all necessary configuration for an SSL context (client or server). + * The configuration itself is immutable, but the {@link #getKeyConfig() key config} and + * {@link #getTrustConfig() trust config} may depend on reading key and certificate material + * from files (see {@link #getDependentFiles()}, and the content of those files may change. + */ +public class SslConfiguration { + + private final SslTrustConfig trustConfig; + private final SslKeyConfig keyConfig; + private final SslVerificationMode verificationMode; + private final SslClientAuthenticationMode clientAuth; + private final List ciphers; + private final List supportedProtocols; + + public SslConfiguration(SslTrustConfig trustConfig, SslKeyConfig keyConfig, SslVerificationMode verificationMode, + SslClientAuthenticationMode clientAuth, List ciphers, List supportedProtocols) { + if (ciphers == null || ciphers.isEmpty()) { + throw new SslConfigException("cannot configure SSL/TLS without any supported cipher suites"); + } + if (supportedProtocols == null || supportedProtocols.isEmpty()) { + throw new SslConfigException("cannot configure SSL/TLS without any supported protocols"); + } + this.trustConfig = Objects.requireNonNull(trustConfig, "trust config cannot be null"); + this.keyConfig = Objects.requireNonNull(keyConfig, "key config cannot be null"); + this.verificationMode = Objects.requireNonNull(verificationMode, "verification mode cannot be null"); + this.clientAuth = Objects.requireNonNull(clientAuth, "client authentication cannot be null"); + this.ciphers = Collections.unmodifiableList(ciphers); + this.supportedProtocols = Collections.unmodifiableList(supportedProtocols); + } + + public SslTrustConfig getTrustConfig() { + return trustConfig; + } + + public SslKeyConfig getKeyConfig() { + return keyConfig; + } + + public SslVerificationMode getVerificationMode() { + return verificationMode; + } + + public SslClientAuthenticationMode getClientAuth() { + return clientAuth; + } + + public List getCipherSuites() { + return ciphers; + } + + public List getSupportedProtocols() { + return supportedProtocols; + } + + /** + * @return A collection of files that are used by this SSL configuration. If the contents of these files change, then any + * subsequent call to {@link #createSslContext()} (or similar methods) may create a context with different behaviour. + * It is recommended that these files be monitored for changes, and a new ssl-context is created whenever any of the files are modified. + */ + public Collection getDependentFiles() { + Set paths = new HashSet<>(keyConfig.getDependentFiles()); + paths.addAll(trustConfig.getDependentFiles()); + return paths; + } + + /** + * 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 + * contents of their referenced files (see {@link #getDependentFiles()}, consecutive calls to this method may + * return ssl-contexts with different configurations. + */ + public SSLContext createSslContext() { + final X509ExtendedKeyManager keyManager = keyConfig.createKeyManager(); + final X509ExtendedTrustManager trustManager = trustConfig.createTrustManager(); + try { + SSLContext sslContext = SSLContext.getInstance(contextProtocol()); + sslContext.init(new X509ExtendedKeyManager[] { keyManager }, new X509ExtendedTrustManager[] { trustManager }, null); + return sslContext; + } catch (GeneralSecurityException e) { + throw new SslConfigException("cannot create ssl context", e); + } + } + + /** + * Picks the best (highest security / most recent standard) SSL/TLS protocol (/version) that is supported by the + * {@link #getSupportedProtocols() configured protocols}. + */ + private String contextProtocol() { + if (supportedProtocols.isEmpty()) { + throw new SslConfigException("no SSL/TLS protocols have been configured"); + } + for (String tryProtocol : Arrays.asList("TLSv1.2", "TLSv1.1", "TLSv1", "SSLv3")) { + if (supportedProtocols.contains(tryProtocol)) { + return tryProtocol; + } + } + return "SSL"; + } + + @Override + public String toString() { + return getClass().getSimpleName() + '{' + + "trustConfig=" + trustConfig + + ", keyConfig=" + keyConfig + + ", verificationMode=" + verificationMode + + ", clientAuth=" + clientAuth + + ", ciphers=" + ciphers + + ", supportedProtocols=" + supportedProtocols + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final SslConfiguration that = (SslConfiguration) o; + return Objects.equals(this.trustConfig, that.trustConfig) && + Objects.equals(this.keyConfig, that.keyConfig) && + this.verificationMode == that.verificationMode && + this.clientAuth == that.clientAuth && + Objects.equals(this.ciphers, that.ciphers) && + Objects.equals(this.supportedProtocols, that.supportedProtocols); + } + + @Override + public int hashCode() { + return Objects.hash(trustConfig, keyConfig, verificationMode, clientAuth, ciphers, supportedProtocols); + } +} diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationKeys.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationKeys.java new file mode 100644 index 0000000000000..6e717f1c4cd11 --- /dev/null +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationKeys.java @@ -0,0 +1,181 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.ssl; + +import javax.net.ssl.TrustManagerFactory; +import java.security.KeyStore; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Utility class for handling the standard setting keys for use in SSL configuration. + * + * @see SslConfiguration + * @see SslConfigurationLoader + */ +public class SslConfigurationKeys { + /** + * The SSL/TLS protocols (i.e. versions) that should be used + */ + public static final String PROTOCOLS = "supported_protocols"; + + /** + * The SSL/TLS cipher suites that should be used + */ + public static final String CIPHERS = "cipher_suites"; + + /** + * Whether certificate and/or hostname verification should be used + */ + public static final String VERIFICATION_MODE = "verification_mode"; + + /** + * When operating as a server, whether to request/require client certificates + */ + public static final String CLIENT_AUTH = "client_authentication"; + + // Trust + /** + * A list of paths to PEM formatted certificates that should be trusted as CAs + */ + public static final String CERTIFICATE_AUTHORITIES = "certificate_authorities"; + /** + * The path to a KeyStore file (in a format supported by this JRE) that should be used as a trust-store + */ + public static final String TRUSTSTORE_PATH = "truststore.path"; + /** + * The password for the file configured in {@link #TRUSTSTORE_PATH}, as a secure setting. + */ + public static final String TRUSTSTORE_SECURE_PASSWORD = "truststore.secure_password"; + /** + * The password for the file configured in {@link #TRUSTSTORE_PATH}, as a non-secure setting. + * The use of this setting {@link #isDeprecated(String) is deprecated}. + */ + public static final String TRUSTSTORE_LEGACY_PASSWORD = "truststore.password"; + /** + * The {@link KeyStore#getType() keystore type} for the file configured in {@link #TRUSTSTORE_PATH}. + */ + public static final String TRUSTSTORE_TYPE = "truststore.type"; + /** + * The {@link TrustManagerFactory#getAlgorithm() trust management algorithm} to use when configuring trust + * with a {@link #TRUSTSTORE_PATH truststore}. + */ + public static final String TRUSTSTORE_ALGORITHM = "truststore.algorithm"; + + // Key Management + // -- Keystore + /** + * The path to a KeyStore file (in a format supported by this JRE) that should be used for key management + */ + public static final String KEYSTORE_PATH = "keystore.path"; + /** + * The password for the file configured in {@link #KEYSTORE_PATH}, as a secure setting. + */ + public static final String KEYSTORE_SECURE_PASSWORD = "keystore.secure_password"; + /** + * The password for the file configured in {@link #KEYSTORE_PATH}, as a non-secure setting. + * The use of this setting {@link #isDeprecated(String) is deprecated}. + */ + public static final String KEYSTORE_LEGACY_PASSWORD = "keystore.password"; + /** + * The password for the key within the {@link #KEYSTORE_PATH configured keystore}, as a secure setting. + * If no key password is specified, it will default to the keystore password. + */ + public static final String KEYSTORE_SECURE_KEY_PASSWORD = "keystore.secure_key_password"; + /** + * The password for the key within the {@link #KEYSTORE_PATH configured keystore}, as a non-secure setting. + * The use of this setting {@link #isDeprecated(String) is deprecated}. + * If no key password is specified, it will default to the keystore password. + */ + public static final String KEYSTORE_LEGACY_KEY_PASSWORD = "keystore.key_password"; + /** + * The {@link KeyStore#getType() keystore type} for the file configured in {@link #KEYSTORE_PATH}. + */ + public static final String KEYSTORE_TYPE = "keystore.type"; + /** + * The {@link javax.net.ssl.KeyManagerFactory#getAlgorithm() key management algorithm} to use when + * connstructing a Key manager from a {@link #KEYSTORE_PATH keystore}. + */ + public static final String KEYSTORE_ALGORITHM = "keystore.algorithm"; + // -- PEM + /** + * The path to a PEM formatted file that contains the certificate to be used as part of key management + */ + public static final String CERTIFICATE = "certificate"; + /** + * The path to a PEM formatted file that contains the private key for the configured {@link #CERTIFICATE}. + */ + public static final String KEY = "key"; + /** + * The password to read the configured {@link #KEY}, as a secure setting. + * This (or the {@link #KEY_LEGACY_PASSPHRASE legacy fallback}) is required if the key file is encrypted. + */ + public static final String KEY_SECURE_PASSPHRASE = "secure_key_passphrase"; + /** + * The password to read the configured {@link #KEY}, as a non-secure setting. + * The use of this setting {@link #isDeprecated(String) is deprecated}. + */ + public static final String KEY_LEGACY_PASSPHRASE = "key_passphrase"; + + private static final Set DEPRECATED_KEYS = new HashSet<>( + Arrays.asList(TRUSTSTORE_LEGACY_PASSWORD, KEYSTORE_LEGACY_PASSWORD, KEYSTORE_LEGACY_KEY_PASSWORD, KEY_LEGACY_PASSPHRASE) + ); + + private SslConfigurationKeys() { + throw new IllegalStateException("Utility class should not be instantiated"); + } + + /** + * The list of keys that are used to load a non-secure, non-list setting + */ + public static List getStringKeys() { + return Arrays.asList( + VERIFICATION_MODE, CLIENT_AUTH, + TRUSTSTORE_PATH, TRUSTSTORE_LEGACY_PASSWORD, TRUSTSTORE_TYPE, TRUSTSTORE_TYPE, + KEYSTORE_PATH, KEYSTORE_LEGACY_PASSWORD, KEYSTORE_LEGACY_KEY_PASSWORD, KEYSTORE_TYPE, KEYSTORE_ALGORITHM, + CERTIFICATE, KEY, KEY_LEGACY_PASSPHRASE + ); + } + + /** + * The list of keys that are used to load a non-secure, list setting + */ + public static List getListKeys() { + return Arrays.asList(PROTOCOLS, CIPHERS, CERTIFICATE_AUTHORITIES); + } + + /** + * The list of keys that are used to load a secure setting (such as a password) that would typically be stored in the elasticsearch + * keystore. + */ + public static List getSecureStringKeys() { + return Arrays.asList(TRUSTSTORE_SECURE_PASSWORD, KEYSTORE_SECURE_PASSWORD, KEYSTORE_SECURE_KEY_PASSWORD, KEY_SECURE_PASSPHRASE); + } + + /** + * @return {@code true} if the provided key is a deprecated setting + */ + public static boolean isDeprecated(String key) { + return DEPRECATED_KEYS.contains(key); + } + +} 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 new file mode 100644 index 0000000000000..186d20b1ea858 --- /dev/null +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationLoader.java @@ -0,0 +1,371 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.ssl; + +import javax.crypto.Cipher; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManagerFactory; +import java.nio.file.Path; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.elasticsearch.common.ssl.KeyStoreUtil.inferKeyStoreType; +import static org.elasticsearch.common.ssl.SslConfigurationKeys.CERTIFICATE; +import static org.elasticsearch.common.ssl.SslConfigurationKeys.CERTIFICATE_AUTHORITIES; +import static org.elasticsearch.common.ssl.SslConfigurationKeys.CIPHERS; +import static org.elasticsearch.common.ssl.SslConfigurationKeys.CLIENT_AUTH; +import static org.elasticsearch.common.ssl.SslConfigurationKeys.KEY; +import static org.elasticsearch.common.ssl.SslConfigurationKeys.KEYSTORE_ALGORITHM; +import static org.elasticsearch.common.ssl.SslConfigurationKeys.KEYSTORE_LEGACY_KEY_PASSWORD; +import static org.elasticsearch.common.ssl.SslConfigurationKeys.KEYSTORE_LEGACY_PASSWORD; +import static org.elasticsearch.common.ssl.SslConfigurationKeys.KEYSTORE_PATH; +import static org.elasticsearch.common.ssl.SslConfigurationKeys.KEYSTORE_SECURE_KEY_PASSWORD; +import static org.elasticsearch.common.ssl.SslConfigurationKeys.KEYSTORE_SECURE_PASSWORD; +import static org.elasticsearch.common.ssl.SslConfigurationKeys.KEYSTORE_TYPE; +import static org.elasticsearch.common.ssl.SslConfigurationKeys.KEY_LEGACY_PASSPHRASE; +import static org.elasticsearch.common.ssl.SslConfigurationKeys.KEY_SECURE_PASSPHRASE; +import static org.elasticsearch.common.ssl.SslConfigurationKeys.PROTOCOLS; +import static org.elasticsearch.common.ssl.SslConfigurationKeys.TRUSTSTORE_ALGORITHM; +import static org.elasticsearch.common.ssl.SslConfigurationKeys.TRUSTSTORE_LEGACY_PASSWORD; +import static org.elasticsearch.common.ssl.SslConfigurationKeys.TRUSTSTORE_PATH; +import static org.elasticsearch.common.ssl.SslConfigurationKeys.TRUSTSTORE_SECURE_PASSWORD; +import static org.elasticsearch.common.ssl.SslConfigurationKeys.TRUSTSTORE_TYPE; +import static org.elasticsearch.common.ssl.SslConfigurationKeys.VERIFICATION_MODE; + +/** + * Loads {@link SslConfiguration} from settings. + * This class handles the logic of interpreting the various "ssl.*" configuration settings and their interactions + * (as well as being aware of dependencies and conflicts between different settings). + * The constructed {@code SslConfiguration} has sensible defaults for any settings that are not explicitly configured, + * and these defaults can be overridden through the various {@code setDefaultXyz} methods. + * It is {@code abstract} because this library has minimal dependencies, so the extraction of the setting values from + * the underlying setting source must be handled by the code that makes use of this class. + * + * @see SslConfiguration + * @see SslConfigurationKeys + */ +public abstract class SslConfigurationLoader { + + static final List DEFAULT_PROTOCOLS = Arrays.asList("TLSv1.2", "TLSv1.1", "TLSv1"); + static final List DEFAULT_CIPHERS = loadDefaultCiphers(); + private static final char[] EMPTY_PASSWORD = new char[0]; + + private final String settingPrefix; + + private SslTrustConfig defaultTrustConfig; + private SslKeyConfig defaultKeyConfig; + private SslVerificationMode defaultVerificationMode; + private SslClientAuthenticationMode defaultClientAuth; + private List defaultCiphers; + private List defaultProtocols; + + /** + * Construct a new loader with the "standard" default values. + * + * @param settingPrefix The prefix to apply to all settings that are loaded. It may be the empty string, otherwise it + * must end in a "." (period). For example, if the prefix is {@code "reindex.ssl."} then the keys that are + * passed to methods like {@link #getSettingAsString(String)} will be in the form + * {@code "reindex.ssl.verification_mode"}, and those same keys will be reported in error messages (via + * {@link SslConfigException}). + */ + public SslConfigurationLoader(String settingPrefix) { + this.settingPrefix = settingPrefix == null ? "" : settingPrefix; + if (this.settingPrefix.isEmpty() == false && this.settingPrefix.endsWith(".") == false) { + throw new IllegalArgumentException("Setting prefix [" + settingPrefix + "] must be blank or end in '.'"); + } + this.defaultTrustConfig = new DefaultJdkTrustConfig(); + this.defaultKeyConfig = EmptyKeyConfig.INSTANCE; + this.defaultVerificationMode = SslVerificationMode.FULL; + this.defaultClientAuth = SslClientAuthenticationMode.OPTIONAL; + this.defaultProtocols = DEFAULT_PROTOCOLS; + this.defaultCiphers = DEFAULT_CIPHERS; + } + + /** + * Change the default trust config. + * The initial trust config is {@link DefaultJdkTrustConfig}, which trusts the JDK's default CA certs + */ + public void setDefaultTrustConfig(SslTrustConfig defaultTrustConfig) { + this.defaultTrustConfig = defaultTrustConfig; + } + + /** + * Change the default key config. + * The initial key config is {@link EmptyKeyConfig}, which does not provide any keys + */ + public void setDefaultKeyConfig(SslKeyConfig defaultKeyConfig) { + this.defaultKeyConfig = defaultKeyConfig; + } + + /** + * Change the default verification mode. + * The initial verification mode is {@link SslVerificationMode#FULL}. + */ + public void setDefaultVerificationMode(SslVerificationMode defaultVerificationMode) { + this.defaultVerificationMode = defaultVerificationMode; + } + + /** + * Change the default client authentication mode. + * The initial client auth mode is {@link SslClientAuthenticationMode#OPTIONAL}. + */ + public void setDefaultClientAuth(SslClientAuthenticationMode defaultClientAuth) { + this.defaultClientAuth = defaultClientAuth; + } + + /** + * Change the default supported ciphers. + * The initial cipher list depends on the availability of {@link #has256BitAES() 256 bit AES}. + * + * @see #loadDefaultCiphers() + */ + public void setDefaultCiphers(List defaultCiphers) { + this.defaultCiphers = defaultCiphers; + } + + /** + * Change the default SSL/TLS protocol list. + * The initial protocol list is defined by {@link #DEFAULT_PROTOCOLS} + */ + public void setDefaultProtocols(List defaultProtocols) { + this.defaultProtocols = defaultProtocols; + } + + /** + * 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()}). + *

+ * The setting should be returned as a string, and this class will convert it to the relevant type. + * + * @throws Exception If a {@link RuntimeException} is thrown, it will be rethrown unwrapped. All checked exceptions are wrapped in + * {@link SslConfigException} before being rethrown. + */ + protected abstract String getSettingAsString(String key) throws Exception; + + /** + * Clients of this class should implement this method to load a fully-qualified key from the preferred secure settings source. + * This method will be called for any setting keys that are marked as being + * {@link SslConfigurationKeys#getSecureStringKeys() secure} settings. + * + * @throws Exception If a {@link RuntimeException} is thrown, it will be rethrown unwrapped. All checked exceptions are wrapped in + * {@link SslConfigException} before being rethrown. + */ + protected abstract char[] getSecureSetting(String key) throws Exception; + + /** + * 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 list settings (see {@link SslConfigurationKeys#getListKeys()}). + *

+ * The setting should be returned as a list of strings, and this class will convert the values to the relevant type. + * + * @throws Exception If a {@link RuntimeException} is thrown, it will be rethrown unwrapped. All checked exceptions are wrapped in + * {@link SslConfigException} before being rethrown. + */ + protected abstract List getSettingAsList(String key) throws Exception; + + /** + * Resolve all necessary configuration settings, and load a {@link SslConfiguration}. + * + * @param basePath The base path to use for any settings that represent file paths. Typically points to the Elasticsearch + * configuration directory. + * @throws SslConfigException For any problems with the configuration, or with loading the required SSL classes. + */ + public SslConfiguration load(Path basePath) { + Objects.requireNonNull(basePath, "Base Path cannot be null"); + final List protocols = resolveListSetting(PROTOCOLS, Function.identity(), defaultProtocols); + final List ciphers = resolveListSetting(CIPHERS, Function.identity(), defaultCiphers); + 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); + + if (protocols == null || protocols.isEmpty()) { + throw new SslConfigException("no protocols configured in [" + settingPrefix + PROTOCOLS + "]"); + } + if (ciphers == null || ciphers.isEmpty()) { + throw new SslConfigException("no cipher suites configured in [" + settingPrefix + CIPHERS + "]"); + } + return new SslConfiguration(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); + + if (certificateAuthorities != null && trustStorePath != null) { + throw new SslConfigException("cannot specify both [" + settingPrefix + CERTIFICATE_AUTHORITIES + "] and [" + + settingPrefix + TRUSTSTORE_PATH + "]"); + } + if (verificationMode.isCertificateVerificationEnabled() == false) { + return TrustEverythingConfig.TRUST_EVERYTHING; + } + if (certificateAuthorities != null) { + return new PemTrustConfig(certificateAuthorities); + } + 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 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); + + if (certificatePath != null && keyStorePath != null) { + throw new SslConfigException("cannot specify both [" + settingPrefix + CERTIFICATE + "] and [" + + settingPrefix + KEYSTORE_PATH + "]"); + } + + if (certificatePath != null || keyPath != null) { + if (keyPath == null) { + throw new SslConfigException("cannot specify [" + settingPrefix + CERTIFICATE + "] without also setting [" + + settingPrefix + KEY + "]"); + } + if (certificatePath == null) { + throw new SslConfigException("cannot specify [" + settingPrefix + KEYSTORE_PATH + "] without also setting [" + + settingPrefix + CERTIFICATE + "]"); + } + final char[] password = resolvePasswordSetting(KEY_SECURE_PASSPHRASE, KEY_LEGACY_PASSPHRASE); + return new PemKeyConfig(certificatePath, keyPath, password); + } + + if (keyStorePath != null) { + final char[] storePassword = resolvePasswordSetting(KEYSTORE_SECURE_PASSWORD, KEYSTORE_LEGACY_PASSWORD); + char[] keyPassword = resolvePasswordSetting(KEYSTORE_SECURE_KEY_PASSWORD, KEYSTORE_LEGACY_KEY_PASSWORD); + if (keyPassword.length == 0) { + keyPassword = storePassword; + } + 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 defaultKeyConfig; + } + + private char[] resolvePasswordSetting(String secureSettingKey, String legacySettingKey) { + final char[] securePassword = resolveSecureSetting(secureSettingKey, null); + final String legacyPassword = resolveSetting(legacySettingKey, Function.identity(), null); + if (securePassword == null) { + if (legacyPassword == null) { + return EMPTY_PASSWORD; + } else { + return legacyPassword.toCharArray(); + } + } else { + if (legacyPassword != null) { + throw new SslConfigException("cannot specify both [" + settingPrefix + secureSettingKey + "] and [" + + settingPrefix + legacySettingKey + "]"); + } else { + return securePassword; + } + } + } + + private V resolveSetting(String key, Function parser, V defaultValue) { + try { + String setting = getSettingAsString(settingPrefix + key); + if (setting == null || setting.isEmpty()) { + return defaultValue; + } + return parser.apply(setting); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new SslConfigException("cannot retrieve setting [" + settingPrefix + key + "]", e); + } + } + + private char[] resolveSecureSetting(String key, char[] defaultValue) { + try { + char[] setting = getSecureSetting(settingPrefix + key); + if (setting == null || setting.length == 0) { + return defaultValue; + } + return setting; + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new SslConfigException("cannot retrieve secure setting [" + settingPrefix + key + "]", e); + } + + } + + private List resolveListSetting(String key, Function parser, List defaultValue) { + try { + final List list = getSettingAsList(settingPrefix + key); + if (list == null || list.isEmpty()) { + return defaultValue; + } + return list.stream().map(parser).collect(Collectors.toList()); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new SslConfigException("cannot retrieve setting [" + settingPrefix + key + "]", e); + } + } + + private static List loadDefaultCiphers() { + final List ciphers128 = Arrays.asList( + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_128_CBC_SHA256", + "TLS_RSA_WITH_AES_128_CBC_SHA" + ); + final List ciphers256 = Arrays.asList( + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA256", + "TLS_RSA_WITH_AES_256_CBC_SHA" + ); + if (has256BitAES()) { + List ciphers = new ArrayList<>(ciphers256.size() + ciphers128.size()); + ciphers.addAll(ciphers256); + ciphers.addAll(ciphers128); + return ciphers; + } else { + return ciphers128; + } + } + + private static boolean has256BitAES() { + try { + return Cipher.getMaxAllowedKeyLength("AES") > 128; + } catch (NoSuchAlgorithmException e) { + // No AES? Things are going to be very weird, but technically that means we don't have 256 bit AES, so ... + return 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 new file mode 100644 index 0000000000000..4f5e6b8669310 --- /dev/null +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslKeyConfig.java @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.ssl; + +import javax.net.ssl.X509ExtendedKeyManager; +import java.nio.file.Path; +import java.util.Collection; + +/** + * An interface for building a key manager at runtime. + * The method for constructing the key manager is implementation dependent. + */ +public interface SslKeyConfig { + + /** + * @return A collection of files that are read by this config object. + * The {@link #createKeyManager()} method will read these files dynamically, so the behaviour of this key config may change whenever + * any of these files are modified. + */ + Collection getDependentFiles(); + + /** + * @return A new {@link X509ExtendedKeyManager}. + * @throws SslConfigException if there is a problem configuring the key manager. + */ + X509ExtendedKeyManager createKeyManager(); + +} + 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 new file mode 100644 index 0000000000000..2cd61218e6bb7 --- /dev/null +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslTrustConfig.java @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.ssl; + +import javax.net.ssl.X509ExtendedTrustManager; +import java.nio.file.Path; +import java.util.Collection; + +/** + * An interface for building a trust manager at runtime. + * The method for constructing the trust manager is implementation dependent. + */ +public interface SslTrustConfig { + + /** + * @return A collection of files that are read by this config object. + * The {@link #createTrustManager()} method will read these files dynamically, so the behaviour of this trust config may change if + * any of these files are modified. + */ + Collection getDependentFiles(); + + /** + * @return A new {@link X509ExtendedTrustManager}. + * @throws SslConfigException if there is a problem configuring the trust manager. + */ + X509ExtendedTrustManager createTrustManager(); + +} + diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslVerificationMode.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslVerificationMode.java new file mode 100644 index 0000000000000..eee6e9cb2d6a1 --- /dev/null +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslVerificationMode.java @@ -0,0 +1,104 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.common.ssl; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Represents the verification mode to be used for SSL connections. + */ +public enum SslVerificationMode { + /** + * Verify neither the hostname, nor the provided certificate. + */ + NONE { + @Override + public boolean isHostnameVerificationEnabled() { + return false; + } + + @Override + public boolean isCertificateVerificationEnabled() { + return false; + } + }, + /** + * Verify the provided certificate against the trust chain, but do not verify the hostname. + */ + CERTIFICATE { + @Override + public boolean isHostnameVerificationEnabled() { + return false; + } + + @Override + public boolean isCertificateVerificationEnabled() { + return true; + } + }, + /** + * Verify the provided certificate against the trust chain, and also verify that the hostname to which this client is connected + * matches one of the Subject-Alternative-Names in the certificate. + */ + FULL { + @Override + public boolean isHostnameVerificationEnabled() { + return true; + } + + @Override + public boolean isCertificateVerificationEnabled() { + return true; + } + }; + + /** + * @return true if hostname verification is enabled + */ + public abstract boolean isHostnameVerificationEnabled(); + + /** + * @return true if certificate verification is enabled + */ + public abstract boolean isCertificateVerificationEnabled(); + + private static final Map LOOKUP = Collections.unmodifiableMap(buildLookup()); + + private static Map buildLookup() { + Map map = new LinkedHashMap<>(3); + map.put("none", NONE); + map.put("certificate", CERTIFICATE); + map.put("full", FULL); + return map; + } + + public static SslVerificationMode parse(String value) { + final SslVerificationMode mode = LOOKUP.get(value.toLowerCase(Locale.ROOT)); + if (mode == null) { + final String allowedValues = LOOKUP.keySet().stream().collect(Collectors.joining(",")); + throw new SslConfigException("could not resolve ssl client verification mode, unknown value [" + + value + "], recognised values are [" + allowedValues + "]"); + } + return mode; + } +} 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 new file mode 100644 index 0000000000000..683aaaaa06cb4 --- /dev/null +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreKeyConfig.java @@ -0,0 +1,106 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.ssl; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.X509ExtendedKeyManager; +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; + +/** + * 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; + 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)}. + * @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()}). + */ + 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; + } + + @Override + public Collection getDependentFiles() { + return Collections.singleton(path); + } + + @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); + } + } + + /** + * 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); + } + +} 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 new file mode 100644 index 0000000000000..0dc0a3818c8ed --- /dev/null +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreTrustConfig.java @@ -0,0 +1,90 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.ssl; + +import javax.net.ssl.X509ExtendedTrustManager; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; + +/** + * A {@link SslTrustConfig} that builds a Trust Manager from a keystore file. + */ +final class StoreTrustConfig implements SslTrustConfig { + private final Path path; + private final char[] password; + private final String type; + private final String algorithm; + + /** + * @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)}. + * @param algorithm The algorithm to use for the Trust Manager (see {@link javax.net.ssl.TrustManagerFactory#getAlgorithm()}). + */ + StoreTrustConfig(Path path, char[] password, String type, String algorithm) { + this.path = path; + this.type = type; + this.algorithm = algorithm; + this.password = password; + } + + @Override + public Collection getDependentFiles() { + return Collections.singleton(path); + } + + @Override + public X509ExtendedTrustManager createTrustManager() { + try { + final KeyStore store = KeyStoreUtil.readKeyStore(path, type, password); + checkTrustStore(store); + 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); + } + } + + /** + * Verifies that the keystore contains at least 1 trusted certificate entry. + */ + private void checkTrustStore(KeyStore store) throws GeneralSecurityException { + Enumeration aliases = store.aliases(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + if (store.isCertificateEntry(alias)) { + 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); + } + +} 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 new file mode 100644 index 0000000000000..f3ed83a7e7d7b --- /dev/null +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/TrustEverythingConfig.java @@ -0,0 +1,92 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.ssl; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; +import java.net.Socket; +import java.nio.file.Path; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.Collections; + +/** + * 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 { + + static final TrustEverythingConfig TRUST_EVERYTHING = new TrustEverythingConfig(); + + private TrustEverythingConfig() { + // single instances + } + + /** + * The {@link X509ExtendedTrustManager} that will trust all certificates. + * All methods are implemented as a no-op and do not throw exceptions regardless of the certificate presented. + */ + private static final X509ExtendedTrustManager TRUST_MANAGER = new X509ExtendedTrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s, Socket socket) { + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s, Socket socket) { + } + + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) { + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) { + } + + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s) { + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s) { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }; + + @Override + public Collection getDependentFiles() { + return Collections.emptyList(); + } + + @Override + public X509ExtendedTrustManager createTrustManager() { + return TRUST_MANAGER; + } + + @Override + public String toString() { + return "trust everything"; + } +} diff --git a/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/DefaultJdkTrustConfigTests.java b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/DefaultJdkTrustConfigTests.java new file mode 100644 index 0000000000000..b1aad439e47e1 --- /dev/null +++ b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/DefaultJdkTrustConfigTests.java @@ -0,0 +1,79 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.ssl; + +import org.elasticsearch.test.ESTestCase; +import org.junit.Assert; + +import javax.net.ssl.X509ExtendedTrustManager; +import java.security.cert.X509Certificate; +import java.util.Locale; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.stream.Stream; + +import static org.hamcrest.Matchers.emptyArray; +import static org.hamcrest.Matchers.emptyIterable; +import static org.hamcrest.Matchers.not; + +public class DefaultJdkTrustConfigTests extends ESTestCase { + + private static final BiFunction EMPTY_SYSTEM_PROPERTIES = (key, defaultValue) -> defaultValue; + + public void testGetSystemTrustStoreWithNoSystemProperties() throws Exception { + final DefaultJdkTrustConfig trustConfig = new DefaultJdkTrustConfig((key, defaultValue) -> defaultValue); + assertThat(trustConfig.getDependentFiles(), emptyIterable()); + final X509ExtendedTrustManager trustManager = trustConfig.createTrustManager(); + assertStandardIssuers(trustManager); + } + + public void testGetNonPKCS11TrustStoreWithPasswordSet() throws Exception { + final DefaultJdkTrustConfig trustConfig = new DefaultJdkTrustConfig(EMPTY_SYSTEM_PROPERTIES, "fakepassword".toCharArray()); + assertThat(trustConfig.getDependentFiles(), emptyIterable()); + final X509ExtendedTrustManager trustManager = trustConfig.createTrustManager(); + assertStandardIssuers(trustManager); + } + + private void assertStandardIssuers(X509ExtendedTrustManager trustManager) { + assertThat(trustManager.getAcceptedIssuers(), not(emptyArray())); + // This is a sample of the CAs that we expect on every JRE. + // We can safely change this list if the JRE's issuer list changes, but we want to assert something useful. + assertHasTrustedIssuer(trustManager, "VeriSign"); + assertHasTrustedIssuer(trustManager, "GeoTrust"); + assertHasTrustedIssuer(trustManager, "DigiCert"); + assertHasTrustedIssuer(trustManager, "thawte"); + assertHasTrustedIssuer(trustManager, "COMODO"); + } + + private void assertHasTrustedIssuer(X509ExtendedTrustManager trustManager, String name) { + final String lowerName = name.toLowerCase(Locale.ROOT); + final Optional ca = Stream.of(trustManager.getAcceptedIssuers()) + .filter(cert -> cert.getSubjectDN().getName().toLowerCase(Locale.ROOT).contains(lowerName)) + .findAny(); + if (ca.isPresent() == false) { + logger.info("Failed to find issuer [{}] in trust manager, but did find ...", lowerName); + for (X509Certificate cert : trustManager.getAcceptedIssuers()) { + logger.info(" - {}", cert.getSubjectDN().getName().replaceFirst("^\\w+=([^,]+),.*", "$1")); + } + Assert.fail("Cannot find trusted issuer with name [" + name + "]."); + } + } + +} 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 new file mode 100644 index 0000000000000..8a5bb469e3c2c --- /dev/null +++ b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/PemKeyConfigTests.java @@ -0,0 +1,148 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.ssl; + +import org.elasticsearch.test.ESTestCase; +import org.hamcrest.Matchers; + +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.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +import static org.hamcrest.Matchers.arrayWithSize; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.iterableWithSize; +import static org.hamcrest.Matchers.notNullValue; + +public class PemKeyConfigTests extends ESTestCase { + private static final int IP_NAME = 7; + private static final int DNS_NAME = 2; + + public void testBuildKeyConfigFromPemFilesWithoutPassword() 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)); + assertCertificateAndKey(keyConfig, "CN=cert1"); + } + + public void testBuildKeyConfigFromPemFilesWithPassword() 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)); + assertCertificateAndKey(keyConfig, "CN=cert2"); + } + + 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()); + assertPasswordIsIncorrect(keyConfig, key); + } + + 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]); + assertFileNotFound(keyConfig, "certificate", cert); + } + + 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]); + assertFileNotFound(keyConfig, "private key", key); + } + + public void testKeyConfigReloadsFileContents() throws Exception { + final Path cert1 = getDataPath("/certs/cert1/cert1.crt"); + final Path key1 = getDataPath("/certs/cert1/cert1.key"); + final Path cert2 = getDataPath("/certs/cert2/cert2.crt"); + final Path key2 = getDataPath("/certs/cert2/cert2.key"); + final Path cert = createTempFile("cert", ".crt"); + final Path key = createTempFile("cert", ".key"); + + final PemKeyConfig keyConfig = new PemKeyConfig(cert, key, new char[0]); + + Files.copy(cert1, cert, StandardCopyOption.REPLACE_EXISTING); + Files.copy(key1, key, StandardCopyOption.REPLACE_EXISTING); + assertCertificateAndKey(keyConfig, "CN=cert1"); + + Files.copy(cert2, cert, StandardCopyOption.REPLACE_EXISTING); + Files.copy(key2, key, StandardCopyOption.REPLACE_EXISTING); + assertPasswordIsIncorrect(keyConfig, key); + + Files.copy(cert1, cert, StandardCopyOption.REPLACE_EXISTING); + Files.copy(key1, key, StandardCopyOption.REPLACE_EXISTING); + assertCertificateAndKey(keyConfig, "CN=cert1"); + + Files.delete(cert); + assertFileNotFound(keyConfig, "certificate", cert); + } + + private void assertCertificateAndKey(PemKeyConfig keyConfig, String expectedDN) throws CertificateParsingException { + final X509ExtendedKeyManager keyManager = keyConfig.createKeyManager(); + assertThat(keyManager, notNullValue()); + + final PrivateKey privateKey = keyManager.getPrivateKey("key"); + assertThat(privateKey, notNullValue()); + assertThat(privateKey.getAlgorithm(), is("RSA")); + + final X509Certificate[] chain = keyManager.getCertificateChain("key"); + assertThat(chain, notNullValue()); + assertThat(chain, arrayWithSize(1)); + final X509Certificate certificate = chain[0]; + assertThat(certificate.getIssuerDN().getName(), is("CN=Test CA 1")); + assertThat(certificate.getSubjectDN().getName(), is(expectedDN)); + assertThat(certificate.getSubjectAlternativeNames(), iterableWithSize(2)); + assertThat(certificate.getSubjectAlternativeNames(), containsInAnyOrder( + Arrays.asList(DNS_NAME, "localhost"), + Arrays.asList(IP_NAME, "127.0.0.1") + )); + } + + 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(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(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 new file mode 100644 index 0000000000000..3d78976e1e83b --- /dev/null +++ b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/PemTrustConfigTests.java @@ -0,0 +1,150 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.ssl; + +import org.elasticsearch.test.ESTestCase; +import org.hamcrest.Matchers; + +import javax.net.ssl.X509ExtendedTrustManager; +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.Principal; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class PemTrustConfigTests extends ESTestCase { + + 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)); + 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)); + 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, randomByteArrayOfLength(128), StandardOpenOption.APPEND); + final PemTrustConfig trustConfig = new PemTrustConfig(Collections.singletonList(ca)); + 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)); + 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); + } + + 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); + } + + public void testTrustConfigReloadsFileContents() 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 Path ca1 = createTempFile("ca1", ".crt"); + final Path ca2 = createTempFile("ca2", ".crt"); + + final PemTrustConfig trustConfig = new PemTrustConfig(Arrays.asList(ca1, ca2)); + + Files.copy(cert1, ca1, StandardCopyOption.REPLACE_EXISTING); + Files.copy(cert2, ca2, StandardCopyOption.REPLACE_EXISTING); + assertCertificateChain(trustConfig, "CN=Test CA 1", "CN=Test CA 2"); + + Files.copy(cert3, ca2, StandardCopyOption.REPLACE_EXISTING); + assertCertificateChain(trustConfig, "CN=Test CA 1", "CN=Test CA 3"); + + Files.delete(ca1); + assertFileNotFound(trustConfig, ca1); + + Files.write(ca1, randomByteArrayOfLength(128), StandardOpenOption.CREATE); + assertInvalidFileFormat(trustConfig, ca1); + } + + private void assertCertificateChain(PemTrustConfig trustConfig, String... caNames) { + final X509ExtendedTrustManager trustManager = trustConfig.createTrustManager(); + final X509Certificate[] issuers = trustManager.getAcceptedIssuers(); + final Set issuerNames = Stream.of(issuers) + .map(X509Certificate::getSubjectDN) + .map(Principal::getName) + .collect(Collectors.toSet()); + + assertThat(issuerNames, Matchers.containsInAnyOrder(caNames)); + } + + private void assertEmptyFile(PemTrustConfig trustConfig, Path file) { + final SslConfigException exception = expectThrows(SslConfigException.class, trustConfig::createTrustManager); + assertThat(exception.getMessage(), Matchers.containsString(file.toAbsolutePath().toString())); + assertThat(exception.getMessage(), Matchers.containsString("failed to parse any certificates")); + } + + private void assertInvalidFileFormat(PemTrustConfig trustConfig, Path file) { + if (inFipsJvm()) { + // When running on BC-FIPS, an invalid file format behaves like an empty file + assertEmptyFile(trustConfig, file); + return; + } + final SslConfigException exception = expectThrows(SslConfigException.class, trustConfig::createTrustManager); + assertThat(exception.getMessage(), Matchers.containsString(file.toAbsolutePath().toString())); + assertThat(exception.getMessage(), Matchers.containsString("cannot create trust")); + assertThat(exception.getMessage(), Matchers.containsString("PEM")); + 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("PEM")); + assertThat(exception.getMessage(), Matchers.containsString(file.toAbsolutePath().toString())); + assertThat(exception.getCause(), Matchers.instanceOf(NoSuchFileException.class)); + } +} 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 new file mode 100644 index 0000000000000..60f0cd168ce1e --- /dev/null +++ b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/PemUtilsTests.java @@ -0,0 +1,219 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.ssl; + +import org.elasticsearch.test.ESTestCase; + +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.Key; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.util.function.Supplier; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.core.StringContains.containsString; + +public class PemUtilsTests extends ESTestCase { + + private static final Supplier EMPTY_PASSWORD = () -> new char[0]; + private static final Supplier TESTNODE_PASSWORD = "testnode"::toCharArray; + + 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); + assertThat(privateKey, notNullValue()); + assertThat(privateKey, equalTo(key)); + } + + 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); + assertThat(privateKey, notNullValue()); + assertThat(privateKey, equalTo(key)); + } + + 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); + assertThat(privateKey, notNullValue()); + assertThat(privateKey, equalTo(key)); + } + + 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); + assertThat(privateKey, notNullValue()); + assertThat(privateKey, equalTo(key)); + } + + public void testReadEncryptedPKCS8Key() throws Exception { + assumeFalse("Can't run in a FIPS JVM, PBE KeySpec is not available", inFipsJvm()); + Key key = getKeyFromKeystore("RSA"); + assertThat(key, notNullValue()); + assertThat(key, instanceOf(PrivateKey.class)); + PrivateKey privateKey = PemUtils.readPrivateKey(getDataPath + ("/certs/pem-utils/key_pkcs8_encrypted.pem"), TESTNODE_PASSWORD); + assertThat(privateKey, notNullValue()); + assertThat(privateKey, equalTo(key)); + } + + 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); + assertThat(privateKey, notNullValue()); + assertThat(privateKey, equalTo(key)); + } + + public void testReadAESEncryptedPKCS1Key() throws Exception { + Key key = getKeyFromKeystore("RSA"); + 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); + + assertThat(privateKey, notNullValue()); + assertThat(privateKey, equalTo(key)); + } + + 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); + + assertThat(privateKey, notNullValue()); + assertThat(privateKey, equalTo(key)); + } + + 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); + + assertThat(privateKey, notNullValue()); + assertThat(privateKey, equalTo(key)); + } + + 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"), + EMPTY_PASSWORD); + + assertThat(privateKey, notNullValue()); + assertThat(privateKey, equalTo(key)); + } + + 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); + + assertThat(privateKey, notNullValue()); + assertThat(privateKey, equalTo(key)); + } + + 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); + + assertThat(privateKey, notNullValue()); + assertThat(privateKey, equalTo(key)); + } + + 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"), + EMPTY_PASSWORD); + + assertThat(privateKey, notNullValue()); + assertThat(privateKey, equalTo(key)); + } + + 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); + + assertThat(privateKey, notNullValue()); + assertThat(privateKey, equalTo(key)); + } + + public void testReadUnsupportedKey() { + final Path path = getDataPath("/certs/pem-utils/key_unsupported.pem"); + SslConfigException e = expectThrows(SslConfigException.class, () -> PemUtils.readPrivateKey(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() { + final Path path = getDataPath("/certs/pem-utils/testnode.crt"); + SslConfigException e = expectThrows(SslConfigException.class, () -> PemUtils.readPrivateKey(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")); + } + + public void testReadEmptyFile() { + final Path path = getDataPath("/certs/pem-utils/empty.pem"); + SslConfigException e = expectThrows(SslConfigException.class, () -> PemUtils.readPrivateKey(path, TESTNODE_PASSWORD)); + assertThat(e.getMessage(), containsString("file is empty")); + assertThat(e.getMessage(), containsString(path.toAbsolutePath().toString())); + } + + private Key getKeyFromKeystore(String algo) throws Exception { + Path keystorePath = getDataPath("/certs/pem-utils/testnode.jks"); + try (InputStream in = Files.newInputStream(keystorePath)) { + KeyStore keyStore = KeyStore.getInstance("jks"); + keyStore.load(in, "testnode".toCharArray()); + return keyStore.getKey("testnode_" + algo, "testnode".toCharArray()); + } + } +} 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 new file mode 100644 index 0000000000000..20a161b78fd5f --- /dev/null +++ b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/SslConfigurationLoaderTests.java @@ -0,0 +1,220 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.ssl; + +import org.elasticsearch.common.settings.MockSecureSettings; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.ESTestCase; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManagerFactory; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +public class SslConfigurationLoaderTests extends ESTestCase { + + private final Path certRoot = getDataPath("/certs/ca1/ca.crt").getParent().getParent(); + + private Settings settings; + private MockSecureSettings secureSettings = new MockSecureSettings(); + private SslConfigurationLoader loader = new SslConfigurationLoader("test.ssl.") { + @Override + protected String getSettingAsString(String key) throws Exception { + return settings.get(key); + } + + @Override + protected char[] getSecureSetting(String key) throws Exception { + final SecureString secStr = secureSettings.getString(key); + return secStr == null ? null : secStr.getChars(); + } + + @Override + protected List getSettingAsList(String key) throws Exception { + return settings.getAsList(key); + } + }; + + /** + * A test for non-trust, non-key configurations. + * These are straight forward and can all be tested together + */ + public void testBasicConfigurationOptions() { + final SslVerificationMode verificationMode = randomFrom(SslVerificationMode.values()); + final SslClientAuthenticationMode clientAuth = randomFrom(SslClientAuthenticationMode.values()); + final String[] ciphers = generateRandomStringArray(8, 12, false, false); + final String[] protocols = generateRandomStringArray(4, 5, false, false); + settings = Settings.builder() + .put("test.ssl.verification_mode", verificationMode.name().toLowerCase(Locale.ROOT)) + .put("test.ssl.client_authentication", clientAuth.name().toLowerCase(Locale.ROOT)) + .putList("test.ssl.cipher_suites", ciphers) + .putList("test.ssl.supported_protocols", protocols) + .build(); + final SslConfiguration configuration = loader.load(certRoot); + assertThat(configuration.getClientAuth(), is(clientAuth)); + assertThat(configuration.getVerificationMode(), is(verificationMode)); + assertThat(configuration.getCipherSuites(), equalTo(Arrays.asList(ciphers))); + assertThat(configuration.getSupportedProtocols(), equalTo(Arrays.asList(protocols))); + if (verificationMode == SslVerificationMode.NONE) { + final SslTrustConfig trustConfig = configuration.getTrustConfig(); + assertThat(trustConfig, instanceOf(TrustEverythingConfig.class)); + } + } + + public void testLoadTrustFromPemCAs() { + settings = Settings.builder() + .putList("test.ssl.certificate_authorities", "ca1/ca.crt", "ca2/ca.crt", "ca3/ca.crt") + .build(); + final SslConfiguration configuration = loader.load(certRoot); + final SslTrustConfig trustConfig = configuration.getTrustConfig(); + assertThat(trustConfig, instanceOf(PemTrustConfig.class)); + assertThat(trustConfig.getDependentFiles(), + containsInAnyOrder(getDataPath("/certs/ca1/ca.crt"), getDataPath("/certs/ca2/ca.crt"), getDataPath("/certs/ca3/ca.crt"))); + assertThat(trustConfig.createTrustManager(), notNullValue()); + } + + public void testLoadTrustFromPkcs12() { + final Settings.Builder builder = Settings.builder().put("test.ssl.truststore.path", "ca-all/ca.p12"); + if (randomBoolean()) { + builder.put("test.ssl.truststore.password", "p12-pass"); + } else { + secureSettings.setString("test.ssl.truststore.secure_password", "p12-pass"); + } + if (randomBoolean()) { + // If this is not set, the loader will guess from the extension + builder.put("test.ssl.truststore.type", "PKCS12"); + } + if (randomBoolean()) { + builder.put("test.ssl.truststore.algorithm", TrustManagerFactory.getDefaultAlgorithm()); + } + settings = builder.build(); + final SslConfiguration configuration = loader.load(certRoot); + final SslTrustConfig trustConfig = configuration.getTrustConfig(); + assertThat(trustConfig, instanceOf(StoreTrustConfig.class)); + assertThat(trustConfig.getDependentFiles(), containsInAnyOrder(getDataPath("/certs/ca-all/ca.p12"))); + assertThat(trustConfig.createTrustManager(), notNullValue()); + } + + public void testLoadTrustFromJKS() { + final Settings.Builder builder = Settings.builder().put("test.ssl.truststore.path", "ca-all/ca.jks"); + if (randomBoolean()) { + builder.put("test.ssl.truststore.password", "jks-pass"); + } else { + secureSettings.setString("test.ssl.truststore.secure_password", "jks-pass"); + } + if (randomBoolean()) { + // If this is not set, the loader will guess from the extension + builder.put("test.ssl.truststore.type", "jks"); + } + if (randomBoolean()) { + builder.put("test.ssl.truststore.algorithm", TrustManagerFactory.getDefaultAlgorithm()); + } + settings = builder.build(); + final SslConfiguration configuration = loader.load(certRoot); + final SslTrustConfig trustConfig = configuration.getTrustConfig(); + assertThat(trustConfig, instanceOf(StoreTrustConfig.class)); + assertThat(trustConfig.getDependentFiles(), containsInAnyOrder(getDataPath("/certs/ca-all/ca.jks"))); + assertThat(trustConfig.createTrustManager(), notNullValue()); + } + + public void testLoadKeysFromPemFiles() { + final boolean usePassword = randomBoolean(); + final boolean useLegacyPassword = usePassword && randomBoolean(); + final String certName = usePassword ? "cert2" : "cert1"; + final Settings.Builder builder = Settings.builder() + .put("test.ssl.certificate", certName + "/" + certName + ".crt") + .put("test.ssl.key", certName + "/" + certName + ".key"); + if (usePassword) { + if (useLegacyPassword) { + builder.put("test.ssl.key_passphrase", "c2-pass"); + } else { + secureSettings.setString("test.ssl.secure_key_passphrase", "c2-pass"); + } + } + settings = builder.build(); + final SslConfiguration configuration = loader.load(certRoot); + final SslKeyConfig keyConfig = configuration.getKeyConfig(); + assertThat(keyConfig, instanceOf(PemKeyConfig.class)); + assertThat(keyConfig.getDependentFiles(), containsInAnyOrder( + getDataPath("/certs/" + certName + "/" + certName + ".crt"), getDataPath("/certs/" + certName + "/" + certName + ".key"))); + assertThat(keyConfig.createKeyManager(), notNullValue()); + } + + public void testLoadKeysFromPKCS12() { + final Settings.Builder builder = Settings.builder() + .put("test.ssl.keystore.path", "cert-all/certs.p12"); + if (randomBoolean()) { + builder.put("test.ssl.keystore.password", "p12-pass"); + } else { + secureSettings.setString("test.ssl.keystore.secure_password", "p12-pass"); + } + if (randomBoolean()) { + // If this is not set, the loader will guess from the extension + builder.put("test.ssl.keystore.type", "PKCS12"); + } + if (randomBoolean()) { + builder.put("test.ssl.keystore.algorithm", KeyManagerFactory.getDefaultAlgorithm()); + } + settings = builder.build(); + final SslConfiguration configuration = loader.load(certRoot); + final SslKeyConfig keyConfig = configuration.getKeyConfig(); + assertThat(keyConfig, instanceOf(StoreKeyConfig.class)); + assertThat(keyConfig.getDependentFiles(), containsInAnyOrder(getDataPath("/certs/cert-all/certs.p12"))); + assertThat(keyConfig.createKeyManager(), notNullValue()); + } + + public void testLoadKeysFromJKS() { + assumeFalse("Can't use JKS/PKCS12 keystores in a FIPS JVM", inFipsJvm()); + final Settings.Builder builder = Settings.builder() + .put("test.ssl.keystore.path", "cert-all/certs.jks"); + if (randomBoolean()) { + builder.put("test.ssl.keystore.password", "jks-pass"); + } else { + secureSettings.setString("test.ssl.keystore.secure_password", "jks-pass"); + } + if (randomBoolean()) { + builder.put("test.ssl.keystore.key_password", "key-pass"); + } else { + secureSettings.setString("test.ssl.keystore.secure_key_password", "key-pass"); + } + if (randomBoolean()) { + // If this is not set, the loader will guess from the extension + builder.put("test.ssl.keystore.type", "jks"); + } + if (randomBoolean()) { + builder.put("test.ssl.keystore.algorithm", KeyManagerFactory.getDefaultAlgorithm()); + } + settings = builder.build(); + final SslConfiguration configuration = loader.load(certRoot); + final SslKeyConfig keyConfig = configuration.getKeyConfig(); + assertThat(keyConfig, instanceOf(StoreKeyConfig.class)); + assertThat(keyConfig.getDependentFiles(), containsInAnyOrder(getDataPath("/certs/cert-all/certs.jks"))); + assertThat(keyConfig.createKeyManager(), notNullValue()); + } +} 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 new file mode 100644 index 0000000000000..b8986462ebe47 --- /dev/null +++ b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/SslConfigurationTests.java @@ -0,0 +1,140 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.ssl; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.EqualsHashCodeTestUtils; +import org.hamcrest.Matchers; +import org.mockito.Mockito; + +import javax.net.ssl.SSLContext; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.elasticsearch.common.ssl.SslConfigurationLoader.DEFAULT_CIPHERS; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +public class SslConfigurationTests extends ESTestCase { + + static final String[] VALID_PROTOCOLS = { "TLSv1.2", "TLSv1.1", "TLSv1", "SSLv3", "SSLv2Hello", "SSLv2" }; + + public void testBasicConstruction() { + final SslTrustConfig trustConfig = Mockito.mock(SslTrustConfig.class); + Mockito.when(trustConfig.toString()).thenReturn("TEST-TRUST"); + final SslKeyConfig keyConfig = Mockito.mock(SslKeyConfig.class); + Mockito.when(keyConfig.toString()).thenReturn("TEST-KEY"); + final SslVerificationMode verificationMode = randomFrom(SslVerificationMode.values()); + final SslClientAuthenticationMode clientAuth = randomFrom(SslClientAuthenticationMode.values()); + 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); + + assertThat(configuration.getTrustConfig(), is(trustConfig)); + assertThat(configuration.getKeyConfig(), is(keyConfig)); + assertThat(configuration.getVerificationMode(), is(verificationMode)); + assertThat(configuration.getClientAuth(), is(clientAuth)); + assertThat(configuration.getCipherSuites(), is(ciphers)); + assertThat(configuration.getSupportedProtocols(), is(protocols)); + + assertThat(configuration.toString(), containsString("TEST-TRUST")); + assertThat(configuration.toString(), containsString("TEST-KEY")); + assertThat(configuration.toString(), containsString(verificationMode.toString())); + assertThat(configuration.toString(), containsString(clientAuth.toString())); + assertThat(configuration.toString(), containsString(randomFrom(ciphers))); + assertThat(configuration.toString(), containsString(randomFrom(protocols))); + } + + public void testEqualsAndHashCode() { + final SslTrustConfig trustConfig = Mockito.mock(SslTrustConfig.class); + final SslKeyConfig keyConfig = Mockito.mock(SslKeyConfig.class); + final SslVerificationMode verificationMode = randomFrom(SslVerificationMode.values()); + final SslClientAuthenticationMode clientAuth = randomFrom(SslClientAuthenticationMode.values()); + 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); + + EqualsHashCodeTestUtils.checkEqualsAndHashCode(configuration, + orig -> new SslConfiguration(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(), + randomValueOtherThan(orig.getVerificationMode(), () -> randomFrom(SslVerificationMode.values())), + orig.getClientAuth(), orig.getCipherSuites(), orig.getSupportedProtocols()); + case 2: + return new SslConfiguration(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(), + orig.getVerificationMode(), orig.getClientAuth(), DEFAULT_CIPHERS, orig.getSupportedProtocols()); + case 4: + default: + return new SslConfiguration(orig.getTrustConfig(), orig.getKeyConfig(), orig.getVerificationMode(), + orig.getClientAuth(), orig.getCipherSuites(), Arrays.asList(VALID_PROTOCOLS)); + } + }); + } + + public void testDependentFiles() { + final SslTrustConfig trustConfig = Mockito.mock(SslTrustConfig.class); + final SslKeyConfig keyConfig = Mockito.mock(SslKeyConfig.class); + final SslConfiguration configuration = new SslConfiguration(trustConfig, keyConfig, + randomFrom(SslVerificationMode.values()), randomFrom(SslClientAuthenticationMode.values()), + DEFAULT_CIPHERS, SslConfigurationLoader.DEFAULT_PROTOCOLS); + + final Path dir = createTempDir(); + final Path file1 = dir.resolve(randomAlphaOfLength(1) + ".pem"); + final Path file2 = dir.resolve(randomAlphaOfLength(2) + ".pem"); + final Path file3 = dir.resolve(randomAlphaOfLength(3) + ".pem"); + final Path file4 = dir.resolve(randomAlphaOfLength(4) + ".pem"); + final Path file5 = dir.resolve(randomAlphaOfLength(5) + ".pem"); + + Mockito.when(trustConfig.getDependentFiles()).thenReturn(Arrays.asList(file1, file2)); + Mockito.when(keyConfig.getDependentFiles()).thenReturn(Arrays.asList(file3, file4, file5)); + assertThat(configuration.getDependentFiles(), Matchers.containsInAnyOrder(file1, file2, file3, file4, file5)); + } + + 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, + randomFrom(SslVerificationMode.values()), randomFrom(SslClientAuthenticationMode.values()), + DEFAULT_CIPHERS, Collections.singletonList(protocol)); + + Mockito.when(trustConfig.createTrustManager()).thenReturn(null); + Mockito.when(keyConfig.createKeyManager()).thenReturn(null); + final SSLContext sslContext = configuration.createSslContext(); + assertThat(sslContext.getProtocol(), equalTo(protocol)); + + Mockito.verify(trustConfig).createTrustManager(); + Mockito.verify(keyConfig).createKeyManager(); + Mockito.verifyNoMoreInteractions(trustConfig, keyConfig); + } + +} 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 new file mode 100644 index 0000000000000..f5fcc16c6a023 --- /dev/null +++ b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/StoreKeyConfigTests.java @@ -0,0 +1,215 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.ssl; + +import org.elasticsearch.test.ESTestCase; +import org.hamcrest.Matchers; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.X509ExtendedKeyManager; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +import static org.hamcrest.Matchers.arrayWithSize; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; +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 StoreKeyConfigTests extends ESTestCase { + + private static final int IP_NAME = 7; + private static final int DNS_NAME = 2; + + private static final char[] P12_PASS = "p12-pass".toCharArray(); + private static final char[] JKS_PASS = "jks-pass".toCharArray(); + + 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()); + assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(p12)); + assertKeysLoaded(keyConfig, "cert1"); + } + + 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()); + 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 StoreKeyConfig keyConfig = new StoreKeyConfig(jks, JKS_PASS, "jks", "key-pass".toCharArray(), + KeyManagerFactory.getDefaultAlgorithm()); + assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(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 StoreKeyConfig keyConfig = new StoreKeyConfig(jks, P12_PASS, "jks", "key-pass".toCharArray(), + KeyManagerFactory.getDefaultAlgorithm()); + assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(jks)); + assertPasswordIsIncorrect(keyConfig, jks); + } + + 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()); + assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(jks)); + assertPasswordIsIncorrect(keyConfig, jks); + } + + 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()); + assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(path)); + assertFileNotFound(keyConfig, path); + } + + public void testMissingKeyEntriesFailsWithMeaningfulMessage() throws Exception { + assumeFalse("Can't use JKS/PKCS12 keystores in a FIPS JVM", inFipsJvm()); + final Path ks; + final char[] password; + final String type; + if (randomBoolean()) { + type = "PKCS12"; + ks = getDataPath("/certs/ca-all/ca.p12"); + password = P12_PASS; + } else { + type = "jks"; + ks = getDataPath("/certs/ca-all/ca.jks"); + password = JKS_PASS; + } + final StoreKeyConfig keyConfig = new StoreKeyConfig(ks, password, type, password, KeyManagerFactory.getDefaultAlgorithm()); + assertThat(keyConfig.getDependentFiles(), Matchers.containsInAnyOrder(ks)); + assertNoPrivateKeyEntries(keyConfig, ks); + } + + public void testKeyConfigReloadsFileContents() throws Exception { + assumeFalse("Can't use JKS/PKCS12 keystores in a FIPS JVM", inFipsJvm()); + final Path cert1 = getDataPath("/certs/cert1/cert1.p12"); + final Path cert2 = getDataPath("/certs/cert2/cert2.p12"); + final Path jks = getDataPath("/certs/cert-all/certs.jks"); + + final Path p12 = createTempFile("cert", ".p12"); + + final StoreKeyConfig keyConfig = new StoreKeyConfig(p12, P12_PASS, "PKCS12", P12_PASS, KeyManagerFactory.getDefaultAlgorithm()); + + Files.copy(cert1, p12, StandardCopyOption.REPLACE_EXISTING); + assertKeysLoaded(keyConfig, "cert1"); + assertKeysNotLoaded(keyConfig, "cert2"); + + Files.copy(jks, p12, StandardCopyOption.REPLACE_EXISTING); + // Because (a) cannot load a JKS as a PKCS12 & (b) the password is wrong. + assertBadKeyStore(keyConfig, p12); + + Files.copy(cert2, p12, StandardCopyOption.REPLACE_EXISTING); + assertKeysLoaded(keyConfig, "cert2"); + assertKeysNotLoaded(keyConfig, "cert1"); + + Files.delete(p12); + assertFileNotFound(keyConfig, p12); + } + + private void assertKeysLoaded(StoreKeyConfig keyConfig, String... names) throws CertificateParsingException { + final X509ExtendedKeyManager keyManager = keyConfig.createKeyManager(); + assertThat(keyManager, notNullValue()); + + for (String name : names) { + final PrivateKey privateKey = keyManager.getPrivateKey(name); + assertThat(privateKey, notNullValue()); + assertThat(privateKey.getAlgorithm(), is("RSA")); + + final X509Certificate[] chain = keyManager.getCertificateChain(name); + assertThat(chain, notNullValue()); + assertThat(chain, arrayWithSize(1)); + final X509Certificate certificate = chain[0]; + assertThat(certificate.getIssuerDN().getName(), is("CN=Test CA 1")); + assertThat(certificate.getSubjectDN().getName(), is("CN=" + name)); + assertThat(certificate.getSubjectAlternativeNames(), iterableWithSize(2)); + assertThat(certificate.getSubjectAlternativeNames(), containsInAnyOrder( + Arrays.asList(DNS_NAME, "localhost"), + Arrays.asList(IP_NAME, "127.0.0.1") + )); + } + } + + private void assertKeysNotLoaded(StoreKeyConfig keyConfig, String... names) throws CertificateParsingException { + final X509ExtendedKeyManager keyManager = keyConfig.createKeyManager(); + assertThat(keyManager, notNullValue()); + + for (String name : names) { + final PrivateKey privateKey = keyManager.getPrivateKey(name); + assertThat(privateKey, nullValue()); + } + } + + private void assertPasswordIsIncorrect(StoreKeyConfig keyConfig, Path key) { + final SslConfigException exception = expectThrows(SslConfigException.class, keyConfig::createKeyManager); + assertThat(exception.getMessage(), containsString("keystore")); + assertThat(exception.getMessage(), containsString(key.toAbsolutePath().toString())); + if (exception.getCause() instanceof GeneralSecurityException) { + assertThat(exception.getMessage(), containsString("password")); + } else { + assertThat(exception.getCause(), instanceOf(IOException.class)); + assertThat(exception.getCause().getMessage(), containsString("password")); + } + } + + private void assertBadKeyStore(StoreKeyConfig keyConfig, Path key) { + final SslConfigException exception = expectThrows(SslConfigException.class, keyConfig::createKeyManager); + assertThat(exception.getMessage(), containsString("keystore")); + assertThat(exception.getMessage(), containsString(key.toAbsolutePath().toString())); + assertThat(exception.getCause(), instanceOf(IOException.class)); + } + + private void assertFileNotFound(StoreKeyConfig keyConfig, Path file) { + final SslConfigException exception = expectThrows(SslConfigException.class, keyConfig::createKeyManager); + assertThat(exception.getMessage(), containsString("keystore")); + assertThat(exception.getMessage(), containsString(file.toAbsolutePath().toString())); + assertThat(exception.getMessage(), containsString("does not exist")); + assertThat(exception.getCause(), nullValue()); + } + + private void assertNoPrivateKeyEntries(StoreKeyConfig keyConfig, Path file) { + final SslConfigException exception = expectThrows(SslConfigException.class, keyConfig::createKeyManager); + assertThat(exception.getMessage(), containsString("keystore")); + assertThat(exception.getMessage(), containsString(file.toAbsolutePath().toString())); + assertThat(exception.getMessage(), containsString("does not contain a private key entry")); + } +} 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 new file mode 100644 index 0000000000000..207bf9179415f --- /dev/null +++ b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/StoreTrustConfigTests.java @@ -0,0 +1,169 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.ssl; + +import org.elasticsearch.test.ESTestCase; +import org.hamcrest.Matchers; + +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedTrustManager; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.security.Principal; +import java.security.cert.X509Certificate; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.nullValue; + +public class StoreTrustConfigTests extends ESTestCase { + + private static final char[] P12_PASS = "p12-pass".toCharArray(); + private static final char[] JKS_PASS = "jks-pass".toCharArray(); + private static final String DEFAULT_ALGORITHM = TrustManagerFactory.getDefaultAlgorithm(); + + 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)); + 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)); + assertCertificateChain(trustConfig, "CN=Test CA 1", "CN=Test CA 2", "CN=Test CA 3"); + } + + 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); + 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); + } + + public void testIncorrectPasswordFailsWithMeaningfulMessage() throws Exception { + 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); + } + + public void testMissingTrustEntriesFailsWithMeaningfulMessage() throws Exception { + assumeFalse("Can't use JKS/PKCS12 keystores in a FIPS JVM", inFipsJvm()); + final Path ks; + final char[] password; + final String type; + if (randomBoolean()) { + type = "PKCS12"; + ks = getDataPath("/certs/cert-all/certs.p12"); + password = P12_PASS; + } else { + type = "jks"; + ks = getDataPath("/certs/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); + } + + public void testTrustConfigReloadsKeysStoreContents() throws Exception { + assumeFalse("Can't use JKS/PKCS12 keystores in a FIPS JVM", inFipsJvm()); + final Path ks1 = getDataPath("/certs/ca1/ca.p12"); + final Path ksAll = getDataPath("/certs/ca-all/ca.p12"); + + final Path ks = createTempFile("ca", "p12"); + + final StoreTrustConfig trustConfig = new StoreTrustConfig(ks, P12_PASS, "PKCS12", DEFAULT_ALGORITHM); + + Files.copy(ks1, ks, StandardCopyOption.REPLACE_EXISTING); + assertCertificateChain(trustConfig, "CN=Test CA 1"); + + Files.delete(ks); + assertFileNotFound(trustConfig, ks); + + Files.write(ks, randomByteArrayOfLength(128), StandardOpenOption.CREATE); + assertInvalidFileFormat(trustConfig, ks); + + Files.copy(ksAll, ks, StandardCopyOption.REPLACE_EXISTING); + assertCertificateChain(trustConfig, "CN=Test CA 1", "CN=Test CA 2", "CN=Test CA 3"); + } + + private void assertCertificateChain(StoreTrustConfig trustConfig, String... caNames) { + final X509ExtendedTrustManager trustManager = trustConfig.createTrustManager(); + final X509Certificate[] issuers = trustManager.getAcceptedIssuers(); + final Set issuerNames = Stream.of(issuers) + .map(X509Certificate::getSubjectDN) + .map(Principal::getName) + .collect(Collectors.toSet()); + + assertThat(issuerNames, Matchers.containsInAnyOrder(caNames)); + } + + 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.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()); + } + + private void assertPasswordIsIncorrect(StoreTrustConfig trustConfig, Path key) { + final SslConfigException exception = expectThrows(SslConfigException.class, trustConfig::createTrustManager); + assertThat(exception.getMessage(), containsString("keystore")); + assertThat(exception.getMessage(), containsString(key.toAbsolutePath().toString())); + assertThat(exception.getMessage(), containsString("password")); + } + + 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())); + } + +} diff --git a/libs/ssl-config/src/test/resources/certs/README.txt b/libs/ssl-config/src/test/resources/certs/README.txt new file mode 100644 index 0000000000000..a04a31011b4dd --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/README.txt @@ -0,0 +1,75 @@ +#!/bin/bash +# +# This is README describes how the certificates in this directory were created. +# This file can also be executed as a script +# + +# 1. Create first CA PEM ("ca1") + +elasticsearch-certutil ca --pem --out ca1.zip --days 9999 --ca-dn "CN=Test CA 1" +unzip ca1.zip +mv ca ca1 + +# 2. Create first CA PEM ("ca2") + +elasticsearch-certutil ca --pem --out ca2.zip --days 9999 --ca-dn "CN=Test CA 2" +unzip ca2.zip +mv ca ca2 + +# 3. Create first CA PEM ("ca3") + +elasticsearch-certutil ca --pem --out ca3.zip --days 9999 --ca-dn "CN=Test CA 3" +unzip ca3.zip +mv ca ca3 + +# 4. Create "cert1" PEM + +elasticsearch-certutil cert --pem --out cert1.zip --name cert1 --ip 127.0.0.1 --dns localhost --days 9999 --ca-key ca1/ca.key --ca-cert ca1/ca.crt +unzip cert1.zip + +# 5. Create "cert2" PEM (same as cert1, but with a password) + +elasticsearch-certutil cert --pem --out cert2.zip --name cert2 --ip 127.0.0.1 --dns localhost --days 9999 --ca-key ca1/ca.key --ca-cert ca1/ca.crt --pass "c2-pass" +unzip cert2.zip + +# 6. Convert CAs to PKCS#12 + +for n in 1 2 3 +do + keytool -importcert -file ca${n}/ca.crt -alias ca -keystore ca${n}/ca.p12 -storetype PKCS12 -storepass p12-pass -v + keytool -importcert -file ca${n}/ca.crt -alias ca${n} -keystore ca-all/ca.p12 -storetype PKCS12 -storepass p12-pass -v +done + +# 7. Convert CAs to JKS + +for n in 1 2 3 +do + keytool -importcert -file ca${n}/ca.crt -alias ca${n} -keystore ca-all/ca.jks -storetype jks -storepass jks-pass -v +done + +# 8. Convert Certs to PKCS#12 + +for Cert in cert1 cert2 +do + openssl pkcs12 -export -out $Cert/$Cert.p12 -inkey $Cert/$Cert.key -in $Cert/$Cert.crt -name $Cert -passout pass:p12-pass +done + +# 9. Import Certs into single PKCS#12 keystore + +for Cert in cert1 cert2 +do + keytool -importkeystore -noprompt \ + -srckeystore $Cert/$Cert.p12 -srcstoretype PKCS12 -srcstorepass p12-pass \ + -destkeystore cert-all/certs.p12 -deststoretype PKCS12 -deststorepass p12-pass +done + +# 10. Import Certs into single JKS keystore with separate key-password + +for Cert in cert1 cert2 +do + keytool -importkeystore -noprompt \ + -srckeystore $Cert/$Cert.p12 -srcstoretype PKCS12 -srcstorepass p12-pass \ + -destkeystore cert-all/certs.jks -deststoretype jks -deststorepass jks-pass + keytool -keypasswd -keystore cert-all/certs.jks -alias $Cert -keypass p12-pass -new key-pass -storepass jks-pass +done + diff --git a/libs/ssl-config/src/test/resources/certs/ca-all/ca.jks b/libs/ssl-config/src/test/resources/certs/ca-all/ca.jks new file mode 100644 index 0000000000000..0c00ff13d906f Binary files /dev/null and b/libs/ssl-config/src/test/resources/certs/ca-all/ca.jks differ diff --git a/libs/ssl-config/src/test/resources/certs/ca-all/ca.p12 b/libs/ssl-config/src/test/resources/certs/ca-all/ca.p12 new file mode 100644 index 0000000000000..6f13fc1a92747 Binary files /dev/null and b/libs/ssl-config/src/test/resources/certs/ca-all/ca.p12 differ diff --git a/libs/ssl-config/src/test/resources/certs/ca1/ca.crt b/libs/ssl-config/src/test/resources/certs/ca1/ca.crt new file mode 100644 index 0000000000000..08e351d886c2f --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/ca1/ca.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIUZ0xcthORO/ye5P1Ia/IarOGvwHYwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAxMJVGVzdCBDQSAxMB4XDTE5MDEwMzA3MzgyNloXDTQ2MDUy +MDA3MzgyNlowFDESMBAGA1UEAxMJVGVzdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEArrEcyCaTpx0JCZdNAhb/nGROBRNPl2QdKuFM1pLRMoKl +1XAMYRy88B1jSuteP18O+pk83F6jV7byYyp8f616nTHqoFfzl4rN/iM02b63/oQd +qSSn4oPyiPfsS4I49taSJnH+8slbNg9iyiwoFnywcVaj1X9t+DAQXDFxNczpuIiq +Q8apxoORiJz4U/sC9dNOV4y8DnB0Vi6Ypb3npMvt/H3mvHC6tRuibVNSh2oBSa3o +gA1+ovxmGXavxfX2uquE+R4umgTr3HiHBviFvw2o1EPc9wHbR0iuyUtym3REIFko +VmdzIanZEtdk3HyHa7wggP9zMWfETVZuurJ64VhL0wIDAQABo1MwUTAdBgNVHQ4E +FgQUh3fjY8KpBOoVTBJ5bcenE/g9dZcwHwYDVR0jBBgwFoAUh3fjY8KpBOoVTBJ5 +bcenE/g9dZcwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAWTDz +r1a7K41KefdsjMM75z6Vxm5nXeGDItmUWaerVVUmRkeln+bbY0uReoHELuTA76uR +TMt9fOAmXpmfhbssRKv9TffOg5nb5IAvjDDRkCIXCJvBHcNLuYsTkjC7beuQEfSg +3ayoRWfaF4EliTk96pEsnGNz0szWkhx/oWvZ8pvY/HA80xxiAbIFDgh4MJhwiCeh +1PwbvUx+i6VYfM/6eIGk1WIY5wJR56Dj8afVsOED+hn2Rs5oWFXY0Eu6XNHJfAFg +RyaTrL3+X9SK08yUJwQMFnW/k4IUKWi3JyWb3PwGOqIjUXIfNH/WkwMZSErkyNln +mHWm8kQbx9OLpi5BlQ== +-----END CERTIFICATE----- diff --git a/libs/ssl-config/src/test/resources/certs/ca1/ca.key b/libs/ssl-config/src/test/resources/certs/ca1/ca.key new file mode 100644 index 0000000000000..990ad2d005d34 --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/ca1/ca.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEArrEcyCaTpx0JCZdNAhb/nGROBRNPl2QdKuFM1pLRMoKl1XAM +YRy88B1jSuteP18O+pk83F6jV7byYyp8f616nTHqoFfzl4rN/iM02b63/oQdqSSn +4oPyiPfsS4I49taSJnH+8slbNg9iyiwoFnywcVaj1X9t+DAQXDFxNczpuIiqQ8ap +xoORiJz4U/sC9dNOV4y8DnB0Vi6Ypb3npMvt/H3mvHC6tRuibVNSh2oBSa3ogA1+ +ovxmGXavxfX2uquE+R4umgTr3HiHBviFvw2o1EPc9wHbR0iuyUtym3REIFkoVmdz +IanZEtdk3HyHa7wggP9zMWfETVZuurJ64VhL0wIDAQABAoIBAG6T1fAr2xLRIkNb +7ncAL9TC+U/lJWBjEsNt0cGRNbKPWIF+Z5ehJUeoko195y6d8VFXZlrn3OVM/Kkg +36XCHfca/bV5dsvaJQJVLsMWIkmNP2kttsd/Viq1JHG3gG9e6yxCxGrSYlYZ7yKi +SM3TJ6zWduZRvz52ziRNd6fiiZ8wc/wwWMXvXoMJ39EhGdZKKTowvKU59oV4ieg8 +vEK1V2ec2D6RN6m0CHzm4erKZSIJoMJAhKV1D42+z73XhJuv/gbIlUlXrG1l4XW9 +Sd59dU9v4NGCv6BpqQqC//fVwW7KfTGwmMtL7AP5vria/dzf9EyZxPk+HLgC6bHK +Y3TU2WECgYEA9x0A/aB/4CepYAGUxIlc8r+ylhYTWmUafjvBJ8fPQs2PQi5JgE2s +HE4yRcnPIZPaDMYDUPluXoSkirX5jYqQAVeOhTut8tTwV1FMf57P470FPVMgV5X6 +axsiMBIcRvYAc1cq7n7k45Ix8YNKfp5WjG1r7XLR9Xa5Q0Bno9WxII8CgYEAtPlg +NuJnSnrka9jZQQTvzp6ULP448MWdjHmYmj93lUIC9XL3hkPqu+cZjZT5C2xf/36w +5wEAHSNVO8SUjJ1bKgjyfyvaxosonbrDv3TWl1Ib8NFmumXjMzCw6LfDmrJbZc8X +VA7VG1HClgPmojDc61s/F7unRbRUcIowP1dVOn0CgYA/u/xQbf/tSW129JFxK1iM +x4KBEUqGiwMNQc4su20qdqgXUqbkb6QPXN+8fjNtHpwjpUKftOWRfTaPDCZEKlO/ +9NwuYtkXg3JFoxNO6yAFRfA/A9yYmncO/t2PdmxSpQoytW2+O34/b6pv9wPUqnP6 +HhKzGGUsoSVhQhA5All/4wKBgA9Nv0sk3iM4PTS5g7Wx2y2Xz2P2o44IyAfnCHaS +w2QFzwY+kJv0BleZdVm5rU2//mY2qnL+bKoKIN0LBJzXeawWUZtbdAayId8kugTo +tnTZZq94pb1BfHMJvQwQ7iOYzY3Qc2KSVocW5OOWtNwmUag9cRpqrfyBAVr69JWG +pxhpAoGAUYkzE88ay83byRR+I/bVLDyI/OLs9mSfPJ1BbntDnmzv/ReUJCr9cZvo +18/iQyhICA1IO+V0JgQPRY7cz44cBTb9QiQJvtRtFyyKLBC9roYZ7Y8CfTAcX73H +7yY5UdS82r/Quu2Kp9EUbrTqmev7h8k5/kjXkvIdLv7soLMGJh8= +-----END RSA PRIVATE KEY----- diff --git a/libs/ssl-config/src/test/resources/certs/ca1/ca.p12 b/libs/ssl-config/src/test/resources/certs/ca1/ca.p12 new file mode 100644 index 0000000000000..0fd23865b718d Binary files /dev/null and b/libs/ssl-config/src/test/resources/certs/ca1/ca.p12 differ diff --git a/libs/ssl-config/src/test/resources/certs/ca2/ca.crt b/libs/ssl-config/src/test/resources/certs/ca2/ca.crt new file mode 100644 index 0000000000000..1abadd52667e5 --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/ca2/ca.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIUdWLBV2ebmR6Ktf9mWqnlKvVwG7QwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAxMJVGVzdCBDQSAyMB4XDTE5MDEwMzA3Mzk1MloXDTQ2MDUy +MDA3Mzk1MlowFDESMBAGA1UEAxMJVGVzdCBDQSAyMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAnuHEkW7zlFqUYTDEWmrEoKaEc4ERcyGklDdJiOEUvqiy +fo2DPjkQp9+Ix6bkQQSog9cPfYr3+Fp+LMpS5f59PtXLl6lIBjQhcRc+Bh82QLMG +h1yXBbxLmbl0sDfkkv+Y+T17wowN4mWhrwmXYTaAV633Nx72h3Q3NJfeeWpJkPv9 +8x88pRRejxfjTisc/X7CsYjmFMrbMemmMQ4Xk3Uhu2PBv4Ln+69oyADHHVQoKPJy +g2l2PrZGRvxdqlf5iLH8oaxLy83tAbdbwu0CeopIG0ET9XtbsHK7IKxwsCWSy/+J +qvV0Gl9urQexyYRkF7AN4Jy53w3Qvrj4RtBxdkneDQIDAQABo1MwUTAdBgNVHQ4E +FgQUTqJ154oBVLiPoWsZEq5O0R6cWr4wHwYDVR0jBBgwFoAUTqJ154oBVLiPoWsZ +Eq5O0R6cWr4wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAHvHI +Dkhev16VcAWJTT4Ti8oJNClfneehkAMXZhRegBYUNbD+4KbURcz8TVSybMb86nZV +/Lkr8gffYXYZb2ibElvNv0gprutzplCpYCNg/iu8bvSw86dr62s9IKIkAtlDkxzz +jlVKhoUUTKngBNT1CVPMYwJyBwUDpa346DxcQ636b0QU9lQPikYqORaj/9IOc5IX +MriFFpX0gC9U8y+gvGdmFmSkufGRZlJS75/fJfVxpB4Qm9FNoGkJy+wZDbw4TfIO +mZhXbAycIUMsYJ8BjUKhlqXWqGC/6afVRHKekSsAbXo6AGk9ty8i6itj/aeUB33w +BqPROIFyhhdUAZSHyg== +-----END CERTIFICATE----- diff --git a/libs/ssl-config/src/test/resources/certs/ca2/ca.key b/libs/ssl-config/src/test/resources/certs/ca2/ca.key new file mode 100644 index 0000000000000..7616591a75ad5 --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/ca2/ca.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAnuHEkW7zlFqUYTDEWmrEoKaEc4ERcyGklDdJiOEUvqiyfo2D +PjkQp9+Ix6bkQQSog9cPfYr3+Fp+LMpS5f59PtXLl6lIBjQhcRc+Bh82QLMGh1yX +BbxLmbl0sDfkkv+Y+T17wowN4mWhrwmXYTaAV633Nx72h3Q3NJfeeWpJkPv98x88 +pRRejxfjTisc/X7CsYjmFMrbMemmMQ4Xk3Uhu2PBv4Ln+69oyADHHVQoKPJyg2l2 +PrZGRvxdqlf5iLH8oaxLy83tAbdbwu0CeopIG0ET9XtbsHK7IKxwsCWSy/+JqvV0 +Gl9urQexyYRkF7AN4Jy53w3Qvrj4RtBxdkneDQIDAQABAoIBABppr/L5ffboxAgQ +QmRBoaSPai+FgnAgZKrbMhdWS8uSYfIV9n6OoA04ZRXD0ehZLOaWBxY41xZrfNRX +Ykan8wxSIIF6++VEH1ccpQwBflRtLqWsJ9MlRXAt2488C3zAjx7IMN3byKcdfC6M +KqVXmSh6XEHGnPdRw6ezo6GNoONALVXKVZkeHKeMpjfThoA/ydGTpG72AxDY/EW+ +RLyHniKAm90VddPrjnWwgCKL8nG8avdznqS2hGKka1o5a32waYCFb13rZi3IyhnR +Lu9oVhbPDENkhK4R4KfG4baus24y5HGihB4FdzbemBK7zqrAvbxR3FuIZRJNmh0v +tTHaKCECgYEA6vPg+mx3zXwip/wlIyeD6F/4SMR3G+m77UoqswXQcjnKCljsBDDg +kLCnVXjmzY6bryCMbAiecZrNVDiqGGgwi/Cj2xjTgH9YS7VMGgWvsCtRBrdLB1zR +X7EdkOtcbVFByKU/U2WHc1piHYxwTTWtDaDJKCp/CnL/veJb7xJQoRUCgYEArR1f +x6BnnBokERXIc/qI0ff1vtRwvewju7RVXZRU5cgS89GcV9vJjGDHS+2wXhyEAIFH +PoICF/zgiJJAXdK1k1noUotMGOEKo//sUNb+mk6anWkHa10KHQMnW7PDnzhIuUeH +C6n5b5oJBRBFwK7BULmk+27A/zx1SdW9CSH8VxkCgYEAybrn+lxTaN0irHU0NcDh +4w0zktcNJaxELPM3QkrFtK2lqci7rMWCqvjiU+Lg2LGPPoiFyOSFlilCDwQwF5Ct +zhmptp7USkoMt8RMOTOUq4Alq8yI4SNyqeTa6+kJjNrtzqcDfkl4STTbdV91tPVX +RpI85P3H4mLm7lSCdvyUuhkCgYAHNsEmBXYr2B8Gozy+MIOBFG8mK54jG/MFQGeK +RcMf7C12AZcdRiho9CN584a09UU+7CQ2454It933cvjBsCUm5ck7n1hldQNHgEOt +vrfPYFUrGBRaEf945AfA14XgXa0SI3vqLYQadXXIwzvU4rNllMbeP2hFepR8pi6B +cewdCQKBgGkc5J/oWnxMcamlMP/W7Vk7YxsPiEO6KMx6m9b1VY0G5ACHEkTP+Glp +uDgLw5Me/dLG+hvY9NV9GPG76boRBGhFbriPpVARa49rY0ShTDPmz+SzqZ0L/N09 +msH+f0K9O+KvXjw9h7lHF/vg/exrRDS6my5bgmj/UtIHwgmcSUdm +-----END RSA PRIVATE KEY----- diff --git a/libs/ssl-config/src/test/resources/certs/ca2/ca.p12 b/libs/ssl-config/src/test/resources/certs/ca2/ca.p12 new file mode 100644 index 0000000000000..fdd374531870c Binary files /dev/null and b/libs/ssl-config/src/test/resources/certs/ca2/ca.p12 differ diff --git a/libs/ssl-config/src/test/resources/certs/ca3/ca.crt b/libs/ssl-config/src/test/resources/certs/ca3/ca.crt new file mode 100644 index 0000000000000..3f408375378d4 --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/ca3/ca.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCjCCAfKgAwIBAgIVAJMN07EKmD5RR6PmEZDjqSWBpRY9MA0GCSqGSIb3DQEB +CwUAMBQxEjAQBgNVBAMTCVRlc3QgQ0EgMzAeFw0xOTAxMDMwNzQwMTlaFw00NjA1 +MjAwNzQwMTlaMBQxEjAQBgNVBAMTCVRlc3QgQ0EgMzCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAJEPRYQiGOmfaHdOiCWPqzWgmHgbdw32yaQFKUOqqAQn +OhkujPg2jJdqMjp7aFrZjLjVi2WyHdAPe3ibuUHT6PyTmcgtDxe+xAUJCG6vJKpy +vrDNXVq9rotts+gEKeRbhr0lw2lppDhNNGiKkRm7R2sNRTyIW0vBNujicSWcli2H +hQDjFWornjcd8OBFLvhY3tSwic8uwuSAYgrZfWHOgBWSC1xqUFHa6561K264yeNZ +8cyb41euYAsohdQ/VPPFu3ISEYlZwTwwp/e2l8IpgKpB59Mrdc1TonpD5h3XMiW8 +iPJE0eZ9KJSv1SzEty2nno4s7LAu6cvwXQD2RP2vY4cCAwEAAaNTMFEwHQYDVR0O +BBYEFAQZfDT971m21ReFP+rO5pTFrLLIMB8GA1UdIwQYMBaAFAQZfDT971m21ReF +P+rO5pTFrLLIMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAEHK +JublAXM3FprLQRU+MAIQyZp4VpSanxL/JE6UkGv3P0aQUQZt7BR+uv7xWc1Ygrk9 +gOK7pZh9vXZSUc6NFEa05O9lXUvYy9UN+iFxlWPe3niPHt8lW/6oUqMfnE2ePIG6 +Iaoaks29qwcbKYnl4ZcurdbcCfCoc7GAQYE1zKO4fJqnRogFaoPEwHJ5+5nmd4Xl +8TP/v4ISHzB/cyCIiA9O8EssRXLBxwd7zgY0kicBKgy3/Rtd/HfGLunZdBBun6Hk +T5L+SVLGLIoFFahA7NQUQPKzrx8KMgh6SiskWblc8pk4/Q1Z2q9E1L69z4SDl1oq +HDYgZBcLjn4QXONgz8Y= +-----END CERTIFICATE----- diff --git a/libs/ssl-config/src/test/resources/certs/ca3/ca.key b/libs/ssl-config/src/test/resources/certs/ca3/ca.key new file mode 100644 index 0000000000000..3626d71ba2032 --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/ca3/ca.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAkQ9FhCIY6Z9od06IJY+rNaCYeBt3DfbJpAUpQ6qoBCc6GS6M ++DaMl2oyOntoWtmMuNWLZbId0A97eJu5QdPo/JOZyC0PF77EBQkIbq8kqnK+sM1d +Wr2ui22z6AQp5FuGvSXDaWmkOE00aIqRGbtHaw1FPIhbS8E26OJxJZyWLYeFAOMV +aiueNx3w4EUu+Fje1LCJzy7C5IBiCtl9Yc6AFZILXGpQUdrrnrUrbrjJ41nxzJvj +V65gCyiF1D9U88W7chIRiVnBPDCn97aXwimAqkHn0yt1zVOiekPmHdcyJbyI8kTR +5n0olK/VLMS3LaeejizssC7py/BdAPZE/a9jhwIDAQABAoIBADld7sIIsg2Ca0/z +kMg5/x2gO2wUgIrXNHtXRzBphzTNRp662Ck5eXRQHTkfoO985bgbS5uWS1ADL3NN +MoCkC5oHzWNq3nMnkGHlZp5PSZLW+i71qJvANA0T/3gcXWzf/XNEQfmoO7fAYJ+P +XT7t35qojt8XlfNpoAuNse2L9aBfQ8knEP7Aym/G3ko9mD1dvm2YKYYkzpLKYOoE +5MpCNCAcdn/va3bqEEgzq8ogwO9gNwerLnXCbId6xpInwfoTE6I15UySmeDQg+8P +nDiVWd+/xCEjFVBNwxDG1BjG0j2AlUZoDFuBeRmn48SjgRTc02PRdGKdC0Mys+1o +ZMIEpqECgYEA39/9vfGAQpz4ssLFL+AqKKRmgJwt/ZRkHtUK5w+c3dZBi1aRhWMY +5mrQt6USSm5YovJR6bxvjlD/nVwnENprLYdlrwAIEbT9jpVFtGHZqQumrH3JmGtu +Uz3mjZuHXblgoGUfAL6X20qK891iJoQ5VUtWYrP0ndd1f8CyE7/qzWkCgYEApeAD +aCBUMFBtmmXXrrQWBTu9tTgn26jMmFk/p5NBeSKGs2GCUB1sRgPUc35NCdtIetjs +frQlsEOKTD2PAnnTKL2ZFuk/mxFwQIXJVjAnhMICm92J8UL4GjWH1EuaLRQqks57 +n52dd9mDZFFE1lAfAihq9BEdLfwgFKlgoyl8W28CgYAzmwt/tGKveEWv10vjDFZL +hhIGxXmogYNOxCc+OhAb5t63At6Kk9xSiP7RxmBf/e26qgcNzR0d/jfeCzcKIH8i +QJrE60nw4vqr2mb1/LRSzle+XUSSOPl2gMdbjyV2ClxmvMiXwFd6+kTrj/WnEUWy +Dqq8F+VkWR1BtKaX/N5gOQKBgHsBNqWFq8i0K8LeGOYFx3qUBacYAH6kmyuyq0CC +M4A3uTnWakMsvnjhKC+JDmnrwcDPkfiXcIdYXnsQ/zbvzkWc66SQzUkZ0msWiuou +BXAuSq74xu0xIziUT6h/c9JP7Q42rnf78qTImOXQWkKu4X/BJybcdg3+tG999xqn +jf9jAoGAG4hzKaKqvqKOfDGr3Eebu2/uS73xtUTxiUheqIqoTJHO4jjWOcjKI1my +91pnfC/t3fGLB2ObqDTw+H3jRf7gqIkMbUAswjpMX3GpOm2+eI5U9B8y5dV5PoUt +RV32fzc/xu9Ib2i3Q29vuHrZczb4lxpi8UVWCLcrPuvC2lM8QTY= +-----END RSA PRIVATE KEY----- diff --git a/libs/ssl-config/src/test/resources/certs/ca3/ca.p12 b/libs/ssl-config/src/test/resources/certs/ca3/ca.p12 new file mode 100644 index 0000000000000..1331cec05d77e Binary files /dev/null and b/libs/ssl-config/src/test/resources/certs/ca3/ca.p12 differ diff --git a/libs/ssl-config/src/test/resources/certs/cert-all/certs.jks b/libs/ssl-config/src/test/resources/certs/cert-all/certs.jks new file mode 100644 index 0000000000000..4cf839ce8d046 Binary files /dev/null and b/libs/ssl-config/src/test/resources/certs/cert-all/certs.jks differ diff --git a/libs/ssl-config/src/test/resources/certs/cert-all/certs.p12 b/libs/ssl-config/src/test/resources/certs/cert-all/certs.p12 new file mode 100644 index 0000000000000..b971a1e39c83b Binary files /dev/null and b/libs/ssl-config/src/test/resources/certs/cert-all/certs.p12 differ diff --git a/libs/ssl-config/src/test/resources/certs/cert1/cert1.crt b/libs/ssl-config/src/test/resources/certs/cert1/cert1.crt new file mode 100644 index 0000000000000..51f39295f62e3 --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/cert1/cert1.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDGzCCAgOgAwIBAgIUCYPL1cogC+8WOfSZytklHmrHQh4wDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAxMJVGVzdCBDQSAxMB4XDTE5MDEwMzA3NDA0MloXDTQ2MDUy +MDA3NDA0MlowEDEOMAwGA1UEAxMFY2VydDEwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCWz6ITDTlkTLueB30Jx0+7sWHdlM5ObZjWhMQ1eyJD0gYU/gkH +2C88IN/PtSv04tzFS6PA4KPDLIyaAhczPlGElSansiui//CpieCI4tt5c2BgVo3X +dJaylYoW3CRILUrlSBOMUmJCQEokverxMrz8DeppNxRfj99pQkoxUkmFMZj/C7XN +VYrTttdF1li5FUtWJxw234OUfum3PQIzz6YTmoPtLrJ2fB8I4CH8R5hwGcryhBSA +qq8pgy61aTPCgEBZ1c4Dvl65X8dG2QEVPjwMZnnbGjvlZefOgkmAWJ1VjihA3GVg +O2mx4tB4D2x5K/OAxh2foZkDVhqJfBkOblLnAgMBAAGjaTBnMB0GA1UdDgQWBBRM +RZ6Qlozj5hWTqf3+oTznFyZTsDAfBgNVHSMEGDAWgBSHd+NjwqkE6hVMEnltx6cT ++D11lzAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwCQYDVR0TBAIwADANBgkq +hkiG9w0BAQsFAAOCAQEAOTUJ64T6kO2H51j2bKIof4ij4yoDD86gLmUF7qXB2Wt4 +tMDCqs9+5VnRzSWY1652mpwPClcK/MfE26PR6DUunoES+8VSbARWh0OB6zsAAWyp +WJ4RxlfYdNpJZjpx3umLGj4yeCh0iOhfoArBUT3vaJJrea+rTro4UFE2Z29uWALr +NvjKZ0Qrn1DMP3N9b7y81dR9RMlzeqk5tlPhAqhHzQM0hDdFKA5uIFn71QQpd5SI +y8MpllWFGGq/+5m7FD0t71GQ/m5xCyfUiqQU31Nj3ThU21SPHBqZIZvQ/na/OaAf +GySn+0ZHAvyNRTL2y2Fk/YAY68kgx2E44H5YSqbFJA== +-----END CERTIFICATE----- diff --git a/libs/ssl-config/src/test/resources/certs/cert1/cert1.key b/libs/ssl-config/src/test/resources/certs/cert1/cert1.key new file mode 100644 index 0000000000000..461ae5ee31ba7 --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/cert1/cert1.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAls+iEw05ZEy7ngd9CcdPu7Fh3ZTOTm2Y1oTENXsiQ9IGFP4J +B9gvPCDfz7Ur9OLcxUujwOCjwyyMmgIXMz5RhJUmp7Irov/wqYngiOLbeXNgYFaN +13SWspWKFtwkSC1K5UgTjFJiQkBKJL3q8TK8/A3qaTcUX4/faUJKMVJJhTGY/wu1 +zVWK07bXRdZYuRVLViccNt+DlH7ptz0CM8+mE5qD7S6ydnwfCOAh/EeYcBnK8oQU +gKqvKYMutWkzwoBAWdXOA75euV/HRtkBFT48DGZ52xo75WXnzoJJgFidVY4oQNxl +YDtpseLQeA9seSvzgMYdn6GZA1YaiXwZDm5S5wIDAQABAoIBABCilJEfa045/JQA +5XT3rD7a4R2s9VjHVA2NlYsEqxHqD8uu/dYEraknQyjJJjEb+Rg2MLjszoOP3W57 +fo2jeSBzx1DGIXQYYTaCQ+c1htoNtPrLcVfrv1exkQrWe5YOkO1blvRqffYq20LU +RB8Y5qmy60Fx1uh3mUAmFML9/agYVJo4yCxnNDrMg9UjF4bn/39uOf6C7mEVJRTl +7ET5wcbdl10EOWW2m60hJOQLSOY9N1eafFEO+V2Xb80PC2t3Mqt5+T7n0CKCx/p9 +4F7QAz+hsfksY3oTUUXwL0KoJTLdJrjCoG4mWJ/Re3qEKJqmMfT4XpJKrF7HfgcK +RCyH06kCgYEA/5TVQK4G1Dc4LnSCmCb+ECQvmGRBtK6Alh3Txb4IwGHDGMfC8W4O +gt03A8ZE92pjITHd1+cLykKBsmaVmEtiyD7YL5G3mumR1YdMFEBSZdxOTeD95+aL +YxTofPsDIUIPSFecRWwri7TyYcvUGyDchL0vDc6Gp95/ZFFgt26uxAsCgYEAlw7e +g2McHws9cSAfouULbKbf6jXzy21t6CeqJGID/kjdUws63prcQvmFtFHWrv3rKO09 +hgb3Kd3gUz8t9tAD3F718bSZwzLASwO5ujPHZQVRTotutgCGeKPgXqzyVWeo45ji +4DfQl53jG0aA1DzoZSA3/owcuX6CVGPLzQnhehUCgYAHe9UuuqnKhv9nJNQ6HlIs +KNMX9D+USdPMEX2E+caJ05MB47+KkD1uiYm125VjZUMX0rz7OHG472+ayLQyrGpt +EKIF6o9kwtgZV4fbw/Jltyi30RG+O5rzQMZ5+mOiEqwd4yrZQYyY36iFQpGoZbLv +VBbPoa+BtNsoFdXuKRiG9wKBgBjJhdXFc53ceE6R2N8f+onvsBp8k+6znC9WIuMp +ekJFrpur4hMZEj+jNj9qlnHMlMP4efn+NpyWHfNLEL3JUHje1Di/S+Pt9gPZLqbR +TEzVXIwo8RfIakhti6m9c150ThBazA/C2OWoMNYO8aDiBbhiWw3X6/a8PaKfZZfV +oTwpAoGASCTx55uThl9rN+XDKFXN500K4r+Q9OBOEkfDuDUERpBohUfoy8dO5eiT +mGiqx5P0hoxEC70vnw5fJz4ZpSJ7LcpCfq2TezknJP3MZKwTdBM/pODSPMU5YCW4 +T9ocEQui5PKOTLlVo1QKrG8w6f/YMfZuGa9zP/LmTLZEado9nuk= +-----END RSA PRIVATE KEY----- diff --git a/libs/ssl-config/src/test/resources/certs/cert1/cert1.p12 b/libs/ssl-config/src/test/resources/certs/cert1/cert1.p12 new file mode 100644 index 0000000000000..2635a887bc87e Binary files /dev/null and b/libs/ssl-config/src/test/resources/certs/cert1/cert1.p12 differ diff --git a/libs/ssl-config/src/test/resources/certs/cert2/cert2.crt b/libs/ssl-config/src/test/resources/certs/cert2/cert2.crt new file mode 100644 index 0000000000000..3b7ca01e48bae --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/cert2/cert2.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDGzCCAgOgAwIBAgIULGiQ5jnAntO91sS7Al5aUv8/jg8wDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAxMJVGVzdCBDQSAxMB4XDTE5MDEwMzA3NDE1NVoXDTQ2MDUy +MDA3NDE1NVowEDEOMAwGA1UEAxMFY2VydDIwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDQbV6x3P9sstd5zKkjOylk+/X1j0vI6C93HwkF3d9NwhMoV1zq +aSj2SmAQCz4mIjMlAFR9mm6F+3sb8xkMFJD2Mypc5dW6TS5krhbhJdMpoVbceZtS +yuIsvQi1GT2Uwyu89doDiUNBIANqaexrK5x2S/Fy4L8dNl1x2k6PJi6zVpvbnNLV +TSbuSMp3oY23PpX/m6wzJlCYicO8ucMhPwmC0OL9WJNKny4vuEPdiV6/LwCVS4Vr +UpfNqgXLzRMJEbx7C2QEG7T6o2g2101oANBuPaDZcwIQ//EI3IkT10JcABIbwsdj +/Oqj0cf7iEW1IfJWx+kRVT8NfEA5QjL1KQ4HAgMBAAGjaTBnMB0GA1UdDgQWBBS+ +/gUxdwfGB0XcPHsbM6Qw50S2OTAfBgNVHSMEGDAWgBSHd+NjwqkE6hVMEnltx6cT ++D11lzAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwCQYDVR0TBAIwADANBgkq +hkiG9w0BAQsFAAOCAQEAiztFGa3uZVb6Fs8evN4CU+hPFYdhF57lEfT6Xa1OUD1B +5e5rDOZfVloy4gzLdtNCS9lieTgB7Yc3wDUCm0cS48JCMxykSRTI6M0Fmofsgd5d +OKR7CB+jR1Egj6nZren62XCgqjuJ+dbP4DinY6TifFzFX1vOD4RTb0mEn2WHL/JB +DnDxOETmBtKFyueprMtXkTO23dXsYXQeo2Gyih+t5ksqMnxCW2GFkkOqrOUQY8CF +6CVmmb9UCYk3f3Av9TedqJ8Cmoe+HSP0R2oxpnc7cd1v37QPxWgHETro9zS4FBrt +6KgNTP99b+aLxeeuMKJuRVR+TCZPuyYbBzUfp2ZZOg== +-----END CERTIFICATE----- diff --git a/libs/ssl-config/src/test/resources/certs/cert2/cert2.key b/libs/ssl-config/src/test/resources/certs/cert2/cert2.key new file mode 100644 index 0000000000000..1384b2daf309a --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/cert2/cert2.key @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,68F4EFD94D4D9BDF + +TL01d1JqaN0P1sb8284jOGpLn42cubkLY7JPj+bmpL2PEH9cT2xo7MM5cwvNSbPX +nuZ1CMFSFqAyxulh/rfZwU1BumKLiwM+ep0A/lWwKJEor+ancCBdkIHWfSg3Jc0i ++nHhL3e7+W+XRfSlafQQkhFkeOXs0hSa4WYX6tymvMAf8OLAenKB/0MOvKjd6EOu +ZIvnBxAjAaDoRr4X4izIVlNmsIbvYARW9WjjcK2FZhNn+WLLTkfwEkl0glRl440L +ET9qAjoH7j5KL5yKw5h2HhALSLaXdiAfCGvhpU4K0Afpb225mvR/uL3HLpJEGKKb +DuoK99zvjHqReq1YndIR688ioc0O4dUvujZKgn8OR2JvGJVSc+mgaIWadfs2aADz +CJIlqnSJa1EW6qm6knLJwieEBoNHbeFszrCKrYdy7uicys9PR1XsoMrOly+HhVnR +PChA3gV6AVIjyMAUkFLg4NAHjgDbb81lu8ENFmlJcjgVeXDMykrpBmhzmhSPtOEb +6bdQCKuA7zXIpJCmP66ZSuNHxikrfqLJjXW3PojLuCx5nfO0akpxjSeyLHzv/YXV +YxnpLJxMybG1KUHyDHRmDrd+UPzLnh52O/g9NoiAlluUcblH+BKer18dJdGSl95G +P+Ted08S0yP2niNz6XHv5KbztzsP73n0w82akCQB8ZAsGJ0CNw7S7N6tWam/UkMN +tvM9nFCjvwah1funu8nj9QyWrzac6ngPP0s8tcKG0ahLEJYn7Hx2Oqn7K39fbMkX +UOJVNEBQP2Anf5dJMfYPEI0xihv3ec1RxpipOm9DKQ878jxnqxgZxa+Pab9j/JxQ +j+BGaoOnYj2jHBnt4nCP75F0BEZaGxsZX2MjJySK+5jy2WW3JRC/E0qPkL6oBvT2 +3e37fep4XKSjqR9lSYjW+AlAVw2uPkwxDp5sD7XFGsH3SM1qj54NWwljpKXnOTbQ +Xws18VEiWsQ3kD7ft11w8/67Bb27TsWEJRo1vCC4KsYBCyjEOrp13aJxpSiNI24W +oSmrQTsCco1Yrxfs0noNu0FzfJHV6qmHR7ps0fj+AWJyquYbIEK1762+r2uutgSg +ZVaPWLkm9uq5K5rNXsj3w1L1CK4YcDre0yt+Kg4Pt3OQR2lF8CpABguA3gR9kO2g +9N8hAfiOq5MDliU+9r6Cr/dPkdzV0Eo971HdfaLD7S/pjSP37fcTcOpDhwNt3UcK +amhR/Zm8Ll414zc/iNiVTcu6+chzSY9Yhwfa8A/XuIfoqMrTiaBMPlxe0tNKaKVs +09d6U5JoTQJcnei/4ODkIIYOzsmMtHgKtmeg86AB8yeLNgqWCrJi+Fxnha+cJ9No ++STMQP4vS8qgRe5XkRGaAZBHyPAwxOou1Iu167LjlFFl0YSYUZTChha5XBG2Uhm8 +FeIH1Ip+83fxMoyiYROOdeMuLXtQIndA3fmjduBEO0kPtAwQ2xfH4g59XnnSL97S +9AslnPnUWzDR0zBr4GQBNAKLaMIFGzhDZEzaooYetoeDYSczil/Rf1D6wyM6Cgjq +BK7c0kNum9uXaDbYCh6spzYua0j1noqsBTm9V25178lNbkQ6yAPdeYCxRmUXHtXk +-----END RSA PRIVATE KEY----- diff --git a/libs/ssl-config/src/test/resources/certs/cert2/cert2.p12 b/libs/ssl-config/src/test/resources/certs/cert2/cert2.p12 new file mode 100644 index 0000000000000..269e855f77098 Binary files /dev/null and b/libs/ssl-config/src/test/resources/certs/cert2/cert2.p12 differ diff --git a/libs/ssl-config/src/test/resources/certs/pem-utils/README.asciidoc b/libs/ssl-config/src/test/resources/certs/pem-utils/README.asciidoc new file mode 100644 index 0000000000000..0136e967106e1 --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/pem-utils/README.asciidoc @@ -0,0 +1,149 @@ += Keystore Details +This document details the steps used to create the certificate and keystore files in this directory. + +== Instructions on generating self-signed certificates +The certificates in this directory have been generated using the following openssl configuration and commands. + +OpenSSL Configuration File is located in this directory as `openssl_config.cnf`. + +NOTE: The `alt_names` section provides the Subject Alternative Names for each certificate. This is necessary for testing +with hostname verification enabled. + +[source,shell] +----------------------------------------------------------------------------------------------------------- +openssl req -new -x509 -extensions v3_req -out .cert -keyout .pem -days 1460 -config config.cnf +----------------------------------------------------------------------------------------------------------- + +When prompted the password is always set to the value of . + +Because we intend to import these certificates into a Java Keystore file, they certificate and private key must be combined +in a PKCS12 certificate. + +[source,shell] +----------------------------------------------------------------------------------------------------------- +openssl pkcs12 -export -name -in .cert -inkey .pem -out .p12 +----------------------------------------------------------------------------------------------------------- + +== Creating the Keystore +We need to create a keystore from the created PKCS12 certificate. + +[source,shell] +----------------------------------------------------------------------------------------------------------- +keytool -importkeystore -destkeystore .jks -srckeystore .p12 -srcstoretype pkcs12 -alias +----------------------------------------------------------------------------------------------------------- + +The keystore is now created and has the private/public key pair. You can import additional trusted certificates using +`keytool -importcert`. When doing so make sure to specify an alias so that others can recreate the keystore if necessary. + +=== Changes and additions for removing Bouncy Castle Dependency + +`testnode-unprotected.pem` is simply the decrypted `testnode.pem` +------ +openssl rsa -in testnode.pem -out testnode-unprotected.pem +------ + +`rsa_key_pkcs8_plain.pem` is the same plaintext key encoded in `PKCS#8` +------ +openssl pkcs8 -topk8 -inform PEM -outform PEM -in testnode-unprotected.pem -out rsa_key_pkcs8_plain.pem -nocrypt +------ + +`testnode-aes{128,192,256}.pem` is the testnode.pem private key, encrypted with `AES-128`, `AES-192` and `AES-256` +respectively, encoded in `PKCS#1` +[source,shell] +------ +openssl rsa -aes128 -in testnode-unprotected.pem -out testnode-aes128.pem +------ +[source,shell] +------ +openssl rsa -aes192 -in testnode-unprotected.pem -out testnode-aes192.pem +------ +[source,shell] +------ +openssl rsa -aes256 -in testnode-unprotected.pem -out testnode-aes256.pem +------ + +Adding `DSA` and `EC` Keys to the Keystore + +[source,shell] +------ +keytool -genkeypair -keyalg DSA -alias testnode_dsa -keystore testnode.jks -storepass testnode \ + -keypass testnode -validity 10000 -keysize 1024 -dname "CN=Elasticsearch Test Node" \ + -ext SAN=dns:localhost,dns:localhost.localdomain,dns:localhost4,dns:localhost4.localdomain4,dns:localhost6,dns:localhost6.localdomain6,ip:127.0.0.1,ip:0:0:0:0:0:0:0:1 +------ +[source,shell] +------ +keytool -genkeypair -keyalg EC -alias testnode_ec -keystore testnode.jks -storepass testnode \ + -keypass testnode -validity 10000 -keysize 256 -dname "CN=Elasticsearch Test Node" \ + -ext SAN=dns:localhost,dns:localhost.localdomain,dns:localhost4,dns:localhost4.localdomain4,dns:localhost6,dns:localhost6.localdomain6,ip:127.0.0.1,ip:0:0:0:0:0:0:0:1 +------ + +Exporting the `DSA` and `EC` private keys from the keystore + +[source,shell] +---- +keytool -importkeystore -srckeystore testnode.jks -destkeystore dsa.p12 -deststoretype PKCS12 \ + -srcalias testnode_dsa -deststorepass testnode -destkeypass testnode +---- +[source,shell] +---- +openssl pkcs12 -in dsa.p12 -nodes -nocerts | openssl pkcs8 -topk8 -nocrypt -outform pem \ + -out dsa_key_pkcs8_plain.pem +---- +[source,shell] +---- +keytool -importkeystore -srckeystore testnode.jks -destkeystore ec.p12 -deststoretype PKCS12 \ + -srcalias testnode_ec -deststorepass testnode -destkeypass testnode +---- +[source,shell] +---- +openssl pkcs12 -in ec.p12 -nodes -nocerts | openssl pkcs8 -topk8 -nocrypt -outform pem \ + -out ec_key_pkcs8_plain.pem +---- + + + +Create `PKCS#8` encrypted key from the encrypted `PKCS#1` encoded `testnode.pem` +[source,shell] +----- +openssl pkcs8 -topk8 -inform PEM -outform PEM -in testnode.pem -out key_pkcs8_encrypted.pem +----- +[source,shell] +----- +ssh-keygen -t ed25519 -f key_unsupported.pem +----- + + +Convert `prime256v1-key-noparam.pem` to `PKCS#8` format +----- +openssl pkcs8 -topk8 -in prime256v1-key-noparam.pem -nocrypt -out prime256v1-key-noparam-pkcs8.pem +----- + +Generate the keys and self-signed certificates in `nodes/self/` : + +------ +openssl req -newkey rsa:2048 -keyout n1.c1.key -x509 -days 3650 -subj "/CN=n1.c1" -reqexts SAN \ + -extensions SAN -config <(cat /etc/ssl/openssl.cnf \ + <(printf "[SAN]\nsubjectAltName=otherName.1:2.5.4.3;UTF8:node1.cluster1")) -out n1.c1.crt +------ + + +Create a `CA` keypair for testing +[source,shell] +----- +openssl req -newkey rsa:2048 -nodes -keyout ca.key -x509 -subj "/CN=certAuth" -days 10000 -out ca.crt +----- + +Generate Certificates signed with our CA for testing +[source,shell] +------ + openssl req -new -newkey rsa:2048 -keyout n2.c2.key -reqexts SAN -extensions SAN \ + -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=otherName.1:2.5.4.3;UTF8:node2.cluster2"))\ + -out n2.c2.csr +------ + +[source,shell] +------ +openssl x509 -req -in n2.c2.csr -extensions SAN -CA ca.crt -CAkey ca.key -CAcreateserial \ + -extfile <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=otherName.1:2.5.4.3;UTF8:node2.cluster2"))\ + -out n2.c2.crt -days 10000 +------ diff --git a/libs/ssl-config/src/test/resources/certs/pem-utils/corrupted_key_pkcs8_plain.pem b/libs/ssl-config/src/test/resources/certs/pem-utils/corrupted_key_pkcs8_plain.pem new file mode 100644 index 0000000000000..4b2271a6f3971 --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/pem-utils/corrupted_key_pkcs8_plain.pem @@ -0,0 +1,24 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDesZnVBuxbT4y7 +KtIuYx8MUq0sGQgVbxXSBG66sWDU9Qoo1HUyra0xXCONgRMBT9RjSIpk7OOC9g8q +ENNgFO179YdHVkrgJhW/tNBf+C0VAb+B79zu7SwtyH2nt9t378dmItL+sERkMiiG ++BS/O+cDz44hifDiS7Eqj/mJugAhLjWSUyD+UBObxXvUsxjryKeG3vX9mRCgAcqB +xH3PjI1i9DVaoobwMbwpE5eW2WXexOspuXnMmGfrrR6z/VmdHqe/C3rGdJOX+Y0c +yOR+/Vuzisn+nLeo/GJx2hIif8rKiNRyAdUXfx+4DLYJBN2NUbl9aP2LP6ZC8ubf +6qwhhB0XAgMBAAECggEBAKuzP6qSNfaJNTayY2/EmRHFRSP1ANiV17sgE8f6L3DC +pdypQtuaMSkXo4nc9SxTwqvyKFJ8m0ZENZj3dCJmwFyNCIqmLAD7HFW9MdRs40WJ +HYEv0aaeUyvRo6CHD74/r/w96XTZr0GZssmtyUFRDGNRyoJter7gIW9xprLcKHFr +YTmdaAXbOm5W/K3844EBouTYzYnZYWQjB3jT/g5dIic3AtLb5YfGlpaXXb74xTOU +BqY1uKonGiDCh0aXXRl2Ucyre6FWslNNy4cAAXm6/5GT6iMo7wDXQftvtyK2IszP +IFcOG6xcAaJjgZ5wvM3ch0qNhQi4vL7c4Bm5JS9meoECgYEA88ItaVrfm2osX/6/ +fA8wYxxYU5RQRyOgLuzBXoRkISynLJaLVj2gFOQxVQeUK++xK6R182RQatOJcWFT +WwmIL3CchCwnnXgPvMc51iFKY94DbdvrRatP8c5sSk7IQlpS3aVa7f7DCqexggr5 +3PYysuiLirL+n9I1oZiUxpsS6/cCgYEA6eCcDshQzb7UQfWy//BRMp7u6DDuq+54 +38kJIFsPX0/CGyWsiFYEac8VH7jaGof99j7Zuebeb50TX57ZCBEK2LaHe474ggkY +GGSoo3VWBn44A1P5ADaRGRwJ4/u79qAg0ldnyxFHWtW+Wbn11DoOg40rl+DOnFBJ +W+bWJn4az+ECgYEAzWduDt5lmLfiRs4LG4ZNFudWwq8y6o9ptsEIvRXArnfLM3Z0 +Waq6T4Bu1aD6Sf/EAuul/QAmB67TnbgOnqMsoBU7vuDaTQZT9JbI9Ni+r+Lwbs2n +tuCCEFgKxp8Wf1tPgriJJA3O2xauLNAE9x57YGk21Ry6FYD0coR5sdYRHscCgYEA +lGQM4Fw82K5RoqAwOK/T9RheYTha1v/x9ZtqjPr53/GNKQhYVhCtsCzSLFRvHhJX +EpyCLK/NRmgVWMBC2BloFmSJxd3K00bN4PxM+5mBQZFoHMR04qu8mH/vzpV0h2DG +Mm9+zZti+MFRi0CwNz2248T4ed8LeKaARS1LhxTQEkECgYBFsPNkfGWyP4zsgzFs diff --git a/libs/ssl-config/src/test/resources/certs/pem-utils/dsa_key_openssl_encrypted.pem b/libs/ssl-config/src/test/resources/certs/pem-utils/dsa_key_openssl_encrypted.pem new file mode 100644 index 0000000000000..a251de23f4879 --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/pem-utils/dsa_key_openssl_encrypted.pem @@ -0,0 +1,15 @@ +-----BEGIN DSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,BE9A0B63873F6B7A + +lGSpJkwN0J9p+2Wm58706EYz6mmjgz7okjMtsR87GMIiK/wVwjKmyUa73QTVVs15 +N/EOySftBk3VUSPx9G1ZMxKpp3l/hvkIcsDDfCPAZFqwdQQJ8BEeF9jDd5ZoI6Yz +Yus1+X8A1OpX1O7PCZ08e2fLeVuEWg62/JQcNukuvL7AKm+qa1sda5/ktquv2eMZ +nbTiOE3Xe+uDsgABQdy1h4EsMEaMdE6QrWdxLGWDGcdzSzfltvnhmmsK2CQsV4e1 +huQeb8ylShJuIr+mgtKgUlIlJwSd7ka8hIdmGt1LO9+NZOPUGN04daQkETtfwsmu +YIYkh66CuLbT4nZny64Spa7AeINSmf9GA72/QtRSo3M7Khlw/95Lz24iKAy7/Lbt +AKYenSQeJtlNgWzPcDIeUrIzXXmAXHN5YGMg/7X0h7EGu5BxYbLydkBRvSkV9gzU +Ms6JD5aON10DQhjIUwUcBnhSnwPPpIVa2xf9mqytkcg+zDgr57ygZ9n4D+iv4jiC +ZJuFCFrgeqHrCEKRphWRckyhPo25ix9XXv7FmUw8jxb/3uTk93CS4Wv5LK4JkK6Z +AyF99S2kDqsE1u71qHJU2w== +-----END DSA PRIVATE KEY----- diff --git a/libs/ssl-config/src/test/resources/certs/pem-utils/dsa_key_openssl_plain.pem b/libs/ssl-config/src/test/resources/certs/pem-utils/dsa_key_openssl_plain.pem new file mode 100644 index 0000000000000..a64642fc9ab0c --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/pem-utils/dsa_key_openssl_plain.pem @@ -0,0 +1,12 @@ +-----BEGIN DSA PRIVATE KEY----- +MIIBuwIBAAKBgQD9f1OBHXUSKVLfSpwu7OTn9hG3UjzvRADDHj+AtlEmaUVdQCJR ++1k9jVj6v8X1ujD2y5tVbNeBO4AdNG/yZmC3a5lQpaSfn+gEexAiwk+7qdf+t8Yb ++DtX58aophUPBPuD9tPFHsMCNVQTWhaRMvZ1864rYdcq7/IiAxmd0UgBxwIVAJdg +UI8VIwvMspK5gqLrhAvwWBz1AoGBAPfhoIXWmz3ey7yrXDa4V7l5lK+7+jrqgvlX +TAs9B4JnUVlXjrrUWU/mcQcQgYC0SRZxI+hMKBYTt88JMozIpuE8FnqLVHyNKOCj +rh4rs6Z1kW6jfwv6ITVi8ftiegEkO8yk8b6oUZCJqIPf4VrlnwaSi2ZegHtVJWQB +TDv+z0kqAoGAd0xuuUUSAXsXaQ/dp9ThBTVzdVhGk6VAcWb403uMXUyXKsnCIAST +m6bVWKjNxO1EsP3Slyd5CwbqIRUBK5NjzdQP/hHGtEIbqtYKY1VZI7T91Lk8/Dc/ +p9Vgh27bPR8Yq8wPKU3EIJzYi0Nw8AxZf10yK+5tQ6pPUa3dH6lXt5oCFF1LyfuB +qBYh7hyIsfkb+cZoQ57t +-----END DSA PRIVATE KEY----- diff --git a/libs/ssl-config/src/test/resources/certs/pem-utils/dsa_key_openssl_plain_with_params.pem b/libs/ssl-config/src/test/resources/certs/pem-utils/dsa_key_openssl_plain_with_params.pem new file mode 100644 index 0000000000000..0a2ea861b9b66 --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/pem-utils/dsa_key_openssl_plain_with_params.pem @@ -0,0 +1,18 @@ +-----BEGIN DSA PARAMETERS----- +ThisisnotvalidabutwedontparseiteitherwaykFJyVA+0q1vAej5iQVmUvu1y +fuA5axTA5IT86U7acP0KV8eawbDXVhqiP0zcSeP731coxJaUHC6XB0FVMhYi4fZn +fexykg9Kxe/QBfDtcj3CEJNH/xoptJQVx3hi+0BPPK8+eUXTjwkQerGMwUD7UQak +xuUS/22GakHZV5G/kCc= +-----END DSA PARAMETERS----- +-----BEGIN DSA PRIVATE KEY----- +MIIBuwIBAAKBgQD9f1OBHXUSKVLfSpwu7OTn9hG3UjzvRADDHj+AtlEmaUVdQCJR ++1k9jVj6v8X1ujD2y5tVbNeBO4AdNG/yZmC3a5lQpaSfn+gEexAiwk+7qdf+t8Yb ++DtX58aophUPBPuD9tPFHsMCNVQTWhaRMvZ1864rYdcq7/IiAxmd0UgBxwIVAJdg +UI8VIwvMspK5gqLrhAvwWBz1AoGBAPfhoIXWmz3ey7yrXDa4V7l5lK+7+jrqgvlX +TAs9B4JnUVlXjrrUWU/mcQcQgYC0SRZxI+hMKBYTt88JMozIpuE8FnqLVHyNKOCj +rh4rs6Z1kW6jfwv6ITVi8ftiegEkO8yk8b6oUZCJqIPf4VrlnwaSi2ZegHtVJWQB +TDv+z0kqAoGAd0xuuUUSAXsXaQ/dp9ThBTVzdVhGk6VAcWb403uMXUyXKsnCIAST +m6bVWKjNxO1EsP3Slyd5CwbqIRUBK5NjzdQP/hHGtEIbqtYKY1VZI7T91Lk8/Dc/ +p9Vgh27bPR8Yq8wPKU3EIJzYi0Nw8AxZf10yK+5tQ6pPUa3dH6lXt5oCFF1LyfuB +qBYh7hyIsfkb+cZoQ57t +-----END DSA PRIVATE KEY----- diff --git a/libs/ssl-config/src/test/resources/certs/pem-utils/dsa_key_pkcs8_plain.pem b/libs/ssl-config/src/test/resources/certs/pem-utils/dsa_key_pkcs8_plain.pem new file mode 100644 index 0000000000000..fc5f17ce89897 --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/pem-utils/dsa_key_pkcs8_plain.pem @@ -0,0 +1,9 @@ +-----BEGIN PRIVATE KEY----- +MIIBSwIBADCCASwGByqGSM44BAEwggEfAoGBAP1/U4EddRIpUt9KnC7s5Of2EbdS +PO9EAMMeP4C2USZpRV1AIlH7WT2NWPq/xfW6MPbLm1Vs14E7gB00b/JmYLdrmVCl +pJ+f6AR7ECLCT7up1/63xhv4O1fnxqimFQ8E+4P208UewwI1VBNaFpEy9nXzrith +1yrv8iIDGZ3RSAHHAhUAl2BQjxUjC8yykrmCouuEC/BYHPUCgYEA9+GghdabPd7L +vKtcNrhXuXmUr7v6OuqC+VdMCz0HgmdRWVeOutRZT+ZxBxCBgLRJFnEj6EwoFhO3 +zwkyjMim4TwWeotUfI0o4KOuHiuzpnWRbqN/C/ohNWLx+2J6ASQ7zKTxvqhRkImo +g9/hWuWfBpKLZl6Ae1UlZAFMO/7PSSoEFgIUXUvJ+4GoFiHuHIix+Rv5xmhDnu0= +-----END PRIVATE KEY----- diff --git a/libs/ssl-config/src/test/resources/certs/pem-utils/ec_key_openssl_encrypted.pem b/libs/ssl-config/src/test/resources/certs/pem-utils/ec_key_openssl_encrypted.pem new file mode 100644 index 0000000000000..69dfde4b3c502 --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/pem-utils/ec_key_openssl_encrypted.pem @@ -0,0 +1,7 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,692E4272CB077E56A0D4772B323EFB14 + +BXvDiK0ulUFKw1fDq5TMVb9gAXCeWCGUGOg/+A65aaxd1zU+aR2dxhCGXjsiLzRn +YFSZR2J/L7YP1qvWC7f0NQ== +-----END EC PRIVATE KEY----- diff --git a/libs/ssl-config/src/test/resources/certs/pem-utils/ec_key_openssl_plain.pem b/libs/ssl-config/src/test/resources/certs/pem-utils/ec_key_openssl_plain.pem new file mode 100644 index 0000000000000..e1d0a6a8319c0 --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/pem-utils/ec_key_openssl_plain.pem @@ -0,0 +1,4 @@ +-----BEGIN EC PRIVATE KEY----- +MDECAQEEILEXCgqp9wZqKVmG6HTESPeCyx2O4TDoFqyILz7OGocEoAoGCCqGSM49 +AwEH +-----END EC PRIVATE KEY----- diff --git a/libs/ssl-config/src/test/resources/certs/pem-utils/ec_key_openssl_plain_with_params.pem b/libs/ssl-config/src/test/resources/certs/pem-utils/ec_key_openssl_plain_with_params.pem new file mode 100644 index 0000000000000..2ad57473236b3 --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/pem-utils/ec_key_openssl_plain_with_params.pem @@ -0,0 +1,7 @@ +-----BEGIN EC PARAMETERS----- +Notvalidbutnotparsed +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MDECAQEEILEXCgqp9wZqKVmG6HTESPeCyx2O4TDoFqyILz7OGocEoAoGCCqGSM49 +AwEH +-----END EC PRIVATE KEY----- diff --git a/libs/ssl-config/src/test/resources/certs/pem-utils/ec_key_pkcs8_plain.pem b/libs/ssl-config/src/test/resources/certs/pem-utils/ec_key_pkcs8_plain.pem new file mode 100644 index 0000000000000..7e6de54424702 --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/pem-utils/ec_key_pkcs8_plain.pem @@ -0,0 +1,4 @@ +-----BEGIN PRIVATE KEY----- +MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCCxFwoKqfcGailZhuh0 +xEj3gssdjuEw6BasiC8+zhqHBA== +-----END PRIVATE KEY----- diff --git a/libs/ssl-config/src/test/resources/certs/pem-utils/empty.pem b/libs/ssl-config/src/test/resources/certs/pem-utils/empty.pem new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/libs/ssl-config/src/test/resources/certs/pem-utils/key_pkcs8_encrypted.pem b/libs/ssl-config/src/test/resources/certs/pem-utils/key_pkcs8_encrypted.pem new file mode 100644 index 0000000000000..28059d5a2266d --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/pem-utils/key_pkcs8_encrypted.pem @@ -0,0 +1,29 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIE6TAbBgkqhkiG9w0BBQMwDgQI2jwlFL0XId0CAggABIIEyMujZbpG6zKb2pVu +soamTaoLcZwNofS9ncGIEH1nbI8UpPY81VeOIBm4mneDt8RU5bIOXP4IZEZY9uU+ +pugKQ3hT8vBQjJujjuctUPaFxB0kGEeITOInY2jn2BFDbUgy5Z7EVD4G2K06SDDK +oD+twbzZo9x34VizwpHHb8wE+DFyYc+sp+Re2Qk3FReKgjdJezfcRHbKrrlx2rJ+ +k/YAPmzcFYVbuUiB6HY6BGzSJO1JxT8iNJE+Hmk3ZLXG590hp0vuGSkY/ihbeix4 +1rQs7u4riqXJ+DJBmXt/wXUij0/k6s4igACNsT2MkZkGEDkzqzE+kj2VYOHSX+Wd +5W0WCfftcsIQ8eow4ACec/Ns9ionLjx1xnbTjRMkpGgbVsreupU9AQ4MhLNNgwyl +six/cxUxTvH8Modd0/4KQFkeo352A6+DKCaPZ87SoF2Rge1otcJaZVcX1gBvIztB +/xzYwyUydQEwblU0kCYWRgxlKP9jxFoke2RX1BodRfAMNDxS0XyYrA/JzB7ZRsS7 +QGYPy/PPb014U3KhpJdjwbNu2VaCVdGryYA9+BTP+Vzwcp8MZoMPnnjnBh1YyVAj +c7oyzKU5e5SVsYni1Kt/536YxQgFCAUHv/g+zQqqGEvyiMXhsCwVpoy7UcFYgmlw +40g3+ejwvlO3YA67gQQKebEv6/Laz1hVNiXT0m3okAXWxXgF/g2eBO5NTRdtaWn3 +VNH5ub+LOr6cMhk9BAtKgRG+xeh8/2SqH12UbwtlmxqnBAfHtqlE6yJ1ViMDHxF9 +101xJlEONmC3fcEAjShK6LEbFwPJns3WbGc0ds36CzXWtO29XGssr+YoiF9e3Eus +/XQjmjOJxRoWkNEYsxlJ3IRH2vUcdCoAp8IlD7JYxx8UBCSJDBo7/0QKU6INeWjo +5+aNbaLAJULSKo1LTZjjANm+G+KcSnbn5Ed8fmY+D61A5/7WMIVxq/uDLFvxCnRG +QcLbtqbPztiWwWZOuTwNTA3bfAhEG0ZzNr+0z33jW5T9ChvdutgxJMf3Khazx9cx +mWyCpJwtSv7hSbp4nCS2fmHCum2yIrOnou8TSOlQFERZ3UEZMgLpWeupH/W5C3Ad +rOspFrK6K8a/iNSs5OdYUIK2iHddTs5u7AEZ9I5MTuYnccuGuXfQTTA06TJvJTax +c2oDbXMnXs4pHLiiSRp34IHIYubdrj8X5vTODC5djl8h1167ToXo5zGdXqT1om+u +4ndNLbbI1vld5G7KAL6TlTETg+N7S8v3KYoBEWzykwgqqppWnWTqPWQxM8Iph5ly +AQlzz7feERi/h/s57RZ5ksoVAdbtk2U6wgHnLrWhKZ7+ZOAfpNAjGHwWyXTzylXo +zQ9A8Kmd0jBMsru4fsGpldf4lTsqO/abUSWrAAREGnlz/ZjEb944Yox7JUhWC15C +WxXK2rFbiF3S0HtEvU17rdn4HCsZBilnY+hTpHj1MA6O451/A3ghxGXFKz/9LUcS +YBRQJaSM3hTqC3WoTVBeVc5nCFOpu4F89JqhEgXOLKweueMbTMRSNm93tXWT13s3 +Q/o0pNJv/K6+bIQwsX/oDafMXcW7STxQJObbAleRbcn8/rGS2eEnVZ6907faUR/L +7eu9vgAa/jh9FHpZ0Q== +-----END ENCRYPTED PRIVATE KEY----- diff --git a/libs/ssl-config/src/test/resources/certs/pem-utils/key_unsupported.pem b/libs/ssl-config/src/test/resources/certs/pem-utils/key_unsupported.pem new file mode 100644 index 0000000000000..96f95848d099f --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/pem-utils/key_unsupported.pem @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACBqIPMG94HL7zedFzsvi45mHS8ZuyLQXqvHpHobcdNCJAAAAJimRM7VpkTO +1QAAAAtzc2gtZWQyNTUxOQAAACBqIPMG94HL7zedFzsvi45mHS8ZuyLQXqvHpHobcdNCJA +AAAEBvVc8FVPGUs3LZ1o+LnjW4uUlEnk/5LQQ9yO2eiI3SFGog8wb3gcvvN50XOy+LjmYd +Lxm7ItBeq8ekehtx00IkAAAAEWlvYW5uaXNAc2VjdXJlYm94AQIDBA== +-----END OPENSSH PRIVATE KEY----- diff --git a/libs/ssl-config/src/test/resources/certs/pem-utils/rsa_key_pkcs8_plain.pem b/libs/ssl-config/src/test/resources/certs/pem-utils/rsa_key_pkcs8_plain.pem new file mode 100644 index 0000000000000..dd1675957f69f --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/pem-utils/rsa_key_pkcs8_plain.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDesZnVBuxbT4y7 +KtIuYx8MUq0sGQgVbxXSBG66sWDU9Qoo1HUyra0xXCONgRMBT9RjSIpk7OOC9g8q +ENNgFO179YdHVkrgJhW/tNBf+C0VAb+B79zu7SwtyH2nt9t378dmItL+sERkMiiG ++BS/O+cDz44hifDiS7Eqj/mJugAhLjWSUyD+UBObxXvUsxjryKeG3vX9mRCgAcqB +xH3PjI1i9DVaoobwMbwpE5eW2WXexOspuXnMmGfrrR6z/VmdHqe/C3rGdJOX+Y0c +yOR+/Vuzisn+nLeo/GJx2hIif8rKiNRyAdUXfx+4DLYJBN2NUbl9aP2LP6ZC8ubf +6qwhhB0XAgMBAAECggEBAKuzP6qSNfaJNTayY2/EmRHFRSP1ANiV17sgE8f6L3DC +pdypQtuaMSkXo4nc9SxTwqvyKFJ8m0ZENZj3dCJmwFyNCIqmLAD7HFW9MdRs40WJ +HYEv0aaeUyvRo6CHD74/r/w96XTZr0GZssmtyUFRDGNRyoJter7gIW9xprLcKHFr +YTmdaAXbOm5W/K3844EBouTYzYnZYWQjB3jT/g5dIic3AtLb5YfGlpaXXb74xTOU +BqY1uKonGiDCh0aXXRl2Ucyre6FWslNNy4cAAXm6/5GT6iMo7wDXQftvtyK2IszP +IFcOG6xcAaJjgZ5wvM3ch0qNhQi4vL7c4Bm5JS9meoECgYEA88ItaVrfm2osX/6/ +fA8wYxxYU5RQRyOgLuzBXoRkISynLJaLVj2gFOQxVQeUK++xK6R182RQatOJcWFT +WwmIL3CchCwnnXgPvMc51iFKY94DbdvrRatP8c5sSk7IQlpS3aVa7f7DCqexggr5 +3PYysuiLirL+n9I1oZiUxpsS6/cCgYEA6eCcDshQzb7UQfWy//BRMp7u6DDuq+54 +38kJIFsPX0/CGyWsiFYEac8VH7jaGof99j7Zuebeb50TX57ZCBEK2LaHe474ggkY +GGSoo3VWBn44A1P5ADaRGRwJ4/u79qAg0ldnyxFHWtW+Wbn11DoOg40rl+DOnFBJ +W+bWJn4az+ECgYEAzWduDt5lmLfiRs4LG4ZNFudWwq8y6o9ptsEIvRXArnfLM3Z0 +Waq6T4Bu1aD6Sf/EAuul/QAmB67TnbgOnqMsoBU7vuDaTQZT9JbI9Ni+r+Lwbs2n +tuCCEFgKxp8Wf1tPgriJJA3O2xauLNAE9x57YGk21Ry6FYD0coR5sdYRHscCgYEA +lGQM4Fw82K5RoqAwOK/T9RheYTha1v/x9ZtqjPr53/GNKQhYVhCtsCzSLFRvHhJX +EpyCLK/NRmgVWMBC2BloFmSJxd3K00bN4PxM+5mBQZFoHMR04qu8mH/vzpV0h2DG +Mm9+zZti+MFRi0CwNz2248T4ed8LeKaARS1LhxTQEkECgYBFsPNkfGWyP4zsgzFs +3tMgXnIgl3Lh+vnEIzVakASf3RZrSucJhA713u5L9YB64wPdVJp4YZIoEmHebP9J +Jt1f9ghcWk6ffUVBQJPmWuRbB/BU8SI+kgtf50Jnizbfm5qoQEt2UdGUbwU3P1+t +z4SnBvIZ3b2inN+Hwdm5onOBlw== +-----END PRIVATE KEY----- diff --git a/libs/ssl-config/src/test/resources/certs/pem-utils/testnode-aes128.pem b/libs/ssl-config/src/test/resources/certs/pem-utils/testnode-aes128.pem new file mode 100644 index 0000000000000..b4448ec8afaf3 --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/pem-utils/testnode-aes128.pem @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,AD45A956510B909DCCACCE07DE3BA1C2 + +Vk+KErTbsSdjNO5vaCpik/OLkaOQ4Fm3rNIUrQPMEBiK/TXnHMvC/X1DZenSwA8W +yHuSpoAAg/HjQv5UskRtn6Rt74ALViM4hO6BleNxr/8lIBZAeLNjqoGwf62MyExV +rraRhXvYepiTnVSQDYuTafxdIXqzg7O5kYcR46gpphXTjMWDMLxsEiKQ1u51lPFU +SzxSMGMKiJL3PAXuWyoKgUihw6sv+mVPzq4MVcZKTrlcNRGRFQWUhVzqNd5Qdx/v +vBUFbWVcMXx4tSsx/WtIOiUwZTbmLk4dpXysb0+Tp6lb+7AQ2RR+9tkBWEdBPUx9 +qkBfFdAvfnA5vKR0SwAZU0dFaDWlQD2ktCJv4hwPN0XYMIv5WW9HoA+R88y+dhHT +sYgM3eEusQv9byC+XCzxPNg40yC8X9TG2z2deMUl6ippsrTULPx1WaoLf12x1Yl3 +vZ7MFB2hvJmWYofjTVz7Xa3FMH1dhJgBTwpUY//EgPhSaTrEMGwrXJQk40nam/LX +KjK/acvYmZHZZZJ+E0Pv481tFiiWVlXqfI9Tw1ffi4EzezhQTtzz2EBHaanHNEFa +C+7XQnxmBoNPpwOBh4Lh9oLcDN9uOGBLb+dIzn2cNQZXhBCKI8IV14YtZGZYhRHg +D+q7V6I/lEd1WNerHZRNI9o4ZBTJl+7GlJ1gveDTdcx28hCdC5oae6ZwIzSZPuDA +JPF3vr2yci7JsUpBqnaSnxpz5eKYbng3WjqweBXNgRWLhF8HT8fmWNJyvYjWpg+x +c8vh/FEM1HY3jsxE8NtIAlObJDMm/K/k8keVbbGm8c58oKdO4kdM+Z6aLe53nFo8 +ISwxsps//eak25Rx2H0bNvO4LVhqNHPXyYQ2nqtx7UpEgndrggHP7n3vcjtdE1f3 +N83gSm6SIVIeQJom16Z5Cjm4PRvJltIf2njpLTeP43eMoYNNVSCr2iZyrSNXnEes +TI47HidjCNkCp5ahPnuzzyeBCo9L9x2odTNOrga8sBii7VQBE3cGhAFkaUf0E6os +gpPqUWHkXE9Nb6H6EBR4gwbdpUqcgrm+kp6Ei5N/z7gSfV91WO45WmMLpCPmlPDQ +An+drt25y+AhaIEmoczUGAiz3jOdyd6Xqw+dVXGb9WPxXL4YnXgr4mSC2am9Vad6 +/MgIqYfqA/AOW1wY2dhoqfAGG2ITadFh82W6cqMhmeDQtDFb6/s96O4e46zev+fP +Nhro3k+JnL3InC9qAvkxEa/FpbL205X3X3FTXM6xK9ZDvq8+hbPxCjg73mXQfbbG +0/M8hE5hDgILTPiHhHFzGVNjYTAvjNnttg1n7+A52WGs+Hfwlf2x10p8Y2YwyOon +qfEMM3G1C3sDzEYmo+w0IZ+pesMWejMPOFiHYRCWVl8r5jx9lTSvbB3Xj+0Ygyo9 +15iLEGyr623I8LDBegqpNntlhX+AeHuJcthPRB6Jl2S0Q1xikD4fW1Ge29/l9Ndi +7TvZoSGh1jfA71pE1Ay2RyH5PMNj8KJvTGZPFEuIdzDUKlJkC3xUEvl6Q0prU171 +d/ka98AxLR9jUur0ARqxsckd5IXDTlZqsRs8W/gk5FP9RibiN7upiJcKgwYddiJx +-----END RSA PRIVATE KEY----- diff --git a/libs/ssl-config/src/test/resources/certs/pem-utils/testnode-aes192.pem b/libs/ssl-config/src/test/resources/certs/pem-utils/testnode-aes192.pem new file mode 100644 index 0000000000000..4696e77bd3f64 --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/pem-utils/testnode-aes192.pem @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-192-CBC,FACBF3734C8DD3C53F31E294D7E8DC16 + +9g2VpXQljNeeag2/jh0b1aKE+xcbkNKfIMeUljhiOxULegO53Apn/THshhJhtgPG +VYRlmk1ImCnwbWiy4C7WVXbOh1yGbYMPLipbtjEI7dr7OPbRX+GYn2Sln6iW9K61 +A019xPz1dLJ4bciNf5gcq5Wf/Qxj8R33ZPqANIHyMeZDSdGqFu+BQyQuQtJqFLkv +nokev80VIRuxinfmV3RSdUHo3g7iXRNq10bwxV+fns5fyzm5eq4q8Ac0M2NbhWds +wVl2gft73W41nXFqgS17Xo7cuAIdE07EGXVOq7UGKwLvAkgRWhZEt0BJZKB3XQAs +GfApMSOfIfTIS0YFjmkbGMKfprc8cgqPyDafKLDAGwViTWfM5oO2duium7OjV80g +eaL6iAImxFzfg3n8hsHg31iisM5p6d9VegXlY7YacdkFR11LN47nXoFU9l9vtKPG +TSouB4/0Dw4eCxmfbmJiO4pe8jn4pk4XhMszqc0Q+fRkHXeEigQgFsI4SSkuNk7r +EPSMqPSHpB5SkLyccfvd/wSBv1DvfdMIA5+CUUj3qAT7pm6tvtj0ZnXXnUVexlfp +9+mPMrP0oJ8fSX5kQksCbw+a4C+1ffCzU4S1CUVKboopHzbU2LG80XvjPqXGj2OL +++ghD7OjcD7DqWkO81FQPadrHqWMa8gf2rHmuamZh58LIpattu99lIHVHfFJhYlg +s8EEJQRLa7V4/1Mx9uZGKNmjHNzw/QGW5VqZ3MoVTuXQ3uKmfsXdUTpGRszkJzU1 +zpIOGOMWctWcCmTXpYEhYfiNcPK/WyHntlQJpUgutX/Pho4Q9dP0U1fgsHiKTcRJ +IAg3/pdCiv48K3Wx8Ib+J09mx4wP0rYnaT6f3LSTV+O8u+D8swjngDJ9vYOnyBQt +Z5nYrCpQcvaTGhWAQdz9OqAmPwjY7aLn3hbT0Jf3aFxH3uiWJi0UE3ahLhNWiDTU +PT1VtQ1fSt/ZpJM6KduR1aBFYcEyPIE/MQq9Y2jcYKrIyc4OqkZBwVOFZtRx8cQ7 +tsy1iY3FJjKllp1VdDKRtPs1oKqyX3k446iYryjZs3cDbWV+H5MSwxh7yqw+j5qE +XfvhaImoDFAEisep+w2i7nu80D5uNhFr9bHC/MnRCVlzO1HfrNNns1Oncey1ebJL +PSmpYAiArym6m6fIM9EtTtUrkNUmU0LeqfAaDUmGgtufmmExOtH7/pEuOfbCzoO9 +ZX+TMBRMlOGg55Wc+J597AyEg9mqGKqgoPF8Si2qEElOFYVlaZ88YGPaXKLKI2DA +T7LXYlf+njThf948xsgM41JxE2VG6Ibo3ucHXFEF+QVk+Arrv8jQEGNc1n6cv4Ep +ICoWwHAWN4gvACBi6V0C8Mb5V9cRL6hkCsVZUyOGOKm580qiakxmUe+xGHuMW7Cs +208L5Lsgnn4ynRKLT0yfup73XdQzut/Bkws4ECdDSoSH45VNMR7bdjoGsWkCn5n/ +gbU8PWTPYL907KLpwRBx8fvmOgP2lLBj2gmwyJeowRlzc1MLtsUnH/7H2YSQJgbX +0ZKIRHASwjpnlL4uhp1QMn9Nj9H++MiJ59q7kUmZBJstlbyAw11HkP4cwCIccNO4 +-----END RSA PRIVATE KEY----- diff --git a/libs/ssl-config/src/test/resources/certs/pem-utils/testnode-aes256.pem b/libs/ssl-config/src/test/resources/certs/pem-utils/testnode-aes256.pem new file mode 100644 index 0000000000000..64c765456a1ff --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/pem-utils/testnode-aes256.pem @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,134008CB231A5AD0F27EB8F6FB18A873 + +aJRdAed/XZ+Rl6/s/TwOw8rj+sw2ficvnKjCVJj5wt0+qD2NumPpkXmK9J0+SP21 +Mzzm8H0pQRWrI78vwfFXUxUmQMAuavB9k8HuvZtj1b4GvfHrT/BBbs5wS0RPbE6N +xZuvTvr5UMYFsP85lotcooau3CLtkVXz9ucMQv9v1r2dBvq/7owzl3M+AxhS1oU2 +f8qc3Q411RhVQl29tZha9gidfzBvOO2HH8AqjHxWMHw448oo/b+fXVrpezD1LkmP +0JxP+kJDt1KCiwXj7oRAMaHbHemA2HS713TK+6HammQroF0PCB33Dasy6zaPmP5G +HiJAHvBiblc+vCT7D1lUQCmbjRmeoSESq/P3l8Jhag+wT8SSm5nGaiX7aYHqc00Q +17Gw5e8/iWOU+c3DjCH5qXZFxVrpJgSvVBrrnF3y4sQCG41QpPC7X3mWYWLHZ5vX +GxcI4f1aJ+jECDTvdpE9KL6ncZ05p3A3wr+FqrDPJTb+S1mpD0f6lRhnKILXK83N ++EbRVRTCH5QIx5ZepX28ykuZQa6vHGtnL9WXLX4ZgAIe2abMA5hNs72Hi47LUrss +lA3gMdydKA/WtoimBLqb9brEy9qFsP/2YatKnyXYkjeCgtTQ5LELWSqnFkzQ51wk +VPhT8SqXcPIe9rrNmf7xwJvcZ0IS4tEkT+TovFAs1lo86bCx7VKfWfxcWG/FvW1L +5/1tU4uhpXLOjhOvWOx56zqxt9RORMw3SEh3At4vVHqT2xQAStT1d9QU0/QiM3EE +pBf9uQjRfzlwXph6Gs8XmQYLjSwHurT8hrkoa4/czhE4v7BTst+q6fB3gtxOgV6z +GVBsRK0Lz0ldd56UvnzyChUpE8EFE/Kv6P7T8cgTPnTcGchO4hcKyC31doAFn0pU +LURMC5szvRUEHbPriz9/9qeHBLFMAmGkCfXpwjNoynsKA7/VlAd/44CP82Ljd8Fb +PdwXjz8JNAL+gg3q8Xz4S+z6ZNXVJ1U9GDxjesp7QRbhl1J/ynsGyqIADUmPKjyk +8yFihQYBiZdgiYaOBl9F2X0SINUKaANmVO7HJG+WbPs68fcObfFHRWugC7FljY+b +Az6tNhkKVerCXBEMsZ9XNY05SsyAvcKsWcJbxon9ecIeu7/N8k9eseUL0xQg1oQX +L6wjgmS2ckpPnKVFPXhujZb45PtYEA2ObGd6fPV+82cSgfFM6sPorAmmFhThBXa+ +nE8o72MPVvdUFas3Fs7YugxeFTh9jO4zp/3XA+fFfpxPQbwWjnjxS87OAB+AF6iy +Ul/jZP46kDOnyLdMLvSf5Oq8A73bdGa/09ODsoWjrXlyYmfUZPKPGQ5Hbs5cUSvs +GciJvb3o3OYfSjkn6DVF95f53TiJ9pbGY+zG85f/F3BwbqpRmNYLyxvl4ZzjLs+U +PN24gC78ROzgvHAhY0Ta6PQw8SN5FEoQGmOQT2otZc+Apu1J1Z85mpxd0dYPh29m +kWvx13gZSGxCvNttqfqcRQJTOerQ4PRIyMDJG/sou8hDU51X9USAfjs1spuE6X/a +PIGNpM2TIOaqU/IIFJrGx01vVBhYGvYF8D/q+wwwnjJGYQl7Hscc+JdFmhWE0T2R +-----END RSA PRIVATE KEY----- diff --git a/libs/ssl-config/src/test/resources/certs/pem-utils/testnode-unprotected.pem b/libs/ssl-config/src/test/resources/certs/pem-utils/testnode-unprotected.pem new file mode 100644 index 0000000000000..1602461b11517 --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/pem-utils/testnode-unprotected.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA3rGZ1QbsW0+MuyrSLmMfDFKtLBkIFW8V0gRuurFg1PUKKNR1 +Mq2tMVwjjYETAU/UY0iKZOzjgvYPKhDTYBTte/WHR1ZK4CYVv7TQX/gtFQG/ge/c +7u0sLch9p7fbd+/HZiLS/rBEZDIohvgUvzvnA8+OIYnw4kuxKo/5iboAIS41klMg +/lATm8V71LMY68inht71/ZkQoAHKgcR9z4yNYvQ1WqKG8DG8KROXltll3sTrKbl5 +zJhn660es/1ZnR6nvwt6xnSTl/mNHMjkfv1bs4rJ/py3qPxicdoSIn/KyojUcgHV +F38fuAy2CQTdjVG5fWj9iz+mQvLm3+qsIYQdFwIDAQABAoIBAQCrsz+qkjX2iTU2 +smNvxJkRxUUj9QDYlde7IBPH+i9wwqXcqULbmjEpF6OJ3PUsU8Kr8ihSfJtGRDWY +93QiZsBcjQiKpiwA+xxVvTHUbONFiR2BL9GmnlMr0aOghw++P6/8Pel02a9BmbLJ +rclBUQxjUcqCbXq+4CFvcaay3Chxa2E5nWgF2zpuVvyt/OOBAaLk2M2J2WFkIwd4 +0/4OXSInNwLS2+WHxpaWl12++MUzlAamNbiqJxogwodGl10ZdlHMq3uhVrJTTcuH +AAF5uv+Rk+ojKO8A10H7b7citiLMzyBXDhusXAGiY4GecLzN3IdKjYUIuLy+3OAZ +uSUvZnqBAoGBAPPCLWla35tqLF/+v3wPMGMcWFOUUEcjoC7swV6EZCEspyyWi1Y9 +oBTkMVUHlCvvsSukdfNkUGrTiXFhU1sJiC9wnIQsJ514D7zHOdYhSmPeA23b60Wr +T/HObEpOyEJaUt2lWu3+wwqnsYIK+dz2MrLoi4qy/p/SNaGYlMabEuv3AoGBAOng +nA7IUM2+1EH1sv/wUTKe7ugw7qvueN/JCSBbD19PwhslrIhWBGnPFR+42hqH/fY+ +2bnm3m+dE1+e2QgRCti2h3uO+IIJGBhkqKN1VgZ+OANT+QA2kRkcCeP7u/agINJX +Z8sRR1rVvlm59dQ6DoONK5fgzpxQSVvm1iZ+Gs/hAoGBAM1nbg7eZZi34kbOCxuG +TRbnVsKvMuqPabbBCL0VwK53yzN2dFmquk+AbtWg+kn/xALrpf0AJgeu0524Dp6j +LKAVO77g2k0GU/SWyPTYvq/i8G7Np7bgghBYCsafFn9bT4K4iSQNztsWrizQBPce +e2BpNtUcuhWA9HKEebHWER7HAoGBAJRkDOBcPNiuUaKgMDiv0/UYXmE4Wtb/8fWb +aoz6+d/xjSkIWFYQrbAs0ixUbx4SVxKcgiyvzUZoFVjAQtgZaBZkicXdytNGzeD8 +TPuZgUGRaBzEdOKrvJh/786VdIdgxjJvfs2bYvjBUYtAsDc9tuPE+HnfC3imgEUt +S4cU0BJBAoGARbDzZHxlsj+M7IMxbN7TIF5yIJdy4fr5xCM1WpAEn90Wa0rnCYQO +9d7uS/WAeuMD3VSaeGGSKBJh3mz/SSbdX/YIXFpOn31FQUCT5lrkWwfwVPEiPpIL +X+dCZ4s235uaqEBLdlHRlG8FNz9frc+EpwbyGd29opzfh8HZuaJzgZc= +-----END RSA PRIVATE KEY----- diff --git a/libs/ssl-config/src/test/resources/certs/pem-utils/testnode.crt b/libs/ssl-config/src/test/resources/certs/pem-utils/testnode.crt new file mode 100644 index 0000000000000..08c160bcea5ff --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/pem-utils/testnode.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID0zCCArugAwIBAgIJALi5bDfjMszLMA0GCSqGSIb3DQEBCwUAMEgxDDAKBgNV +BAoTA29yZzEWMBQGA1UECxMNZWxhc3RpY3NlYXJjaDEgMB4GA1UEAxMXRWxhc3Rp +Y3NlYXJjaCBUZXN0IE5vZGUwHhcNMTUwOTIzMTg1MjU3WhcNMTkwOTIyMTg1MjU3 +WjBIMQwwCgYDVQQKEwNvcmcxFjAUBgNVBAsTDWVsYXN0aWNzZWFyY2gxIDAeBgNV +BAMTF0VsYXN0aWNzZWFyY2ggVGVzdCBOb2RlMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA3rGZ1QbsW0+MuyrSLmMfDFKtLBkIFW8V0gRuurFg1PUKKNR1 +Mq2tMVwjjYETAU/UY0iKZOzjgvYPKhDTYBTte/WHR1ZK4CYVv7TQX/gtFQG/ge/c +7u0sLch9p7fbd+/HZiLS/rBEZDIohvgUvzvnA8+OIYnw4kuxKo/5iboAIS41klMg +/lATm8V71LMY68inht71/ZkQoAHKgcR9z4yNYvQ1WqKG8DG8KROXltll3sTrKbl5 +zJhn660es/1ZnR6nvwt6xnSTl/mNHMjkfv1bs4rJ/py3qPxicdoSIn/KyojUcgHV +F38fuAy2CQTdjVG5fWj9iz+mQvLm3+qsIYQdFwIDAQABo4G/MIG8MAkGA1UdEwQC +MAAwHQYDVR0OBBYEFEMMWLWQi/g83PzlHYqAVnty5L7HMIGPBgNVHREEgYcwgYSC +CWxvY2FsaG9zdIIVbG9jYWxob3N0LmxvY2FsZG9tYWluggpsb2NhbGhvc3Q0ghds +b2NhbGhvc3Q0LmxvY2FsZG9tYWluNIIKbG9jYWxob3N0NoIXbG9jYWxob3N0Ni5s +b2NhbGRvbWFpbjaHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwDQYJKoZIhvcNAQEL +BQADggEBAMjGGXT8Nt1tbl2GkiKtmiuGE2Ej66YuZ37WSJViaRNDVHLlg87TCcHe +k2rdO+6sFqQbbzEfwQ05T7xGmVu7tm54HwKMRugoQ3wct0bQC5wEWYN+oMDvSyO6 +M28mZwWb4VtR2IRyWP+ve5DHwTM9mxWa6rBlGzsQqH6YkJpZojzqk/mQTug+Y8aE +mVoqRIPMHq9ob+S9qd5lp09+MtYpwPfTPx/NN+xMEooXWW/ARfpGhWPkg/FuCu4z +1tFmCqHgNcWirzMm3dQpF78muE9ng6OB2MXQwL4VgnVkxmlZNHbkR2v/t8MyZJxC +y4g6cTMM3S/UMt5/+aIB2JAuMKyuD+A= +-----END CERTIFICATE----- diff --git a/libs/ssl-config/src/test/resources/certs/pem-utils/testnode.jks b/libs/ssl-config/src/test/resources/certs/pem-utils/testnode.jks new file mode 100644 index 0000000000000..ebe6146124e8f Binary files /dev/null and b/libs/ssl-config/src/test/resources/certs/pem-utils/testnode.jks differ diff --git a/libs/ssl-config/src/test/resources/certs/pem-utils/testnode.pem b/libs/ssl-config/src/test/resources/certs/pem-utils/testnode.pem new file mode 100644 index 0000000000000..5a67e1033440d --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/pem-utils/testnode.pem @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,9D867F7E0C94D013 + +dVoVCjPeg1wgS7rVtOvGfQcrZyLkx393aWRnFq45tbjKBVuITtJ9vI7o4QXOV/15 +Gnb6WhXGIdWrzsxEAd46K6hIuNSISd4Emsx6c2Q5hTqWXXfexbOZBNfTtXtdJPnJ +1jAaikhtztLo3JSLTKNY5sNxd+XbaQyYVUWvueK6zOaIIMETvB+VPVFd9i1ROibk +Sgdtyj01KjkoalifqK/tA0CIYNKL0S6/eoK3UhAlpIprlpV+cnXa940C6bjLeJPt +PMAGGp5RrplxSgrSerw3I9DOWkHGtpqzIka3XneNUXJP8k4HUJ+aZkGH2ZILKS8d +4KMIb+KZSpHEGn+6uGccWLtZZmAjWJrDw56JbQtSHdRYLBRSOjLbTvQoPu/2Hpli +7HOxbotlvjptMunncq5aqK57SHA1dh0cwF7J3LUmGFJ67eoz+VV3b5qMn4MopSeI +mS16Ydd3nGpjSrln/elM0CQxqWfcOAXRZpDpFUQoXcBrLVzvz2DBl/0CrTRLhgzi +CO+5/IVcBWRlYpRNGgjjP7q0j6URID3jk5J06fYQXmBiwQT5j+GZqqzpMCJ9mIy2 +1O9SN1hebJnIcEU+E0njn/MGjlYdPywhaCy8pqElp6Q8TUEJpwLRFO/owCoBet/n +ZmCXUjfCGhc1pWHufFcDEQ6xMgEWWY/tdwCZeSU7EhErTjCbfupg+55A5fpDml0m +3wH4CFcuRjlqyx6Ywixm1ATeitDtJl5HQTw6b8OtEXwSgRmZ0eSqSRVk9QbVS7gu +IpQe09/Zimb5HzjZqZ3fdqHlcW4xax8hyJeyIvF5ZJ57eY8CBvu/wP2GDn26QnvF +xQqdfDbq1H4JmpwUHpbFwBoQK4Q6WFd1z4EA9bRQeo3H9PoqoOwMDjzajwLRF7b7 +q6tYH/n9PyHwdf1c4fFwgSmL1toXGfKlA9hjIaLsRSDD6srT5EdUk78bsnddwI51 +tu7C7P4JG+h1VdRNMNTlqtileWsIE7Nn2A1OkcUxZdF5mamENpDpJcHePLto6c8q +FKiwyFMsxhgsj6HK2HqO+UA4sX5Ni4oHwiPmb//EZLn045M5i1AN26KosJmb8++D +sgR5reWRy+UqJCTYblVg+7Dx++ggUnfxVyQEsWmw5r5f4KU5wXBkvoVMGtPNa9DE +n/uLtObD1qkNL38pRsr2OGRchYCgEoKGqEISBP4knfGXLOlWiW/246j9QzI97r1u +tvy7fKg28G7AUz9l6bpewsPHefBUeRQeieP9eJINaEpxkF/w2RpKDLpQjWxwDDOM +s+D0mrBMJve17AmJ8rMw6dIQPZYNZ88/jz1uQuUwQ2YlbmtZbCG81k9YMFGEU9XS +cyhJxj8hvYnt2PR5Z9/cJPyWOs0m/ufOeeQQ8SnU/lzmrQnpzUd2Z6p5i/B7LdRP +n1kX+l1qynuPnjvBz4nJQE0p6nzW8RyCDSniC9mtYtZmhgC8icqxgbvS7uEOBIYJ +NbK+0bEETTO34iY/JVTIqLOw3iQZYMeUpxpj6Phgx/oooxMTquMecPKNgeVtaBst +qjTNPX0ti1/HYpZqzYi8SV8YjHSJWCVMsZjKPr3W/HIcCKqYoIfgzi83Ha2KMQx6 +-----END RSA PRIVATE KEY----- diff --git a/libs/ssl-config/src/test/resources/certs/pem-utils/testnode_with_bagattrs.pem b/libs/ssl-config/src/test/resources/certs/pem-utils/testnode_with_bagattrs.pem new file mode 100644 index 0000000000000..ce8299cd070fc --- /dev/null +++ b/libs/ssl-config/src/test/resources/certs/pem-utils/testnode_with_bagattrs.pem @@ -0,0 +1,32 @@ +Bag Attributes + friendlyName: testnode_rsa + localKeyID: 54 69 6D 65 20 31 35 32 35 33 33 36 38 32 39 33 39 37 +Key Attributes: +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDesZnVBuxbT4y7 +KtIuYx8MUq0sGQgVbxXSBG66sWDU9Qoo1HUyra0xXCONgRMBT9RjSIpk7OOC9g8q +ENNgFO179YdHVkrgJhW/tNBf+C0VAb+B79zu7SwtyH2nt9t378dmItL+sERkMiiG ++BS/O+cDz44hifDiS7Eqj/mJugAhLjWSUyD+UBObxXvUsxjryKeG3vX9mRCgAcqB +xH3PjI1i9DVaoobwMbwpE5eW2WXexOspuXnMmGfrrR6z/VmdHqe/C3rGdJOX+Y0c +yOR+/Vuzisn+nLeo/GJx2hIif8rKiNRyAdUXfx+4DLYJBN2NUbl9aP2LP6ZC8ubf +6qwhhB0XAgMBAAECggEBAKuzP6qSNfaJNTayY2/EmRHFRSP1ANiV17sgE8f6L3DC +pdypQtuaMSkXo4nc9SxTwqvyKFJ8m0ZENZj3dCJmwFyNCIqmLAD7HFW9MdRs40WJ +HYEv0aaeUyvRo6CHD74/r/w96XTZr0GZssmtyUFRDGNRyoJter7gIW9xprLcKHFr +YTmdaAXbOm5W/K3844EBouTYzYnZYWQjB3jT/g5dIic3AtLb5YfGlpaXXb74xTOU +BqY1uKonGiDCh0aXXRl2Ucyre6FWslNNy4cAAXm6/5GT6iMo7wDXQftvtyK2IszP +IFcOG6xcAaJjgZ5wvM3ch0qNhQi4vL7c4Bm5JS9meoECgYEA88ItaVrfm2osX/6/ +fA8wYxxYU5RQRyOgLuzBXoRkISynLJaLVj2gFOQxVQeUK++xK6R182RQatOJcWFT +WwmIL3CchCwnnXgPvMc51iFKY94DbdvrRatP8c5sSk7IQlpS3aVa7f7DCqexggr5 +3PYysuiLirL+n9I1oZiUxpsS6/cCgYEA6eCcDshQzb7UQfWy//BRMp7u6DDuq+54 +38kJIFsPX0/CGyWsiFYEac8VH7jaGof99j7Zuebeb50TX57ZCBEK2LaHe474ggkY +GGSoo3VWBn44A1P5ADaRGRwJ4/u79qAg0ldnyxFHWtW+Wbn11DoOg40rl+DOnFBJ +W+bWJn4az+ECgYEAzWduDt5lmLfiRs4LG4ZNFudWwq8y6o9ptsEIvRXArnfLM3Z0 +Waq6T4Bu1aD6Sf/EAuul/QAmB67TnbgOnqMsoBU7vuDaTQZT9JbI9Ni+r+Lwbs2n +tuCCEFgKxp8Wf1tPgriJJA3O2xauLNAE9x57YGk21Ry6FYD0coR5sdYRHscCgYEA +lGQM4Fw82K5RoqAwOK/T9RheYTha1v/x9ZtqjPr53/GNKQhYVhCtsCzSLFRvHhJX +EpyCLK/NRmgVWMBC2BloFmSJxd3K00bN4PxM+5mBQZFoHMR04qu8mH/vzpV0h2DG +Mm9+zZti+MFRi0CwNz2248T4ed8LeKaARS1LhxTQEkECgYBFsPNkfGWyP4zsgzFs +3tMgXnIgl3Lh+vnEIzVakASf3RZrSucJhA713u5L9YB64wPdVJp4YZIoEmHebP9J +Jt1f9ghcWk6ffUVBQJPmWuRbB/BU8SI+kgtf50Jnizbfm5qoQEt2UdGUbwU3P1+t +z4SnBvIZ3b2inN+Hwdm5onOBlw== +-----END PRIVATE KEY----- diff --git a/settings.gradle b/settings.gradle index 40fb419b0bf3c..95285e20da6ce 100644 --- a/settings.gradle +++ b/settings.gradle @@ -96,6 +96,7 @@ if (isEclipse) { projects << 'libs:secure-sm-tests' projects << 'libs:grok-tests' projects << 'libs:geo-tests' + projects << 'libs:ssl-config' } include projects.toArray(new String[0]) @@ -134,7 +135,12 @@ if (isEclipse) { project(":libs:geo").projectDir = new File(rootProject.projectDir, 'libs/geo/src/main') project(":libs:geo").buildFileName = 'eclipse-build.gradle' project(":libs:geo-tests").projectDir = new File(rootProject.projectDir, 'libs/geo/src/test') - project(":libs:geo-tests").buildFileName = 'eclipse-build.gradle'} + project(":libs:geo-tests").buildFileName = 'eclipse-build.gradle' + project(":libs:ssl-config").projectDir = new File(rootProject.projectDir, 'libs/ssl-config/src/main') + project(":libs:ssl-config").buildFileName = 'eclipse-build.gradle' + project(":libs:ssl-config-tests").projectDir = new File(rootProject.projectDir, 'libs/ssl-config/src/test') + project(":libs:ssl-config-tests").buildFileName = 'eclipse-build.gradle' +} // look for extra plugins for elasticsearch File extraProjects = new File(rootProject.projectDir.parentFile, "${dirName}-extra") @@ -146,3 +152,4 @@ if (extraProjects.exists()) { project(":libs:cli").name = 'elasticsearch-cli' project(":libs:geo").name = 'elasticsearch-geo' +project(":libs:ssl-config").name = 'elasticsearch-ssl-config'