فهرست منبع

Add SSL Configuration Library (#37287)

This introduces a new ssl-config library that can parse
and validate SSL/TLS settings and files.

It supports the standard configuration settings as used in the
Elastic Stack such as "ssl.verification_mode" and
"ssl.certificate_authorities" as well as all file formats used
in other parts of Elasticsearch security (such as PEM, JKS,
PKCS#12, PKCS#8, et al).
Tim Vernum 6 سال پیش
والد
کامیت
6d99e790b3
70فایلهای تغییر یافته به همراه5044 افزوده شده و 1 حذف شده
  1. 42 0
      libs/ssl-config/build.gradle
  2. 128 0
      libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/DefaultJdkTrustConfig.java
  3. 297 0
      libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/DerParser.java
  4. 52 0
      libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/EmptyKeyConfig.java
  5. 163 0
      libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/KeyStoreUtil.java
  6. 122 0
      libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemKeyConfig.java
  7. 122 0
      libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemTrustConfig.java
  8. 613 0
      libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemUtils.java
  9. 101 0
      libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslClientAuthenticationMode.java
  10. 33 0
      libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigException.java
  11. 164 0
      libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfiguration.java
  12. 181 0
      libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationKeys.java
  13. 371 0
      libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationLoader.java
  14. 46 0
      libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslKeyConfig.java
  15. 46 0
      libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslTrustConfig.java
  16. 104 0
      libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslVerificationMode.java
  17. 106 0
      libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreKeyConfig.java
  18. 90 0
      libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreTrustConfig.java
  19. 92 0
      libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/TrustEverythingConfig.java
  20. 79 0
      libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/DefaultJdkTrustConfigTests.java
  21. 148 0
      libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/PemKeyConfigTests.java
  22. 150 0
      libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/PemTrustConfigTests.java
  23. 219 0
      libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/PemUtilsTests.java
  24. 220 0
      libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/SslConfigurationLoaderTests.java
  25. 140 0
      libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/SslConfigurationTests.java
  26. 215 0
      libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/StoreKeyConfigTests.java
  27. 169 0
      libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/StoreTrustConfigTests.java
  28. 75 0
      libs/ssl-config/src/test/resources/certs/README.txt
  29. BIN
      libs/ssl-config/src/test/resources/certs/ca-all/ca.jks
  30. BIN
      libs/ssl-config/src/test/resources/certs/ca-all/ca.p12
  31. 19 0
      libs/ssl-config/src/test/resources/certs/ca1/ca.crt
  32. 27 0
      libs/ssl-config/src/test/resources/certs/ca1/ca.key
  33. BIN
      libs/ssl-config/src/test/resources/certs/ca1/ca.p12
  34. 19 0
      libs/ssl-config/src/test/resources/certs/ca2/ca.crt
  35. 27 0
      libs/ssl-config/src/test/resources/certs/ca2/ca.key
  36. BIN
      libs/ssl-config/src/test/resources/certs/ca2/ca.p12
  37. 19 0
      libs/ssl-config/src/test/resources/certs/ca3/ca.crt
  38. 27 0
      libs/ssl-config/src/test/resources/certs/ca3/ca.key
  39. BIN
      libs/ssl-config/src/test/resources/certs/ca3/ca.p12
  40. BIN
      libs/ssl-config/src/test/resources/certs/cert-all/certs.jks
  41. BIN
      libs/ssl-config/src/test/resources/certs/cert-all/certs.p12
  42. 19 0
      libs/ssl-config/src/test/resources/certs/cert1/cert1.crt
  43. 27 0
      libs/ssl-config/src/test/resources/certs/cert1/cert1.key
  44. BIN
      libs/ssl-config/src/test/resources/certs/cert1/cert1.p12
  45. 19 0
      libs/ssl-config/src/test/resources/certs/cert2/cert2.crt
  46. 30 0
      libs/ssl-config/src/test/resources/certs/cert2/cert2.key
  47. BIN
      libs/ssl-config/src/test/resources/certs/cert2/cert2.p12
  48. 149 0
      libs/ssl-config/src/test/resources/certs/pem-utils/README.asciidoc
  49. 24 0
      libs/ssl-config/src/test/resources/certs/pem-utils/corrupted_key_pkcs8_plain.pem
  50. 15 0
      libs/ssl-config/src/test/resources/certs/pem-utils/dsa_key_openssl_encrypted.pem
  51. 12 0
      libs/ssl-config/src/test/resources/certs/pem-utils/dsa_key_openssl_plain.pem
  52. 18 0
      libs/ssl-config/src/test/resources/certs/pem-utils/dsa_key_openssl_plain_with_params.pem
  53. 9 0
      libs/ssl-config/src/test/resources/certs/pem-utils/dsa_key_pkcs8_plain.pem
  54. 7 0
      libs/ssl-config/src/test/resources/certs/pem-utils/ec_key_openssl_encrypted.pem
  55. 4 0
      libs/ssl-config/src/test/resources/certs/pem-utils/ec_key_openssl_plain.pem
  56. 7 0
      libs/ssl-config/src/test/resources/certs/pem-utils/ec_key_openssl_plain_with_params.pem
  57. 4 0
      libs/ssl-config/src/test/resources/certs/pem-utils/ec_key_pkcs8_plain.pem
  58. 0 0
      libs/ssl-config/src/test/resources/certs/pem-utils/empty.pem
  59. 29 0
      libs/ssl-config/src/test/resources/certs/pem-utils/key_pkcs8_encrypted.pem
  60. 7 0
      libs/ssl-config/src/test/resources/certs/pem-utils/key_unsupported.pem
  61. 28 0
      libs/ssl-config/src/test/resources/certs/pem-utils/rsa_key_pkcs8_plain.pem
  62. 30 0
      libs/ssl-config/src/test/resources/certs/pem-utils/testnode-aes128.pem
  63. 30 0
      libs/ssl-config/src/test/resources/certs/pem-utils/testnode-aes192.pem
  64. 30 0
      libs/ssl-config/src/test/resources/certs/pem-utils/testnode-aes256.pem
  65. 27 0
      libs/ssl-config/src/test/resources/certs/pem-utils/testnode-unprotected.pem
  66. 23 0
      libs/ssl-config/src/test/resources/certs/pem-utils/testnode.crt
  67. BIN
      libs/ssl-config/src/test/resources/certs/pem-utils/testnode.jks
  68. 30 0
      libs/ssl-config/src/test/resources/certs/pem-utils/testnode.pem
  69. 32 0
      libs/ssl-config/src/test/resources/certs/pem-utils/testnode_with_bagattrs.pem
  70. 8 1
      settings.gradle

+ 42 - 0
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'
+}

+ 128 - 0
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<String, String, String> 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<String, String, String> 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<String, String, String> 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<String, String, String> systemProperties) {
+        return systemProperties.apply("javax.net.ssl.trustStoreType", "").equalsIgnoreCase("PKCS11");
+    }
+
+    private static char[] getSystemTrustStorePassword(BiFunction<String, String, String> systemProperties) {
+        return systemProperties.apply("javax.net.ssl.trustStorePassword", "").toCharArray();
+    }
+
+    @Override
+    public Collection<Path> 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);
+    }
+}

