diff --git a/Docs/CERTIFICATE_GENERATION.md b/Docs/CERTIFICATE_GENERATION.md new file mode 100644 index 00000000..43987c9c --- /dev/null +++ b/Docs/CERTIFICATE_GENERATION.md @@ -0,0 +1,137 @@ +# Certificate Generation + +Use the script below to generate certificates for running an extension. +The certificate for the extension needs to be in the same chain as the +root certificate for the OpenSearch cluster defined in the `plugins.security.ssl.http.pemtrustedcas_filepath` +setting of `opensearch.yml` + + +## Certificate Generation Script + +``` +#! /bin/bash + +openssl genrsa -out root-ca-key.pem 2048 +openssl req -new -x509 -sha256 -key root-ca-key.pem -subj "/C=US/ST=NEW YORK/L=BROOKLYN/O=OPENSEARCH/OU=SECURITY/CN=ROOT" -out root-ca.pem -days 730 + +openssl genrsa -out extension-01-key-temp.pem 2048 +openssl pkcs8 -inform PEM -outform PEM -in extension-01-key-temp.pem -topk8 -nocrypt -v1 PBE-SHA1-3DES -out extension-01-key.pem +openssl req -new -key extension-01-key.pem -subj "/C=US/ST=NEW YORK/L=BROOKLYN/O=OPENSEARCH/OU=SECURITY/CN=extension-01" -out extension-01.csr +echo 'subjectAltName=DNS:extension-01' | tee -a extension-01.ext +echo 'subjectAltName=IP:172.20.0.11' | tee -a extension-01.ext +openssl x509 -req -in extension-01.csr -CA root-ca.pem -CAkey root-ca-key.pem -CAcreateserial -sha256 -out extension-01.pem -days 730 -extfile extension-01.ext + +rm extension-01-key-temp.pem +rm extension-01.csr +rm extension-01.ext +rm root-ca.srl +``` + +## Certificate Generation Script for OpenSearch with single node and admin cert + +``` +#! /bin/bash + +openssl genrsa -out admin-key-temp.pem 2048 +openssl pkcs8 -inform PEM -outform PEM -in admin-key-temp.pem -topk8 -nocrypt -v1 PBE-SHA1-3DES -out admin-key.pem +openssl req -new -key admin-key.pem -subj "/C=US/ST=NEW YORK/L=BROOKLYN/O=OPENSEARCH/OU=SECURITY/CN=A" -out admin.csr +openssl x509 -req -in admin.csr -CA root-ca.pem -CAkey root-ca-key.pem -CAcreateserial -sha256 -out admin.pem -days 730 +openssl genrsa -out os-node-01-key-temp.pem 2048 +openssl pkcs8 -inform PEM -outform PEM -in os-node-01-key-temp.pem -topk8 -nocrypt -v1 PBE-SHA1-3DES -out os-node-01-key.pem +openssl req -new -key os-node-01-key.pem -subj "/C=US/ST=NEW YORK/L=BROOKLYN/O=OPENSEARCH/OU=SECURITY/CN=os-node-01" -out os-node-01.csr +echo 'subjectAltName=DNS:os-node-01' | tee -a os-node-01.ext +echo 'subjectAltName=IP:172.20.0.11' | tee -a os-node-01.ext +openssl x509 -req -in os-node-01.csr -CA root-ca.pem -CAkey root-ca-key.pem -CAcreateserial -sha256 -out os-node-01.pem -days 730 -extfile os-node-01.ext + +rm admin-key-temp.pem +rm admin.csr +rm os-node-01-key-temp.pem +rm os-node-01.csr +rm os-node-01.ext +rm root-ca.srl +``` + +## Install Security plugin and run in SSL only mode + +To test an extension running with SSL and connected to an OpenSearch node with SSL you must install +the security plugin in the OpenSearch node and run it in SSL only mode. + +Follow the steps below to test an extension running with TLS and connect to an OpenSearch node with +the security plugin and SSL enabled: + +1. Create a local distribution of [OpenSearch](https://github.com/opensearch-project/opensearch) and move the output to location you would like to install +OpenSearch into: + +``` +cd opensearch +./gradlew localDistro +mv distribution/archives/darwin-tar/build/install/opensearch--SNAPSHOT/ ~/opensearch +``` + +2. Assemble the [Security plugin](https://github.com/opensearch-project/security) and move the output to the same directory + +``` +cd security +./gradlew assemble +mv build/distributions/opensearch-security-.0-SNAPSHOT.zip ~/opensearch +``` + +3. Install the Security plugin and associated certificates created above + +3.1. Navigate to the root of the OpenSearch installation: + +``` +cd ~/opensearch/opensearch--SNAPSHOT/ +./bin/opensearch-plugin install file:$HOME/opensearch/opensearch-security-.0-SNAPSHOT.zip +``` + +3.2 Copy the certificates generated above into `config/` directory + +The certificates needed are: + +- `os-node-01.pem` +- `os-node-01-key.pem` +- `root-ca.pem` + +3.3 Add settings in `opensearch.yml` + +Add the following settings in `opensearch.yml` + +``` +opensearch.experimental.feature.extensions.enabled: true +plugins.security.ssl_only: true +plugins.security.ssl.transport.pemcert_filepath: os-node-01.pem +plugins.security.ssl.transport.pemkey_filepath: os-node-01-key.pem +plugins.security.ssl.transport.pemtrustedcas_filepath: root-ca.pem +plugins.security.ssl.transport.enforce_hostname_verification: false +plugins.security.ssl.http.enabled: true +plugins.security.ssl.http.pemcert_filepath: os-node-01.pem +plugins.security.ssl.http.pemkey_filepath: os-node-01-key.pem +plugins.security.ssl.http.pemtrustedcas_filepath: root-ca.pem +network.host: 0.0.0.0 +``` + +4. Install certificates on extension + +Installation for the OpenSearch node is now complete, the rest of the installation is for the extension. + +Create a `config/` folder in the extension's home directory and install the certificates generated above. + +The certificates you need to add are: + +- `extension-01.pem` +- `extension-01-key.pem` +- `root-ca.pem` + +4.1 Add references to these certifications in extension settings file and enable SSL + +Add the following settings to the extension setting files. i.e. `helloworld-settings.yml` + +``` +ssl.transport.enabled: true +ssl.transport.pemcert_filepath: extension-01.pem +ssl.transport.pemkey_filepath: extension-01-key.pem +ssl.transport.pemtrustedcas_filepath: root-ca.pem +ssl.transport.enforce_hostname_verification: false +path.home: +``` \ No newline at end of file diff --git a/src/main/java/org/opensearch/sdk/ExtensionSettings.java b/src/main/java/org/opensearch/sdk/ExtensionSettings.java index afc70bf3..cba0ae16 100644 --- a/src/main/java/org/opensearch/sdk/ExtensionSettings.java +++ b/src/main/java/org/opensearch/sdk/ExtensionSettings.java @@ -15,10 +15,34 @@ import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; +import java.util.HashMap; import java.util.Map; +import java.util.Set; import org.yaml.snakeyaml.Yaml; +import static org.opensearch.sdk.ssl.SSLConfigConstants.SSL_TRANSPORT_CLIENT_PEMCERT_FILEPATH; +import static org.opensearch.sdk.ssl.SSLConfigConstants.SSL_TRANSPORT_CLIENT_PEMKEY_FILEPATH; +import static org.opensearch.sdk.ssl.SSLConfigConstants.SSL_TRANSPORT_CLIENT_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.sdk.ssl.SSLConfigConstants.SSL_TRANSPORT_ENABLED; +import static org.opensearch.sdk.ssl.SSLConfigConstants.SSL_TRANSPORT_ENABLED_CIPHERS; +import static org.opensearch.sdk.ssl.SSLConfigConstants.SSL_TRANSPORT_ENABLED_PROTOCOLS; +import static org.opensearch.sdk.ssl.SSLConfigConstants.SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION; +import static org.opensearch.sdk.ssl.SSLConfigConstants.SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME; +import static org.opensearch.sdk.ssl.SSLConfigConstants.SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED; +import static org.opensearch.sdk.ssl.SSLConfigConstants.SSL_TRANSPORT_KEYSTORE_ALIAS; +import static org.opensearch.sdk.ssl.SSLConfigConstants.SSL_TRANSPORT_KEYSTORE_FILEPATH; +import static org.opensearch.sdk.ssl.SSLConfigConstants.SSL_TRANSPORT_KEYSTORE_TYPE; +import static org.opensearch.sdk.ssl.SSLConfigConstants.SSL_TRANSPORT_PEMCERT_FILEPATH; +import static org.opensearch.sdk.ssl.SSLConfigConstants.SSL_TRANSPORT_PEMKEY_FILEPATH; +import static org.opensearch.sdk.ssl.SSLConfigConstants.SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.sdk.ssl.SSLConfigConstants.SSL_TRANSPORT_SERVER_PEMCERT_FILEPATH; +import static org.opensearch.sdk.ssl.SSLConfigConstants.SSL_TRANSPORT_SERVER_PEMKEY_FILEPATH; +import static org.opensearch.sdk.ssl.SSLConfigConstants.SSL_TRANSPORT_SERVER_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.sdk.ssl.SSLConfigConstants.SSL_TRANSPORT_TRUSTSTORE_ALIAS; +import static org.opensearch.sdk.ssl.SSLConfigConstants.SSL_TRANSPORT_TRUSTSTORE_FILEPATH; +import static org.opensearch.sdk.ssl.SSLConfigConstants.SSL_TRANSPORT_TRUSTSTORE_TYPE; + /** * This class encapsulates the settings for an Extension. */ @@ -30,6 +54,33 @@ public class ExtensionSettings { private String opensearchAddress; private String opensearchPort; + public static final Set SECURITY_SETTINGS_KEYS = Set.of( + "path.home", // TODO Find the right place to put this setting + SSL_TRANSPORT_CLIENT_PEMCERT_FILEPATH, + SSL_TRANSPORT_CLIENT_PEMKEY_FILEPATH, + SSL_TRANSPORT_CLIENT_PEMTRUSTEDCAS_FILEPATH, + SSL_TRANSPORT_ENABLED, + SSL_TRANSPORT_ENABLED_CIPHERS, + SSL_TRANSPORT_ENABLED_PROTOCOLS, + SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION, + SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME, + SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED, + SSL_TRANSPORT_KEYSTORE_ALIAS, + SSL_TRANSPORT_KEYSTORE_FILEPATH, + SSL_TRANSPORT_KEYSTORE_TYPE, + SSL_TRANSPORT_PEMCERT_FILEPATH, + SSL_TRANSPORT_PEMKEY_FILEPATH, + SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH, + SSL_TRANSPORT_SERVER_PEMCERT_FILEPATH, + SSL_TRANSPORT_SERVER_PEMKEY_FILEPATH, + SSL_TRANSPORT_SERVER_PEMTRUSTEDCAS_FILEPATH, + SSL_TRANSPORT_TRUSTSTORE_ALIAS, + SSL_TRANSPORT_TRUSTSTORE_FILEPATH, + SSL_TRANSPORT_TRUSTSTORE_TYPE + ); + + private Map securitySettings; + /** * Jackson requires a no-arg constructor. */ @@ -54,6 +105,29 @@ public ExtensionSettings(String extensionName, String hostAddress, String hostPo this.hostPort = hostPort; this.opensearchAddress = opensearchAddress; this.opensearchPort = opensearchPort; + this.securitySettings = Map.of(); + } + + /** + * Instantiate this class using the specified parameters. + * + * @param extensionName The extension name. Provided to OpenSearch as a response to initialization query. Must match the defined extension name in OpenSearch. + * @param hostAddress The IP Address to bind this extension to. + * @param hostPort The port to bind this extension to. + * @param opensearchAddress The IP Address on which OpenSearch is running. + * @param opensearchPort The port on which OpenSearch is running. + * @param securitySettings A generic map of any settings set in the config file that are not default setting keys + */ + public ExtensionSettings( + String extensionName, + String hostAddress, + String hostPort, + String opensearchAddress, + String opensearchPort, + Map securitySettings + ) { + this(extensionName, hostAddress, hostPort, opensearchAddress, opensearchPort); + this.securitySettings = securitySettings; } public String getExtensionName() { @@ -76,6 +150,10 @@ public String getOpensearchPort() { return opensearchPort; } + public Map getSecuritySettings() { + return securitySettings; + } + @Override public String toString() { return "ExtensionSettings{extensionName=" @@ -88,6 +166,8 @@ public String toString() { + opensearchAddress + ", opensearchPort=" + opensearchPort + + ", securitySettings=" + + securitySettings + "}"; } @@ -109,12 +189,19 @@ public static ExtensionSettings readSettingsFromYaml(String extensionSettingsPat if (extensionMap == null) { throw new IOException("extension.yml is empty"); } + Map securitySettings = new HashMap<>(); + for (String settingKey : extensionMap.keySet()) { + if (SECURITY_SETTINGS_KEYS.contains(settingKey)) { + securitySettings.put(settingKey, extensionMap.get(settingKey).toString()); + } + } return new ExtensionSettings( extensionMap.get("extensionName").toString(), extensionMap.get("hostAddress").toString(), extensionMap.get("hostPort").toString(), extensionMap.get("opensearchAddress").toString(), - extensionMap.get("opensearchPort").toString() + extensionMap.get("opensearchPort").toString(), + securitySettings ); } catch (URISyntaxException e) { throw new IOException("Error reading from extension.yml"); diff --git a/src/main/java/org/opensearch/sdk/ExtensionsRunner.java b/src/main/java/org/opensearch/sdk/ExtensionsRunner.java index 71d502a2..2e09604e 100644 --- a/src/main/java/org/opensearch/sdk/ExtensionsRunner.java +++ b/src/main/java/org/opensearch/sdk/ExtensionsRunner.java @@ -9,6 +9,10 @@ package org.opensearch.sdk; +import com.google.inject.Binder; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.opensearch.action.ActionType; @@ -25,20 +29,20 @@ import org.opensearch.common.settings.Settings; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.discovery.InitializeExtensionRequest; -import org.opensearch.extensions.DiscoveryExtensionNode; import org.opensearch.extensions.AddSettingsUpdateConsumerRequest; -import org.opensearch.extensions.UpdateSettingsRequest; -import org.opensearch.extensions.action.ExtensionActionRequest; +import org.opensearch.extensions.DiscoveryExtensionNode; import org.opensearch.extensions.ExtensionRequest; import org.opensearch.extensions.ExtensionsManager; +import org.opensearch.extensions.UpdateSettingsRequest; +import org.opensearch.extensions.action.ExtensionActionRequest; import org.opensearch.index.IndicesModuleRequest; +import org.opensearch.sdk.action.SDKActionModule; +import org.opensearch.sdk.handlers.AcknowledgedResponseHandler; import org.opensearch.sdk.api.ActionExtension; import org.opensearch.sdk.handlers.ClusterSettingsResponseHandler; import org.opensearch.sdk.handlers.ClusterStateResponseHandler; import org.opensearch.sdk.handlers.EnvironmentSettingsResponseHandler; import org.opensearch.sdk.handlers.ExtensionActionRequestHandler; -import org.opensearch.sdk.action.SDKActionModule; -import org.opensearch.sdk.handlers.AcknowledgedResponseHandler; import org.opensearch.sdk.handlers.ExtensionDependencyResponseHandler; import org.opensearch.sdk.handlers.ExtensionsIndicesModuleNameRequestHandler; import org.opensearch.sdk.handlers.ExtensionsIndicesModuleRequestHandler; @@ -56,11 +60,6 @@ import org.opensearch.transport.TransportService; import org.opensearch.transport.TransportSettings; -import com.google.inject.Binder; -import com.google.inject.Guice; -import com.google.inject.Injector; -import com.google.inject.Key; - import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -74,6 +73,8 @@ import java.util.function.Consumer; import java.util.stream.Collectors; +import static org.opensearch.sdk.ssl.SSLConfigConstants.SSL_TRANSPORT_ENABLED; + /** * The primary class to run an extension. *

@@ -179,11 +180,20 @@ protected ExtensionsRunner(Extension extension) throws IOException { // These must have getters from this class to be accessible via createComponents // If they require later initialization, create a concrete wrapper class and update the internals ExtensionSettings extensionSettings = extension.getExtensionSettings(); - this.settings = Settings.builder() + Settings.Builder settingsBuilder = Settings.builder() .put(NODE_NAME_SETTING, extensionSettings.getExtensionName()) .put(TransportSettings.BIND_HOST.getKey(), extensionSettings.getHostAddress()) - .put(TransportSettings.PORT.getKey(), extensionSettings.getHostPort()) - .build(); + .put(TransportSettings.PORT.getKey(), extensionSettings.getHostPort()); + boolean sslEnabled = extensionSettings.getSecuritySettings().containsKey(SSL_TRANSPORT_ENABLED) + && "true".equals(extensionSettings.getSecuritySettings().get(SSL_TRANSPORT_ENABLED)); + if (sslEnabled) { + for (String settingsKey : ExtensionSettings.SECURITY_SETTINGS_KEYS) { + addSettingsToBuilder(settingsBuilder, settingsKey, extensionSettings); + } + } + String sslText = sslEnabled ? "enabled" : "disabled"; + logger.info("SSL is " + sslText + " for transport"); + this.settings = settingsBuilder.build(); final List> executorBuilders = extension.getExecutorBuilders(settings); @@ -250,6 +260,12 @@ protected ExtensionsRunner(Extension extension) throws IOException { } } + private void addSettingsToBuilder(Settings.Builder settingsBuilder, String settingKey, ExtensionSettings extensionSettings) { + if (extensionSettings.getSecuritySettings().containsKey(settingKey)) { + settingsBuilder.put(settingKey, extensionSettings.getSecuritySettings().get(settingKey)); + } + } + @SuppressWarnings({ "unchecked", "rawtypes" }) private void injectComponents(Binder b) { extension.createComponents(this).stream().forEach(p -> b.bind((Class) p.getClass()).toInstance(p)); diff --git a/src/main/java/org/opensearch/sdk/NettyTransport.java b/src/main/java/org/opensearch/sdk/NettyTransport.java index ba6f2baa..a19f2108 100644 --- a/src/main/java/org/opensearch/sdk/NettyTransport.java +++ b/src/main/java/org/opensearch/sdk/NettyTransport.java @@ -9,6 +9,7 @@ package org.opensearch.sdk; +import java.nio.file.Path; import java.util.Collections; import org.opensearch.Version; @@ -18,6 +19,10 @@ import org.opensearch.common.util.PageCacheRecycler; import org.opensearch.indices.breaker.CircuitBreakerService; import org.opensearch.indices.breaker.NoneCircuitBreakerService; +import org.opensearch.sdk.ssl.DefaultSslKeyStore; +import org.opensearch.sdk.ssl.SSLConfigConstants; +import org.opensearch.sdk.ssl.SSLNettyTransport; +import org.opensearch.sdk.ssl.SslKeyStore; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.SharedGroupFactory; import org.opensearch.transport.TransportInterceptor; @@ -57,6 +62,11 @@ public Netty4Transport getNetty4Transport(Settings settings, ThreadPool threadPo final CircuitBreakerService circuitBreakerService = new NoneCircuitBreakerService(); + boolean transportSSLEnabled = settings.getAsBoolean( + SSLConfigConstants.SSL_TRANSPORT_ENABLED, + SSLConfigConstants.SSL_TRANSPORT_ENABLED_DEFAULT + ); + Netty4Transport transport = new Netty4Transport( settings, Version.CURRENT, @@ -68,6 +78,22 @@ public Netty4Transport getNetty4Transport(Settings settings, ThreadPool threadPo new SharedGroupFactory(settings) ); + if (transportSSLEnabled) { + Path configPath = Path.of("").toAbsolutePath().resolve("config"); + SslKeyStore sks = new DefaultSslKeyStore(settings, configPath); + transport = new SSLNettyTransport( + settings, + Version.CURRENT, + threadPool, + networkService, + pageCacheRecycler, + extensionsRunner.getNamedWriteableRegistry().getRegistry(), + circuitBreakerService, + sks, + new SharedGroupFactory(settings) + ); + } + return transport; } diff --git a/src/main/java/org/opensearch/sdk/handlers/ExtensionsInitRequestHandler.java b/src/main/java/org/opensearch/sdk/handlers/ExtensionsInitRequestHandler.java index 90444878..0a931363 100644 --- a/src/main/java/org/opensearch/sdk/handlers/ExtensionsInitRequestHandler.java +++ b/src/main/java/org/opensearch/sdk/handlers/ExtensionsInitRequestHandler.java @@ -48,6 +48,7 @@ public ExtensionsInitRequestHandler(ExtensionsRunner extensionsRunner) { public InitializeExtensionResponse handleExtensionInitRequest(InitializeExtensionRequest extensionInitRequest) { logger.info("Registering Extension Request received from OpenSearch"); extensionsRunner.opensearchNode = extensionInitRequest.getSourceNode(); + extensionsRunner.getThreadPool().getThreadContext().putHeader("extension_unique_id", extensionInitRequest.getExtension().getId()); extensionsRunner.setUniqueId(extensionInitRequest.getExtension().getId()); // TODO: Remove above two lines in favor of the below when refactoring SDKTransportService sdkTransportService = extensionsRunner.getSdkTransportService(); @@ -65,7 +66,10 @@ public InitializeExtensionResponse handleExtensionInitRequest(InitializeExtensio extensionsRunner.setExtensionNode(extensionInitRequest.getExtension()); // TODO: replace with sdkTransportService.getTransportService() TransportService extensionTransportService = extensionsRunner.getExtensionTransportService(); - extensionTransportService.connectToNode(extensionsRunner.opensearchNode); + extensionTransportService.connectToNodeAsExtension( + extensionsRunner.opensearchNode, + extensionInitRequest.getExtension().getId() + ); extensionsRunner.sendRegisterRestActionsRequest(extensionTransportService); extensionsRunner.sendRegisterCustomSettingsRequest(extensionTransportService); sdkTransportService.sendRegisterTransportActionsRequest(extensionsRunner.getSdkActionModule().getActions()); diff --git a/src/main/java/org/opensearch/sdk/ssl/DefaultSslKeyStore.java b/src/main/java/org/opensearch/sdk/ssl/DefaultSslKeyStore.java new file mode 100644 index 00000000..91fb4715 --- /dev/null +++ b/src/main/java/org/opensearch/sdk/ssl/DefaultSslKeyStore.java @@ -0,0 +1,854 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.ssl; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.AccessController; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import javax.crypto.Cipher; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLParameters; + +import io.netty.handler.codec.http2.Http2SecurityUtil; +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +//import org.bouncycastle.asn1.ASN1InputStream; +//import org.bouncycastle.asn1.ASN1ObjectIdentifier; +//import org.bouncycastle.asn1.ASN1Primitive; +//import org.bouncycastle.asn1.ASN1Sequence; +//import org.bouncycastle.asn1.ASN1String; +//import org.bouncycastle.asn1.ASN1TaggedObject; + +import org.opensearch.OpenSearchException; +import org.opensearch.OpenSearchSecurityException; +import org.opensearch.SpecialPermission; +import org.opensearch.common.settings.Settings; +import org.opensearch.env.Environment; +import org.opensearch.sdk.ssl.util.CertFileProps; +import org.opensearch.sdk.ssl.util.CertFromFile; +import org.opensearch.sdk.ssl.util.CertFromKeystore; +import org.opensearch.sdk.ssl.util.CertFromTruststore; +import org.opensearch.sdk.ssl.util.ExceptionUtils; +import org.opensearch.sdk.ssl.util.KeystoreProps; +import org.opensearch.transport.NettyAllocator; + +import static org.opensearch.sdk.ssl.SecureSSLSettings.SSLSetting.SSL_TRANSPORT_CLIENT_KEYSTORE_KEYPASSWORD; +import static org.opensearch.sdk.ssl.SecureSSLSettings.SSLSetting.SSL_TRANSPORT_CLIENT_PEMKEY_PASSWORD; +import static org.opensearch.sdk.ssl.SecureSSLSettings.SSLSetting.SSL_TRANSPORT_KEYSTORE_KEYPASSWORD; +import static org.opensearch.sdk.ssl.SecureSSLSettings.SSLSetting.SSL_TRANSPORT_KEYSTORE_PASSWORD; +import static org.opensearch.sdk.ssl.SecureSSLSettings.SSLSetting.SSL_TRANSPORT_PEMKEY_PASSWORD; +import static org.opensearch.sdk.ssl.SecureSSLSettings.SSLSetting.SSL_TRANSPORT_SERVER_KEYSTORE_KEYPASSWORD; +import static org.opensearch.sdk.ssl.SecureSSLSettings.SSLSetting.SSL_TRANSPORT_SERVER_PEMKEY_PASSWORD; +import static org.opensearch.sdk.ssl.SecureSSLSettings.SSLSetting.SSL_TRANSPORT_TRUSTSTORE_PASSWORD; + +/** + * Default SSL Key Store. This class contains methods to setup SSL for an extension + */ +public class DefaultSslKeyStore implements SslKeyStore { + + private static final String DEFAULT_STORE_TYPE = "JKS"; + + private void printJCEWarnings() { + try { + final int aesMaxKeyLength = Cipher.getMaxAllowedKeyLength("AES"); + + if (aesMaxKeyLength < 256) { + log.info( + "AES-256 not supported, max key length for AES is {} bit." + + " (This is not an issue, it just limits possible encryption strength. To enable AES 256, install 'Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files')", + aesMaxKeyLength + ); + } + } catch (final NoSuchAlgorithmException e) { + log.error("AES encryption not supported (SG 1). ", e); + } + } + + private final Settings settings; + private final Logger log = LogManager.getLogger(this.getClass()); + public final SslProvider sslTransportServerProvider; + public final SslProvider sslTransportClientProvider; + private final boolean transportSSLEnabled; + private List enabledTransportCiphersJDKProvider; + private List enabledTransportProtocolsJDKProvider; + private SslContext transportServerSslContext; + private SslContext transportClientSslContext; + private X509Certificate[] transportCerts; + private final Environment env; + + /** + * Constructs a DefaultSslKeyStore + * @param settings The SSL settings + * @param configPath The path to the config directory for this extension + */ + public DefaultSslKeyStore(final Settings settings, final Path configPath) { + super(); + + this.settings = settings; + Environment _env; + try { + _env = new Environment(settings, configPath); + } catch (IllegalStateException e) { + _env = null; + } + env = _env; + transportSSLEnabled = settings.getAsBoolean( + SSLConfigConstants.SSL_TRANSPORT_ENABLED, + SSLConfigConstants.SSL_TRANSPORT_ENABLED_DEFAULT + ); + + if (transportSSLEnabled) { + sslTransportClientProvider = SslContext.defaultClientProvider(); + sslTransportServerProvider = SslContext.defaultServerProvider(); + } else if (transportSSLEnabled) { + sslTransportClientProvider = sslTransportServerProvider = SslProvider.JDK; + } else { + sslTransportClientProvider = sslTransportServerProvider = null; + } + + initEnabledSSLCiphers(); + initSSLConfig(); + printJCEWarnings(); + + log.info("TLS Transport Client Provider : {}", sslTransportClientProvider); + log.info("TLS Transport Server Provider : {}", sslTransportServerProvider); + + log.debug( + "sslTransportClientProvider:{} with ciphers {}", + sslTransportClientProvider, + getEnabledSSLCiphers(sslTransportClientProvider) + ); + log.debug( + "sslTransportServerProvider:{} with ciphers {}", + sslTransportServerProvider, + getEnabledSSLCiphers(sslTransportServerProvider) + ); + + log.info("Enabled TLS protocols for transport layer : {}", Arrays.toString(getEnabledSSLProtocols(sslTransportServerProvider))); + + log.debug( + "sslTransportClientProvider:{} with protocols {}", + sslTransportClientProvider, + getEnabledSSLProtocols(sslTransportClientProvider) + ); + log.debug( + "sslTransportServerProvider:{} with protocols {}", + sslTransportServerProvider, + getEnabledSSLProtocols(sslTransportServerProvider) + ); + + if (transportSSLEnabled + && (getEnabledSSLCiphers(sslTransportClientProvider).isEmpty() || getEnabledSSLCiphers(sslTransportServerProvider).isEmpty())) { + throw new OpenSearchSecurityException("no valid cipher suites for transport protocol"); + } + + if (transportSSLEnabled && getEnabledSSLCiphers(sslTransportServerProvider).isEmpty()) { + throw new OpenSearchSecurityException("no ssl protocols for transport protocol"); + } + + if (transportSSLEnabled && getEnabledSSLCiphers(sslTransportClientProvider).isEmpty()) { + throw new OpenSearchSecurityException("no ssl protocols for transport protocol"); + } + } + + private String resolve(String propName, boolean mustBeValid) { + + final String originalPath = settings.get(propName, null); + String path = originalPath; + log.debug("Value for {} is {}", propName, originalPath); + + if (env != null && originalPath != null && originalPath.length() > 0) { + path = env.configDir().resolve(originalPath).toAbsolutePath().toString(); + log.debug("Resolved {} to {} against {}", originalPath, path, env.configDir().toAbsolutePath().toString()); + } + + if (mustBeValid) { + checkPath(path, propName); + } + + if ("".equals(path)) { + path = null; + } + + return path; + } + + private void initSSLConfig() { + + if (env == null) { + log.info("No config directory, key- and truststore files are resolved absolutely"); + } else { + log.info( + "Config directory is {}/, from there the key- and truststore files are resolved relatively", + env.configDir().toAbsolutePath() + ); + } + + if (transportSSLEnabled) { + initTransportSSLConfig(); + } + } + + /** + * Initializes certs used for node to node communication + */ + public void initTransportSSLConfig() { + // when extendedKeyUsageEnabled and we use keyStore, client/server certs will be in the + // same keyStore file + // when extendedKeyUsageEnabled and we use rawFiles, client/server certs will be in + // different files + // That's why useRawFiles checks for extra location + final boolean useKeyStore = settings.hasValue(SSLConfigConstants.SSL_TRANSPORT_KEYSTORE_FILEPATH); + final boolean useRawFiles = settings.hasValue(SSLConfigConstants.SSL_TRANSPORT_PEMCERT_FILEPATH) + || (settings.hasValue(SSLConfigConstants.SSL_TRANSPORT_SERVER_PEMCERT_FILEPATH) + && settings.hasValue(SSLConfigConstants.SSL_TRANSPORT_CLIENT_PEMCERT_FILEPATH)); + + final boolean extendedKeyUsageEnabled = settings.getAsBoolean( + SSLConfigConstants.SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED, + SSLConfigConstants.SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED_DEFAULT + ); + + if (useKeyStore) { + + final String keystoreFilePath = resolve(SSLConfigConstants.SSL_TRANSPORT_KEYSTORE_FILEPATH, true); + final String keystoreType = settings.get(SSLConfigConstants.SSL_TRANSPORT_KEYSTORE_TYPE, DEFAULT_STORE_TYPE); + final String keystorePassword = SSL_TRANSPORT_KEYSTORE_PASSWORD.getSetting(settings, SSLConfigConstants.DEFAULT_STORE_PASSWORD); + + final String truststoreFilePath = resolve(SSLConfigConstants.SSL_TRANSPORT_TRUSTSTORE_FILEPATH, true); + + if (settings.get(SSLConfigConstants.SSL_TRANSPORT_TRUSTSTORE_FILEPATH, null) == null) { + throw new OpenSearchException( + SSLConfigConstants.SSL_TRANSPORT_TRUSTSTORE_FILEPATH + " must be set if transport ssl is requested." + ); + } + + final String truststoreType = settings.get(SSLConfigConstants.SSL_TRANSPORT_TRUSTSTORE_TYPE, DEFAULT_STORE_TYPE); + final String truststorePassword = SSL_TRANSPORT_TRUSTSTORE_PASSWORD.getSetting(settings); + + KeystoreProps keystoreProps = new KeystoreProps(keystoreFilePath, keystoreType, keystorePassword); + + KeystoreProps truststoreProps = new KeystoreProps(truststoreFilePath, truststoreType, truststorePassword); + try { + CertFromKeystore certFromKeystore; + CertFromTruststore certFromTruststore; + if (extendedKeyUsageEnabled) { + final String truststoreServerAlias = settings.get(SSLConfigConstants.SSL_TRANSPORT_SERVER_TRUSTSTORE_ALIAS, null); + final String truststoreClientAlias = settings.get(SSLConfigConstants.SSL_TRANSPORT_CLIENT_TRUSTSTORE_ALIAS, null); + final String keystoreServerAlias = settings.get(SSLConfigConstants.SSL_TRANSPORT_SERVER_KEYSTORE_ALIAS, null); + final String keystoreClientAlias = settings.get(SSLConfigConstants.SSL_TRANSPORT_CLIENT_KEYSTORE_ALIAS, null); + final String serverKeyPassword = SSL_TRANSPORT_SERVER_KEYSTORE_KEYPASSWORD.getSetting(settings, keystorePassword); + final String clientKeyPassword = SSL_TRANSPORT_CLIENT_KEYSTORE_KEYPASSWORD.getSetting(settings, keystorePassword); + + // we require all aliases to be set explicitly + // because they should be different for client and server + if (keystoreServerAlias == null + || keystoreClientAlias == null + || truststoreServerAlias == null + || truststoreClientAlias == null) { + throw new OpenSearchException( + SSLConfigConstants.SSL_TRANSPORT_SERVER_KEYSTORE_ALIAS + + ", " + + SSLConfigConstants.SSL_TRANSPORT_CLIENT_KEYSTORE_ALIAS + + ", " + + SSLConfigConstants.SSL_TRANSPORT_SERVER_TRUSTSTORE_ALIAS + + ", " + + SSLConfigConstants.SSL_TRANSPORT_CLIENT_TRUSTSTORE_ALIAS + + " must be set when " + + SSLConfigConstants.SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED + + " is true." + ); + } + + certFromKeystore = new CertFromKeystore( + keystoreProps, + keystoreServerAlias, + keystoreClientAlias, + serverKeyPassword, + clientKeyPassword + ); + certFromTruststore = new CertFromTruststore(truststoreProps, truststoreServerAlias, truststoreClientAlias); + } else { + // when alias is null, we take first entry in the store + final String truststoreAlias = settings.get(SSLConfigConstants.SSL_TRANSPORT_TRUSTSTORE_ALIAS, null); + final String keystoreAlias = settings.get(SSLConfigConstants.SSL_TRANSPORT_KEYSTORE_ALIAS, null); + final String keyPassword = SSL_TRANSPORT_KEYSTORE_KEYPASSWORD.getSetting(settings, keystorePassword); + + certFromKeystore = new CertFromKeystore(keystoreProps, keystoreAlias, keyPassword); + certFromTruststore = new CertFromTruststore(truststoreProps, truststoreAlias); + } + + validateNewCerts(transportCerts, certFromKeystore.getCerts()); + transportServerSslContext = buildSSLServerContext( + certFromKeystore.getServerKey(), + certFromKeystore.getServerCert(), + certFromTruststore.getServerTrustedCerts(), + getEnabledSSLCiphers(this.sslTransportServerProvider), + this.sslTransportServerProvider, + ClientAuth.REQUIRE + ); + transportClientSslContext = buildSSLClientContext( + certFromKeystore.getClientKey(), + certFromKeystore.getClientCert(), + certFromTruststore.getClientTrustedCerts(), + getEnabledSSLCiphers(sslTransportClientProvider), + sslTransportClientProvider + ); + setTransportSSLCerts(certFromKeystore.getCerts()); + } catch (final Exception e) { + logExplanation(e); + throw new OpenSearchSecurityException("Error while initializing transport SSL layer: " + e.toString(), e); + } + + } else if (useRawFiles) { + try { + CertFromFile certFromFile; + if (extendedKeyUsageEnabled) { + CertFileProps clientCertProps = new CertFileProps( + resolve(SSLConfigConstants.SSL_TRANSPORT_CLIENT_PEMCERT_FILEPATH, true), + resolve(SSLConfigConstants.SSL_TRANSPORT_CLIENT_PEMKEY_FILEPATH, true), + resolve(SSLConfigConstants.SSL_TRANSPORT_CLIENT_PEMTRUSTEDCAS_FILEPATH, true), + SSL_TRANSPORT_CLIENT_PEMKEY_PASSWORD.getSetting(settings) + ); + + CertFileProps serverCertProps = new CertFileProps( + resolve(SSLConfigConstants.SSL_TRANSPORT_SERVER_PEMCERT_FILEPATH, true), + resolve(SSLConfigConstants.SSL_TRANSPORT_SERVER_PEMKEY_FILEPATH, true), + resolve(SSLConfigConstants.SSL_TRANSPORT_SERVER_PEMTRUSTEDCAS_FILEPATH, true), + SSL_TRANSPORT_SERVER_PEMKEY_PASSWORD.getSetting(settings) + ); + + certFromFile = new CertFromFile(clientCertProps, serverCertProps); + } else { + CertFileProps certProps = new CertFileProps( + resolve(SSLConfigConstants.SSL_TRANSPORT_PEMCERT_FILEPATH, true), + resolve(SSLConfigConstants.SSL_TRANSPORT_PEMKEY_FILEPATH, true), + resolve(SSLConfigConstants.SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH, true), + SSL_TRANSPORT_PEMKEY_PASSWORD.getSetting(settings) + ); + certFromFile = new CertFromFile(certProps); + } + + validateNewCerts(transportCerts, certFromFile.getCerts()); + transportServerSslContext = buildSSLServerContext( + certFromFile.getServerPemKey(), + certFromFile.getServerPemCert(), + certFromFile.getServerTrustedCas(), + certFromFile.getServerPemKeyPassword(), + getEnabledSSLCiphers(this.sslTransportServerProvider), + this.sslTransportServerProvider, + ClientAuth.REQUIRE + ); + transportClientSslContext = buildSSLClientContext( + certFromFile.getClientPemKey(), + certFromFile.getClientPemCert(), + certFromFile.getClientTrustedCas(), + certFromFile.getClientPemKeyPassword(), + getEnabledSSLCiphers(sslTransportClientProvider), + sslTransportClientProvider + ); + setTransportSSLCerts(certFromFile.getCerts()); + + } catch (final Exception e) { + logExplanation(e); + throw new OpenSearchSecurityException("Error while initializing transport SSL layer from PEM: " + e.toString(), e); + } + } else { + throw new OpenSearchException( + SSLConfigConstants.SSL_TRANSPORT_KEYSTORE_FILEPATH + + " or " + + SSLConfigConstants.SSL_TRANSPORT_SERVER_PEMCERT_FILEPATH + + " and " + + SSLConfigConstants.SSL_TRANSPORT_CLIENT_PEMCERT_FILEPATH + + " must be set if transport ssl is requested." + ); + } + } + + /** + * If the current and new certificates are same, skip remaining checks. + * For new X509 cert to be valid Issuer, Subject DN must be the same and + * new certificates should expire after current ones. + * @param currentX509Certs Array of current x509 certificates + * @param newX509Certs Array of x509 certificates which will replace our current cert + * @throws Exception if certificate is invalid + */ + private void validateNewCerts(final X509Certificate[] currentX509Certs, final X509Certificate[] newX509Certs) throws Exception { + + // First time we init certs ignore validity check + if (currentX509Certs == null) { + return; + } + + if (areSameCerts(currentX509Certs, newX509Certs)) { + return; + } + + // Check if new X509 certs have valid expiry date + if (!hasValidExpiryDates(currentX509Certs, newX509Certs)) { + throw new Exception("New certificates should not expire before the current ones."); + } + + // Check if new X509 certs have valid IssuerDN, SubjectDN or SAN + if (!hasValidDNs(currentX509Certs, newX509Certs)) { + throw new Exception("New Certs do not have valid Issuer DN, Subject DN or SAN."); + } + } + + /** + * Check if new X509 certs have same IssuerDN/SubjectDN as current certificates. + * @param currentX509Certs Array of current X509Certificates. + * @param newX509Certs Array of new X509Certificates. + * @return true if all Issuer DN and Subject DN pairs match; false otherwise. + * @throws Exception if certificate is invalid. + */ + private boolean hasValidDNs(final X509Certificate[] currentX509Certs, final X509Certificate[] newX509Certs) { + + final Function formatDNString = cert -> { + final String issuerDn = cert != null && cert.getIssuerX500Principal() != null ? cert.getIssuerX500Principal().getName() : ""; + final String subjectDn = cert != null && cert.getSubjectX500Principal() != null ? cert.getSubjectX500Principal().getName() : ""; + final String san = getSubjectAlternativeNames(cert); + return String.format("%s/%s/%s", issuerDn, subjectDn, san); + }; + + final List currentCertDNList = Arrays.stream(currentX509Certs).map(formatDNString).sorted().collect(Collectors.toList()); + + final List newCertDNList = Arrays.stream(newX509Certs).map(formatDNString).sorted().collect(Collectors.toList()); + + return currentCertDNList.equals(newCertDNList); + } + + /** + * Check if new X509 certs have expiry date after the current X509 certs. + * @param currentX509Certs Array of current X509Certificates. + * @param newX509Certs Array of new X509Certificates. + * @return true if all of the new certificates expire after the currentX509 certificates. + * @throws Exception if certificate is invalid. + */ + private boolean hasValidExpiryDates(final X509Certificate[] currentX509Certs, final X509Certificate[] newX509Certs) { + + // Get earliest expiry date for current certificates + final Date earliestExpiryDate = Arrays.stream(currentX509Certs).map(c -> c.getNotAfter()).min(Date::compareTo).get(); + + // New certificates that expire before or on the same date as the current ones are invalid. + boolean newCertsExpireBeforeCurrentCerts = Arrays.stream(newX509Certs).anyMatch(cert -> { + Date notAfterDate = cert.getNotAfter(); + return notAfterDate.before(earliestExpiryDate) || notAfterDate.equals(earliestExpiryDate); + }); + + return !newCertsExpireBeforeCurrentCerts; + } + + /** + * Check if new X509 certs have same signature has as the current X509 certs. + * @param currentX509Certs Array of current X509Certificates. + * @param newX509Certs Array of new X509Certificates. + * @return true if all of the new certificates have the same signature as currentX509 certificates. + * @return false if any new certificate signature is different than currentX509 certificates + */ + + private boolean areSameCerts(final X509Certificate[] currentX509Certs, final X509Certificate[] newX509Certs) { + + final Function certificateSignature = cert -> { + final byte[] signature = cert != null && cert.getSignature() != null ? cert.getSignature() : null; + return new String(signature, StandardCharsets.UTF_8); + }; + + final Set currentCertSignatureSet = Arrays.stream(currentX509Certs).map(certificateSignature).collect(Collectors.toSet()); + + final Set newCertSignatureSet = Arrays.stream(newX509Certs).map(certificateSignature).collect(Collectors.toSet()); + + return currentCertSignatureSet.equals(newCertSignatureSet); + } + + /** + * + * @return Returns a server SSL Transport engine for this extension based on settings + * @throws SSLException + */ + public SSLEngine createServerTransportSSLEngine() throws SSLException { + final SSLEngine engine = transportServerSslContext.newEngine(NettyAllocator.getAllocator()); + engine.setEnabledProtocols(getEnabledSSLProtocols(this.sslTransportServerProvider)); + return engine; + } + + /** + * + * @param peerHost The peer hostname + * @param peerPort The peer port + * @return Returns a client SSL Transport engine for this extension based on settings + * @throws SSLException + */ + public SSLEngine createClientTransportSSLEngine(final String peerHost, final int peerPort) throws SSLException { + if (peerHost != null) { + final SSLEngine engine = transportClientSslContext.newEngine(NettyAllocator.getAllocator(), peerHost, peerPort); + + final SSLParameters sslParams = new SSLParameters(); + sslParams.setEndpointIdentificationAlgorithm("HTTPS"); + engine.setSSLParameters(sslParams); + engine.setEnabledProtocols(getEnabledSSLProtocols(this.sslTransportClientProvider)); + return engine; + } else { + final SSLEngine engine = transportClientSslContext.newEngine(NettyAllocator.getAllocator()); + engine.setEnabledProtocols(getEnabledSSLProtocols(this.sslTransportClientProvider)); + return engine; + } + + } + + /** + * Sets the transport X509Certificates. + * @param certs New X509 Certificates + */ + private void setTransportSSLCerts(X509Certificate[] certs) { + this.transportCerts = certs; + } + + private List getEnabledSSLCiphers(final SslProvider provider) { + if (provider == null) { + return Collections.emptyList(); + } + + return enabledTransportCiphersJDKProvider; + } + + private String[] getEnabledSSLProtocols(final SslProvider provider) { + if (provider == null) { + return new String[0]; + } + + return (enabledTransportProtocolsJDKProvider).toArray(new String[0]); + } + + @SuppressWarnings("removal") + private void initEnabledSSLCiphers() { + + final List secureTransportSSLCiphers = SSLConfigConstants.getSecureSSLCiphers(settings); + final List secureTransportSSLProtocols = Arrays.asList(SSLConfigConstants.getSecureSSLProtocols(settings)); + + SSLEngine engine = null; + List jdkSupportedCiphers = null; + List jdkSupportedProtocols = null; + try { + final SSLContext serverContext = SSLContext.getInstance("TLS"); + serverContext.init(null, null, null); + engine = serverContext.createSSLEngine(); + jdkSupportedCiphers = Arrays.asList(engine.getEnabledCipherSuites()); + jdkSupportedProtocols = Arrays.asList(engine.getEnabledProtocols()); + log.debug("JVM supports the following {} protocols {}", jdkSupportedProtocols.size(), jdkSupportedProtocols); + log.debug("JVM supports the following {} ciphers {}", jdkSupportedCiphers.size(), jdkSupportedCiphers); + + if (jdkSupportedProtocols.contains("TLSv1.3")) { + log.info("JVM supports TLSv1.3"); + } + + } catch (final Throwable e) { + log.error("Unable to determine supported ciphers due to ", e); + } finally { + if (engine != null) { + try { + engine.closeInbound(); + } catch (SSLException e) { + log.debug("Unable to close inbound ssl engine", e); + } + engine.closeOutbound(); + } + } + + if (jdkSupportedCiphers == null + || jdkSupportedCiphers.isEmpty() + || jdkSupportedProtocols == null + || jdkSupportedProtocols.isEmpty()) { + throw new OpenSearchException("Unable to determine supported ciphers or protocols"); + } + + enabledTransportCiphersJDKProvider = new ArrayList(jdkSupportedCiphers); + enabledTransportCiphersJDKProvider.retainAll(secureTransportSSLCiphers); + + enabledTransportProtocolsJDKProvider = new ArrayList(jdkSupportedProtocols); + enabledTransportProtocolsJDKProvider.retainAll(secureTransportSSLProtocols); + } + + private SslContext buildSSLServerContext( + final PrivateKey _key, + final X509Certificate[] _cert, + final X509Certificate[] _trustedCerts, + final Iterable ciphers, + final SslProvider sslProvider, + final ClientAuth authMode + ) throws SSLException { + + final SslContextBuilder _sslContextBuilder = configureSSLServerContextBuilder( + SslContextBuilder.forServer(_key, _cert), + sslProvider, + ciphers, + authMode + ); + + if (_trustedCerts != null && _trustedCerts.length > 0) { + _sslContextBuilder.trustManager(_trustedCerts); + } + + return buildSSLContext0(_sslContextBuilder); + } + + private SslContext buildSSLServerContext( + final File _key, + final File _cert, + final File _trustedCerts, + final String pwd, + final Iterable ciphers, + final SslProvider sslProvider, + final ClientAuth authMode + ) throws SSLException { + + final SslContextBuilder _sslContextBuilder = configureSSLServerContextBuilder( + SslContextBuilder.forServer(_cert, _key, pwd), + sslProvider, + ciphers, + authMode + ); + + if (_trustedCerts != null) { + _sslContextBuilder.trustManager(_trustedCerts); + } + + return buildSSLContext0(_sslContextBuilder); + } + + private SslContextBuilder configureSSLServerContextBuilder( + final SslContextBuilder builder, + final SslProvider sslProvider, + final Iterable ciphers, + final ClientAuth authMode + ) { + return builder.ciphers( + Stream.concat(Http2SecurityUtil.CIPHERS.stream(), StreamSupport.stream(ciphers.spliterator(), false)) + .collect(Collectors.toSet()), + SupportedCipherSuiteFilter.INSTANCE + ) + .clientAuth(Objects.requireNonNull(authMode)) + .sessionCacheSize(0) + .sessionTimeout(0) + .sslProvider(sslProvider) + .applicationProtocolConfig( + new ApplicationProtocolConfig( + Protocol.ALPN, + // NO_ADVERTISE is currently the only mode supported by both OpenSsl and JDK providers. + SelectorFailureBehavior.NO_ADVERTISE, + // ACCEPT is currently the only mode supported by both OpenSsl and JDK providers. + SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2, + ApplicationProtocolNames.HTTP_1_1 + ) + ); + } + + private SslContext buildSSLClientContext( + final PrivateKey _key, + final X509Certificate[] _cert, + final X509Certificate[] _trustedCerts, + final Iterable ciphers, + final SslProvider sslProvider + ) throws SSLException { + + final SslContextBuilder _sslClientContextBuilder = SslContextBuilder.forClient() + .ciphers(ciphers) + .applicationProtocolConfig(ApplicationProtocolConfig.DISABLED) + .sessionCacheSize(0) + .sessionTimeout(0) + .sslProvider(sslProvider) + .trustManager(_trustedCerts) + .keyManager(_key, _cert); + + return buildSSLContext0(_sslClientContextBuilder); + + } + + private SslContext buildSSLClientContext( + final File _key, + final File _cert, + final File _trustedCerts, + final String pwd, + final Iterable ciphers, + final SslProvider sslProvider + ) throws SSLException { + + final SslContextBuilder _sslClientContextBuilder = SslContextBuilder.forClient() + .ciphers(ciphers) + .applicationProtocolConfig(ApplicationProtocolConfig.DISABLED) + .sessionCacheSize(0) + .sessionTimeout(0) + .sslProvider(sslProvider) + .trustManager(_trustedCerts) + .keyManager(_cert, _key, pwd); + + return buildSSLContext0(_sslClientContextBuilder); + + } + + @SuppressWarnings("removal") + private SslContext buildSSLContext0(final SslContextBuilder sslContextBuilder) throws SSLException { + + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + SslContext sslContext = null; + try { + sslContext = AccessController.doPrivileged(new PrivilegedExceptionAction() { + @Override + public SslContext run() throws Exception { + return sslContextBuilder.build(); + } + }); + } catch (final PrivilegedActionException e) { + throw (SSLException) e.getCause(); + } + + return sslContext; + } + + private void logExplanation(Exception e) { + if (ExceptionUtils.findMsg(e, "not contain valid private key") != null) { + log.error( + "Your keystore or PEM does not contain a key. " + + "If you specified a key password, try removing it. " + + "If you did not specify a key password, perhaps you need to if the key is in fact password-protected. " + + "Maybe you just confused keys and certificates." + ); + } + + if (ExceptionUtils.findMsg(e, "not contain valid certificates") != null) { + log.error("Your keystore or PEM does not contain a certificate. Maybe you confused keys and certificates."); + } + } + + private static void checkPath(String keystoreFilePath, String fileNameLogOnly) { + + if (keystoreFilePath == null || keystoreFilePath.length() == 0) { + throw new OpenSearchException("Empty file path for " + fileNameLogOnly); + } + + if (Files.isDirectory(Paths.get(keystoreFilePath), LinkOption.NOFOLLOW_LINKS)) { + throw new OpenSearchException("Is a directory: " + keystoreFilePath + " Expected a file for " + fileNameLogOnly); + } + + if (!Files.isReadable(Paths.get(keystoreFilePath))) { + throw new OpenSearchException( + "Unable to read " + + keystoreFilePath + + " (" + + Paths.get(keystoreFilePath) + + "). Please make sure this files exists and is readable regarding to permissions. Property: " + + fileNameLogOnly + ); + } + } + + @Override + public String getSubjectAlternativeNames(X509Certificate cert) { + String san = ""; + try { + Collection> altNames = cert != null && cert.getSubjectAlternativeNames() != null + ? cert.getSubjectAlternativeNames() + : null; + if (altNames != null) { + Collection> sans = new ArrayList<>(); + for (List altName : altNames) { + Integer type = (Integer) altName.get(0); + sans.add(altName); + // otherName requires parsing to string + // if (type == 0) { + // List otherName = getOtherName(altName); + // if (otherName != null) { + // sans.add(Arrays.asList(type, otherName)); + // } + // } else { + // sans.add(altName); + // } + } + san = sans.toString(); + } + } catch (CertificateParsingException e) { + log.error("Issue parsing SubjectAlternativeName:", e); + } + + return san; + } + + // private List getOtherName(List altName) { + // ASN1Primitive oct = null; + // try { + // byte[] altNameBytes = (byte[]) altName.get(1); + // oct = (new ASN1InputStream(new ByteArrayInputStream(altNameBytes)).readObject()); + // } catch (IOException e) { + // throw new RuntimeException("Could not read ASN1InputStream", e); + // } + // if (oct instanceof ASN1TaggedObject) { + // oct = ((ASN1TaggedObject) oct).getObject(); + // } + // ASN1Sequence seq = ASN1Sequence.getInstance(oct); + // + // // Get object identifier from first in sequence + // ASN1ObjectIdentifier asnOID = (ASN1ObjectIdentifier) seq.getObjectAt(0); + // String oid = asnOID.getId(); + // + // // Get value of object from second element + // final ASN1TaggedObject obj = (ASN1TaggedObject) seq.getObjectAt(1); + // // Could be tagged twice due to bug in java cert.getSubjectAltName + // ASN1Primitive prim = obj.getObject(); + // if (prim instanceof ASN1TaggedObject) { + // prim = ASN1TaggedObject.getInstance(((ASN1TaggedObject) prim)).getObject(); + // } + // + // if (prim instanceof ASN1String) { + // return Collections.unmodifiableList(Arrays.asList(oid, ((ASN1String) prim).getString())); + // } + // + // return null; + // } +} diff --git a/src/main/java/org/opensearch/sdk/ssl/SSLConfigConstants.java b/src/main/java/org/opensearch/sdk/ssl/SSLConfigConstants.java new file mode 100644 index 00000000..c5735761 --- /dev/null +++ b/src/main/java/org/opensearch/sdk/ssl/SSLConfigConstants.java @@ -0,0 +1,217 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.ssl; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.opensearch.common.settings.Settings; + +/** + * Class containing constants for SSL + */ +public final class SSLConfigConstants { + public static final String SSL_TRANSPORT_ENABLED = "ssl.transport.enabled"; + // TODO Replace this with true when security changes are complete + public static final boolean SSL_TRANSPORT_ENABLED_DEFAULT = false; + public static final String SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION = "ssl.transport.enforce_hostname_verification"; + public static final String SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME = "ssl.transport.resolve_hostname"; + + public static final String SSL_TRANSPORT_KEYSTORE_ALIAS = "ssl.transport.keystore_alias"; + public static final String SSL_TRANSPORT_SERVER_KEYSTORE_ALIAS = "ssl.transport.server.keystore_alias"; + public static final String SSL_TRANSPORT_CLIENT_KEYSTORE_ALIAS = "ssl.transport.client.keystore_alias"; + + public static final String SSL_TRANSPORT_KEYSTORE_FILEPATH = "ssl.transport.keystore_filepath"; + public static final String SSL_TRANSPORT_PEMKEY_FILEPATH = "ssl.transport.pemkey_filepath"; + public static final String SSL_TRANSPORT_PEMCERT_FILEPATH = "ssl.transport.pemcert_filepath"; + + public static final String SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH = "ssl.transport.pemtrustedcas_filepath"; + public static final String SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED = "ssl.transport.extended_key_usage_enabled"; + public static final boolean SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED_DEFAULT = false; + public static final String SSL_TRANSPORT_SERVER_PEMKEY_FILEPATH = "ssl.transport.server.pemkey_filepath"; + public static final String SSL_TRANSPORT_SERVER_PEMCERT_FILEPATH = "ssl.transport.server.pemcert_filepath"; + public static final String SSL_TRANSPORT_SERVER_PEMTRUSTEDCAS_FILEPATH = "ssl.transport.server.pemtrustedcas_filepath"; + public static final String SSL_TRANSPORT_CLIENT_PEMKEY_FILEPATH = "ssl.transport.client.pemkey_filepath"; + public static final String SSL_TRANSPORT_CLIENT_PEMCERT_FILEPATH = "ssl.transport.client.pemcert_filepath"; + public static final String SSL_TRANSPORT_CLIENT_PEMTRUSTEDCAS_FILEPATH = "ssl.transport.client.pemtrustedcas_filepath"; + + public static final String SSL_TRANSPORT_KEYSTORE_TYPE = "ssl.transport.keystore_type"; + + public static final String SSL_TRANSPORT_TRUSTSTORE_ALIAS = "ssl.transport.truststore_alias"; + public static final String SSL_TRANSPORT_SERVER_TRUSTSTORE_ALIAS = "ssl.transport.server.truststore_alias"; + public static final String SSL_TRANSPORT_CLIENT_TRUSTSTORE_ALIAS = "ssl.transport.client.truststore_alias"; + + public static final String SSL_TRANSPORT_TRUSTSTORE_FILEPATH = "ssl.transport.truststore_filepath"; + public static final String SSL_TRANSPORT_TRUSTSTORE_TYPE = "ssl.transport.truststore_type"; + public static final String SSL_TRANSPORT_ENABLED_CIPHERS = "ssl.transport.enabled_ciphers"; + public static final String SSL_TRANSPORT_ENABLED_PROTOCOLS = "ssl.transport.enabled_protocols"; + public static final String DEFAULT_STORE_PASSWORD = "changeit"; // #16 + + private static final String[] _SECURE_SSL_PROTOCOLS = { "TLSv1.3", "TLSv1.2", "TLSv1.1" }; + + /** + * + * @param settings SSL Settings + * @return returns an array of the secure SSL protocols + */ + public static final String[] getSecureSSLProtocols(Settings settings) { + List configuredProtocols = null; + + if (settings != null) { + configuredProtocols = settings.getAsList(SSL_TRANSPORT_ENABLED_PROTOCOLS, Collections.emptyList()); + } + + if (configuredProtocols != null && configuredProtocols.size() > 0) { + return configuredProtocols.toArray(new String[0]); + } + + return _SECURE_SSL_PROTOCOLS.clone(); + } + + // @formatter:off + private static final String[] _SECURE_SSL_CIPHERS = { + // TLS__WITH_ + + // Example (including unsafe ones) + // Protocol: TLS, SSL + // Key Exchange RSA, Diffie-Hellman, ECDH, SRP, PSK + // Authentication RSA, DSA, ECDSA + // Bulk Ciphers RC4, 3DES, AES + // Message Authentication HMAC-SHA256, HMAC-SHA1, HMAC-MD5 + + // thats what chrome 48 supports (https://cc.dcsec.uni-hannover.de/) + // (c0,2b)ECDHE-ECDSA-AES128-GCM-SHA256128 BitKey exchange: ECDH, encryption: AES, MAC: SHA256. + // (c0,2f)ECDHE-RSA-AES128-GCM-SHA256128 BitKey exchange: ECDH, encryption: AES, MAC: SHA256. + // (00,9e)DHE-RSA-AES128-GCM-SHA256128 BitKey exchange: DH, encryption: AES, MAC: SHA256. + // (cc,14)ECDHE-ECDSA-CHACHA20-POLY1305-SHA256128 BitKey exchange: ECDH, encryption: ChaCha20 Poly1305, MAC: SHA256. + // (cc,13)ECDHE-RSA-CHACHA20-POLY1305-SHA256128 BitKey exchange: ECDH, encryption: ChaCha20 Poly1305, MAC: SHA256. + // (c0,0a)ECDHE-ECDSA-AES256-SHA256 BitKey exchange: ECDH, encryption: AES, MAC: SHA1. + // (c0,14)ECDHE-RSA-AES256-SHA256 BitKey exchange: ECDH, encryption: AES, MAC: SHA1. + // (00,39)DHE-RSA-AES256-SHA256 BitKey exchange: DH, encryption: AES, MAC: SHA1. + // (c0,09)ECDHE-ECDSA-AES128-SHA128 BitKey exchange: ECDH, encryption: AES, MAC: SHA1. + // (c0,13)ECDHE-RSA-AES128-SHA128 BitKey exchange: ECDH, encryption: AES, MAC: SHA1. + // (00,33)DHE-RSA-AES128-SHA128 BitKey exchange: DH, encryption: AES, MAC: SHA1. + // (00,9c)RSA-AES128-GCM-SHA256128 BitKey exchange: RSA, encryption: AES, MAC: SHA256. + // (00,35)RSA-AES256-SHA256 BitKey exchange: RSA, encryption: AES, MAC: SHA1. + // (00,2f)RSA-AES128-SHA128 BitKey exchange: RSA, encryption: AES, MAC: SHA1. + // (00,0a)RSA-3DES-EDE-SHA168 BitKey exchange: RSA, encryption: 3DES, MAC: SHA1. + + // thats what firefox 42 supports (https://cc.dcsec.uni-hannover.de/) + // (c0,2b) ECDHE-ECDSA-AES128-GCM-SHA256 + // (c0,2f) ECDHE-RSA-AES128-GCM-SHA256 + // (c0,0a) ECDHE-ECDSA-AES256-SHA + // (c0,09) ECDHE-ECDSA-AES128-SHA + // (c0,13) ECDHE-RSA-AES128-SHA + // (c0,14) ECDHE-RSA-AES256-SHA + // (00,33) DHE-RSA-AES128-SHA + // (00,39) DHE-RSA-AES256-SHA + // (00,2f) RSA-AES128-SHA + // (00,35) RSA-AES256-SHA + // (00,0a) RSA-3DES-EDE-SHA + + // Mozilla modern browsers + // https://wiki.mozilla.org/Security/Server_Side_TLS + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_DHE_DSS_WITH_AES_128_GCM_SHA256", + "TLS_DHE_DSS_WITH_AES_256_GCM_SHA384", + "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", + "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_ECDSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256", + "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", + "TLS_DHE_DSS_WITH_AES_256_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", + + // TLS 1.3 + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256", // Open SSL >= 1.1.1 and Java >= 12 + + // TLS 1.2 CHACHA20 POLY1305 supported by Java >= 12 and + // OpenSSL >= 1.1.0 + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + + // IBM + "SSL_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "SSL_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "SSL_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "SSL_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "SSL_DHE_RSA_WITH_AES_128_GCM_SHA256", + "SSL_DHE_DSS_WITH_AES_128_GCM_SHA256", + "SSL_DHE_DSS_WITH_AES_256_GCM_SHA384", + "SSL_DHE_RSA_WITH_AES_256_GCM_SHA384", + "SSL_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "SSL_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + "SSL_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "SSL_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "SSL_ECDHE_RSA_WITH_AES_256_CBC_SHA384", + "SSL_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", + "SSL_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "SSL_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "SSL_DHE_RSA_WITH_AES_128_CBC_SHA256", + "SSL_DHE_RSA_WITH_AES_128_CBC_SHA", + "SSL_DHE_DSS_WITH_AES_128_CBC_SHA256", + "SSL_DHE_RSA_WITH_AES_256_CBC_SHA256", + "SSL_DHE_DSS_WITH_AES_256_CBC_SHA", + "SSL_DHE_RSA_WITH_AES_256_CBC_SHA" + + // some others + // "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + // "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", + // "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + // "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", + // "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", + // "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + // "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", + // "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", + // "TLS_RSA_WITH_AES_128_CBC_SHA256", + // "TLS_RSA_WITH_AES_128_GCM_SHA256", + // "TLS_RSA_WITH_AES_128_CBC_SHA", + // "TLS_RSA_WITH_AES_256_CBC_SHA", + }; + // @formatter:on + + /** + * + * @param settings SSL Settings + * @return Returns a list of the secure SSL ciphers + */ + public static final List getSecureSSLCiphers(Settings settings) { + + List configuredCiphers = null; + + if (settings != null) { + configuredCiphers = settings.getAsList(SSL_TRANSPORT_ENABLED_CIPHERS, Collections.emptyList()); + } + + if (configuredCiphers != null && configuredCiphers.size() > 0) { + return configuredCiphers; + } + + return Collections.unmodifiableList(Arrays.asList(_SECURE_SSL_CIPHERS)); + } + + private SSLConfigConstants() { + + } + +} diff --git a/src/main/java/org/opensearch/sdk/ssl/SSLConnectionTestResult.java b/src/main/java/org/opensearch/sdk/ssl/SSLConnectionTestResult.java new file mode 100644 index 00000000..48609ee8 --- /dev/null +++ b/src/main/java/org/opensearch/sdk/ssl/SSLConnectionTestResult.java @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.ssl; + +/** + * Return codes for SSLConnectionTestUtil.testConnection() + */ +public enum SSLConnectionTestResult { + /** + * OpenSearch Ping to the server failed. + */ + OPENSEARCH_PING_FAILED, + /** + * Server does not support SSL. + */ + SSL_NOT_AVAILABLE, + /** + * Server supports SSL. + */ + SSL_AVAILABLE +} diff --git a/src/main/java/org/opensearch/sdk/ssl/SSLNettyTransport.java b/src/main/java/org/opensearch/sdk/ssl/SSLNettyTransport.java new file mode 100644 index 00000000..38593d0f --- /dev/null +++ b/src/main/java/org/opensearch/sdk/ssl/SSLNettyTransport.java @@ -0,0 +1,328 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.ssl; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.DecoderException; +import io.netty.handler.ssl.SslHandler; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.ExceptionsHelper; +import org.opensearch.Version; +import org.opensearch.cluster.node.DiscoveryNode; +import org.opensearch.common.io.stream.NamedWriteableRegistry; +import org.opensearch.common.network.NetworkService; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.PageCacheRecycler; +import org.opensearch.indices.breaker.CircuitBreakerService; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.SharedGroupFactory; +import org.opensearch.transport.TcpChannel; +import org.opensearch.transport.netty4.Netty4Transport; + +/** + * Class that setups up secure TLS channel for this extension to use with transport requests + */ +public class SSLNettyTransport extends Netty4Transport { + + private static final Logger logger = LogManager.getLogger(SSLNettyTransport.class); + private final SslKeyStore ossks; + + /** + * + * @param settings SSL Settings + * @param version version + * @param threadPool threadPool + * @param networkService networkService + * @param pageCacheRecycler pageCacheRecycler + * @param namedWriteableRegistry namedWriteableRegistry + * @param circuitBreakerService circuitBreakerService + * @param ossks ossks + * @param sharedGroupFactory sharedGroupFactory + */ + public SSLNettyTransport( + final Settings settings, + final Version version, + final ThreadPool threadPool, + final NetworkService networkService, + final PageCacheRecycler pageCacheRecycler, + final NamedWriteableRegistry namedWriteableRegistry, + final CircuitBreakerService circuitBreakerService, + final SslKeyStore ossks, + SharedGroupFactory sharedGroupFactory + ) { + super( + settings, + version, + threadPool, + networkService, + pageCacheRecycler, + namedWriteableRegistry, + circuitBreakerService, + sharedGroupFactory + ); + + this.ossks = ossks; + } + + /** + * + * @param channel The channel + * @param e Exception + */ + @Override + public void onException(TcpChannel channel, Exception e) { + + Throwable cause = e; + + if (e instanceof DecoderException && e != null) { + cause = e.getCause(); + } + + logger.error("Exception during establishing a SSL connection: " + cause, cause); + + super.onException(channel, e); + } + + /** + * + * @param name name of channel + * @return ChannelHandler + */ + @Override + protected ChannelHandler getServerChannelInitializer(String name) { + return new SSLServerChannelInitializer(name); + } + + /** + * + * @param node Node this channel is connected to + * @return ChannelHandler + */ + @Override + protected ChannelHandler getClientChannelInitializer(DiscoveryNode node) { + return new SSLClientChannelInitializer(node); + } + + /** + * SSLServerChannelInitializer + */ + protected class SSLServerChannelInitializer extends Netty4Transport.ServerChannelInitializer { + + /** + * + * @param name name of this channel + */ + public SSLServerChannelInitializer(String name) { + super(name); + } + + /** + * + * @param ch the {@link Channel} which was registered. + * @throws Exception + */ + @Override + protected void initChannel(Channel ch) throws Exception { + super.initChannel(ch); + + final SslHandler sslHandler = new SslHandler(ossks.createServerTransportSSLEngine()); + ch.pipeline().addFirst("ssl_server", sslHandler); + } + + /** + * + * @param ctx ChannelHandlerContext + * @param cause Throwable + * @throws Exception + */ + @Override + public final void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + if (cause instanceof DecoderException && cause != null) { + cause = cause.getCause(); + } + + logger.error("Exception during establishing a SSL connection: " + cause, cause); + + super.exceptionCaught(ctx, cause); + } + } + + /** + * ClientSSLHandler + */ + protected static class ClientSSLHandler extends ChannelOutboundHandlerAdapter { + private final Logger log = LogManager.getLogger(this.getClass()); + private final SslKeyStore sks; + private final boolean hostnameVerificationEnabled; + private final boolean hostnameVerificationResovleHostName; + + /** + * + * @param sks Security Keystore + * @param hostnameVerificationEnabled flag to indicate if hostname verification is enabled + * @param hostnameVerificationResolveHostName flag to indicate if hostnames should be resolved with hostname + * verification + */ + private ClientSSLHandler( + final SslKeyStore sks, + final boolean hostnameVerificationEnabled, + final boolean hostnameVerificationResolveHostName + ) { + this.sks = sks; + this.hostnameVerificationEnabled = hostnameVerificationEnabled; + this.hostnameVerificationResovleHostName = hostnameVerificationResolveHostName; + } + + /** + * + * @param ctx ChannelHandlerContext + * @param cause Throwable + * @throws Exception + */ + @Override + public final void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + if (cause instanceof DecoderException && cause != null) { + cause = cause.getCause(); + } + + logger.error("Exception during establishing a SSL connection: " + cause, cause); + + super.exceptionCaught(ctx, cause); + } + + /** + * + * @param ctx the {@link ChannelHandlerContext} for which the connect operation is made + * @param remoteAddress the {@link SocketAddress} to which it should connect + * @param localAddress the {@link SocketAddress} which is used as source on connect + * @param promise the {@link ChannelPromise} to notify once the operation completes + * @throws Exception + */ + @Override + public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) + throws Exception { + SSLEngine engine = null; + try { + if (hostnameVerificationEnabled) { + final InetSocketAddress inetSocketAddress = (InetSocketAddress) remoteAddress; + String hostname = null; + if (hostnameVerificationResovleHostName) { + hostname = inetSocketAddress.getHostName(); + } else { + hostname = inetSocketAddress.getHostString(); + } + + if (log.isDebugEnabled()) { + log.debug( + "Hostname of peer is {} ({}/{}) with hostnameVerificationResolveHostName: {}", + hostname, + inetSocketAddress.getHostName(), + inetSocketAddress.getHostString(), + hostnameVerificationResovleHostName + ); + } + + engine = sks.createClientTransportSSLEngine(hostname, inetSocketAddress.getPort()); + } else { + engine = sks.createClientTransportSSLEngine(null, -1); + } + } catch (final SSLException e) { + throw ExceptionsHelper.convertToOpenSearchException(e); + } + final SslHandler sslHandler = new SslHandler(engine); + ctx.pipeline().replace(this, "ssl_client", sslHandler); + super.connect(ctx, remoteAddress, localAddress, promise); + } + } + + /** + * SSLClientChannelInitializer + */ + protected class SSLClientChannelInitializer extends Netty4Transport.ClientChannelInitializer { + private final boolean hostnameVerificationEnabled; + private final boolean hostnameVerificationResovleHostName; + private final DiscoveryNode node; + private SSLConnectionTestResult connectionTestResult; + + /** + * + * @param node The node to connect to + */ + @SuppressWarnings("removal") + public SSLClientChannelInitializer(DiscoveryNode node) { + this.node = node; + hostnameVerificationEnabled = settings.getAsBoolean(SSLConfigConstants.SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION, true); + hostnameVerificationResovleHostName = settings.getAsBoolean( + SSLConfigConstants.SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME, + true + ); + + connectionTestResult = SSLConnectionTestResult.SSL_AVAILABLE; + } + + /** + * + * @param ch the {@link Channel} which was registered. + * @throws Exception + */ + @Override + protected void initChannel(Channel ch) throws Exception { + super.initChannel(ch); + + if (connectionTestResult == SSLConnectionTestResult.OPENSEARCH_PING_FAILED) { + logger.error( + "SSL dual mode is enabled but dual mode handshake and OpenSearch ping has failed during client connection setup, closing channel" + ); + ch.close(); + return; + } + + if (connectionTestResult == SSLConnectionTestResult.SSL_AVAILABLE) { + logger.debug("Connection to {} needs to be ssl, adding ssl handler to the client channel ", node.getHostName()); + ch.pipeline() + .addFirst( + "client_ssl_handler", + new ClientSSLHandler(ossks, hostnameVerificationEnabled, hostnameVerificationResovleHostName) + ); + } else { + logger.debug("Connection to {} needs to be non ssl", node.getHostName()); + } + } + + /** + * + * @param ctx ChannelHandlerContext + * @param cause Throwable + * @throws Exception + */ + @Override + public final void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + if (cause instanceof DecoderException && cause != null) { + cause = cause.getCause(); + } + + logger.error("Exception during establishing a SSL connection: " + cause, cause); + + super.exceptionCaught(ctx, cause); + } + } +} diff --git a/src/main/java/org/opensearch/sdk/ssl/SecureSSLSettings.java b/src/main/java/org/opensearch/sdk/ssl/SecureSSLSettings.java new file mode 100644 index 00000000..604a1973 --- /dev/null +++ b/src/main/java/org/opensearch/sdk/ssl/SecureSSLSettings.java @@ -0,0 +1,148 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.ssl; + +import java.util.Optional; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.common.settings.SecureSetting; +import org.opensearch.common.settings.SecureString; +import org.opensearch.common.settings.Setting; +import org.opensearch.common.settings.Settings; + +import static org.opensearch.sdk.ssl.SSLConfigConstants.DEFAULT_STORE_PASSWORD; + +/** + * Container for secured settings (passwords for certs, keystores) and the now deprecated original settings + */ +public final class SecureSSLSettings { + private static final Logger LOG = LogManager.getLogger(SecureSSLSettings.class); + + private static final String SECURE_SUFFIX = "_secure"; + private static final String PREFIX = "ssl"; + private static final String TRANSPORT_PREFIX = PREFIX + ".transport"; + + /** + * SSLSetting + */ + public enum SSLSetting { + // transport settings + SSL_TRANSPORT_PEMKEY_PASSWORD(TRANSPORT_PREFIX + ".pemkey_password"), + SSL_TRANSPORT_SERVER_PEMKEY_PASSWORD(TRANSPORT_PREFIX + ".server.pemkey_password"), + SSL_TRANSPORT_CLIENT_PEMKEY_PASSWORD(TRANSPORT_PREFIX + ".client.pemkey_password"), + SSL_TRANSPORT_KEYSTORE_PASSWORD(TRANSPORT_PREFIX + ".keystore_password"), + SSL_TRANSPORT_KEYSTORE_KEYPASSWORD(TRANSPORT_PREFIX + ".keystore_keypassword"), + SSL_TRANSPORT_SERVER_KEYSTORE_KEYPASSWORD(TRANSPORT_PREFIX + ".server.keystore_keypassword"), + SSL_TRANSPORT_CLIENT_KEYSTORE_KEYPASSWORD(TRANSPORT_PREFIX + ".client.keystore_keypassword"), + SSL_TRANSPORT_TRUSTSTORE_PASSWORD(TRANSPORT_PREFIX + ".truststore_password", DEFAULT_STORE_PASSWORD); + + /** + * + * @param insecurePropertyName insecure property name + */ + SSLSetting(String insecurePropertyName) { + this(insecurePropertyName, null); + } + + /** + * + * @param insecurePropertyName insecure property name + * @param defaultValue default value + */ + SSLSetting(String insecurePropertyName, String defaultValue) { + this.insecurePropertyName = insecurePropertyName; + this.propertyName = String.format("%s%s", this.insecurePropertyName, SECURE_SUFFIX); + this.defaultValue = defaultValue; + } + + public final String insecurePropertyName; + + public final String propertyName; + + public final String defaultValue; + + /** + * + * @return Returns the setting with a secure string + */ + public Setting asSetting() { + return SecureSetting.secureString(this.propertyName, new InsecureFallbackStringSetting(this.insecurePropertyName)); + } + + /** + * + * @return Returns the setting with an insecure string + */ + public Setting asInsecureSetting() { + return new InsecureFallbackStringSetting(this.insecurePropertyName); + } + + /** + * + * @param settings Gets this setting from all settings + * @return The value of the setting + */ + public String getSetting(Settings settings) { + return this.getSetting(settings, this.defaultValue); + } + + /** + * + * @param settings Gets this setting from all settings + * @param defaultValue default value if the setting does not exist + * @return The value of the setting + */ + public String getSetting(Settings settings, String defaultValue) { + return Optional.of(this.asSetting().get(settings)) + .filter(ss -> ss.length() > 0) + .map(SecureString::toString) + .orElse(defaultValue); + } + } + + private SecureSSLSettings() {} + + /** + * Alternative to InsecureStringSetting, which doesn't raise an exception if allow_insecure_settings is false, but + * instead log.WARNs the violation. This is to appease a potential cyclic dependency between commons-utils + */ + private static class InsecureFallbackStringSetting extends Setting { + private final String name; + + /** + * + * @param name name of the insecure setting + */ + private InsecureFallbackStringSetting(String name) { + super(name, "", s -> new SecureString(s.toCharArray()), Property.Deprecated, Property.Filtered, Property.NodeScope); + this.name = name; + } + + /** + * + * @param settings all settings + * @return returns a secure string of the setting + */ + public SecureString get(Settings settings) { + if (this.exists(settings)) { + LOG.warn( + "Setting [{}] has a secure counterpart [{}{}] which should be used instead - allowing for legacy SSL setups", + this.name, + this.name, + SECURE_SUFFIX + ); + } + + return super.get(settings); + } + } +} diff --git a/src/main/java/org/opensearch/sdk/ssl/SslKeyStore.java b/src/main/java/org/opensearch/sdk/ssl/SslKeyStore.java new file mode 100644 index 00000000..711b5d89 --- /dev/null +++ b/src/main/java/org/opensearch/sdk/ssl/SslKeyStore.java @@ -0,0 +1,49 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.ssl; + +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; + +/** + * Interface for an SslKeyStore + */ +public interface SslKeyStore { + + /** + * + * @return SSLEngine + * @throws SSLException + */ + public SSLEngine createServerTransportSSLEngine() throws SSLException; + + /** + * + * @param peerHost Peer Hostname + * @param peerPort Peer Port + * @return SSLEngine + * @throws SSLException + */ + public SSLEngine createClientTransportSSLEngine(String peerHost, int peerPort) throws SSLException; + + /** + * + * @param cert Public Certificate + * @return Returns the san from the certificate + */ + public String getSubjectAlternativeNames(X509Certificate cert); + + /** + * Initialize SSL config + */ + public void initTransportSSLConfig(); +} diff --git a/src/main/java/org/opensearch/sdk/ssl/util/CertFileProps.java b/src/main/java/org/opensearch/sdk/ssl/util/CertFileProps.java new file mode 100644 index 00000000..b79144da --- /dev/null +++ b/src/main/java/org/opensearch/sdk/ssl/util/CertFileProps.java @@ -0,0 +1,50 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.ssl.util; + +/** + * File that contains properties of a certificate file + */ +public class CertFileProps { + private final String pemCertFilePath; + private final String pemKeyFilePath; + private final String trustedCasFilePath; + private final String pemKeyPassword; + + /** + * + * @param pemCertFilePath Path to the certificate file in the .pem format + * @param pemKeyFilePath Path to the private key file in the .pem format + * @param trustedCasFilePath Path to the trusted CA file + * @param pemKeyPassword Password for the private key file + */ + public CertFileProps(String pemCertFilePath, String pemKeyFilePath, String trustedCasFilePath, String pemKeyPassword) { + this.pemCertFilePath = pemCertFilePath; + this.pemKeyFilePath = pemKeyFilePath; + this.trustedCasFilePath = trustedCasFilePath; + this.pemKeyPassword = pemKeyPassword; + } + + public String getPemCertFilePath() { + return pemCertFilePath; + } + + public String getPemKeyFilePath() { + return pemKeyFilePath; + } + + public String getTrustedCasFilePath() { + return trustedCasFilePath; + } + + public String getPemKeyPassword() { + return pemKeyPassword; + } +} diff --git a/src/main/java/org/opensearch/sdk/ssl/util/CertFromFile.java b/src/main/java/org/opensearch/sdk/ssl/util/CertFromFile.java new file mode 100644 index 00000000..e0f41788 --- /dev/null +++ b/src/main/java/org/opensearch/sdk/ssl/util/CertFromFile.java @@ -0,0 +1,115 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.ssl.util; + +import java.io.File; +import java.security.cert.X509Certificate; + +/** + * Class with methods for reading in a certificate from a file + */ +public class CertFromFile { + private final CertFileProps clientCertProps; + private final CertFileProps serverCertProps; + + private final File serverPemCert; + private final File serverPemKey; + private final File serverTrustedCas; + + private final File clientPemCert; + private final File clientPemKey; + private final File clientTrustedCas; + + private final X509Certificate[] loadedCerts; + + /** + * + * @param clientCertProps Client certificate properties + * @param serverCertProps Server certificate properties + * @throws Exception + */ + public CertFromFile(CertFileProps clientCertProps, CertFileProps serverCertProps) throws Exception { + this.serverCertProps = serverCertProps; + this.serverPemCert = new File(serverCertProps.getPemCertFilePath()); + this.serverPemKey = new File(serverCertProps.getPemKeyFilePath()); + this.serverTrustedCas = nullOrFile(serverCertProps.getTrustedCasFilePath()); + + this.clientCertProps = clientCertProps; + this.clientPemCert = new File(clientCertProps.getPemCertFilePath()); + this.clientPemKey = new File(clientCertProps.getPemKeyFilePath()); + this.clientTrustedCas = nullOrFile(clientCertProps.getTrustedCasFilePath()); + + loadedCerts = new X509Certificate[] { + PemKeyReader.loadCertificateFromFile(clientCertProps.getPemCertFilePath()), + PemKeyReader.loadCertificateFromFile(serverCertProps.getPemCertFilePath()) }; + } + + /** + * + * @param certProps Certificate properties + * @throws Exception + */ + public CertFromFile(CertFileProps certProps) throws Exception { + this.serverCertProps = certProps; + this.serverPemCert = new File(certProps.getPemCertFilePath()); + this.serverPemKey = new File(certProps.getPemKeyFilePath()); + this.serverTrustedCas = nullOrFile(certProps.getTrustedCasFilePath()); + + this.clientCertProps = serverCertProps; + this.clientPemCert = serverPemCert; + this.clientPemKey = serverPemKey; + this.clientTrustedCas = serverTrustedCas; + + loadedCerts = new X509Certificate[] { PemKeyReader.loadCertificateFromFile(certProps.getPemCertFilePath()) }; + } + + public X509Certificate[] getCerts() { + return loadedCerts; + } + + public File getServerPemKey() { + return serverPemKey; + } + + public File getServerPemCert() { + return serverPemCert; + } + + public File getServerTrustedCas() { + return serverTrustedCas; + } + + public String getServerPemKeyPassword() { + return serverCertProps.getPemKeyPassword(); + } + + public File getClientPemKey() { + return clientPemKey; + } + + public File getClientPemCert() { + return clientPemCert; + } + + public File getClientTrustedCas() { + return clientTrustedCas; + } + + public String getClientPemKeyPassword() { + return clientCertProps.getPemKeyPassword(); + } + + private File nullOrFile(String path) { + if (path != null) { + return new File(path); + } + return null; + } +} diff --git a/src/main/java/org/opensearch/sdk/ssl/util/CertFromKeystore.java b/src/main/java/org/opensearch/sdk/ssl/util/CertFromKeystore.java new file mode 100644 index 00000000..57ce7acc --- /dev/null +++ b/src/main/java/org/opensearch/sdk/ssl/util/CertFromKeystore.java @@ -0,0 +1,154 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.ssl.util; + +import org.opensearch.OpenSearchException; + +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Class that contains methods for reading in a certificate from a keystore + */ +public class CertFromKeystore { + + private final KeystoreProps keystoreProps; + private final String serverKeystoreAlias; + private final String clientKeystoreAlias; + + private PrivateKey serverKey; + private X509Certificate[] serverCert; + private final char[] serverKeyPassword; + + private PrivateKey clientKey; + private X509Certificate[] clientCert; + private final char[] clientKeyPassword; + + private X509Certificate[] loadedCerts; + + /** + * + * @param keystoreProps Keystore Props + * @param keystoreAlias Keystore Alias + * @param keyPassword Keystore Password + * @throws CertificateException + * @throws NoSuchAlgorithmException + * @throws KeyStoreException + * @throws IOException + * @throws UnrecoverableKeyException + */ + public CertFromKeystore(KeystoreProps keystoreProps, String keystoreAlias, String keyPassword) throws CertificateException, + NoSuchAlgorithmException, KeyStoreException, IOException, UnrecoverableKeyException { + this.keystoreProps = keystoreProps; + final KeyStore ks = keystoreProps.loadKeystore(); + + this.serverKeystoreAlias = keystoreAlias; + this.serverKeyPassword = Utils.toCharArray(keyPassword); + this.serverCert = SSLCertificateHelper.exportServerCertChain(ks, serverKeystoreAlias); + this.serverKey = SSLCertificateHelper.exportDecryptedKey(ks, serverKeystoreAlias, this.serverKeyPassword); + + this.clientKeystoreAlias = keystoreAlias; + this.clientKeyPassword = serverKeyPassword; + this.clientCert = serverCert; + this.clientKey = serverKey; + + this.loadedCerts = serverCert; + + validate(); + } + + /** + * + * @param keystoreProps Keystore Props + * @param serverKeystoreAlias Server Keystore Alias + * @param clientKeystoreAlias Client Keystore Alias + * @param serverKeyPassword Server Key Password + * @param clientKeyPassword Client Key Password + * @throws CertificateException + * @throws NoSuchAlgorithmException + * @throws KeyStoreException + * @throws IOException + * @throws UnrecoverableKeyException + */ + public CertFromKeystore( + KeystoreProps keystoreProps, + String serverKeystoreAlias, + String clientKeystoreAlias, + String serverKeyPassword, + String clientKeyPassword + ) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, UnrecoverableKeyException { + this.keystoreProps = keystoreProps; + final KeyStore ks = keystoreProps.loadKeystore(); + + this.serverKeystoreAlias = serverKeystoreAlias; + this.serverKeyPassword = Utils.toCharArray(serverKeyPassword); + this.serverCert = SSLCertificateHelper.exportServerCertChain(ks, serverKeystoreAlias); + this.serverKey = SSLCertificateHelper.exportDecryptedKey(ks, serverKeystoreAlias, this.serverKeyPassword); + + this.clientKeystoreAlias = clientKeystoreAlias; + this.clientKeyPassword = Utils.toCharArray(clientKeyPassword); + this.clientCert = SSLCertificateHelper.exportServerCertChain(ks, clientKeystoreAlias); + this.clientKey = SSLCertificateHelper.exportDecryptedKey(ks, clientKeystoreAlias, this.clientKeyPassword); + + List allCerts = new ArrayList<>(serverCert.length + clientCert.length); + Collections.addAll(allCerts, serverCert); + Collections.addAll(allCerts, clientCert); + this.loadedCerts = allCerts.toArray(new X509Certificate[allCerts.size()]); + + validate(); + } + + private void validate() { + if (serverKey == null) { + throw new OpenSearchException("No key found in " + keystoreProps.getFilePath() + " with alias " + serverKeystoreAlias); + } + + if (serverCert == null || serverCert.length == 0) { + throw new OpenSearchException("No certificates found in " + keystoreProps.getFilePath() + " with alias " + serverKeystoreAlias); + } + + if (clientKey == null) { + throw new OpenSearchException("No key found in " + keystoreProps.getFilePath() + " with alias " + clientKeystoreAlias); + } + + if (clientCert == null || clientCert.length == 0) { + throw new OpenSearchException("No certificates found in " + keystoreProps.getFilePath() + " with alias " + clientKeystoreAlias); + } + } + + public X509Certificate[] getCerts() { + return loadedCerts; + } + + public PrivateKey getServerKey() { + return serverKey; + } + + public X509Certificate[] getServerCert() { + return serverCert; + } + + public PrivateKey getClientKey() { + return clientKey; + } + + public X509Certificate[] getClientCert() { + return clientCert; + } +} diff --git a/src/main/java/org/opensearch/sdk/ssl/util/CertFromTruststore.java b/src/main/java/org/opensearch/sdk/ssl/util/CertFromTruststore.java new file mode 100644 index 00000000..237dfbc3 --- /dev/null +++ b/src/main/java/org/opensearch/sdk/ssl/util/CertFromTruststore.java @@ -0,0 +1,116 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.ssl.util; + +import org.opensearch.OpenSearchException; + +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * Helper class with methods to read a certificate from a truststore + */ +public class CertFromTruststore { + private final KeystoreProps keystoreProps; + + private final String serverTruststoreAlias; + private final X509Certificate[] serverTrustedCerts; + + private final String clientTruststoreAlias; + private final X509Certificate[] clientTrustedCerts; + + /** + * Default constructor + */ + public CertFromTruststore() { + keystoreProps = null; + serverTruststoreAlias = null; + serverTrustedCerts = null; + clientTruststoreAlias = null; + clientTrustedCerts = null; + } + + /** + * + * @return An empty CertFromTruststore object + */ + public static CertFromTruststore Empty() { + return new CertFromTruststore(); + } + + /** + * + * @param keystoreProps Keystore Props + * @param truststoreAlias Truststore Alias + * @throws CertificateException + * @throws NoSuchAlgorithmException + * @throws KeyStoreException + * @throws IOException + */ + public CertFromTruststore(KeystoreProps keystoreProps, String truststoreAlias) throws CertificateException, NoSuchAlgorithmException, + KeyStoreException, IOException { + this.keystoreProps = keystoreProps; + final KeyStore ts = keystoreProps.loadKeystore(); + + serverTruststoreAlias = truststoreAlias; + serverTrustedCerts = SSLCertificateHelper.exportRootCertificates(ts, truststoreAlias); + + clientTruststoreAlias = serverTruststoreAlias; + clientTrustedCerts = serverTrustedCerts; + + validate(); + } + + /** + * + * @param keystoreProps Keystore Props + * @param serverTruststoreAlias Server Truststore Alias + * @param clientTruststoreAlias Client Truststore Alias + * @throws CertificateException + * @throws NoSuchAlgorithmException + * @throws KeyStoreException + * @throws IOException + */ + public CertFromTruststore(KeystoreProps keystoreProps, String serverTruststoreAlias, String clientTruststoreAlias) + throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException { + this.keystoreProps = keystoreProps; + final KeyStore ts = this.keystoreProps.loadKeystore(); + + this.serverTruststoreAlias = serverTruststoreAlias; + serverTrustedCerts = SSLCertificateHelper.exportRootCertificates(ts, this.serverTruststoreAlias); + + this.clientTruststoreAlias = clientTruststoreAlias; + clientTrustedCerts = SSLCertificateHelper.exportRootCertificates(ts, this.clientTruststoreAlias); + + validate(); + } + + private void validate() { + if (serverTrustedCerts == null || serverTrustedCerts.length == 0) { + throw new OpenSearchException("No truststore configured for server certs"); + } + + if (clientTrustedCerts == null || clientTrustedCerts.length == 0) { + throw new OpenSearchException("No truststore configured for client certs"); + } + } + + public X509Certificate[] getServerTrustedCerts() { + return serverTrustedCerts; + } + + public X509Certificate[] getClientTrustedCerts() { + return clientTrustedCerts; + } +} diff --git a/src/main/java/org/opensearch/sdk/ssl/util/ExceptionUtils.java b/src/main/java/org/opensearch/sdk/ssl/util/ExceptionUtils.java new file mode 100644 index 00000000..e03da980 --- /dev/null +++ b/src/main/java/org/opensearch/sdk/ssl/util/ExceptionUtils.java @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.ssl.util; + +/** + * File with help methods for handling exceptions related to SSL + */ +public class ExceptionUtils { + + /** + * + * @param e Throwable + * @param msg Message to find within message text + * @return Returns the throwable if it contains the message text + */ + public static Throwable findMsg(final Throwable e, String msg) { + + if (e == null) { + return null; + } + + if (e.getMessage() != null && e.getMessage().contains(msg)) { + return e; + } + + final Throwable cause = e.getCause(); + if (cause == null) { + return null; + } + return findMsg(cause, msg); + } +} diff --git a/src/main/java/org/opensearch/sdk/ssl/util/KeystoreProps.java b/src/main/java/org/opensearch/sdk/ssl/util/KeystoreProps.java new file mode 100644 index 00000000..70ba6051 --- /dev/null +++ b/src/main/java/org/opensearch/sdk/ssl/util/KeystoreProps.java @@ -0,0 +1,65 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.ssl.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +/** + * File that contains properties for a keystore + */ +public class KeystoreProps { + private final String filePath; + private final String type; + private final char[] password; + + /** + * + * @param filePath Filepath to the keystore + * @param type The type of keystore + * @param password The password to the keystore + */ + public KeystoreProps(String filePath, String type, String password) { + this.filePath = filePath; + this.type = type; + this.password = Utils.toCharArray(password); + } + + public String getFilePath() { + return filePath; + } + + public String getType() { + return type; + } + + public char[] getPassword() { + return password; + } + + /** + * + * @return Returns the keystore give the keystore props + * @throws KeyStoreException + * @throws IOException + * @throws CertificateException + * @throws NoSuchAlgorithmException + */ + public KeyStore loadKeystore() throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException { + final KeyStore ts = KeyStore.getInstance(type); + ts.load(new FileInputStream(new File(filePath)), password); + return ts; + } +} diff --git a/src/main/java/org/opensearch/sdk/ssl/util/PemKeyReader.java b/src/main/java/org/opensearch/sdk/ssl/util/PemKeyReader.java new file mode 100644 index 00000000..de62d7da --- /dev/null +++ b/src/main/java/org/opensearch/sdk/ssl/util/PemKeyReader.java @@ -0,0 +1,43 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.ssl.util; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.FileInputStream; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +/** + * Class that reads a private key in .pem format + */ +public final class PemKeyReader { + protected static final Logger log = LogManager.getLogger(PemKeyReader.class); + + /** + * + * @param file Path to certificate file + * @return Returns an X509Certificate object + * @throws Exception + */ + public static X509Certificate loadCertificateFromFile(String file) throws Exception { + if (file == null) { + return null; + } + + CertificateFactory fact = CertificateFactory.getInstance("X.509"); + try (FileInputStream is = new FileInputStream(file)) { + return (X509Certificate) fact.generateCertificate(is); + } + } + + private PemKeyReader() {} +} diff --git a/src/main/java/org/opensearch/sdk/ssl/util/SSLCertificateHelper.java b/src/main/java/org/opensearch/sdk/ssl/util/SSLCertificateHelper.java new file mode 100644 index 00000000..f5e17153 --- /dev/null +++ b/src/main/java/org/opensearch/sdk/ssl/util/SSLCertificateHelper.java @@ -0,0 +1,224 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.ssl.util; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.common.Strings; + +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; + +/** + * Helper class with helpful methods related to SSL Certificates + */ +public class SSLCertificateHelper { + + private static final Logger log = LogManager.getLogger(SSLCertificateHelper.class); + private static boolean stripRootFromChain = true; // TODO check + + /** + * + * @param ks Keystore + * @param alias Keystore Alias + * @return Returns an array of root certificates + * @throws KeyStoreException + */ + public static X509Certificate[] exportRootCertificates(final KeyStore ks, final String alias) throws KeyStoreException { + logKeyStore(ks); + + final List trustedCerts = new ArrayList(); + + if (Strings.isNullOrEmpty(alias)) { + + if (log.isDebugEnabled()) { + log.debug("No alias given, will trust all of the certificates in the store"); + } + + final List aliases = toList(ks.aliases()); + + for (final String _alias : aliases) { + + if (ks.isCertificateEntry(_alias)) { + final X509Certificate cert = (X509Certificate) ks.getCertificate(_alias); + if (cert != null) { + trustedCerts.add(cert); + } else { + log.error("Alias {} does not exist", _alias); + } + } + } + } else { + if (ks.isCertificateEntry(alias)) { + final X509Certificate cert = (X509Certificate) ks.getCertificate(alias); + if (cert != null) { + trustedCerts.add(cert); + } else { + log.error("Alias {} does not exist", alias); + } + } else { + log.error("Alias {} does not contain a certificate entry", alias); + } + } + + return trustedCerts.toArray(new X509Certificate[0]); + } + + /** + * + * @param ks Keystore + * @param alias Keystore Alias + * @return Returns the chain of certificates + * @throws KeyStoreException + */ + public static X509Certificate[] exportServerCertChain(final KeyStore ks, String alias) throws KeyStoreException { + logKeyStore(ks); + final List aliases = toList(ks.aliases()); + + if (Strings.isNullOrEmpty(alias)) { + if (aliases.isEmpty()) { + log.error("Keystore does not contain any aliases"); + } else { + alias = aliases.get(0); + log.info("No alias given, use the first one: {}", alias); + } + } + + final Certificate[] certs = ks.getCertificateChain(alias); + if (certs != null && certs.length > 0) { + X509Certificate[] x509Certs = Arrays.copyOf(certs, certs.length, X509Certificate[].class); + + final X509Certificate lastCertificate = x509Certs[x509Certs.length - 1]; + + if (lastCertificate.getBasicConstraints() > -1 + && lastCertificate.getSubjectX500Principal().equals(lastCertificate.getIssuerX500Principal())) { + log.warn("Certificate chain for alias {} contains a root certificate", alias); + + if (stripRootFromChain) { + x509Certs = Arrays.copyOf(certs, certs.length - 1, X509Certificate[].class); + } + } + + return x509Certs; + } else { + log.error("Alias {} does not exist or contain a certificate chain", alias); + } + + return new X509Certificate[0]; + } + + /** + * + * @param ks Keystore + * @param alias Keystore Alias + * @param keyPassword Keystore Password + * @return Returns the private key from the keystore + * @throws KeyStoreException + * @throws UnrecoverableKeyException + * @throws NoSuchAlgorithmException + */ + public static PrivateKey exportDecryptedKey(final KeyStore ks, final String alias, final char[] keyPassword) throws KeyStoreException, + UnrecoverableKeyException, NoSuchAlgorithmException { + logKeyStore(ks); + final List aliases = toList(ks.aliases()); + + String evaluatedAlias = alias; + + if (alias == null && aliases.size() > 0) { + evaluatedAlias = aliases.get(0); + } + + if (evaluatedAlias == null) { + throw new KeyStoreException("null alias, current aliases: " + aliases); + } + + final Key key = ks.getKey(evaluatedAlias, (keyPassword == null || keyPassword.length == 0) ? null : keyPassword); + + if (key == null) { + throw new KeyStoreException("no key alias named " + evaluatedAlias); + } + + if (key instanceof PrivateKey) { + return (PrivateKey) key; + } + + return null; + } + + /** + * + * @param ks Keystore + */ + private static void logKeyStore(final KeyStore ks) { + try { + final List aliases = toList(ks.aliases()); + if (log.isDebugEnabled()) { + log.debug("Keystore has {} entries/aliases", ks.size()); + for (String _alias : aliases) { + log.debug( + "Alias {}: is a certificate entry? {}/is a key entry? {}", + _alias, + ks.isCertificateEntry(_alias), + ks.isKeyEntry(_alias) + ); + Certificate[] certs = ks.getCertificateChain(_alias); + + if (certs != null) { + log.debug("Alias {}: chain len {}", _alias, certs.length); + for (int i = 0; i < certs.length; i++) { + X509Certificate certificate = (X509Certificate) certs[i]; + log.debug( + "cert {} of type {} -> {}", + certificate.getSubjectX500Principal(), + certificate.getBasicConstraints(), + certificate.getSubjectX500Principal().equals(certificate.getIssuerX500Principal()) + ); + } + } + + X509Certificate cert = (X509Certificate) ks.getCertificate(_alias); + + if (cert != null) { + log.debug( + "Alias {}: single cert {} of type {} -> {}", + _alias, + cert.getSubjectX500Principal(), + cert.getBasicConstraints(), + cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal()) + ); + } + } + } + } catch (Exception e) { + log.error("Error logging keystore due to " + e, e); + } + } + + private static List toList(final Enumeration enumeration) { + final List aliases = new ArrayList<>(); + + while (enumeration.hasMoreElements()) { + aliases.add(enumeration.nextElement()); + } + + return Collections.unmodifiableList(aliases); + } +} diff --git a/src/main/java/org/opensearch/sdk/ssl/util/Utils.java b/src/main/java/org/opensearch/sdk/ssl/util/Utils.java new file mode 100644 index 00000000..3ef239a1 --- /dev/null +++ b/src/main/java/org/opensearch/sdk/ssl/util/Utils.java @@ -0,0 +1,51 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.ssl.util; + +/** + * Convenience class that contains utility methods used for SSL + */ +public class Utils { + + /** + * + * @param first First nullable arg + * @param more list of potentially nullable args + * @return The first non-null arg + * @param The type of arg + */ + public static T coalesce(T first, T... more) { + if (first != null) { + return first; + } + + if (more == null || more.length == 0) { + return null; + } + + for (int i = 0; i < more.length; i++) { + T t = more[i]; + if (t != null) { + return t; + } + } + + return null; + } + + /** + * + * @param str String to convert + * @return char[] representation of the input str + */ + public static char[] toCharArray(String str) { + return (str == null || str.length() == 0) ? null : str.toCharArray(); + } +} diff --git a/src/main/resources/sample/helloworld-settings.yml b/src/main/resources/sample/helloworld-settings.yml index 417136e2..c88bab73 100644 --- a/src/main/resources/sample/helloworld-settings.yml +++ b/src/main/resources/sample/helloworld-settings.yml @@ -3,3 +3,9 @@ hostAddress: 127.0.0.1 hostPort: 4532 opensearchAddress: 127.0.0.1 opensearchPort: 9200 +ssl.transport.enabled: true +ssl.transport.pemcert_filepath: certs/extension-01.pem +ssl.transport.pemkey_filepath: certs/extension-01-key.pem +ssl.transport.pemtrustedcas_filepath: certs/root-ca.pem +ssl.transport.enforce_hostname_verification: false +path.home: diff --git a/src/test/java/org/opensearch/sdk/TestExtensionsRunner.java b/src/test/java/org/opensearch/sdk/TestExtensionsRunner.java index f147ac6f..bf343934 100644 --- a/src/test/java/org/opensearch/sdk/TestExtensionsRunner.java +++ b/src/test/java/org/opensearch/sdk/TestExtensionsRunner.java @@ -127,7 +127,7 @@ public void testHandleExtensionInitRequest() throws UnknownHostException { // Set mocked transport service extensionsRunner.setExtensionTransportService(this.transportService); - doNothing().when(this.transportService).connectToNode(sourceNode); + doNothing().when(this.transportService).connectToNodeAsExtension(sourceNode, "opensearch-sdk-1"); InitializeExtensionRequest extensionInitRequest = new InitializeExtensionRequest(sourceNode, extension);