+ 297 - 0
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.
+ * <p>
+ * 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.
+     * <p>
+     *          In BER/DER encoding, length can be encoded in 2 forms:
+     * </p>
+     * <ul>
+     * <li>Short form. One octet. Bit 8 has value "0" and bits 7-1
+     * give the length.
+     * </li>
+     * <li>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.
+     * </li>
+     * </ul>
+     *
+     * @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.
+         * <p>
+         *     The first byte in DER encoding is made of following fields:
+         * </p>
+         * <pre>
+         * -------------------------------------------------
+         * |Bit 8|Bit 7|Bit 6|Bit 5|Bit 4|Bit 3|Bit 2|Bit 1|
+         * -------------------------------------------------
+         * |  Class    | CF  |     +      Type             |
+         * -------------------------------------------------
+         * </pre>
+         * <ul>
+         * <li>Class: Universal, Application, Context or Private
+         * <li>CF: Constructed flag. If 1, the field is constructed.
+         * <li>Type: This is actually called tag in ASN.1. It
+         * indicates data type (Integer, String) or a construct
+         * (sequence, choice, set).
+         * </ul>
+         *
+         * @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();
+    }
+
+}

+ 52 - 0
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<Path> getDependentFiles() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public X509ExtendedKeyManager createKeyManager() {
+        return null;
+    }
+
+    @Override
+    public String toString() {
+        return "empty-key-config";
+    }
+}

+ 163 - 0
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 <em>file name</em> 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<Certificate> 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<Certificate> 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 + "]");
+    }
+
+
+}

+ 122 - 0
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<Path> getDependentFiles() {
+        return Arrays.asList(certificate, key);
+    }
+
+    @Override
+    public X509ExtendedKeyManager createKeyManager() {
+        PrivateKey privateKey = getPrivateKey();
+        List<Certificate> 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<Certificate> 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;
+    }
+}

+ 122 - 0
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<Path> 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
+     * <ol>
+     * <li>validation of the file (contents and accessibility) is deferred, and this constructor will <em>not fail</em> on missing
+     * of invalid files.</li>
+     * <li>
+     * 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.
+     * </li>
+     * </ol>
+     */
+    public PemTrustConfig(List<Path> certificateAuthorities) {
+        this.certificateAuthorities = Collections.unmodifiableList(certificateAuthorities);
+    }
+
+    @Override
+    public Collection<Path> getDependentFiles() {
+        return certificateAuthorities;
+    }
+
+    @Override
+    public X509ExtendedTrustManager createTrustManager() {
+        try {
+            final List<Certificate> 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<Certificate> 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(","));
+    }
+
+}

+ 613 - 0
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<char[]> 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<char[]> passwordSupplier) throws IOException,
+        GeneralSecurityException {
+        StringBuilder sb = new StringBuilder();
+        String line = bReader.readLine();
+        Map<String, String> 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<char[]> passwordSupplier) throws IOException,
+        GeneralSecurityException {
+        StringBuilder sb = new StringBuilder();
+        String line = bReader.readLine();
+        Map<String, String> 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<char[]> passwordSupplier) throws IOException,
+        GeneralSecurityException {
+        StringBuilder sb = new StringBuilder();
+        String line = bReader.readLine();
+        Map<String, String> 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<String, String> pemHeaders, String keyContents, Supplier<char[]> 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)
+     * <p>
+     * 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<Certificate> readCertificates(Collection<Path> certPaths) throws CertificateException, IOException {
+        CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+        List<Certificate> certificates = new ArrayList<>(certPaths.size());
+        for (Path path : certPaths) {
+            try (InputStream input = Files.newInputStream(path)) {
+                final Collection<? extends Certificate> 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);
+        }
+    }
+}

+ 101 - 0
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<String, SslClientAuthenticationMode> LOOKUP = Collections.unmodifiableMap(buildLookup());
+
+    static Map<String, SslClientAuthenticationMode> buildLookup() {
+        final Map<String, SslClientAuthenticationMode> 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;
+    }
+}

+ 33 - 0
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);
+    }
+}

+ 164 - 0
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<String> ciphers;
+    private final List<String> supportedProtocols;
+
+    public SslConfiguration(SslTrustConfig trustConfig, SslKeyConfig keyConfig, SslVerificationMode verificationMode,
+                            SslClientAuthenticationMode clientAuth, List<String> ciphers, List<String> 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<String> getCipherSuites() {
+        return ciphers;
+    }
+
+    public List<String> 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<Path> getDependentFiles() {
+        Set<Path> 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);
+    }
+}

+ 181 - 0
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<String> 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<String> 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<String> 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<String> 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);
+    }
+
+}

+ 371 - 0
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<String> DEFAULT_PROTOCOLS = Arrays.asList("TLSv1.2", "TLSv1.1", "TLSv1");
+    static final List<String> 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<String> defaultCiphers;
+    private List<String> 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<String> 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<String> 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()}).
+     * <p>
+     * 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()}).
+     * <p>
+     * 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<String> 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<String> protocols = resolveListSetting(PROTOCOLS, Function.identity(), defaultProtocols);
+        final List<String> 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<Path> 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> V resolveSetting(String key, Function<String, V> 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 <V> List<V> resolveListSetting(String key, Function<String, V> parser, List<V> defaultValue) {
+        try {
+            final List<String> 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<String> loadDefaultCiphers() {
+        final List<String> 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<String> 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<String> 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;
+        }
+    }
+}

+ 46 - 0
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<Path> getDependentFiles();
+
+    /**
+     * @return A new {@link X509ExtendedKeyManager}.
+     * @throws SslConfigException if there is a problem configuring the key manager.
+     */
+    X509ExtendedKeyManager createKeyManager();
+
+}
+

+ 46 - 0
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<Path> getDependentFiles();
+
+    /**
+     * @return A new {@link X509ExtendedTrustManager}.
+     * @throws SslConfigException if there is a problem configuring the trust manager.
+     */
+    X509ExtendedTrustManager createTrustManager();
+
+}
+

+ 104 - 0
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<String, SslVerificationMode> LOOKUP = Collections.unmodifiableMap(buildLookup());
+
+    private static Map<String, SslVerificationMode> buildLookup() {
+        Map<String, SslVerificationMode> 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;
+    }
+}

+ 106 - 0
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<Path> 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<String> 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);
+    }
+
+}

+ 90 - 0
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<Path> 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 ? "<empty>" : "<non-empty>") + "]", e);
+        }
+    }
+
+    /**
+     * Verifies that the keystore contains at least 1 trusted certificate entry.
+     */
+    private void checkTrustStore(KeyStore store) throws GeneralSecurityException {
+        Enumeration<String> 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);
+    }
+
+}

+ 92 - 0
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<Path> getDependentFiles() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public X509ExtendedTrustManager createTrustManager() {
+        return TRUST_MANAGER;
+    }
+
+    @Override
+    public String toString() {
+        return "trust everything";
+    }
+}

+ 79 - 0
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<String, String, String> 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<X509Certificate> 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 + "].");
+        }
+    }
+
+}

+ 148 - 0
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));
+    }
+}

+ 150 - 0
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<String> 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));
+    }
+}

+ 219 - 0
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<char[]> EMPTY_PASSWORD = () -> new char[0];
+    private static final Supplier<char[]> 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());
+        }
+    }
+}

+ 220 - 0
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<String> 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());
+    }
+}

+ 140 - 0
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<String> ciphers = randomSubsetOf(randomIntBetween(1, DEFAULT_CIPHERS.size()), DEFAULT_CIPHERS);
+        final List<String> 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<String> ciphers = randomSubsetOf(randomIntBetween(1, DEFAULT_CIPHERS.size() - 1), DEFAULT_CIPHERS);
+        final List<String> 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);
+    }
+
+}

+ 215 - 0
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"));
+    }
+}

+ 169 - 0
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<String> 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()));
+    }
+
+}

+ 75 - 0
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
+

BIN
libs/ssl-config/src/test/resources/certs/ca-all/ca.jks


BIN
libs/ssl-config/src/test/resources/certs/ca-all/ca.p12


+ 19 - 0
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-----

+ 27 - 0
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-----

BIN
libs/ssl-config/src/test/resources/certs/ca1/ca.p12


+ 19 - 0
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-----

+ 27 - 0
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-----

BIN
libs/ssl-config/src/test/resources/certs/ca2/ca.p12


+ 19 - 0
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-----

+ 27 - 0
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-----

BIN
libs/ssl-config/src/test/resources/certs/ca3/ca.p12


BIN
libs/ssl-config/src/test/resources/certs/cert-all/certs.jks


BIN
libs/ssl-config/src/test/resources/certs/cert-all/certs.p12


+ 19 - 0
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-----

+ 27 - 0
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-----

BIN
libs/ssl-config/src/test/resources/certs/cert1/cert1.p12


+ 19 - 0
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-----

+ 30 - 0
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-----

BIN
libs/ssl-config/src/test/resources/certs/cert2/cert2.p12


+ 149 - 0
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 <NAME>.cert -keyout <NAME>.pem -days 1460 -config config.cnf
+-----------------------------------------------------------------------------------------------------------
+
+When prompted the password is always set to the value of <NAME>.
+
+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 <NAME> -in <NAME>.cert -inkey <NAME>.pem -out <NAME>.p12
+-----------------------------------------------------------------------------------------------------------
+
+== Creating the Keystore
+We need to create a keystore from the created PKCS12 certificate.
+
+[source,shell]
+-----------------------------------------------------------------------------------------------------------
+keytool -importkeystore -destkeystore <NAME>.jks -srckeystore <NAME>.p12 -srcstoretype pkcs12 -alias <NAME>
+-----------------------------------------------------------------------------------------------------------
+
+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
+------

+ 24 - 0
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

+ 15 - 0
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-----

+ 12 - 0
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-----

+ 18 - 0
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-----

+ 9 - 0
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-----

+ 7 - 0
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-----

+ 4 - 0
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-----

+ 7 - 0
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-----

+ 4 - 0
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-----

+ 0 - 0
libs/ssl-config/src/test/resources/certs/pem-utils/empty.pem


+ 29 - 0
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-----

+ 7 - 0
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-----

+ 28 - 0
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-----

+ 30 - 0
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-----

+ 30 - 0
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-----

+ 30 - 0
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-----

+ 27 - 0
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-----

+ 23 - 0
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-----

BIN
libs/ssl-config/src/test/resources/certs/pem-utils/testnode.jks


+ 30 - 0
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-----

+ 32 - 0
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: <No 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-----

+ 8 - 1
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'