From 6aed5626fee6535ae9b73488a472a5fc134d20ea Mon Sep 17 00:00:00 2001 From: Andrey Pleskach Date: Mon, 9 Sep 2024 20:33:58 +0200 Subject: [PATCH] Refactor DefaultKeyStore Changes: - Refactored DefaultKeyStore into specialized subclasses, each managing a distinct responsibility. - Added missing tests for certificate loading, SSL parameter configuration, and related processes. Signed-off-by: Andrey Pleskach --- build.gradle | 3 + checkstyle/checkstyle.xml | 7 + .../security/OpenSearchSecurityPlugin.java | 15 +- .../dlic/rest/api/SecurityRestApiActions.java | 12 +- .../rest/api/SecuritySSLCertsApiAction.java | 89 ++-- .../TransportCertificatesInfoNodesAction.java | 50 +- .../ssl/OpenSearchSecureSettingsFactory.java | 13 +- .../ssl/OpenSearchSecuritySSLPlugin.java | 14 +- .../security/ssl/SecureSSLSettings.java | 2 +- .../security/ssl/SslConfiguration.java | 148 ++++++ .../security/ssl/SslContextHandler.java | 165 +++++++ .../security/ssl/SslSettingsManager.java | 384 +++++++++++++++ .../security/ssl/config/CertType.java | 33 ++ .../security/ssl/config/Certificate.java | 188 +++++++ .../ssl/config/KeyStoreConfiguration.java | 201 ++++++++ .../security/ssl/config/KeyStoreUtils.java | 218 ++++++++ .../ssl/config/SslCertificatesLoader.java | 171 +++++++ .../security/ssl/config/SslParameters.java | 197 ++++++++ .../ssl/config/TrustStoreConfiguration.java | 185 +++++++ .../ssl/rest/SecuritySSLInfoAction.java | 43 +- .../security/ssl/util/SSLConfigConstants.java | 64 ++- .../security/ssl/CertificatesRule.java | 318 ++++++++++++ .../security/ssl/CertificatesUtils.java | 43 ++ .../ssl/OpenSearchSecuritySSLPluginTest.java | 20 +- .../org/opensearch/security/ssl/SSLTest.java | 2 +- .../SecuritySSLReloadCertsActionTests.java | 9 +- .../security/ssl/SslContextHandlerTest.java | 266 ++++++++++ .../security/ssl/SslSettingsManagerTest.java | 464 ++++++++++++++++++ .../security/ssl/config/CertificateTest.java | 38 ++ .../config/JdkSslCertificatesLoaderTest.java | 318 ++++++++++++ .../config/PemSslCertificatesLoaderTest.java | 174 +++++++ .../ssl/config/SslCertificatesLoaderTest.java | 66 +++ .../ssl/config/SslParametersTest.java | 90 ++++ 33 files changed, 3887 insertions(+), 123 deletions(-) create mode 100644 src/main/java/org/opensearch/security/ssl/SslConfiguration.java create mode 100644 src/main/java/org/opensearch/security/ssl/SslContextHandler.java create mode 100644 src/main/java/org/opensearch/security/ssl/SslSettingsManager.java create mode 100644 src/main/java/org/opensearch/security/ssl/config/CertType.java create mode 100644 src/main/java/org/opensearch/security/ssl/config/Certificate.java create mode 100644 src/main/java/org/opensearch/security/ssl/config/KeyStoreConfiguration.java create mode 100644 src/main/java/org/opensearch/security/ssl/config/KeyStoreUtils.java create mode 100644 src/main/java/org/opensearch/security/ssl/config/SslCertificatesLoader.java create mode 100644 src/main/java/org/opensearch/security/ssl/config/SslParameters.java create mode 100644 src/main/java/org/opensearch/security/ssl/config/TrustStoreConfiguration.java create mode 100644 src/test/java/org/opensearch/security/ssl/CertificatesRule.java create mode 100644 src/test/java/org/opensearch/security/ssl/CertificatesUtils.java create mode 100644 src/test/java/org/opensearch/security/ssl/SslContextHandlerTest.java create mode 100644 src/test/java/org/opensearch/security/ssl/SslSettingsManagerTest.java create mode 100644 src/test/java/org/opensearch/security/ssl/config/CertificateTest.java create mode 100644 src/test/java/org/opensearch/security/ssl/config/JdkSslCertificatesLoaderTest.java create mode 100644 src/test/java/org/opensearch/security/ssl/config/PemSslCertificatesLoaderTest.java create mode 100644 src/test/java/org/opensearch/security/ssl/config/SslCertificatesLoaderTest.java create mode 100644 src/test/java/org/opensearch/security/ssl/config/SslParametersTest.java diff --git a/build.gradle b/build.gradle index db494876ca..887966e6c7 100644 --- a/build.gradle +++ b/build.gradle @@ -686,6 +686,9 @@ dependencies { testImplementation('org.awaitility:awaitility:4.2.2') { exclude(group: 'org.hamcrest', module: 'hamcrest') } + testImplementation "org.bouncycastle:bcpkix-jdk18on:${versions.bouncycastle}" + testImplementation "org.bouncycastle:bcutil-jdk18on:${versions.bouncycastle}" + // Only osx-x86_64, osx-aarch_64, linux-x86_64, linux-aarch_64, windows-x86_64 are available if (osdetector.classifier in ["osx-x86_64", "osx-aarch_64", "linux-x86_64", "linux-aarch_64", "windows-x86_64"]) { testImplementation "io.netty:netty-tcnative-classes:2.0.61.Final" diff --git a/checkstyle/checkstyle.xml b/checkstyle/checkstyle.xml index 04a36c49c1..a9c1a8f765 100644 --- a/checkstyle/checkstyle.xml +++ b/checkstyle/checkstyle.xml @@ -43,6 +43,13 @@ + + + + + + + diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 15d5e4c286..9076b1da1f 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -648,7 +648,7 @@ public List getRestHandlers( evaluator, threadPool, Objects.requireNonNull(auditLog), - sks, + sslSettingsManager, Objects.requireNonNull(userService), sslCertReloadEnabled, passwordHasher @@ -1207,9 +1207,8 @@ public Collection createComponents( components.add(userService); components.add(passwordHasher); - if (!ExternalSecurityKeyStore.hasExternalSslContext(settings)) { - components.add(sks); - } + components.add(sslSettingsManager); + final var allowDefaultInit = settings.getAsBoolean(SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false); final var useClusterState = useClusterStateToInitSecurityConfig(settings); if (!SSLConfig.isSslOnlyMode() && !isDisabled(settings) && allowDefaultInit && useClusterState) { @@ -2167,7 +2166,13 @@ public PluginSubject getPluginSubject(Plugin plugin) { @Override public Optional getSecureSettingFactory(Settings settings) { return Optional.of( - new OpenSearchSecureSettingsFactory(threadPool, sks, evaluateSslExceptionHandler(), securityRestHandler, SSLConfig) + new OpenSearchSecureSettingsFactory( + threadPool, + sslSettingsManager, + evaluateSslExceptionHandler(), + securityRestHandler, + SSLConfig + ) ); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java index 3963e443d8..c28a1bdc1d 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java @@ -25,7 +25,7 @@ import org.opensearch.security.configuration.ConfigurationRepository; import org.opensearch.security.hasher.PasswordHasher; import org.opensearch.security.privileges.PrivilegesEvaluator; -import org.opensearch.security.ssl.SecurityKeyStore; +import org.opensearch.security.ssl.SslSettingsManager; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.user.UserService; import org.opensearch.threadpool.ThreadPool; @@ -46,7 +46,7 @@ public static Collection getHandler( final PrivilegesEvaluator evaluator, final ThreadPool threadPool, final AuditLog auditLog, - final SecurityKeyStore securityKeyStore, + final SslSettingsManager sslSettingsManager, final UserService userService, final boolean certificatesReloadEnabled, final PasswordHasher passwordHasher @@ -97,7 +97,13 @@ public static Collection getHandler( new MultiTenancyConfigApiAction(clusterService, threadPool, securityApiDependencies), new RateLimitersApiAction(clusterService, threadPool, securityApiDependencies), new ConfigUpgradeApiAction(clusterService, threadPool, securityApiDependencies), - new SecuritySSLCertsApiAction(clusterService, threadPool, securityKeyStore, certificatesReloadEnabled, securityApiDependencies), + new SecuritySSLCertsApiAction( + clusterService, + threadPool, + sslSettingsManager, + certificatesReloadEnabled, + securityApiDependencies + ), new CertificatesApiAction(clusterService, threadPool, securityApiDependencies) ); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiAction.java index 7f4bff50ab..5233149c66 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiAction.java @@ -12,11 +12,10 @@ package org.opensearch.security.dlic.rest.api; import java.io.IOException; -import java.security.cert.X509Certificate; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.Stream; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -31,8 +30,10 @@ import org.opensearch.rest.RestRequest.Method; import org.opensearch.security.dlic.rest.validation.ValidationResult; import org.opensearch.security.securityconf.impl.CType; -import org.opensearch.security.ssl.SecurityKeyStore; -import org.opensearch.security.ssl.util.SSLConfigConstants; +import org.opensearch.security.ssl.SslContextHandler; +import org.opensearch.security.ssl.SslSettingsManager; +import org.opensearch.security.ssl.config.CertType; +import org.opensearch.security.ssl.config.Certificate; import org.opensearch.security.support.ConfigConstants; import org.opensearch.threadpool.ThreadPool; @@ -62,23 +63,20 @@ public class SecuritySSLCertsApiAction extends AbstractApiAction { ) ); - private final SecurityKeyStore securityKeyStore; + private final SslSettingsManager sslSettingsManager; private final boolean certificatesReloadEnabled; - private final boolean httpsEnabled; - public SecuritySSLCertsApiAction( final ClusterService clusterService, final ThreadPool threadPool, - final SecurityKeyStore securityKeyStore, + final SslSettingsManager sslSettingsManager, final boolean certificatesReloadEnabled, final SecurityApiDependencies securityApiDependencies ) { super(Endpoint.SSL, clusterService, threadPool, securityApiDependencies); - this.securityKeyStore = securityKeyStore; + this.sslSettingsManager = sslSettingsManager; this.certificatesReloadEnabled = certificatesReloadEnabled; - this.httpsEnabled = securityApiDependencies.settings().getAsBoolean(SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED, true); this.requestHandlersBuilder.configureRequestHandlers(this::securitySSLCertsRequestHandlers); } @@ -108,10 +106,10 @@ private void securitySSLCertsRequestHandlers(RequestHandler.RequestHandlersBuild .verifyAccessForAllMethods() .override( Method.GET, - (channel, request, client) -> withSecurityKeyStore().valid(keyStore -> loadCertificates(channel, keyStore)) + (channel, request, client) -> withSecurityKeyStore().valid(ignore -> loadCertificates(channel)) .error((status, toXContent) -> response(channel, status, toXContent)) ) - .override(Method.PUT, (channel, request, client) -> withSecurityKeyStore().valid(keyStore -> { + .override(Method.PUT, (channel, request, client) -> withSecurityKeyStore().valid(ignore -> { if (!certificatesReloadEnabled) { badRequest( channel, @@ -123,7 +121,7 @@ private void securitySSLCertsRequestHandlers(RequestHandler.RequestHandlersBuild ) ); } else { - reloadCertificates(channel, request, keyStore); + reloadCertificates(channel, request); } }).error((status, toXContent) -> response(channel, status, toXContent))); } @@ -138,65 +136,70 @@ boolean accessHandler(final RestRequest request) { } } - ValidationResult withSecurityKeyStore() { - if (securityKeyStore == null) { + ValidationResult withSecurityKeyStore() { + if (sslSettingsManager == null) { return ValidationResult.error(RestStatus.OK, badRequestMessage("keystore is not initialized")); } - return ValidationResult.success(securityKeyStore); + return ValidationResult.success(sslSettingsManager); } - protected void loadCertificates(final RestChannel channel, final SecurityKeyStore keyStore) throws IOException { + protected void loadCertificates(final RestChannel channel) throws IOException { ok( channel, (builder, params) -> builder.startObject() - .field("http_certificates_list", httpsEnabled ? generateCertDetailList(keyStore.getHttpCerts()) : null) - .field("transport_certificates_list", generateCertDetailList(keyStore.getTransportCerts())) + .field( + "http_certificates_list", + generateCertDetailList( + sslSettingsManager.sslContextHandler(CertType.HTTP).map(SslContextHandler::keyMaterialCertificates).orElse(null) + ) + ) + .field( + "transport_certificates_list", + generateCertDetailList( + sslSettingsManager.sslContextHandler(CertType.TRANSPORT) + .map(SslContextHandler::keyMaterialCertificates) + .orElse(null) + ) + ) .endObject() ); } - private List> generateCertDetailList(final X509Certificate[] certs) { + private List> generateCertDetailList(final Stream certs) { if (certs == null) { return null; } - return Arrays.stream(certs).map(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 = securityKeyStore.getSubjectAlternativeNames(cert); - - final String notBefore = cert != null && cert.getNotBefore() != null ? cert.getNotBefore().toInstant().toString() : ""; - final String notAfter = cert != null && cert.getNotAfter() != null ? cert.getNotAfter().toInstant().toString() : ""; - return ImmutableMap.of( + return certs.map( + c -> ImmutableMap.of( "issuer_dn", - issuerDn, + c.issuer(), "subject_dn", - subjectDn, + c.subject(), "san", - san, + c.subjectAlternativeNames(), "not_before", - notBefore, + c.notBefore(), "not_after", - notAfter - ); - }).collect(Collectors.toList()); + c.notAfter() + ) + ).collect(Collectors.toList()); } - protected void reloadCertificates(final RestChannel channel, final RestRequest request, final SecurityKeyStore keyStore) - throws IOException { + protected void reloadCertificates(final RestChannel channel, final RestRequest request) throws IOException { final String certType = request.param("certType").toLowerCase().trim(); try { switch (certType) { case "http": - if (!httpsEnabled) { + if (sslSettingsManager.sslConfiguration(CertType.HTTP).isPresent()) { + sslSettingsManager.reloadSslContext(CertType.HTTP); + ok(channel, (builder, params) -> builder.startObject().field("message", "updated http certs").endObject()); + } else { badRequest(channel, "SSL for HTTP is disabled"); - return; } - keyStore.initHttpSSLConfig(); - ok(channel, (builder, params) -> builder.startObject().field("message", "updated http certs").endObject()); break; case "transport": - keyStore.initTransportSSLConfig(); + sslSettingsManager.reloadSslContext(CertType.TRANSPORT); + sslSettingsManager.reloadSslContext(CertType.TRANSPORT_CLIENT); ok(channel, (builder, params) -> builder.startObject().field("message", "updated transport certs").endObject()); break; default: diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/ssl/TransportCertificatesInfoNodesAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/ssl/TransportCertificatesInfoNodesAction.java index 681c2c01eb..39edfd570f 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/ssl/TransportCertificatesInfoNodesAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/ssl/TransportCertificatesInfoNodesAction.java @@ -12,22 +12,22 @@ package org.opensearch.security.dlic.rest.api.ssl; import java.io.IOException; -import java.security.cert.X509Certificate; import java.util.List; import java.util.Map; - -import com.google.common.collect.ImmutableList; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.opensearch.action.FailedNodeException; import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.nodes.TransportNodesAction; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.inject.Inject; -import org.opensearch.common.settings.Settings; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.security.ssl.DefaultSecurityKeyStore; -import org.opensearch.security.ssl.util.SSLConfigConstants; +import org.opensearch.security.ssl.SslContextHandler; +import org.opensearch.security.ssl.SslSettingsManager; +import org.opensearch.security.ssl.config.CertType; +import org.opensearch.security.ssl.config.Certificate; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportRequest; import org.opensearch.transport.TransportService; @@ -38,18 +38,15 @@ public class TransportCertificatesInfoNodesAction extends TransportNodesAction< TransportCertificatesInfoNodesAction.NodeRequest, CertificatesNodesResponse.CertificatesNodeResponse> { - private final DefaultSecurityKeyStore securityKeyStore; - - private final boolean httpsEnabled; + private final SslSettingsManager sslSettingsManager; @Inject public TransportCertificatesInfoNodesAction( - final Settings settings, final ThreadPool threadPool, final ClusterService clusterService, final TransportService transportService, final ActionFilters actionFilters, - final DefaultSecurityKeyStore securityKeyStore + final SslSettingsManager sslSettingsManager ) { super( CertificatesActionType.NAME, @@ -62,8 +59,7 @@ public TransportCertificatesInfoNodesAction( ThreadPool.Names.GENERIC, CertificatesNodesResponse.CertificatesNodeResponse.class ); - this.httpsEnabled = settings.getAsBoolean(SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED, true); - this.securityKeyStore = securityKeyStore; + this.sslSettingsManager = sslSettingsManager; } @Override @@ -89,12 +85,6 @@ protected CertificatesNodesResponse.CertificatesNodeResponse newNodeResponse(fin protected CertificatesNodesResponse.CertificatesNodeResponse nodeOperation(final NodeRequest request) { final var sslCertRequest = request.sslCertsInfoNodesRequest; - if (securityKeyStore == null) { - return new CertificatesNodesResponse.CertificatesNodeResponse( - clusterService.localNode(), - new IllegalStateException("keystore is not initialized") - ); - } try { return new CertificatesNodesResponse.CertificatesNodeResponse( clusterService.localNode(), @@ -109,23 +99,27 @@ protected CertificatesInfo loadCertificates(final CertificateType certificateTyp var httpCertificates = List.of(); var transportsCertificates = List.of(); if (CertificateType.isHttp(certificateType)) { - httpCertificates = httpsEnabled ? certificatesDetails(securityKeyStore.getHttpCerts()) : List.of(); + httpCertificates = sslSettingsManager.sslContextHandler(CertType.HTTP) + .map(SslContextHandler::keyMaterialCertificates) + .map(this::certificatesDetails) + .orElse(List.of()); } if (CertificateType.isTransport(certificateType)) { - transportsCertificates = certificatesDetails(securityKeyStore.getTransportCerts()); + transportsCertificates = sslSettingsManager.sslContextHandler(CertType.TRANSPORT) + .map(SslContextHandler::keyMaterialCertificates) + .map(this::certificatesDetails) + .orElse(List.of()); } return new CertificatesInfo(Map.of(CertificateType.HTTP, httpCertificates, CertificateType.TRANSPORT, transportsCertificates)); } - private List certificatesDetails(final X509Certificate[] certs) { - if (certs == null) { + private List certificatesDetails(final Stream certificateStream) { + if (certificateStream == null) { return null; } - final var certificates = ImmutableList.builder(); - for (final var c : certs) { - certificates.add(CertificateInfo.from(c, securityKeyStore.getSubjectAlternativeNames(c))); - } - return certificates.build(); + return certificateStream.map( + c -> new CertificateInfo(c.subject(), c.subjectAlternativeNames(), c.issuer(), c.notAfter(), c.notBefore()) + ).collect(Collectors.toList()); } public static class NodeRequest extends TransportRequest { diff --git a/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java b/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java index 9d482b18a8..43f6cc4f29 100644 --- a/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java +++ b/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java @@ -25,6 +25,7 @@ import org.opensearch.plugins.SecureTransportSettingsProvider; import org.opensearch.plugins.TransportExceptionHandler; import org.opensearch.security.filter.SecurityRestFilter; +import org.opensearch.security.ssl.config.CertType; import org.opensearch.security.ssl.http.netty.Netty4ConditionalDecompressor; import org.opensearch.security.ssl.http.netty.Netty4HttpRequestHeaderVerifier; import org.opensearch.security.ssl.transport.SSLConfig; @@ -36,20 +37,20 @@ public class OpenSearchSecureSettingsFactory implements SecureSettingsFactory { private final ThreadPool threadPool; - private final SecurityKeyStore sks; + private final SslSettingsManager sslSettingsManager; private final SslExceptionHandler sslExceptionHandler; private final SecurityRestFilter restFilter; private final SSLConfig sslConfig; public OpenSearchSecureSettingsFactory( ThreadPool threadPool, - SecurityKeyStore sks, + SslSettingsManager sslSettingsManager, SslExceptionHandler sslExceptionHandler, SecurityRestFilter restFilter, SSLConfig sslConfig ) { this.threadPool = threadPool; - this.sks = sks; + this.sslSettingsManager = sslSettingsManager; this.sslExceptionHandler = sslExceptionHandler; this.restFilter = restFilter; this.sslConfig = sslConfig; @@ -80,12 +81,12 @@ public boolean dualModeEnabled() { @Override public Optional buildSecureServerTransportEngine(Settings settings, Transport transport) throws SSLException { - return Optional.of(sks.createServerTransportSSLEngine()); + return sslSettingsManager.sslContextHandler(CertType.TRANSPORT).map(SslContextHandler::createSSLEngine); } @Override public Optional buildSecureClientTransportEngine(Settings settings, String hostname, int port) throws SSLException { - return Optional.of(sks.createClientTransportSSLEngine(hostname, port)); + return sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT).map(c -> c.createSSLEngine(hostname, port)); } }); } @@ -142,7 +143,7 @@ public void onError(Throwable t) { @Override public Optional buildSecureHttpServerEngine(Settings settings, HttpServerTransport transport) throws SSLException { - return Optional.of(sks.createHTTPSSLEngine()); + return sslSettingsManager.sslContextHandler(CertType.HTTP).map(SslContextHandler::createSSLEngine); } }); } diff --git a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java index c16706c870..25c55f3cbb 100644 --- a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java +++ b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java @@ -126,7 +126,7 @@ public class OpenSearchSecuritySSLPlugin extends Plugin implements SystemIndexPl protected final Settings settings; protected volatile SecurityRestFilter securityRestHandler; protected final SharedGroupFactory sharedGroupFactory; - protected final SecurityKeyStore sks; + protected final SslSettingsManager sslSettingsManager; protected PrincipalExtractor principalExtractor; protected final Path configPath; private final static SslExceptionHandler NOOP_SSL_EXCEPTION_HANDLER = new SslExceptionHandler() { @@ -144,7 +144,7 @@ protected OpenSearchSecuritySSLPlugin(final Settings settings, final Path config this.httpSSLEnabled = false; this.transportSSLEnabled = false; this.extendedKeyUsageEnabled = false; - this.sks = null; + this.sslSettingsManager = null; this.configPath = null; SSLConfig = new SSLConfig(false, false); @@ -246,11 +246,7 @@ public Object run() { log.error("SSL not activated for http and/or transport."); } - if (ExternalSecurityKeyStore.hasExternalSslContext(settings)) { - this.sks = new ExternalSecurityKeyStore(settings); - } else { - this.sks = new DefaultSecurityKeyStore(settings, configPath); - } + this.sslSettingsManager = new SslSettingsManager(new Environment(settings, configPath)); } @Override @@ -311,7 +307,7 @@ public List getRestHandlers( final List handlers = new ArrayList(1); if (!client) { - handlers.add(new SecuritySSLInfoAction(settings, configPath, restController, sks, Objects.requireNonNull(principalExtractor))); + handlers.add(new SecuritySSLInfoAction(settings, configPath, sslSettingsManager, Objects.requireNonNull(principalExtractor))); } return handlers; @@ -675,7 +671,7 @@ public List getSettingsFilter() { @Override public Optional getSecureSettingFactory(Settings settings) { return Optional.of( - new OpenSearchSecureSettingsFactory(threadPool, sks, NOOP_SSL_EXCEPTION_HANDLER, securityRestHandler, SSLConfig) + new OpenSearchSecureSettingsFactory(threadPool, sslSettingsManager, NOOP_SSL_EXCEPTION_HANDLER, securityRestHandler, SSLConfig) ); } diff --git a/src/main/java/org/opensearch/security/ssl/SecureSSLSettings.java b/src/main/java/org/opensearch/security/ssl/SecureSSLSettings.java index 171bb18bb5..5aad07fbdd 100644 --- a/src/main/java/org/opensearch/security/ssl/SecureSSLSettings.java +++ b/src/main/java/org/opensearch/security/ssl/SecureSSLSettings.java @@ -36,7 +36,7 @@ public final class SecureSSLSettings { private static final Logger LOG = LogManager.getLogger(SecureSSLSettings.class); - private static final String SECURE_SUFFIX = "_secure"; + public static final String SECURE_SUFFIX = "_secure"; private static final String PREFIX = "plugins.security.ssl"; private static final String HTTP_PREFIX = PREFIX + ".http"; private static final String TRANSPORT_PREFIX = PREFIX + ".transport"; diff --git a/src/main/java/org/opensearch/security/ssl/SslConfiguration.java b/src/main/java/org/opensearch/security/ssl/SslConfiguration.java new file mode 100644 index 0000000000..2332867bd8 --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/SslConfiguration.java @@ -0,0 +1,148 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl; + +import java.nio.file.Path; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchException; +import org.opensearch.security.ssl.config.Certificate; +import org.opensearch.security.ssl.config.KeyStoreConfiguration; +import org.opensearch.security.ssl.config.SslParameters; +import org.opensearch.security.ssl.config.TrustStoreConfiguration; + +import io.netty.handler.codec.http2.Http2SecurityUtil; +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; + +public class SslConfiguration { + + private final static Logger LOGGER = LogManager.getLogger(SslConfiguration.class); + + private final SslParameters sslParameters; + + private final TrustStoreConfiguration trustStoreConfiguration; + + private final KeyStoreConfiguration keyStoreConfiguration; + + public SslConfiguration( + final SslParameters sslParameters, + final TrustStoreConfiguration trustStoreConfiguration, + final KeyStoreConfiguration keyStoreConfiguration + ) { + this.sslParameters = sslParameters; + this.trustStoreConfiguration = trustStoreConfiguration; + this.keyStoreConfiguration = keyStoreConfiguration; + } + + public List dependentFiles() { + return Stream.concat(keyStoreConfiguration.files().stream(), Stream.of(trustStoreConfiguration.file())) + .collect(Collectors.toList()); + } + + public List certificates() { + return Stream.concat(trustStoreConfiguration.loadCertificates().stream(), keyStoreConfiguration.loadCertificates().stream()) + .collect(Collectors.toList()); + } + + public SslParameters sslParameters() { + return sslParameters; + } + + @SuppressWarnings("removal") + SslContext buildServerSslContext(final boolean validateCertificates) { + try { + return AccessController.doPrivileged( + (PrivilegedExceptionAction) () -> SslContextBuilder.forServer( + keyStoreConfiguration.createKeyManagerFactory(validateCertificates) + ) + .sslProvider(sslParameters.provider()) + .clientAuth(sslParameters.clientAuth()) + .protocols(sslParameters.allowedProtocols().toArray(new String[0])) + // TODO we always add all HTTP 2 ciphers, while maybe it is better to set them differently + .ciphers( + Stream.concat( + Http2SecurityUtil.CIPHERS.stream(), + StreamSupport.stream(sslParameters.allowedCiphers().spliterator(), false) + ).collect(Collectors.toSet()), + SupportedCipherSuiteFilter.INSTANCE + ) + .sessionCacheSize(0) + .sessionTimeout(0) + .applicationProtocolConfig( + new ApplicationProtocolConfig( + ApplicationProtocolConfig.Protocol.ALPN, + // NO_ADVERTISE is currently the only mode supported by both OpenSsl and JDK providers. + ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, + // ACCEPT is currently the only mode supported by both OpenSsl and JDK providers. + ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2, + ApplicationProtocolNames.HTTP_1_1 + ) + ) + .trustManager(trustStoreConfiguration.createTrustManagerFactory(validateCertificates)) + .build() + ); + } catch (PrivilegedActionException e) { + throw new OpenSearchException("Filed to build server SSL context", e); + } + } + + @SuppressWarnings("removal") + SslContext buildClientSslContext(final boolean validateCertificates) { + try { + return AccessController.doPrivileged( + (PrivilegedExceptionAction) () -> SslContextBuilder.forClient() + .sslProvider(sslParameters.provider()) + .protocols(sslParameters.allowedProtocols()) + .ciphers(sslParameters.allowedCiphers()) + .applicationProtocolConfig(ApplicationProtocolConfig.DISABLED) + .sessionCacheSize(0) + .sessionTimeout(0) + .sslProvider(sslParameters.provider()) + .keyManager(keyStoreConfiguration.createKeyManagerFactory(validateCertificates)) + .trustManager(trustStoreConfiguration.createTrustManagerFactory(validateCertificates)) + .build() + ); + } catch (PrivilegedActionException e) { + throw new OpenSearchException("Filed to build client SSL context", e); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SslConfiguration that = (SslConfiguration) o; + return Objects.equals(sslParameters, that.sslParameters) + && Objects.equals(trustStoreConfiguration, that.trustStoreConfiguration) + && Objects.equals(keyStoreConfiguration, that.keyStoreConfiguration); + } + + @Override + public int hashCode() { + return Objects.hash(sslParameters, trustStoreConfiguration, keyStoreConfiguration); + } +} diff --git a/src/main/java/org/opensearch/security/ssl/SslContextHandler.java b/src/main/java/org/opensearch/security/ssl/SslContextHandler.java new file mode 100644 index 0000000000..9fda1641af --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/SslContextHandler.java @@ -0,0 +1,165 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl; + +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.net.ssl.SSLEngine; + +import org.opensearch.security.ssl.config.Certificate; +import org.opensearch.transport.NettyAllocator; + +import io.netty.handler.ssl.SslContext; + +public class SslContextHandler { + + private SslContext sslContext; + + private final SslConfiguration sslConfiguration; + + private final List loadedCertificates; + + public SslContextHandler(final SslConfiguration sslConfiguration) { + this(sslConfiguration, false); + } + + public SslContextHandler(final SslConfiguration sslConfiguration, final boolean client) { + this.sslContext = client ? sslConfiguration.buildClientSslContext(true) : sslConfiguration.buildServerSslContext(true); + this.sslConfiguration = sslConfiguration; + this.loadedCertificates = sslConfiguration.certificates(); + } + + public SSLEngine createSSLEngine() { + return sslContext.newEngine(NettyAllocator.getAllocator()); + } + + public SSLEngine createSSLEngine(final String hostname, final int port) { + return sslContext.newEngine(NettyAllocator.getAllocator(), hostname, port); + } + + public SslConfiguration sslConfiguration() { + return sslConfiguration; + } + + SslContext sslContext() { + return sslContext; + } + + public Stream keyMaterialCertificates() { + return keyMaterialCertificates(loadedCertificates); + } + + Stream keyMaterialCertificates(final List certificates) { + return certificates.stream().filter(Certificate::hasKey); + } + + void reloadSslContext() throws CertificateException { + final var newCertificates = sslConfiguration.certificates(); + + if (sameCertificates(newCertificates)) { + return; + } + validateNewCertificates(newCertificates); + invalidateSessions(); + if (sslContext.isClient()) { + sslContext = sslConfiguration.buildClientSslContext(false); + } else { + sslContext = sslConfiguration.buildServerSslContext(false); + } + loadedCertificates.clear(); + loadedCertificates.addAll(newCertificates); + } + + private boolean sameCertificates(final List newCertificates) { + final Set currentCertSignatureSet = keyMaterialCertificates().map(Certificate::x509Certificate) + .map(X509Certificate::getSignature) + .map(s -> new String(s, StandardCharsets.UTF_8)) + .collect(Collectors.toSet()); + final Set newCertSignatureSet = keyMaterialCertificates(newCertificates).map(Certificate::x509Certificate) + .map(X509Certificate::getSignature) + .map(s -> new String(s, StandardCharsets.UTF_8)) + .collect(Collectors.toSet()); + return currentCertSignatureSet.equals(newCertSignatureSet); + } + + private void validateSubjectDns(final List newCertificates) throws CertificateException { + final List currentSubjectDNs = keyMaterialCertificates().map(Certificate::subject).sorted().collect(Collectors.toList()); + final List newSubjectDNs = keyMaterialCertificates(newCertificates).map(Certificate::subject) + .sorted() + .collect(Collectors.toList()); + if (!currentSubjectDNs.equals(newSubjectDNs)) { + throw new CertificateException( + "New certificates do not have valid Subject DNs. Current Subject DNs " + + currentSubjectDNs + + " new Subject DNs " + + newSubjectDNs + ); + } + } + + private void validateIssuerDns(final List newCertificates) throws CertificateException { + final List currentIssuerDNs = keyMaterialCertificates().map(Certificate::issuer).sorted().collect(Collectors.toList()); + final List newIssuerDNs = keyMaterialCertificates(newCertificates).map(Certificate::issuer) + .sorted() + .collect(Collectors.toList()); + if (!currentIssuerDNs.equals(newIssuerDNs)) { + throw new CertificateException( + "New certificates do not have valid Issuer DNs. Current Issuer DNs: " + + currentIssuerDNs + + " new Issuer DNs: " + + newIssuerDNs + ); + } + } + + private void validateSans(final List newCertificates) throws CertificateException { + final List currentSans = keyMaterialCertificates().map(Certificate::subjectAlternativeNames) + .sorted() + .collect(Collectors.toList()); + final List newSans = keyMaterialCertificates(newCertificates).map(Certificate::subjectAlternativeNames) + .sorted() + .collect(Collectors.toList()); + if (!currentSans.equals(newSans)) { + throw new CertificateException( + "New certificates do not have valid SANs. Current SANs: " + currentSans + " new SANs: " + newSans + ); + } + } + + private void validateNewCertificates(final List newCertificates) throws CertificateException { + for (final var certificate : newCertificates) { + certificate.x509Certificate().checkValidity(); + } + validateSubjectDns(newCertificates); + validateIssuerDns(newCertificates); + validateSans(newCertificates); + } + + private void invalidateSessions() { + final var sessionContext = sslContext.sessionContext(); + if (sessionContext != null) { + for (final var sessionId : Collections.list(sessionContext.getIds())) { + final var session = sessionContext.getSession(sessionId); + if (session != null) { + session.invalidate(); + } + } + } + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/SslSettingsManager.java b/src/main/java/org/opensearch/security/ssl/SslSettingsManager.java new file mode 100644 index 0000000000..381c510894 --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/SslSettingsManager.java @@ -0,0 +1,384 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl; + +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import javax.crypto.Cipher; + +import com.google.common.collect.ImmutableMap; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchException; +import org.opensearch.common.settings.Settings; +import org.opensearch.env.Environment; +import org.opensearch.security.ssl.config.CertType; +import org.opensearch.security.ssl.config.SslCertificatesLoader; +import org.opensearch.security.ssl.config.SslParameters; + +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.OpenSsl; +import io.netty.util.internal.PlatformDependent; + +import static org.opensearch.security.ssl.util.SSLConfigConstants.CLIENT_AUTH_MODE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.EXTENDED_KEY_USAGE_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_CERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_KEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_TRUSTED_CAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED_DEFAULT; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_ENABLE_OPENSSL_IF_AVAILABLE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_KEYSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_TRUSTSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_KEYSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_TRUSTSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED_DEFAULT; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLE_OPENSSL_IF_AVAILABLE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED_DEFAULT; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_KEYSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_TRUSTSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_SERVER_EXTENDED_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_FILEPATH; + +public class SslSettingsManager { + + private final static Logger LOGGER = LogManager.getLogger(SslSettingsManager.class); + + private final Map sslSettingsContexts; + + public SslSettingsManager(final Environment environment) { + this.sslSettingsContexts = buildSslContexts(environment); + } + + public Optional sslConfiguration(final CertType certType) { + return Optional.ofNullable(sslSettingsContexts.get(certType)).map(SslContextHandler::sslConfiguration); + } + + public Optional sslContextHandler(final CertType sslConfigPrefix) { + return Optional.ofNullable(sslSettingsContexts.get(sslConfigPrefix)); + } + + private Map buildSslContexts(final Environment environment) { + final var contexts = new ImmutableMap.Builder(); + final var configurations = loadConfigurations(environment); + Optional.ofNullable(configurations.get(CertType.HTTP)) + .ifPresentOrElse( + sslConfiguration -> contexts.put(CertType.HTTP, new SslContextHandler(sslConfiguration)), + () -> LOGGER.warn("SSL Configuration for HTTP Layer hasn't been set") + ); + Optional.ofNullable(configurations.get(CertType.TRANSPORT)).ifPresentOrElse(sslConfiguration -> { + contexts.put(CertType.TRANSPORT, new SslContextHandler(sslConfiguration)); + final var transportClientConfiguration = Optional.ofNullable(configurations.get(CertType.TRANSPORT_CLIENT)) + .orElse(sslConfiguration); + contexts.put(CertType.TRANSPORT_CLIENT, new SslContextHandler(transportClientConfiguration, true)); + }, () -> LOGGER.warn("SSL Configuration for Transport Layer hasn't been set")); + return contexts.build(); + } + + public synchronized void reloadSslContext(final CertType certType) { + sslContextHandler(certType).ifPresentOrElse(sscContextHandler -> { + LOGGER.info("Reloading {} SSL context", certType.name()); + try { + sscContextHandler.reloadSslContext(); + } catch (CertificateException e) { + throw new OpenSearchException(e); + } + LOGGER.info("{} SSL context reloaded", certType.name()); + }, () -> LOGGER.error("Missing SSL Context for {}", certType.name())); + } + + private Map loadConfigurations(final Environment environment) { + final var settings = environment.settings(); + final var httpSettings = settings.getByPrefix(CertType.HTTP.sslConfigPrefix()); + final var transpotSettings = settings.getByPrefix(CertType.TRANSPORT.sslConfigPrefix()); + if (httpSettings.isEmpty() && transpotSettings.isEmpty()) { + throw new OpenSearchException("No SSL configuration found"); + } + jceWarnings(); + openSslWarnings(settings); + + final var httpEnabled = httpSettings.getAsBoolean(ENABLED, SECURITY_SSL_HTTP_ENABLED_DEFAULT); + final var transportEnabled = transpotSettings.getAsBoolean(ENABLED, SECURITY_SSL_TRANSPORT_ENABLED_DEFAULT); + + final var configurationBuilder = ImmutableMap.builder(); + if (httpEnabled && !clientNode(settings)) { + validateHttpSettings(httpSettings); + final var httpSslParameters = SslParameters.loader(httpSettings).load(true); + final var httpTrustAndKeyStore = new SslCertificatesLoader(CertType.HTTP.sslConfigPrefix()).loadConfiguration(environment); + configurationBuilder.put( + CertType.HTTP, + new SslConfiguration(httpSslParameters, httpTrustAndKeyStore.v1(), httpTrustAndKeyStore.v2()) + ); + LOGGER.info("TLS HTTP Provider : {}", httpSslParameters.provider()); + LOGGER.info("Enabled TLS protocols for HTTP layer : {}", httpSslParameters.allowedProtocols()); + } + final var transportSslParameters = SslParameters.loader(transpotSettings).load(false); + if (transportEnabled) { + if (hasExtendedKeyUsageEnabled(transpotSettings)) { + validateTransportSettings(transpotSettings); + final var transportServerTrustAndKeyStore = new SslCertificatesLoader( + CertType.TRANSPORT.sslConfigPrefix(), + SSL_TRANSPORT_SERVER_EXTENDED_PREFIX + ).loadConfiguration(environment); + configurationBuilder.put( + CertType.TRANSPORT, + new SslConfiguration(transportSslParameters, transportServerTrustAndKeyStore.v1(), transportServerTrustAndKeyStore.v2()) + ); + final var transportClientTrustAndKeyStore = new SslCertificatesLoader( + CertType.TRANSPORT.sslConfigPrefix(), + SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX + ).loadConfiguration(environment); + configurationBuilder.put( + CertType.TRANSPORT_CLIENT, + new SslConfiguration(transportSslParameters, transportClientTrustAndKeyStore.v1(), transportClientTrustAndKeyStore.v2()) + ); + } else { + validateTransportSettings(transpotSettings); + final var transportTrustAndKeyStore = new SslCertificatesLoader(CertType.TRANSPORT.sslConfigPrefix()).loadConfiguration( + environment + ); + configurationBuilder.put( + CertType.TRANSPORT, + new SslConfiguration(transportSslParameters, transportTrustAndKeyStore.v1(), transportTrustAndKeyStore.v2()) + ); + } + LOGGER.info("TLS Transport Client Provider : {}", transportSslParameters.provider()); + LOGGER.info("TLS Transport Server Provider : {}", transportSslParameters.provider()); + LOGGER.info("Enabled TLS protocols for Transport layer : {}", transportSslParameters.allowedProtocols()); + } + return configurationBuilder.build(); + } + + private boolean clientNode(final Settings settings) { + return !"node".equals(settings.get(OpenSearchSecuritySSLPlugin.CLIENT_TYPE)); + } + + private void validateHttpSettings(final Settings httpSettings) { + if (httpSettings == null) return; + if (!httpSettings.getAsBoolean(ENABLED, SECURITY_SSL_HTTP_ENABLED_DEFAULT)) return; + + final var clientAuth = ClientAuth.valueOf(httpSettings.get(CLIENT_AUTH_MODE, ClientAuth.OPTIONAL.name()).toUpperCase(Locale.ROOT)); + + if (hasPemStoreSettings(httpSettings)) { + if (!httpSettings.hasValue(PEM_CERT_FILEPATH) || !httpSettings.hasValue(PEM_KEY_FILEPATH)) { + throw new OpenSearchException( + "Wrong HTTP SSL configuration. " + + String.join(", ", SECURITY_SSL_HTTP_PEMCERT_FILEPATH, SECURITY_SSL_HTTP_PEMKEY_FILEPATH) + + " must be set" + ); + } + if (clientAuth == ClientAuth.REQUIRE && !httpSettings.hasValue(PEM_TRUSTED_CAS_FILEPATH)) { + throw new OpenSearchException( + "Wrong HTTP SSL configuration. " + SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH + " must be set if client auth is required" + ); + } + } else if (hasKeyOrTrustStoreSettings(httpSettings)) { + if (!httpSettings.hasValue(KEYSTORE_FILEPATH)) { + throw new OpenSearchException("Wrong HTTP SSL configuration. " + SECURITY_SSL_HTTP_KEYSTORE_FILEPATH + " must be set"); + } + if (clientAuth == ClientAuth.REQUIRE && !httpSettings.hasValue(TRUSTSTORE_FILEPATH)) { + throw new OpenSearchException( + "Wrong HTTP SSL configuration. " + SECURITY_SSL_HTTP_TRUSTSTORE_FILEPATH + " must be set if client auth is required" + ); + } + } else { + throw new OpenSearchException( + "Wrong HTTP SSL configuration. One of Keystore and Truststore files or X.509 PEM certificates and " + + "PKCS#8 keys groups should be set to configure HTTP layer" + ); + } + } + + private void validateTransportSettings(final Settings transportSettings) { + if (!hasExtendedKeyUsageEnabled(transportSettings)) { + if (hasPemStoreSettings(transportSettings)) { + if (!transportSettings.hasValue(PEM_CERT_FILEPATH) + || !transportSettings.hasValue(PEM_KEY_FILEPATH) + || !transportSettings.hasValue(PEM_TRUSTED_CAS_FILEPATH)) { + throw new OpenSearchException( + "Wrong Transport SSL configuration. " + + String.join( + ",", + SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH, + SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH, + SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH + ) + + " must be set" + ); + } + + } else if (hasKeyOrTrustStoreSettings(transportSettings)) { + verifyKeyAndTrustStoreSettings(transportSettings); + } else { + throw new OpenSearchException( + "Wrong Transport SSL configuration. One of Keystore and Truststore files or X.509 PEM certificates and " + + "PKCS#8 keys groups should be set to configure Transport layer properly" + ); + } + } else { + final var serverTransportSettings = transportSettings.getByPrefix(SSL_TRANSPORT_SERVER_EXTENDED_PREFIX); + final var clientTransportSettings = transportSettings.getByPrefix(SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX); + if (hasKeyOrTrustStoreSettings(transportSettings)) { + verifyKeyAndTrustStoreSettings(transportSettings); + if (!serverTransportSettings.hasValue(KEYSTORE_ALIAS) + || !serverTransportSettings.hasValue(TRUSTSTORE_ALIAS) + || !clientTransportSettings.hasValue(KEYSTORE_ALIAS) + || !clientTransportSettings.hasValue(TRUSTSTORE_ALIAS)) { + throw new OpenSearchException( + "Wrong Transport/Transport Client SSL configuration. " + + String.join( + ",", + SECURITY_SSL_TRANSPORT_SERVER_KEYSTORE_ALIAS, + SECURITY_SSL_TRANSPORT_SERVER_TRUSTSTORE_ALIAS, + SECURITY_SSL_TRANSPORT_CLIENT_KEYSTORE_ALIAS, + SECURITY_SSL_TRANSPORT_CLIENT_TRUSTSTORE_ALIAS + ) + + " must be set if " + + SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED + + " is set" + ); + } + } else if (!hasKeyOrTrustStoreSettings(transportSettings)) { + if (!serverTransportSettings.hasValue(PEM_CERT_FILEPATH) + || !serverTransportSettings.hasValue(PEM_KEY_FILEPATH) + || !serverTransportSettings.hasValue(PEM_TRUSTED_CAS_FILEPATH) + || !clientTransportSettings.hasValue(PEM_CERT_FILEPATH) + || !clientTransportSettings.hasValue(PEM_KEY_FILEPATH) + || !clientTransportSettings.hasValue(PEM_TRUSTED_CAS_FILEPATH)) { + throw new OpenSearchException( + "Wrong Transport/Transport Client SSL configuration. " + + String.join( + ",", + SECURITY_SSL_TRANSPORT_SERVER_PEMCERT_FILEPATH, + SECURITY_SSL_TRANSPORT_SERVER_PEMKEY_FILEPATH, + SECURITY_SSL_TRANSPORT_SERVER_PEMTRUSTEDCAS_FILEPATH, + SECURITY_SSL_TRANSPORT_CLIENT_PEMCERT_FILEPATH, + SECURITY_SSL_TRANSPORT_CLIENT_PEMKEY_FILEPATH, + SECURITY_SSL_TRANSPORT_CLIENT_PEMTRUSTEDCAS_FILEPATH + ) + + " must be set if " + + SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED + + " is set" + ); + } + } else { + throw new OpenSearchException( + "Wrong Transport/Transport Client SSL configuration. One of Keystore and Truststore files or X.509 PEM certificates and " + + "PKCS#8 keys groups should be set to configure HTTP layer" + ); + } + } + } + + private void verifyKeyAndTrustStoreSettings(final Settings settings) { + if (!settings.hasValue(KEYSTORE_FILEPATH) || !settings.hasValue(TRUSTSTORE_FILEPATH)) { + throw new OpenSearchException( + "Wrong Transport/Tran SSL configuration. One of Keystore and Truststore files or X.509 PEM certificates and " + + "PKCS#8 keys groups should be set to configure Transport layer properly" + ); + } + } + + private boolean hasExtendedKeyUsageEnabled(final Settings settings) { + return settings.getAsBoolean(EXTENDED_KEY_USAGE_ENABLED, SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED_DEFAULT); + } + + private boolean hasKeyOrTrustStoreSettings(final Settings settings) { + return settings.hasValue(KEYSTORE_FILEPATH) || settings.hasValue(TRUSTSTORE_FILEPATH); + } + + private boolean hasPemStoreSettings(final Settings settings) { + return settings.hasValue(PEM_KEY_FILEPATH) || settings.hasValue(PEM_CERT_FILEPATH) || settings.hasValue(PEM_TRUSTED_CAS_FILEPATH); + } + + void jceWarnings() { + try { + final int aesMaxKeyLength = Cipher.getMaxAllowedKeyLength("AES"); + + if (aesMaxKeyLength < 256) { + // CS-SUPPRESS-SINGLE: RegexpSingleline Java Cryptography Extension is unrelated to OpenSearch extensions + LOGGER.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 + ); + // CS-ENFORCE-SINGLE + } + } catch (final NoSuchAlgorithmException e) { + LOGGER.error("AES encryption not supported (SG 1). ", e); + } + } + + void openSslWarnings(final Settings settings) { + if (!OpenSearchSecuritySSLPlugin.OPENSSL_SUPPORTED + && OpenSsl.isAvailable() + && (settings.getAsBoolean(SECURITY_SSL_HTTP_ENABLE_OPENSSL_IF_AVAILABLE, true) + || settings.getAsBoolean(SECURITY_SSL_TRANSPORT_ENABLE_OPENSSL_IF_AVAILABLE, true))) { + if (PlatformDependent.javaVersion() < 12) { + LOGGER.warn( + "Support for OpenSSL with Java 11 or prior versions require using Netty allocator. Set " + + "'opensearch.unsafe.use_netty_default_allocator' system property to true" + ); + } else { + LOGGER.warn("Support for OpenSSL with Java 12+ has been removed from OpenSearch Security. Using JDK SSL instead."); + } + } + if (OpenSearchSecuritySSLPlugin.OPENSSL_SUPPORTED && OpenSsl.isAvailable()) { + LOGGER.info("OpenSSL {} ({}) available", OpenSsl.versionString(), OpenSsl.version()); + + if (OpenSsl.version() < 0x10002000L) { + LOGGER.warn( + "Outdated OpenSSL version detected. You should update to 1.0.2k or later. Currently installed: {}", + OpenSsl.versionString() + ); + } + + if (!OpenSsl.supportsHostnameValidation()) { + LOGGER.warn( + "Your OpenSSL version {} does not support hostname verification. You should update to 1.0.2k or later.", + OpenSsl.versionString() + ); + } + + LOGGER.debug("OpenSSL available ciphers {}", OpenSsl.availableOpenSslCipherSuites()); + } else { + LOGGER.warn( + "OpenSSL not available (this is not an error, we simply fallback to built-in JDK SSL) because of {}", + OpenSsl.unavailabilityCause() + ); + } + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/config/CertType.java b/src/main/java/org/opensearch/security/ssl/config/CertType.java new file mode 100644 index 0000000000..09a8dcfae9 --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/config/CertType.java @@ -0,0 +1,33 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl.config; + +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_HTTP_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_CLIENT_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_PREFIX; + +public enum CertType { + HTTP(SSL_HTTP_PREFIX), + TRANSPORT(SSL_TRANSPORT_PREFIX), + TRANSPORT_CLIENT(SSL_TRANSPORT_CLIENT_PREFIX); + + private final String sslConfigPrefix; + + private CertType(String sslConfigPrefix) { + this.sslConfigPrefix = sslConfigPrefix; + } + + public String sslConfigPrefix() { + return sslConfigPrefix; + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/config/Certificate.java b/src/main/java/org/opensearch/security/ssl/config/Certificate.java new file mode 100644 index 0000000000..534148db57 --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/config/Certificate.java @@ -0,0 +1,188 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl.config; + +import java.lang.reflect.Method; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; + +import com.google.common.collect.ImmutableList; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1Object; +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; + +public class Certificate { + + private final static Logger LOGGER = LogManager.getLogger(Certificate.class); + + private final X509Certificate certificate; + + private final String format; + + private final String alias; + + private final boolean hasKey; + + public Certificate(final X509Certificate certificate, final boolean hasKey) { + this(certificate, "pem", null, hasKey); + } + + public Certificate(final X509Certificate certificate, final String format, final String alias, final boolean hasKey) { + this.certificate = certificate; + this.format = format; + this.alias = alias; + this.hasKey = hasKey; + } + + public X509Certificate x509Certificate() { + return certificate; + } + + public String format() { + return format; + } + + public String alias() { + return alias; + } + + public boolean hasKey() { + return hasKey; + } + + public String subjectAlternativeNames() { + return loadSubjectAlternativeNames(); + } + + @Deprecated(since = "since JDK 21", forRemoval = true) + public String loadSubjectAlternativeNames() { + String san = ""; + try { + Collection> altNames = certificate != null && certificate.getSubjectAlternativeNames() != null + ? certificate.getSubjectAlternativeNames() + : null; + if (altNames != null) { + Comparator> comparator = Comparator.comparing((List altName) -> (Integer) altName.get(0)) + .thenComparing((List altName) -> (String) altName.get(1)); + + Set> sans = new TreeSet<>(comparator); + for (List altName : altNames) { + Integer type = (Integer) altName.get(0); + // otherName requires parsing to string + if (type == 0) { + List otherName = parseOtherName(altName); + if (otherName != null) { + sans.add(Arrays.asList(type, otherName)); + } + } else { + sans.add(altName); + } + } + san = sans.toString(); + } + } catch (CertificateParsingException e) { + LOGGER.error("Issue parsing SubjectAlternativeName:", e); + } + + return san; + } + + @Deprecated(since = "since JDK 21", forRemoval = true) + private List parseOtherName(List altName) { + if (altName.size() < 2) { + LOGGER.warn("Couldn't parse subject alternative names"); + return null; + } + try (final ASN1InputStream in = new ASN1InputStream((byte[]) altName.get(1))) { + final ASN1Primitive asn1Primitive = in.readObject(); + final ASN1Sequence sequence = ASN1Sequence.getInstance(asn1Primitive); + final ASN1ObjectIdentifier asn1ObjectIdentifier = ASN1ObjectIdentifier.getInstance(sequence.getObjectAt(0)); + final ASN1TaggedObject asn1TaggedObject = ASN1TaggedObject.getInstance(sequence.getObjectAt(1)); + Method getObjectMethod = getObjectMethod(); + ASN1Object maybeTaggedAsn1Primitive = (ASN1Primitive) getObjectMethod.invoke(asn1TaggedObject); + if (maybeTaggedAsn1Primitive instanceof ASN1TaggedObject) { + maybeTaggedAsn1Primitive = (ASN1Primitive) getObjectMethod.invoke(maybeTaggedAsn1Primitive); + } + if (maybeTaggedAsn1Primitive instanceof ASN1String) { + return ImmutableList.of(asn1ObjectIdentifier.getId(), maybeTaggedAsn1Primitive.toString()); + } else { + LOGGER.warn("Couldn't parse subject alternative names"); + return null; + } + } catch (final Exception ioe) { // catch all exception here since BC throws diff exceptions + throw new RuntimeException("Couldn't parse subject alternative names", ioe); + } + } + + static Method getObjectMethod() throws ClassNotFoundException, NoSuchMethodException { + Class asn1TaggedObjectClass = Class.forName("org.bouncycastle.asn1.ASN1TaggedObject"); + try { + return asn1TaggedObjectClass.getMethod("getBaseObject"); + } catch (NoSuchMethodException ex) { + return asn1TaggedObjectClass.getMethod("getObject"); + } + } + + public String serialNumber() { + return certificate.getSerialNumber().toString(); + } + + public String subject() { + return certificate.getSubjectX500Principal() != null ? certificate.getSubjectX500Principal().getName() : null; + } + + public String issuer() { + return certificate.getIssuerX500Principal() != null ? certificate.getIssuerX500Principal().getName() : null; + } + + public String notAfter() { + return certificate.getNotAfter() != null ? certificate.getNotAfter().toInstant().toString() : null; + } + + public String notBefore() { + return certificate.getNotBefore() != null ? certificate.getNotBefore().toInstant().toString() : null; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Certificate that = (Certificate) o; + return hasKey == that.hasKey + && Objects.equals(certificate, that.certificate) + && Objects.equals(format, that.format) + && Objects.equals(alias, that.alias); + } + + @Override + public int hashCode() { + return Objects.hash(certificate, format, alias, hasKey); + } + + @Override + public String toString() { + return "Certificate{" + "format='" + format + '\'' + ", alias='" + alias + '\'' + ", hasKey=" + hasKey + '}'; + } +} diff --git a/src/main/java/org/opensearch/security/ssl/config/KeyStoreConfiguration.java b/src/main/java/org/opensearch/security/ssl/config/KeyStoreConfiguration.java new file mode 100644 index 0000000000..b1675f093a --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/config/KeyStoreConfiguration.java @@ -0,0 +1,201 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl.config; + +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import javax.net.ssl.KeyManagerFactory; + +import com.google.common.collect.ImmutableList; + +import org.opensearch.OpenSearchException; +import org.opensearch.common.collect.Tuple; + +public interface KeyStoreConfiguration { + + List files(); + + List loadCertificates(); + + default KeyManagerFactory createKeyManagerFactory(boolean validateCertificates) { + final var keyStore = createKeyStore(); + if (validateCertificates) { + KeyStoreUtils.validateKeyStoreCertificates(keyStore.v1()); + } + return buildKeyManagerFactory(keyStore.v1(), keyStore.v2()); + } + + default KeyManagerFactory buildKeyManagerFactory(final KeyStore keyStore, final char[] password) { + try { + final var keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, password); + return keyManagerFactory; + } catch (GeneralSecurityException e) { + throw new OpenSearchException("Failed to create KeyManagerFactory", e); + } + } + + Tuple createKeyStore(); + + final class JdkKeyStoreConfiguration implements KeyStoreConfiguration { + private final Path path; + + private final String type; + + private final String alias; + + private final char[] keyStorePassword; + + private final char[] keyPassword; + + public JdkKeyStoreConfiguration( + final Path path, + final String type, + final String alias, + final char[] keyStorePassword, + final char[] keyPassword + ) { + this.path = path; + this.type = type; + this.alias = alias; + this.keyStorePassword = keyStorePassword; + this.keyPassword = keyPassword; + } + + private void loadCertificateChain(final String alias, final KeyStore keyStore, final ImmutableList.Builder listBuilder) + throws KeyStoreException { + final var cc = keyStore.getCertificateChain(alias); + var first = true; + for (final var c : cc) { + if (c instanceof X509Certificate) { + listBuilder.add(new Certificate((X509Certificate) c, type, alias, first)); + first = false; + } + } + } + + @Override + public List loadCertificates() { + final var keyStore = KeyStoreUtils.loadKeyStore(path, type, keyStorePassword); + final var listBuilder = ImmutableList.builder(); + + try { + if (alias != null) { + if (keyStore.isKeyEntry(alias)) { + loadCertificateChain(alias, keyStore, listBuilder); + } + } else { + for (final var a : Collections.list(keyStore.aliases())) { + if (keyStore.isKeyEntry(a)) { + loadCertificateChain(a, keyStore, listBuilder); + } + } + } + final var list = listBuilder.build(); + if (list.isEmpty()) { + throw new OpenSearchException("The file " + path + " does not contain any certificates"); + } + return listBuilder.build(); + } catch (GeneralSecurityException e) { + throw new OpenSearchException("Couldn't load certificates from file " + path, e); + } + } + + @Override + public List files() { + return List.of(path); + } + + @Override + public Tuple createKeyStore() { + final var keyStore = KeyStoreUtils.newKeyStore(path, type, alias, keyStorePassword, keyPassword); + return Tuple.tuple(keyStore, keyPassword); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + JdkKeyStoreConfiguration that = (JdkKeyStoreConfiguration) o; + return Objects.equals(path, that.path) + && Objects.equals(type, that.type) + && Objects.equals(alias, that.alias) + && Objects.deepEquals(keyStorePassword, that.keyStorePassword) + && Objects.deepEquals(keyPassword, that.keyPassword); + } + + @Override + public int hashCode() { + return Objects.hash(path, type, alias, Arrays.hashCode(keyStorePassword), Arrays.hashCode(keyPassword)); + } + } + + final class PemKeyStoreConfiguration implements KeyStoreConfiguration { + + private final Path certificateChainPath; + + private final Path keyPath; + + private final char[] keyPassword; + + public PemKeyStoreConfiguration(final Path certificateChainPath, final Path keyPath, final char[] keyPassword) { + this.certificateChainPath = certificateChainPath; + this.keyPath = keyPath; + this.keyPassword = keyPassword; + } + + @Override + public List loadCertificates() { + final var certificates = KeyStoreUtils.x509Certificates(certificateChainPath); + final var listBuilder = ImmutableList.builder(); + listBuilder.add(new Certificate(certificates[0], true)); + for (int i = 1; i < certificates.length; i++) { + listBuilder.add(new Certificate(certificates[i], false)); + } + return listBuilder.build(); + } + + @Override + public List files() { + return List.of(certificateChainPath, keyPath); + } + + @Override + public Tuple createKeyStore() { + final var keyStore = KeyStoreUtils.newKeyStoreFromPem(certificateChainPath, keyPath, keyPassword); + return Tuple.tuple(keyStore, keyPassword); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PemKeyStoreConfiguration that = (PemKeyStoreConfiguration) o; + return Objects.equals(certificateChainPath, that.certificateChainPath) + && Objects.equals(keyPath, that.keyPath) + && Objects.deepEquals(keyPassword, that.keyPassword); + } + + @Override + public int hashCode() { + return Objects.hash(certificateChainPath, keyPath, Arrays.hashCode(keyPassword)); + } + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/config/KeyStoreUtils.java b/src/main/java/org/opensearch/security/ssl/config/KeyStoreUtils.java new file mode 100644 index 0000000000..7c063bd312 --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/config/KeyStoreUtils.java @@ -0,0 +1,218 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl.config; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.util.List; +import javax.crypto.NoSuchPaddingException; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSessionContext; + +import org.opensearch.OpenSearchException; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.ssl.ApplicationProtocolNegotiator; +import io.netty.handler.ssl.SslContext; + +final class KeyStoreUtils { + + private final static class SecuritySslContext extends SslContext { + + private SecuritySslContext() {} + + @Override + public boolean isClient() { + throw new UnsupportedOperationException("Method isClient is not supported"); + } + + @Override + public List cipherSuites() { + throw new UnsupportedOperationException("Method cipherSuites is not supported"); + } + + @Override + public ApplicationProtocolNegotiator applicationProtocolNegotiator() { + throw new UnsupportedOperationException("Method applicationProtocolNegotiator is not supported"); + } + + @Override + public SSLEngine newEngine(ByteBufAllocator alloc) { + throw new UnsupportedOperationException("Method newEngine is not supported"); + } + + @Override + public SSLEngine newEngine(ByteBufAllocator alloc, String peerHost, int peerPort) { + throw new UnsupportedOperationException("Method newEngine is not supported"); + } + + @Override + public SSLSessionContext sessionContext() { + throw new UnsupportedOperationException("Method sessionContext is not supported"); + } + + public static X509Certificate[] toX509Certificates(final File file) { + try { + return SslContext.toX509Certificates(file); + } catch (CertificateException e) { + throw new OpenSearchException("Couldn't read SSL certificates from " + file, e); + } + } + + protected static PrivateKey toPrivateKey(File keyFile, String keyPassword) throws InvalidAlgorithmParameterException, + NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeySpecException, IOException, KeyException { + return SslContext.toPrivateKey(keyFile, keyPassword); + } + + } + + public static X509Certificate[] x509Certificates(final Path file) { + final var certificates = SecuritySslContext.toX509Certificates(file.toFile()); + if (certificates == null || certificates.length == 0) { + throw new OpenSearchException("Couldn't read SSL certificates from " + file); + } + return certificates; + } + + public static KeyStore loadTrustStore(final Path path, final String type, final String alias, final char[] password) { + try { + var keyStore = loadKeyStore(path, type, password); + if (alias != null) { + if (!keyStore.isCertificateEntry(alias)) { + throw new OpenSearchException("Alias " + alias + " does not contain a certificate entry"); + } + final var aliasCertificate = (X509Certificate) keyStore.getCertificate(alias); + if (aliasCertificate == null) { + throw new OpenSearchException("Couldn't find SSL certificate for alias " + alias); + } + keyStore = newKeyStore(); + keyStore.setCertificateEntry(alias, aliasCertificate); + } + return keyStore; + } catch (Exception e) { + throw new OpenSearchException("Failed to load trust store from " + path, e); + } + } + + public static KeyStore newTrustStoreFromPem(final Path pemFile) { + try { + final var certs = x509Certificates(pemFile); + final var keyStore = newKeyStore(); + for (int i = 0; i < certs.length; i++) { + final var c = certs[i]; + keyStore.setCertificateEntry("os-sec-plugin-pem-cert-" + i, c); + } + return keyStore; + } catch (final Exception e) { + throw new OpenSearchException("Failed to load SSL certificates from " + pemFile, e); + } + } + + private static KeyStore newKeyStore() throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException { + final var keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null, null); + return keyStore; + } + + public static void validateKeyStoreCertificates(final KeyStore keyStore) { + try { + final var aliases = keyStore.aliases(); + while (aliases.hasMoreElements()) { + final var a = aliases.nextElement(); + if (keyStore.isCertificateEntry(a)) { + final var c = (X509Certificate) keyStore.getCertificate(a); + if (c == null) { + throw new CertificateException("Alias " + a + " does not contain a certificate entry"); + } + c.checkValidity(); + } else if (keyStore.isKeyEntry(a)) { + final var cc = keyStore.getCertificateChain(a); + if (cc == null) { + throw new CertificateException("Alias " + a + " does not contain a certificate chain"); + } + for (final var c : cc) { + ((X509Certificate) c).checkValidity(); + } + } + } + } catch (KeyStoreException e) { + throw new OpenSearchException("Couldn't load keys store", e); + } catch (CertificateException e) { + throw new OpenSearchException("Invalid certificates", e); + } + } + + public static KeyStore loadKeyStore(final Path path, final String type, final char[] password) { + try { + final var keyStore = KeyStore.getInstance(type); + try (final var in = Files.newInputStream(path)) { + keyStore.load(in, password); + return keyStore; + } catch (IOException e) { + throw new RuntimeException(e); + } + } catch (Exception e) { + throw new OpenSearchException("Failed to load keystore from " + path, e); + } + } + + public static KeyStore newKeyStore( + final Path path, + final String type, + final String alias, + final char[] password, + final char[] keyPassword + ) { + try { + var keyStore = loadKeyStore(path, type, password); + if (alias != null) { + if (!keyStore.isKeyEntry(alias)) { + throw new CertificateException("Couldn't find SSL key for alias " + alias); + } + final var certificateChain = keyStore.getCertificateChain(alias); + if (certificateChain == null) { + throw new CertificateException("Couldn't find certificate chain for alias " + alias); + } + final var key = keyStore.getKey(alias, keyPassword); + keyStore = newKeyStore(); + keyStore.setKeyEntry(alias, key, keyPassword, certificateChain); + } + return keyStore; + } catch (final Exception e) { + throw new OpenSearchException("Failed to load key store from " + path, e); + } + } + + public static KeyStore newKeyStoreFromPem(final Path certificateChainPath, final Path keyPath, final char[] keyPassword) { + try { + final var certificateChain = x509Certificates(certificateChainPath); + final var keyStore = newKeyStore(); + final var key = SecuritySslContext.toPrivateKey(keyPath.toFile(), keyPassword != null ? new String(keyPassword) : null); + keyStore.setKeyEntry("key", key, keyPassword, certificateChain); + return keyStore; + } catch (Exception e) { + throw new OpenSearchException("Failed read key from " + keyPath, e); + } + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/config/SslCertificatesLoader.java b/src/main/java/org/opensearch/security/ssl/config/SslCertificatesLoader.java new file mode 100644 index 0000000000..a3f0c39eed --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/config/SslCertificatesLoader.java @@ -0,0 +1,171 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl.config; + +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.security.KeyStore; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchException; +import org.opensearch.common.collect.Tuple; +import org.opensearch.common.settings.SecureSetting; +import org.opensearch.common.settings.Settings; +import org.opensearch.env.Environment; + +import static org.opensearch.security.ssl.SecureSSLSettings.SECURE_SUFFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.DEFAULT_STORE_PASSWORD; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_KEY_PASSWORD; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_PASSWORD; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_TYPE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_CERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_KEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_KEY_PASSWORD; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_TRUSTED_CAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_PASSWORD; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_TYPE; + +public class SslCertificatesLoader { + + private final static Logger LOGGER = LogManager.getLogger(SslCertificatesLoader.class); + + private final String sslConfigSuffix; + + private final String fullSslConfigSuffix; + + public SslCertificatesLoader(final String sslConfigSuffix) { + this(sslConfigSuffix, null); + } + + public SslCertificatesLoader(final String sslConfigSuffix, final String extendedSslConfigSuffix) { + this.sslConfigSuffix = sslConfigSuffix; + this.fullSslConfigSuffix = extendedSslConfigSuffix != null ? sslConfigSuffix + extendedSslConfigSuffix : sslConfigSuffix; + } + + public Tuple loadConfiguration(final Environment environment) { + final var settings = environment.settings(); + final var sslConfigSettings = settings.getByPrefix(fullSslConfigSuffix); + if (settings.hasValue(sslConfigSuffix + KEYSTORE_FILEPATH)) { + return Tuple.tuple( + environment.settings().hasValue(sslConfigSuffix + TRUSTSTORE_FILEPATH) + ? buildJdkTrustStoreConfiguration( + sslConfigSettings, + environment, + resolvePassword(sslConfigSuffix + TRUSTSTORE_PASSWORD, settings, DEFAULT_STORE_PASSWORD) + ) + : TrustStoreConfiguration.EMPTY_CONFIGURATION, + buildJdkKeyStoreConfiguration( + sslConfigSettings, + environment, + resolvePassword(sslConfigSuffix + KEYSTORE_PASSWORD, settings, DEFAULT_STORE_PASSWORD), + resolvePassword(fullSslConfigSuffix + KEYSTORE_KEY_PASSWORD, settings, DEFAULT_STORE_PASSWORD) + ) + ); + } else { + return Tuple.tuple( + sslConfigSettings.hasValue(PEM_TRUSTED_CAS_FILEPATH) + ? new TrustStoreConfiguration.PemTrustStoreConfiguration( + resolvePath(sslConfigSettings.get(PEM_TRUSTED_CAS_FILEPATH), environment) + ) + : TrustStoreConfiguration.EMPTY_CONFIGURATION, + buildPemKeyStoreConfiguration( + sslConfigSettings, + environment, + resolvePassword(fullSslConfigSuffix + PEM_KEY_PASSWORD, settings, null) + ) + ); + } + } + + private char[] resolvePassword(final String legacyPasswordSettings, final Settings settings, final String defaultPassword) { + final var securePasswordSetting = String.format("%s%s", legacyPasswordSettings, SECURE_SUFFIX); + final var securePassword = SecureSetting.secureString(securePasswordSetting, null).get(settings); + final var legacyPassword = settings.get(legacyPasswordSettings, defaultPassword); + if (!securePassword.isEmpty() && legacyPassword != null && !legacyPassword.equals(defaultPassword)) { + throw new OpenSearchException("One of " + legacyPasswordSettings + " or " + securePasswordSetting + " must be set not both"); + } + if (!securePassword.isEmpty()) { + return securePassword.getChars(); + } else { + if (legacyPassword != null) { + LOGGER.warn( + "Setting [{}] has a secure counterpart [{}] which should be used instead - allowing for legacy SSL setups", + legacyPasswordSettings, + securePasswordSetting + ); + return legacyPassword.toCharArray(); + } + } + return null; + } + + private KeyStoreConfiguration.JdkKeyStoreConfiguration buildJdkKeyStoreConfiguration( + final Settings settings, + final Environment environment, + final char[] keyStorePassword, + final char[] keyPassword + ) { + return new KeyStoreConfiguration.JdkKeyStoreConfiguration( + resolvePath(environment.settings().get(sslConfigSuffix + KEYSTORE_FILEPATH), environment), + environment.settings().get(sslConfigSuffix + KEYSTORE_TYPE, KeyStore.getDefaultType()), + settings.get(KEYSTORE_ALIAS, null), + keyStorePassword, + keyPassword + ); + } + + private TrustStoreConfiguration.JdkTrustStoreConfiguration buildJdkTrustStoreConfiguration( + final Settings settings, + final Environment environment, + final char[] trustStorePassword + ) { + return new TrustStoreConfiguration.JdkTrustStoreConfiguration( + resolvePath(environment.settings().get(sslConfigSuffix + TRUSTSTORE_FILEPATH), environment), + environment.settings().get(sslConfigSuffix + TRUSTSTORE_TYPE, KeyStore.getDefaultType()), + settings.get(TRUSTSTORE_ALIAS, null), + trustStorePassword + ); + } + + private KeyStoreConfiguration.PemKeyStoreConfiguration buildPemKeyStoreConfiguration( + final Settings settings, + final Environment environment, + final char[] pemKeyPassword + ) { + return new KeyStoreConfiguration.PemKeyStoreConfiguration( + resolvePath(settings.get(PEM_CERT_FILEPATH), environment), + resolvePath(settings.get(PEM_KEY_FILEPATH), environment), + pemKeyPassword + ); + } + + private Path resolvePath(final String filePath, final Environment environment) { + final var path = environment.configDir().resolve(Path.of(filePath)); + if (Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + throw new OpenSearchException(filePath + " - is a directory"); + } + if (!Files.isReadable(path)) { + throw new OpenSearchException( + "Unable to read the file " + filePath + ". Please make sure this files exists and is readable regarding to permissions" + ); + } + return path; + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/config/SslParameters.java b/src/main/java/org/opensearch/security/ssl/config/SslParameters.java new file mode 100644 index 0000000000..eef14cea0a --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/config/SslParameters.java @@ -0,0 +1,197 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl.config; + +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.net.ssl.SSLContext; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchException; +import org.opensearch.OpenSearchSecurityException; +import org.opensearch.common.settings.Settings; + +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.OpenSsl; +import io.netty.handler.ssl.SslProvider; + +import static org.opensearch.security.ssl.util.SSLConfigConstants.ALLOWED_OPENSSL_HTTP_PROTOCOLS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ALLOWED_OPENSSL_HTTP_PROTOCOLS_PRIOR_OPENSSL_1_1_1_BETA_9; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ALLOWED_OPENSSL_TRANSPORT_PROTOCOLS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ALLOWED_OPENSSL_TRANSPORT_PROTOCOLS_PRIOR_OPENSSL_1_1_1_BETA_9; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ALLOWED_SSL_CIPHERS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ALLOWED_SSL_PROTOCOLS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.CLIENT_AUTH_MODE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ENABLED_CIPHERS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ENABLED_PROTOCOLS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ENABLE_OPENSSL_IF_AVAILABLE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.OPENSSL_1_1_1_BETA_9; +import static org.opensearch.security.ssl.util.SSLConfigConstants.OPENSSL_AVAILABLE; + +public class SslParameters { + + private final SslProvider provider; + + private final ClientAuth clientAuth; + + private final List protocols; + + private final List ciphers; + + private SslParameters(SslProvider provider, final ClientAuth clientAuth, List protocols, List ciphers) { + this.provider = provider; + this.ciphers = ciphers; + this.protocols = protocols; + this.clientAuth = clientAuth; + } + + public ClientAuth clientAuth() { + return clientAuth; + } + + public SslProvider provider() { + return provider; + } + + public List allowedCiphers() { + return ciphers; + } + + public List allowedProtocols() { + return protocols; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SslParameters that = (SslParameters) o; + return provider == that.provider && Objects.equals(ciphers, that.ciphers) && Objects.equals(protocols, that.protocols); + } + + @Override + public int hashCode() { + return Objects.hash(provider, ciphers, protocols); + } + + public static Loader loader(final Settings sslConfigSettings) { + return new Loader(sslConfigSettings); + } + + public static final class Loader { + + private final static Logger LOGGER = LogManager.getLogger(SslParameters.class); + + private final Settings sslConfigSettings; + + public Loader(final Settings sslConfigSettings) { + this.sslConfigSettings = sslConfigSettings; + } + + private SslProvider provider(final Settings settings) { + final var useOpenSslIfAvailable = settings.getAsBoolean(ENABLE_OPENSSL_IF_AVAILABLE, true); + if (OPENSSL_AVAILABLE && useOpenSslIfAvailable) { + return SslProvider.OPENSSL; + } else { + return SslProvider.JDK; + } + } + + private List protocols(final SslProvider provider, final Settings settings, boolean http) { + final var allowedProtocols = settings.getAsList(ENABLED_PROTOCOLS, List.of(ALLOWED_SSL_PROTOCOLS)); + if (provider == SslProvider.OPENSSL) { + final String[] supportedProtocols; + if (OpenSsl.version() > OPENSSL_1_1_1_BETA_9) { + supportedProtocols = http ? ALLOWED_OPENSSL_HTTP_PROTOCOLS : ALLOWED_OPENSSL_TRANSPORT_PROTOCOLS; + } else { + supportedProtocols = http + ? ALLOWED_OPENSSL_HTTP_PROTOCOLS_PRIOR_OPENSSL_1_1_1_BETA_9 + : ALLOWED_OPENSSL_TRANSPORT_PROTOCOLS_PRIOR_OPENSSL_1_1_1_BETA_9; + } + return openSslProtocols(allowedProtocols, supportedProtocols); + } else { + return jdkProtocols(allowedProtocols); + } + } + + private List openSslProtocols(final List allowedSslProtocols, final String... supportedProtocols) { + LOGGER.debug("OpenSSL supports the following {} protocols {}", supportedProtocols.length, supportedProtocols); + return Stream.of(supportedProtocols).filter(allowedSslProtocols::contains).collect(Collectors.toList()); + } + + private List jdkProtocols(final List allowedSslProtocols) { + try { + final var supportedProtocols = SSLContext.getDefault().getDefaultSSLParameters().getProtocols(); + LOGGER.debug("JVM supports the following {} protocols {}", supportedProtocols.length, supportedProtocols); + return Stream.of(supportedProtocols).filter(allowedSslProtocols::contains).collect(Collectors.toList()); + } catch (final NoSuchAlgorithmException e) { + throw new OpenSearchException("Unable to determine supported protocols", e); + } + } + + private List ciphers(final SslProvider provider, final Settings settings) { + final var allowed = settings.getAsList(ENABLED_CIPHERS, List.of(ALLOWED_SSL_CIPHERS)); + final Stream allowedCiphers; + if (provider == SslProvider.OPENSSL) { + LOGGER.debug( + "OpenSSL {} supports the following ciphers (java-style) {}", + OpenSsl.versionString(), + OpenSsl.availableJavaCipherSuites() + ); + LOGGER.debug( + "OpenSSL {} supports the following ciphers (openssl-style) {}", + OpenSsl.versionString(), + OpenSsl.availableOpenSslCipherSuites() + ); + allowedCiphers = allowed.stream().filter(OpenSsl::isCipherSuiteAvailable); + } else { + try { + final var supportedCiphers = SSLContext.getDefault().getDefaultSSLParameters().getCipherSuites(); + LOGGER.debug("JVM supports the following {} ciphers {}", supportedCiphers.length, supportedCiphers); + allowedCiphers = Stream.of(supportedCiphers).filter(allowed::contains); + } catch (final NoSuchAlgorithmException e) { + throw new OpenSearchException("Unable to determine ciphers protocols", e); + } + } + return allowedCiphers.sorted(String::compareTo).collect(Collectors.toList()); + } + + public SslParameters load(final boolean http) { + final var clientAuth = http + ? ClientAuth.valueOf(sslConfigSettings.get(CLIENT_AUTH_MODE, ClientAuth.OPTIONAL.name()).toUpperCase(Locale.ROOT)) + : ClientAuth.REQUIRE; + + final var provider = provider(sslConfigSettings); + final var sslParameters = new SslParameters( + provider, + clientAuth, + protocols(provider, sslConfigSettings, http), + ciphers(provider, sslConfigSettings) + ); + if (sslParameters.allowedProtocols().isEmpty()) { + throw new OpenSearchSecurityException("No ssl protocols for " + (http ? "HTTP" : "Transport") + " layer"); + } + if (sslParameters.allowedCiphers().isEmpty()) { + throw new OpenSearchSecurityException("No valid cipher suites for " + (http ? "HTTP" : "Transport") + " layer"); + } + return sslParameters; + } + + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/config/TrustStoreConfiguration.java b/src/main/java/org/opensearch/security/ssl/config/TrustStoreConfiguration.java new file mode 100644 index 0000000000..4965aa3216 --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/config/TrustStoreConfiguration.java @@ -0,0 +1,185 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl.config; + +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.net.ssl.TrustManagerFactory; + +import com.google.common.collect.ImmutableList; + +import org.opensearch.OpenSearchException; + +public interface TrustStoreConfiguration { + + TrustStoreConfiguration EMPTY_CONFIGURATION = new TrustStoreConfiguration() { + @Override + public Path file() { + return null; + } + + @Override + public List loadCertificates() { + return List.of(); + } + + @Override + public KeyStore createTrustStore() { + return null; + } + + @Override + public TrustManagerFactory createTrustManagerFactory(boolean validateCertificates) { + return null; + } + }; + + Path file(); + + List loadCertificates(); + + default TrustManagerFactory createTrustManagerFactory(boolean validateCertificates) { + final var trustStore = createTrustStore(); + if (validateCertificates) { + KeyStoreUtils.validateKeyStoreCertificates(trustStore); + } + return buildTrustManagerFactory(trustStore); + } + + default TrustManagerFactory buildTrustManagerFactory(final KeyStore keyStore) { + try { + final var trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + return trustManagerFactory; + } catch (GeneralSecurityException e) { + throw new OpenSearchException("Couldn't initialize TrustManagerFactory", e); + } + } + + KeyStore createTrustStore(); + + final class JdkTrustStoreConfiguration implements TrustStoreConfiguration { + + private final Path path; + + private final String type; + + private final String alias; + + private final char[] password; + + public JdkTrustStoreConfiguration(final Path path, final String type, final String alias, final char[] password) { + this.path = path; + this.type = type; + this.alias = alias; + this.password = password; + } + + @Override + public List loadCertificates() { + final var keyStore = KeyStoreUtils.loadKeyStore(path, type, password); + final var listBuilder = ImmutableList.builder(); + try { + if (alias != null) { + listBuilder.add(new Certificate((X509Certificate) keyStore.getCertificate(alias), type, alias, false)); + } else { + for (final var a : Collections.list(keyStore.aliases())) { + if (!keyStore.isCertificateEntry(a)) continue; + final var c = keyStore.getCertificate(a); + if (c instanceof X509Certificate) { + listBuilder.add(new Certificate((X509Certificate) c, type, a, false)); + } + } + } + final var list = listBuilder.build(); + if (list.isEmpty()) { + throw new OpenSearchException("The file " + path + " does not contain any certificates"); + } + return listBuilder.build(); + } catch (GeneralSecurityException e) { + throw new OpenSearchException("Couldn't load certificates from file " + path, e); + } + } + + @Override + public Path file() { + return path; + } + + @Override + public KeyStore createTrustStore() { + return KeyStoreUtils.loadTrustStore(path, type, alias, password); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + JdkTrustStoreConfiguration that = (JdkTrustStoreConfiguration) o; + return Objects.equals(path, that.path) + && Objects.equals(type, that.type) + && Objects.equals(alias, that.alias) + && Objects.deepEquals(password, that.password); + } + + @Override + public int hashCode() { + return Objects.hash(path, type, alias, Arrays.hashCode(password)); + } + } + + final class PemTrustStoreConfiguration implements TrustStoreConfiguration { + + private final Path path; + + public PemTrustStoreConfiguration(final Path path) { + this.path = path; + } + + @Override + public List loadCertificates() { + return Stream.of(KeyStoreUtils.x509Certificates(path)).map(c -> new Certificate(c, false)).collect(Collectors.toList()); + } + + @Override + public Path file() { + return path; + } + + @Override + public KeyStore createTrustStore() { + return KeyStoreUtils.newTrustStoreFromPem(path); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PemTrustStoreConfiguration that = (PemTrustStoreConfiguration) o; + return Objects.equals(path, that.path); + } + + @Override + public int hashCode() { + return Objects.hashCode(path); + } + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLInfoAction.java b/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLInfoAction.java index b9f9e949ec..203a0c7965 100644 --- a/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLInfoAction.java +++ b/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLInfoAction.java @@ -35,11 +35,13 @@ import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; import org.opensearch.security.filter.SecurityRequestFactory; -import org.opensearch.security.ssl.SecurityKeyStore; +import org.opensearch.security.ssl.SslConfiguration; +import org.opensearch.security.ssl.SslSettingsManager; +import org.opensearch.security.ssl.config.CertType; +import org.opensearch.security.ssl.config.SslParameters; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.ssl.util.SSLRequestHelper; import org.opensearch.security.ssl.util.SSLRequestHelper.SSLInfo; @@ -50,7 +52,7 @@ public class SecuritySSLInfoAction extends BaseRestHandler { private static final List routes = Collections.singletonList(new Route(Method.GET, "/_opendistro/_security/sslinfo")); private final Logger log = LogManager.getLogger(this.getClass()); - private final SecurityKeyStore sks; + private final SslSettingsManager sslSettingsManager; final PrincipalExtractor principalExtractor; private final Path configPath; private final Settings settings; @@ -58,13 +60,12 @@ public class SecuritySSLInfoAction extends BaseRestHandler { public SecuritySSLInfoAction( final Settings settings, final Path configPath, - final RestController controller, - final SecurityKeyStore sks, + final SslSettingsManager sslSettingsManager, final PrincipalExtractor principalExtractor ) { super(); this.settings = settings; - this.sks = sks; + this.sslSettingsManager = sslSettingsManager; this.principalExtractor = principalExtractor; this.configPath = configPath; } @@ -103,13 +104,15 @@ public void accept(RestChannel channel) throws Exception { if (showDn == Boolean.TRUE) { builder.field( "peer_certificates_list", - certs == null ? null : Arrays.stream(certs).map(c -> c.getSubjectDN().getName()).collect(Collectors.toList()) + certs == null + ? null + : Arrays.stream(certs).map(c -> c.getSubjectX500Principal().getName()).collect(Collectors.toList()) ); builder.field( "local_certificates_list", localCerts == null ? null - : Arrays.stream(localCerts).map(c -> c.getSubjectDN().getName()).collect(Collectors.toList()) + : Arrays.stream(localCerts).map(c -> c.getSubjectX500Principal().getName()).collect(Collectors.toList()) ); } @@ -122,9 +125,27 @@ public void accept(RestChannel channel) throws Exception { builder.field("ssl_openssl_non_available_cause", openSslUnavailCause == null ? "" : openSslUnavailCause.toString()); builder.field("ssl_openssl_supports_key_manager_factory", OpenSsl.supportsKeyManagerFactory()); builder.field("ssl_openssl_supports_hostname_validation", OpenSsl.supportsHostnameValidation()); - builder.field("ssl_provider_http", sks.getHTTPProviderName()); - builder.field("ssl_provider_transport_server", sks.getTransportServerProviderName()); - builder.field("ssl_provider_transport_client", sks.getTransportClientProviderName()); + builder.field( + "ssl_provider_http", + sslSettingsManager.sslConfiguration(CertType.HTTP) + .map(SslConfiguration::sslParameters) + .map(SslParameters::provider) + .orElse(null) + ); + builder.field( + "ssl_provider_transport_server", + sslSettingsManager.sslConfiguration(CertType.TRANSPORT) + .map(SslConfiguration::sslParameters) + .map(SslParameters::provider) + .orElse(null) + ); + builder.field( + "ssl_provider_transport_client", + sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT) + .map(SslConfiguration::sslParameters) + .map(SslParameters::provider) + .orElse(null) + ); builder.endObject(); response = new BytesRestResponse(RestStatus.OK, builder); diff --git a/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java b/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java index a3b9348496..dfc9ae567e 100644 --- a/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java +++ b/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java @@ -22,9 +22,53 @@ import java.util.List; import org.opensearch.common.settings.Settings; +import org.opensearch.security.ssl.OpenSearchSecuritySSLPlugin; + +import io.netty.handler.ssl.OpenSsl; public final class SSLConfigConstants { + public static final String SSL_PREFIX = "plugins.security.ssl."; + + public static final String HTTP_SETTINGS = "http"; + + public static final String TRANSPORT_SETTINGS = "transport"; + + public static final String SSL_HTTP_PREFIX = SSL_PREFIX + HTTP_SETTINGS + "."; + + public static final String SSL_TRANSPORT_PREFIX = SSL_PREFIX + TRANSPORT_SETTINGS + "."; + + public static final String SSL_TRANSPORT_SERVER_EXTENDED_PREFIX = "server."; + + public static final String SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX = "client."; + + public static final String SSL_TRANSPORT_CLIENT_PREFIX = SSL_PREFIX + TRANSPORT_SETTINGS + SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX; + + public static final String ENABLED = "enabled"; + + public static final String CLIENT_AUTH_MODE = "clientauth_mode"; + + public static final String KEYSTORE_TYPE = "keystore_type"; + public static final String KEYSTORE_ALIAS = "keystore_alias"; + public static final String KEYSTORE_FILEPATH = "keystore_filepath"; + public static final String KEYSTORE_PASSWORD = "keystore_password"; + public static final String KEYSTORE_KEY_PASSWORD = "keystore_keypassword"; + + public static final String TRUSTSTORE_ALIAS = "truststore_alias"; + public static final String TRUSTSTORE_FILEPATH = "truststore_filepath"; + public static final String TRUSTSTORE_TYPE = "truststore_type"; + public static final String TRUSTSTORE_PASSWORD = "truststore_password"; + + public static final String PEM_KEY_FILEPATH = "pemkey_filepath"; + public static final String PEM_CERT_FILEPATH = "pemcert_filepath"; + public static final String PEM_TRUSTED_CAS_FILEPATH = "pemtrustedcas_filepath"; + public static final String EXTENDED_KEY_USAGE_ENABLED = "extended_key_usage_enabled"; + + public static final String ENABLE_OPENSSL_IF_AVAILABLE = "enable_openssl_if_available"; + public static final String ENABLED_PROTOCOLS = "enabled_protocols"; + public static final String ENABLED_CIPHERS = "enabled_ciphers"; + public static final String PEM_KEY_PASSWORD = "pemkey_password"; + public static final String SECURITY_SSL_HTTP_ENABLE_OPENSSL_IF_AVAILABLE = "plugins.security.ssl.http.enable_openssl_if_available"; public static final String SECURITY_SSL_HTTP_ENABLED = "plugins.security.ssl.http.enabled"; public static final boolean SECURITY_SSL_HTTP_ENABLED_DEFAULT = false; @@ -99,7 +143,19 @@ public final class SSLConfigConstants { public static final String JDK_TLS_REJECT_CLIENT_INITIATED_RENEGOTIATION = "jdk.tls.rejectClientInitiatedRenegotiation"; - private static final String[] _SECURE_SSL_PROTOCOLS = { "TLSv1.3", "TLSv1.2", "TLSv1.1" }; + public static final Long OPENSSL_1_1_1_BETA_9 = 0x10101009L; + + public static final String[] ALLOWED_SSL_PROTOCOLS = { "TLSv1.3", "TLSv1.2", "TLSv1.1" }; + + public static final String[] ALLOWED_OPENSSL_HTTP_PROTOCOLS = ALLOWED_SSL_PROTOCOLS; + + public static final String[] ALLOWED_OPENSSL_HTTP_PROTOCOLS_PRIOR_OPENSSL_1_1_1_BETA_9 = { "TLSv1.2", "TLSv1.1", "TLSv1" }; + + public static final String[] ALLOWED_OPENSSL_TRANSPORT_PROTOCOLS = ALLOWED_SSL_PROTOCOLS; + + public static final String[] ALLOWED_OPENSSL_TRANSPORT_PROTOCOLS_PRIOR_OPENSSL_1_1_1_BETA_9 = { "TLSv1.2", "TLSv1.1" }; + + public static final boolean OPENSSL_AVAILABLE = OpenSearchSecuritySSLPlugin.OPENSSL_SUPPORTED && OpenSsl.isAvailable(); public static String[] getSecureSSLProtocols(Settings settings, boolean http) { List configuredProtocols = null; @@ -116,11 +172,11 @@ public static String[] getSecureSSLProtocols(Settings settings, boolean http) { return configuredProtocols.toArray(new String[0]); } - return _SECURE_SSL_PROTOCOLS.clone(); + return ALLOWED_SSL_PROTOCOLS.clone(); } // @formatter:off - private static final String[] _SECURE_SSL_CIPHERS = { + public static final String[] ALLOWED_SSL_CIPHERS = { // TLS__WITH_ // Example (including unsafe ones) @@ -249,7 +305,7 @@ public static List getSecureSSLCiphers(Settings settings, boolean http) return configuredCiphers; } - return Collections.unmodifiableList(Arrays.asList(_SECURE_SSL_CIPHERS)); + return Collections.unmodifiableList(Arrays.asList(ALLOWED_SSL_CIPHERS)); } private SSLConfigConstants() { diff --git a/src/test/java/org/opensearch/security/ssl/CertificatesRule.java b/src/test/java/org/opensearch/security/ssl/CertificatesRule.java new file mode 100644 index 0000000000..a27de233dc --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/CertificatesRule.java @@ -0,0 +1,318 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.List; + +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.rules.ExternalResource; +import org.junit.rules.TemporaryFolder; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.RFC4519Style; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.CertIOException; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +import org.opensearch.common.collect.Tuple; + +public class CertificatesRule extends ExternalResource { + + private final static BouncyCastleProvider BOUNCY_CASTLE_PROVIDER = new BouncyCastleProvider(); + + private final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + final static String DEFAULT_SUBJECT_NAME = "CN=some_access,OU=client,O=client,L=test,C=de"; + + private Path configRootFolder; + + private final String privateKeyPassword = RandomStringUtils.randomAlphabetic(10); + + private X509CertificateHolder caCertificateHolder; + + private X509CertificateHolder accessCertificateHolder; + + private PrivateKey accessCertificatePrivateKey; + + @Override + protected void before() throws Throwable { + super.before(); + temporaryFolder.create(); + configRootFolder = temporaryFolder.newFolder("esHome").toPath(); + final var keyPair = generateKeyPair(); + caCertificateHolder = generateCaCertificate(keyPair); + final var keyAndCertificate = generateAccessCertificate(keyPair); + accessCertificatePrivateKey = keyAndCertificate.v1(); + accessCertificateHolder = keyAndCertificate.v2(); + } + + @Override + protected void after() { + super.after(); + temporaryFolder.delete(); + } + + public Path configRootFolder() { + return configRootFolder; + } + + public String privateKeyPassword() { + return privateKeyPassword; + } + + public X509CertificateHolder caCertificateHolder() { + return caCertificateHolder; + } + + public X509CertificateHolder accessCertificateHolder() { + return accessCertificateHolder; + } + + public X509Certificate x509CaCertificate() throws CertificateException { + return toX509Certificate(caCertificateHolder); + } + + public X509Certificate x509AccessCertificate() throws CertificateException { + return toX509Certificate(accessCertificateHolder); + } + + public PrivateKey accessCertificatePrivateKey() { + return accessCertificatePrivateKey; + } + + public KeyPair generateKeyPair() throws NoSuchAlgorithmException { + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", BOUNCY_CASTLE_PROVIDER); + generator.initialize(4096); + return generator.generateKeyPair(); + } + + public X509CertificateHolder generateCaCertificate(final KeyPair parentKeyPair) throws IOException, NoSuchAlgorithmException, + OperatorCreationException { + return generateCaCertificate(parentKeyPair, generateSerialNumber()); + } + + public X509CertificateHolder generateCaCertificate(final KeyPair parentKeyPair, final BigInteger serialNumber) throws IOException, + NoSuchAlgorithmException, OperatorCreationException { + final var startAndEndDate = generateStartAndEndDate(); + // CS-SUPPRESS-SINGLE: RegexpSingleline Extension should only be used sparingly to keep implementations as generic as possible + return createCertificateBuilder( + DEFAULT_SUBJECT_NAME, + DEFAULT_SUBJECT_NAME, + parentKeyPair.getPublic(), + parentKeyPair.getPublic(), + serialNumber, + startAndEndDate.v1(), + startAndEndDate.v2() + ).addExtension(Extension.basicConstraints, true, new BasicConstraints(true)) + .addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyCertSign | KeyUsage.cRLSign)) + .build(new JcaContentSignerBuilder("SHA256withRSA").setProvider(BOUNCY_CASTLE_PROVIDER).build(parentKeyPair.getPrivate())); + // CS-ENFORCE-SINGLE + } + + public Tuple generateAccessCertificate(final KeyPair parentKeyPair) throws NoSuchAlgorithmException, + IOException, OperatorCreationException { + final var startAndEndDate = generateStartAndEndDate(); + return generateAccessCertificate( + DEFAULT_SUBJECT_NAME, + DEFAULT_SUBJECT_NAME, + parentKeyPair, + generateSerialNumber(), + startAndEndDate.v1(), + startAndEndDate.v2(), + defaultSubjectAlternativeNames() + ); + } + + public Tuple generateAccessCertificate(final KeyPair parentKeyPair, final BigInteger serialNumber) + throws NoSuchAlgorithmException, IOException, OperatorCreationException { + final var startAdnEndDate = generateStartAndEndDate(); + return generateAccessCertificate( + DEFAULT_SUBJECT_NAME, + DEFAULT_SUBJECT_NAME, + parentKeyPair, + serialNumber, + startAdnEndDate.v1(), + startAdnEndDate.v2(), + defaultSubjectAlternativeNames() + ); + } + + public Tuple generateAccessCertificate( + final KeyPair parentKeyPair, + final Instant startDate, + final Instant endDate + ) throws NoSuchAlgorithmException, IOException, OperatorCreationException { + return generateAccessCertificate( + DEFAULT_SUBJECT_NAME, + DEFAULT_SUBJECT_NAME, + parentKeyPair, + generateSerialNumber(), + startDate, + endDate, + defaultSubjectAlternativeNames() + ); + } + + public Tuple generateAccessCertificate( + final KeyPair parentKeyPair, + final Instant startDate, + final Instant endDate, + List sans + ) throws NoSuchAlgorithmException, IOException, OperatorCreationException { + return generateAccessCertificate( + DEFAULT_SUBJECT_NAME, + DEFAULT_SUBJECT_NAME, + parentKeyPair, + generateSerialNumber(), + startDate, + endDate, + sans + ); + } + + public Tuple generateAccessCertificate( + final KeyPair parentKeyPair, + final String subject, + final String issuer + ) throws NoSuchAlgorithmException, IOException, OperatorCreationException { + final var startAndEndDate = generateStartAndEndDate(); + return generateAccessCertificate( + subject, + issuer, + parentKeyPair, + generateSerialNumber(), + startAndEndDate.v1(), + startAndEndDate.v2(), + defaultSubjectAlternativeNames() + ); + } + + public Tuple generateAccessCertificate(final KeyPair parentKeyPair, final List sans) + throws NoSuchAlgorithmException, IOException, OperatorCreationException { + final var startAndEndDate = generateStartAndEndDate(); + return generateAccessCertificate( + DEFAULT_SUBJECT_NAME, + DEFAULT_SUBJECT_NAME, + parentKeyPair, + generateSerialNumber(), + startAndEndDate.v1(), + startAndEndDate.v2(), + sans + ); + } + + public Tuple generateAccessCertificate( + final String subject, + final String issuer, + final KeyPair parentKeyPair, + final BigInteger serialNumber, + final Instant startDate, + final Instant endDate, + final List sans + ) throws NoSuchAlgorithmException, IOException, OperatorCreationException { + final var keyPair = generateKeyPair(); + // CS-SUPPRESS-SINGLE: RegexpSingleline Extension should only be used sparingly to keep implementations as generic as possible + final var certificate = createCertificateBuilder( + subject, + issuer, + keyPair.getPublic(), + parentKeyPair.getPublic(), + serialNumber, + startDate, + endDate + ).addExtension(Extension.basicConstraints, true, new BasicConstraints(false)) + .addExtension( + Extension.keyUsage, + true, + new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation | KeyUsage.keyEncipherment) + ) + .addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth)) + .addExtension(Extension.subjectAlternativeName, false, new DERSequence(sans.toArray(sans.toArray(new ASN1Encodable[0])))) + .build(new JcaContentSignerBuilder("SHA256withRSA").setProvider(BOUNCY_CASTLE_PROVIDER).build(parentKeyPair.getPrivate())); + // CS-ENFORCE-SINGLE + return Tuple.tuple(keyPair.getPrivate(), certificate); + } + + private List defaultSubjectAlternativeNames() { + return List.of( + new GeneralName(GeneralName.registeredID, "1.2.3.4.5.5"), + new GeneralName(GeneralName.dNSName, "localhost"), + new GeneralName(GeneralName.iPAddress, "127.0.0.1") + ); + } + + public X509Certificate toX509Certificate(final X509CertificateHolder x509CertificateHolder) throws CertificateException { + return new JcaX509CertificateConverter().getCertificate(x509CertificateHolder); + } + + private X509v3CertificateBuilder createCertificateBuilder( + final String subject, + final String issuer, + final PublicKey certificatePublicKey, + final PublicKey parentPublicKey, + final BigInteger serialNumber, + final Instant startDate, + final Instant endDate + ) throws NoSuchAlgorithmException, CertIOException { + // CS-SUPPRESS-SINGLE: RegexpSingleline Extension should only be used sparingly to keep implementations as generic as possible + final var subjectName = new X500Name(RFC4519Style.INSTANCE, subject); + final var issuerName = new X500Name(RFC4519Style.INSTANCE, issuer); + final var extUtils = new JcaX509ExtensionUtils(); + return new X509v3CertificateBuilder( + issuerName, + serialNumber, + Date.from(startDate), + Date.from(endDate), + subjectName, + SubjectPublicKeyInfo.getInstance(certificatePublicKey.getEncoded()) + ).addExtension(Extension.authorityKeyIdentifier, false, extUtils.createAuthorityKeyIdentifier(parentPublicKey)) + .addExtension(Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(certificatePublicKey)); + // CS-ENFORCE-SINGLE + } + + Tuple generateStartAndEndDate() { + final var startDate = Instant.now().minusMillis(24 * 3600 * 1000); + final var endDate = Instant.from(startDate).plus(10, ChronoUnit.DAYS); + return Tuple.tuple(startDate, endDate); + } + + public BigInteger generateSerialNumber() { + return BigInteger.valueOf(Instant.now().plusMillis(100).getEpochSecond()); + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/CertificatesUtils.java b/src/test/java/org/opensearch/security/ssl/CertificatesUtils.java new file mode 100644 index 0000000000..7b6ee9fc74 --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/CertificatesUtils.java @@ -0,0 +1,43 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.PrivateKey; +import java.security.SecureRandom; + +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.openssl.PKCS8Generator; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder; +import org.bouncycastle.util.io.pem.PemObject; + +public class CertificatesUtils { + + public static void writePemContent(final Path path, final Object pemContent) throws IOException { + try (JcaPEMWriter writer = new JcaPEMWriter(Files.newBufferedWriter(path))) { + writer.writeObject(pemContent); + } + } + + public static PemObject privateKeyToPemObject(final PrivateKey privateKey, final String password) throws Exception { + return new PKCS8Generator( + PrivateKeyInfo.getInstance(privateKey.getEncoded()), + new JceOpenSSLPKCS8EncryptorBuilder(PKCS8Generator.PBE_SHA1_3DES).setRandom(new SecureRandom()) + .setPassword(password.toCharArray()) + .build() + ).generate(); + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPluginTest.java b/src/test/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPluginTest.java index aefb12c0db..e7e5abaeda 100644 --- a/src/test/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPluginTest.java +++ b/src/test/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPluginTest.java @@ -9,6 +9,7 @@ package org.opensearch.security.ssl; import java.io.IOException; +import java.nio.file.Path; import java.util.Collection; import java.util.List; import java.util.Map; @@ -26,6 +27,7 @@ import org.opensearch.common.network.NetworkModule; import org.opensearch.common.settings.ClusterSettings; import org.opensearch.common.settings.Settings; +import org.opensearch.env.Environment; import org.opensearch.http.HttpServerTransport; import org.opensearch.http.netty4.ssl.SecureNetty4HttpServerTransport; import org.opensearch.plugins.SecureHttpTransportSettingsProvider; @@ -55,17 +57,17 @@ public class OpenSearchSecuritySSLPluginTest extends AbstractSecurityUnitTest { private SecureTransportSettingsProvider secureTransportSettingsProvider; private ClusterSettings clusterSettings; + private Path osPathHome; + @Before public void setUp() { + osPathHome = FileHelper.getAbsoluteFilePathFromClassPath("ssl/kirk-keystore.jks").getParent().getParent(); settings = Settings.builder() + .put(Environment.PATH_HOME_SETTING.getKey(), osPathHome) .put( SSLConfigConstants.SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, FileHelper.getAbsoluteFilePathFromClassPath("ssl/kirk-keystore.jks") ) - .put( - SSLConfigConstants.SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH, - FileHelper.getAbsoluteFilePathFromClassPath("ssl/root-ca.pem") - ) .put( SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.getAbsoluteFilePathFromClassPath("ssl/truststore.jks") @@ -116,7 +118,7 @@ public Optional buildSecureHttpServerEngine(Settings settings, HttpSe @Test public void testRegisterSecureHttpTransport() throws IOException { - try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(settings, null, false)) { + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(settings, osPathHome, false)) { final Map> transports = plugin.getSecureHttpTransports( settings, MOCK_POOL, @@ -140,7 +142,7 @@ public void testRegisterSecureHttpTransport() throws IOException { @Test public void testRegisterSecureTransport() throws IOException { - try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(settings, null, false)) { + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(settings, osPathHome, false)) { final Map> transports = plugin.getSecureTransports( settings, MOCK_POOL, @@ -165,7 +167,7 @@ public void testRegisterSecureTransportWithDeprecatedSecuirtyPluginSettings() th .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION, false) .build(); - try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(deprecated, null, false)) { + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(deprecated, osPathHome, false)) { final Map> transports = plugin.getSecureTransports( deprecated, MOCK_POOL, @@ -190,7 +192,7 @@ public void testRegisterSecureTransportWithNetworkModuleSettings() throws IOExce .put(NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY, false) .build(); - try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(migrated, null, false)) { + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(migrated, osPathHome, false)) { final Map> transports = plugin.getSecureTransports( migrated, MOCK_POOL, @@ -229,7 +231,7 @@ public void testRegisterSecureTransportWithDuplicateSettings() throws IOExceptio .put(NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY, false) .build(); - try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(migrated, null, false)) { + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(migrated, osPathHome, false)) { final Map> transports = plugin.getSecureTransports( migrated, MOCK_POOL, diff --git a/src/test/java/org/opensearch/security/ssl/SSLTest.java b/src/test/java/org/opensearch/security/ssl/SSLTest.java index a6013c7823..20887fccdf 100644 --- a/src/test/java/org/opensearch/security/ssl/SSLTest.java +++ b/src/test/java/org/opensearch/security/ssl/SSLTest.java @@ -569,7 +569,7 @@ public void testHttpsAndNodeSSLFailedCipher() throws Exception { Assert.fail(); } catch (Exception e1) { Throwable e = ExceptionUtils.getRootCause(e1); - Assert.assertTrue(e.toString(), e.toString().contains("no valid cipher")); + Assert.assertTrue(e.toString(), e.toString().contains("No valid cipher")); } } diff --git a/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java b/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java index 244967cf76..30635477eb 100644 --- a/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java +++ b/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java @@ -147,9 +147,12 @@ public void testSSLReloadFail_InvalidDNAndDate() throws Exception { RestHelper.HttpResponse reloadCertsResponse = rh.executePutRequest(RELOAD_TRANSPORT_CERTS_ENDPOINT, null); assertThat(reloadCertsResponse.getStatusCode(), is(500)); assertThat( - "OpenSearchSecurityException[Error while initializing transport SSL layer from PEM: java.lang.Exception: " - + "New Certs do not have valid Issuer DN, Subject DN or SAN.]; nested: Exception[New Certs do not have valid Issuer DN, Subject DN or SAN.];", - is(DefaultObjectMapper.readTree(reloadCertsResponse.getBody()).get("error").get("root_cause").get(0).get("reason").asText()) + DefaultObjectMapper.readTree(reloadCertsResponse.getBody()).get("error").get("root_cause").get(0).get("reason").asText(), + is( + "java.security.cert.CertificateException: " + + "New certificates do not have valid Subject DNs. Current Subject DNs [CN=node-1.example.com,OU=SSL,O=Test,L=Test,C=DE] " + + "new Subject DNs [CN=node-2.example.com,OU=SSL,O=Test,L=Test,C=DE]" + ) ); } diff --git a/src/test/java/org/opensearch/security/ssl/SslContextHandlerTest.java b/src/test/java/org/opensearch/security/ssl/SslContextHandlerTest.java new file mode 100644 index 0000000000..4dea300754 --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/SslContextHandlerTest.java @@ -0,0 +1,266 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl; + +import java.nio.file.Path; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.cert.X509CertificateHolder; + +import org.opensearch.common.settings.Settings; +import org.opensearch.security.ssl.config.KeyStoreConfiguration; +import org.opensearch.security.ssl.config.SslParameters; +import org.opensearch.security.ssl.config.TrustStoreConfiguration; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.opensearch.security.ssl.CertificatesUtils.privateKeyToPemObject; +import static org.opensearch.security.ssl.CertificatesUtils.writePemContent; +import static org.junit.Assert.assertThrows; + +public class SslContextHandlerTest { + + @ClassRule + public static CertificatesRule certificatesRule = new CertificatesRule(); + + Path caCertificatePath; + + Path accessCertificatePath; + + Path accessCertificatePrivateKeyPath; + + @Before + public void setUp() throws Exception { + caCertificatePath = certificatesRule.configRootFolder().resolve("ca_certificate.pem"); + accessCertificatePath = certificatesRule.configRootFolder().resolve("access_certificate.pem"); + accessCertificatePrivateKeyPath = certificatesRule.configRootFolder().resolve("access_certificate_pk.pem"); + writeCertificates( + certificatesRule.caCertificateHolder(), + certificatesRule.accessCertificateHolder(), + certificatesRule.accessCertificatePrivateKey() + ); + } + + void writeCertificates( + final X509CertificateHolder caCertificate, + final X509CertificateHolder accessCertificate, + final PrivateKey accessPrivateKey + ) throws Exception { + writePemContent(caCertificatePath, caCertificate); + writePemContent(accessCertificatePath, accessCertificate); + writePemContent(accessCertificatePrivateKeyPath, privateKeyToPemObject(accessPrivateKey, certificatesRule.privateKeyPassword())); + } + + @Test + public void doesNothingIfCertificatesAreSame() throws Exception { + final var sslContextHandler = sslContextHandler(); + + final var sslContextBefore = sslContextHandler.sslContext(); + sslContextHandler.reloadSslContext(); + + assertThat("SSL Context is the same", sslContextBefore.equals(sslContextHandler.sslContext())); + } + + @Test + public void failsIfCertificatesHasInvalidDates() throws Exception { + final var sslContextHandler = sslContextHandler(); + + final var accessCertificate = certificatesRule.x509AccessCertificate(); + final var keyPair = certificatesRule.generateKeyPair(); + final var newCaCertificate = certificatesRule.generateCaCertificate(keyPair); + var newAccessCertificate = certificatesRule.generateAccessCertificate( + keyPair, + accessCertificate.getNotBefore().toInstant(), + accessCertificate.getNotAfter().toInstant().minus(10, ChronoUnit.DAYS) + ); + + writeCertificates(newCaCertificate, newAccessCertificate.v2(), newAccessCertificate.v1()); + + assertThrows(CertificateException.class, sslContextHandler::reloadSslContext); + + newAccessCertificate = certificatesRule.generateAccessCertificate( + keyPair, + accessCertificate.getNotBefore().toInstant().plus(10, ChronoUnit.DAYS), + accessCertificate.getNotAfter().toInstant().plus(20, ChronoUnit.DAYS) + ); + writeCertificates(newCaCertificate, newAccessCertificate.v2(), newAccessCertificate.v1()); + + assertThrows(CertificateException.class, sslContextHandler::reloadSslContext); + } + + @Test + public void filesIfHasNotValidSubjectDNs() throws Exception { + final var sslContextHandler = sslContextHandler(); + + final var keyPair = certificatesRule.generateKeyPair(); + final var newCaCertificate = certificatesRule.generateCaCertificate(keyPair); + final var currentAccessCertificate = certificatesRule.x509AccessCertificate(); + final var wrongSubjectAccessCertificate = certificatesRule.generateAccessCertificate( + keyPair, + "CN=ddddd,O=client,L=test,C=de", + currentAccessCertificate.getIssuerX500Principal().getName() + ); + + writeCertificates(newCaCertificate, wrongSubjectAccessCertificate.v2(), wrongSubjectAccessCertificate.v1()); + + final var e = assertThrows(CertificateException.class, sslContextHandler::reloadSslContext); + assertThat( + e.getMessage(), + is( + "New certificates do not have valid Subject DNs. " + + "Current Subject DNs [CN=some_access,OU=client,O=client,L=test,C=de] " + + "new Subject DNs [CN=ddddd,O=client,L=test,C=de]" + ) + ); + } + + @Test + public void filesIfHasNotValidIssuerDNs() throws Exception { + final var sslContextHandler = sslContextHandler(); + + final var keyPair = certificatesRule.generateKeyPair(); + final var newCaCertificate = certificatesRule.generateCaCertificate(keyPair); + final var currentAccessCertificate = certificatesRule.x509AccessCertificate(); + final var wrongSubjectAccessCertificate = certificatesRule.generateAccessCertificate( + keyPair, + currentAccessCertificate.getSubjectX500Principal().getName(), + "CN=ddddd,O=client,L=test,C=de" + ); + + writeCertificates(newCaCertificate, wrongSubjectAccessCertificate.v2(), wrongSubjectAccessCertificate.v1()); + + final var e = assertThrows(CertificateException.class, sslContextHandler::reloadSslContext); + assertThat( + e.getMessage(), + is( + "New certificates do not have valid Issuer DNs. " + + "Current Issuer DNs: [CN=some_access,OU=client,O=client,L=test,C=de] " + + "new Issuer DNs: [CN=ddddd,O=client,L=test,C=de]" + ) + ); + } + + @Test + public void filesIfHasNotValidSans() throws Exception { + final var sslContextHandler = sslContextHandler(); + + final var keyPair = certificatesRule.generateKeyPair(); + final var newCaCertificate = certificatesRule.generateCaCertificate(keyPair); + final var wrongSubjectAccessCertificate = certificatesRule.generateAccessCertificate( + keyPair, + List.of(new GeneralName(GeneralName.iPAddress, "127.0.0.3")) + ); + + writeCertificates(newCaCertificate, wrongSubjectAccessCertificate.v2(), wrongSubjectAccessCertificate.v1()); + + final var e = assertThrows(CertificateException.class, sslContextHandler::reloadSslContext); + assertThat( + e.getMessage(), + is( + "New certificates do not have valid SANs. " + + "Current SANs: [[[2, localhost], [7, 127.0.0.1], [8, 1.2.3.4.5.5]]] " + + "new SANs: [[[7, 127.0.0.3]]]" + ) + ); + } + + @Test + public void reloadSslContext() throws Exception { + final var sslContextHandler = sslContextHandler(); + + final var sslContextBefore = sslContextHandler.sslContext(); + + final var keyPair = certificatesRule.generateKeyPair(); + final var newCaCertificate = certificatesRule.generateCaCertificate(keyPair); + final var currentAccessCertificate = certificatesRule.x509AccessCertificate(); + final var newAccessCertificate = certificatesRule.generateAccessCertificate( + keyPair, + currentAccessCertificate.getNotBefore().toInstant(), + currentAccessCertificate.getNotAfter().toInstant().plus(10, ChronoUnit.MINUTES) + ); + + writeCertificates(newCaCertificate, newAccessCertificate.v2(), newAccessCertificate.v1()); + + sslContextHandler.reloadSslContext(); + + assertThat("Context reloaded", is(not(sslContextBefore.equals(sslContextHandler.sslContext())))); + } + + @Test + public void reloadSslContextForShuffledSameSans() throws Exception { + final var sslContextHandler = sslContextHandler(); + + final var sslContextBefore = sslContextHandler.sslContext(); + + final var keyPair = certificatesRule.generateKeyPair(); + final var newCaCertificate = certificatesRule.generateCaCertificate(keyPair); + final var currentAccessCertificate = certificatesRule.accessCertificateHolder(); + + // CS-SUPPRESS-SINGLE: RegexpSingleline Extension should only be used sparingly to keep implementations as generic as possible + final var newAccessCertificate = certificatesRule.generateAccessCertificate( + keyPair, + currentAccessCertificate.getNotBefore().toInstant(), + currentAccessCertificate.getNotAfter().toInstant().plus(10, ChronoUnit.MINUTES), + shuffledSans(currentAccessCertificate.getExtension(Extension.subjectAlternativeName)) + ); + // CS-ENFORCE-SINGLE + + writeCertificates(newCaCertificate, newAccessCertificate.v2(), newAccessCertificate.v1()); + + sslContextHandler.reloadSslContext(); + + assertThat("Context reloaded", is(not(sslContextBefore.equals(sslContextHandler.sslContext())))); + } + + // CS-SUPPRESS-SINGLE: RegexpSingleline Extension should only be used sparingly to keep implementations as generic as possible + List shuffledSans(Extension currentSans) { + final var san1Sequence = ASN1Sequence.getInstance(currentSans.getParsedValue().toASN1Primitive()); + + final var shuffledSans = new ArrayList(); + final var objects = san1Sequence.getObjects(); + while (objects.hasMoreElements()) { + shuffledSans.add(GeneralName.getInstance(objects.nextElement())); + } + + for (int i = 0; i < 5; i++) + Collections.shuffle(shuffledSans); + return shuffledSans; + } + // CS-ENFORCE-SINGLE + + SslContextHandler sslContextHandler() { + final var sslParameters = SslParameters.loader(Settings.EMPTY).load(false); + final var trustStoreConfiguration = new TrustStoreConfiguration.PemTrustStoreConfiguration(caCertificatePath); + final var keyStoreConfiguration = new KeyStoreConfiguration.PemKeyStoreConfiguration( + accessCertificatePath, + accessCertificatePrivateKeyPath, + certificatesRule.privateKeyPassword().toCharArray() + ); + + SslConfiguration sslConfiguration = new SslConfiguration(sslParameters, trustStoreConfiguration, keyStoreConfiguration); + return new SslContextHandler(sslConfiguration, false); + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/SslSettingsManagerTest.java b/src/test/java/org/opensearch/security/ssl/SslSettingsManagerTest.java new file mode 100644 index 0000000000..1aa2c47eb3 --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/SslSettingsManagerTest.java @@ -0,0 +1,464 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl; + +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; + +import com.carrotsearch.randomizedtesting.RandomizedTest; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import org.opensearch.OpenSearchException; +import org.opensearch.common.settings.MockSecureSettings; +import org.opensearch.common.settings.Settings; +import org.opensearch.env.Environment; +import org.opensearch.env.TestEnvironment; +import org.opensearch.security.ssl.config.CertType; + +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.SslContext; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.opensearch.security.ssl.CertificatesUtils.privateKeyToPemObject; +import static org.opensearch.security.ssl.CertificatesUtils.writePemContent; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_CLIENTAUTH_MODE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_KEYSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_HTTP_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_SERVER_EXTENDED_PREFIX; +import static org.opensearch.security.support.ConfigConstants.SECURITY_SSL_ONLY; +import static org.junit.Assert.assertThrows; + +public class SslSettingsManagerTest extends RandomizedTest { + + @ClassRule + public static CertificatesRule certificatesRule = new CertificatesRule(); + + @BeforeClass + public static void setUp() throws Exception { + writeCertificates("ca_http_certificate.pem", "access_http_certificate.pem", "access_http_certificate_pk.pem"); + writeCertificates("ca_transport_certificate.pem", "access_transport_certificate.pem", "access_transport_certificate_pk.pem"); + } + + static void writeCertificates(final String trustedFileName, final String accessFileName, final String accessPkFileName) + throws Exception { + writePemContent(path(trustedFileName), certificatesRule.caCertificateHolder()); + writePemContent(path(accessFileName), certificatesRule.accessCertificateHolder()); + writePemContent( + path(accessPkFileName), + privateKeyToPemObject(certificatesRule.accessCertificatePrivateKey(), certificatesRule.privateKeyPassword()) + ); + } + + static Path path(final String fileName) { + return certificatesRule.configRootFolder().resolve(fileName); + } + + @Test + public void failsIfNoSslSet() throws Exception { + final var settings = defaultSettingsBuilder().build(); + assertThrows(OpenSearchException.class, () -> new SslSettingsManager(TestEnvironment.newEnvironment(settings))); + } + + @Test + public void transportFailsIfNoConfigDefine() throws Exception { + final var noTransportSettings = defaultSettingsBuilder().put(SECURITY_SSL_HTTP_ENABLED, true).build(); + assertThrows(OpenSearchException.class, () -> new SslSettingsManager(TestEnvironment.newEnvironment(noTransportSettings))); + } + + @Test + public void transportFailsIfConfigEnabledButNotDefined() throws Exception { + final var noTransportSettingsButItEnabled = defaultSettingsBuilder().put(SECURITY_SSL_TRANSPORT_ENABLED, true).build(); + assertThrows( + OpenSearchException.class, + () -> new SslSettingsManager(TestEnvironment.newEnvironment(noTransportSettingsButItEnabled)) + ); + } + + @Test + public void transportFailsIfJdkTrustStoreHasNotBeenSet() throws Exception { + final var noTransportSettingsButItEnabled = defaultSettingsBuilder().put(SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, certificatesRule.configRootFolder().toString()) + .build(); + assertThrows( + OpenSearchException.class, + () -> new SslSettingsManager(TestEnvironment.newEnvironment(noTransportSettingsButItEnabled)) + ); + } + + @Test + public void transportFailsIfExtendedKeyUsageEnabledForJdkKeyStoreButNotConfigured() throws Exception { + final var noTransportSettingsButItEnabled = defaultSettingsBuilder().put(SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, certificatesRule.configRootFolder().toString()) + .put(SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, certificatesRule.configRootFolder().toString()) + .put(SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED, true) + .build(); + assertThrows( + OpenSearchException.class, + () -> new SslSettingsManager(TestEnvironment.newEnvironment(noTransportSettingsButItEnabled)) + ); + } + + @Test + public void transportFailsIfExtendedKeyUsageEnabledForPemKeyStoreButNotConfigured() throws Exception { + final var noTransportSettingsButItEnabled = defaultSettingsBuilder().put(SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH, certificatesRule.configRootFolder().toString()) + .put(SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH, certificatesRule.configRootFolder().toString()) + .put(SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED, true) + .build(); + assertThrows( + OpenSearchException.class, + () -> new SslSettingsManager(TestEnvironment.newEnvironment(noTransportSettingsButItEnabled)) + ); + } + + @Test + public void transportFailsIfConfigDisabled() throws Exception { + Settings settings = defaultSettingsBuilder().put(SECURITY_SSL_HTTP_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_ENABLED, false) + .build(); + assertThrows(OpenSearchException.class, () -> new SslSettingsManager(TestEnvironment.newEnvironment(settings))); + } + + @Test + public void httpConfigFailsIfBothPemAndJDKSettingsWereSet() throws Exception { + final var keyStoreSettings = randomFrom(List.of(SECURITY_SSL_HTTP_KEYSTORE_FILEPATH)); + final var pemKeyStoreSettings = randomFrom( + List.of(SECURITY_SSL_HTTP_PEMKEY_FILEPATH, SECURITY_SSL_HTTP_PEMCERT_FILEPATH, SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH) + ); + final var settings = defaultSettingsBuilder().put(SECURITY_SSL_HTTP_ENABLED, true) + .put(keyStoreSettings, "aaa") + .put(pemKeyStoreSettings, "bbb") + .build(); + assertThrows(OpenSearchException.class, () -> new SslSettingsManager(TestEnvironment.newEnvironment(settings))); + } + + @Test + public void httpConfigFailsIfHttpEnabledButButNotDefined() throws Exception { + final var settings = defaultSettingsBuilder().put(SECURITY_SSL_HTTP_ENABLED, true).build(); + assertThrows(OpenSearchException.class, () -> new SslSettingsManager(TestEnvironment.newEnvironment(settings))); + } + + @Test + public void httpConfigFailsIfClientAuthRequiredAndJdkTrustStoreNotSet() throws Exception { + final var settings = defaultSettingsBuilder().put(SECURITY_SSL_HTTP_ENABLED, true) + .put(SECURITY_SSL_HTTP_CLIENTAUTH_MODE, ClientAuth.REQUIRE.name().toLowerCase(Locale.ROOT)) + .put(SECURITY_SSL_HTTP_KEYSTORE_FILEPATH, certificatesRule.configRootFolder().toString()) + .build(); + assertThrows(OpenSearchException.class, () -> new SslSettingsManager(TestEnvironment.newEnvironment(settings))); + } + + @Test + public void httpConfigFailsIfClientAuthRequiredAndPemTrustedCasNotSet() throws Exception { + final var settings = defaultSettingsBuilder().put(SECURITY_SSL_HTTP_ENABLED, true) + .put(SECURITY_SSL_HTTP_CLIENTAUTH_MODE, ClientAuth.REQUIRE.name().toLowerCase(Locale.ROOT)) + .put(SECURITY_SSL_HTTP_PEMKEY_FILEPATH, "aaa") + .put(SECURITY_SSL_HTTP_PEMCERT_FILEPATH, "bbb") + .build(); + assertThrows(OpenSearchException.class, () -> new SslSettingsManager(TestEnvironment.newEnvironment(settings))); + } + + @Test + public void loadConfigurationAndBuildHSslContextForSslOnlyMode() throws Exception { + final var securitySettings = new MockSecureSettings(); + securitySettings.setString(SSL_TRANSPORT_PREFIX + "pemkey_password_secure", certificatesRule.privateKeyPassword()); + securitySettings.setString(SSL_HTTP_PREFIX + "pemkey_password_secure", certificatesRule.privateKeyPassword()); + final var settingsBuilder = defaultSettingsBuilder().setSecureSettings(securitySettings); + withTransportSslSettings( + settingsBuilder, + "ca_transport_certificate.pem", + "access_transport_certificate.pem", + "access_transport_certificate_pk.pem" + ); + withHttpSslSettings(settingsBuilder); + final var transportEnabled = randomBoolean(); + final var sslSettingsManager = new SslSettingsManager( + TestEnvironment.newEnvironment( + settingsBuilder.put(SECURITY_SSL_TRANSPORT_ENABLED, transportEnabled).put(SECURITY_SSL_ONLY, true).build() + ) + ); + + assertThat("Loaded HTTP configuration", sslSettingsManager.sslConfiguration(CertType.HTTP).isPresent()); + if (transportEnabled) { + assertThat("Loaded Transport configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT).isPresent()); + assertThat("Loaded Transport Client configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT).isPresent()); + } else { + assertThat("Didn't load Transport configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT).isEmpty()); + assertThat( + "Didn't load Transport Client configuration", + sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT).isEmpty() + ); + } + + assertThat("Built HTTP SSL Context", sslSettingsManager.sslContextHandler(CertType.HTTP).isPresent()); + if (transportEnabled) { + assertThat("Built Transport SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT).isPresent()); + assertThat("Built Client SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT).isPresent()); + } else { + assertThat("Didn't build Transport SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT).isEmpty()); + assertThat("Didn't build Client SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT).isEmpty()); + } + + assertThat( + "Built Server SSL context for HTTP", + sslSettingsManager.sslContextHandler(CertType.HTTP).map(SslContextHandler::sslContext).map(SslContext::isServer).orElse(false) + ); + } + + @Test + public void loadConfigurationAndBuildSslContextForClientNode() throws Exception { + final var securitySettings = new MockSecureSettings(); + securitySettings.setString(SSL_TRANSPORT_PREFIX + "pemkey_password_secure", certificatesRule.privateKeyPassword()); + securitySettings.setString(SSL_HTTP_PREFIX + "pemkey_password_secure", certificatesRule.privateKeyPassword()); + final var settingsBuilder = defaultSettingsBuilder().setSecureSettings(securitySettings); + withTransportSslSettings( + settingsBuilder, + "ca_transport_certificate.pem", + "access_transport_certificate.pem", + "access_transport_certificate_pk.pem" + ); + withHttpSslSettings(settingsBuilder); + final var sslSettingsManager = new SslSettingsManager( + TestEnvironment.newEnvironment( + settingsBuilder.put("client.type", "client").put(SECURITY_SSL_HTTP_ENABLED, randomBoolean()).build() + ) + ); + + assertThat("Didn't load HTTP configuration", sslSettingsManager.sslConfiguration(CertType.HTTP).isEmpty()); + assertThat("Loaded Transport configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT).isPresent()); + assertThat("Loaded Transport Client configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT).isPresent()); + + assertThat("Didn't build HTTP SSL Context", sslSettingsManager.sslContextHandler(CertType.HTTP).isEmpty()); + assertThat("Built Transport SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT).isPresent()); + assertThat("Built Client SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT).isPresent()); + + assertThat( + "Built Server SSL context for Transport", + sslSettingsManager.sslContextHandler(CertType.TRANSPORT) + .map(SslContextHandler::sslContext) + .map(SslContext::isServer) + .orElse(false) + ); + assertThat( + "Built Client SSL context for Transport Client", + sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT) + .map(SslContextHandler::sslContext) + .map(SslContext::isClient) + .orElse(false) + + ); + } + + @Test + public void loadConfigurationAndBuildSslContexts() throws Exception { + final var securitySettings = new MockSecureSettings(); + securitySettings.setString(SSL_TRANSPORT_PREFIX + "pemkey_password_secure", certificatesRule.privateKeyPassword()); + securitySettings.setString(SSL_HTTP_PREFIX + "pemkey_password_secure", certificatesRule.privateKeyPassword()); + final var settingsBuilder = defaultSettingsBuilder().setSecureSettings(securitySettings); + withTransportSslSettings( + settingsBuilder, + "ca_transport_certificate.pem", + "access_transport_certificate.pem", + "access_transport_certificate_pk.pem" + ); + withHttpSslSettings(settingsBuilder); + final var sslSettingsManager = new SslSettingsManager(TestEnvironment.newEnvironment(settingsBuilder.build())); + assertThat("Loaded HTTP configuration", sslSettingsManager.sslConfiguration(CertType.HTTP).isPresent()); + assertThat("Loaded Transport configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT).isPresent()); + assertThat("Loaded Transport Client configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT).isPresent()); + + assertThat("Built HTTP SSL Context", sslSettingsManager.sslContextHandler(CertType.HTTP).isPresent()); + assertThat("Built Transport SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT).isPresent()); + assertThat("Built Transport Client SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT).isPresent()); + + assertThat( + "Built Server SSL context for HTTP", + sslSettingsManager.sslContextHandler(CertType.HTTP).map(SslContextHandler::sslContext).map(SslContext::isServer).orElse(false) + ); + assertThat( + "Built Server SSL context for Transport", + sslSettingsManager.sslContextHandler(CertType.TRANSPORT) + .map(SslContextHandler::sslContext) + .map(SslContext::isServer) + .orElse(false) + ); + assertThat( + "Built Client SSL context for Transport Client", + sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT) + .map(SslContextHandler::sslContext) + .map(SslContext::isClient) + .orElse(false) + + ); + } + + @Test + public void loadConfigurationAndBuildTransportSslContext() throws Exception { + final var securitySettings = new MockSecureSettings(); + securitySettings.setString(SSL_TRANSPORT_PREFIX + "pemkey_password_secure", certificatesRule.privateKeyPassword()); + final var settingsBuilder = defaultSettingsBuilder().setSecureSettings(securitySettings); + withTransportSslSettings( + settingsBuilder, + "ca_transport_certificate.pem", + "access_transport_certificate.pem", + "access_transport_certificate_pk.pem" + ); + final var sslSettingsManager = new SslSettingsManager(TestEnvironment.newEnvironment(settingsBuilder.build())); + + assertThat("Didn't load HTTP configuration", sslSettingsManager.sslConfiguration(CertType.HTTP).isEmpty()); + assertThat("Loaded Transport configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT).isPresent()); + assertThat("Loaded Transport Client configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT).isPresent()); + assertThat( + "SSL configuration for Transport and Transport Client is the same", + sslSettingsManager.sslConfiguration(CertType.TRANSPORT) + .flatMap(t -> sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT).map(tc -> tc.equals(t))) + .orElse(false) + ); + + assertThat("Built HTTP SSL Context", sslSettingsManager.sslContextHandler(CertType.HTTP).isEmpty()); + assertThat("Built Transport SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT).isPresent()); + assertThat("Built Transport Client SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT).isPresent()); + + assertThat( + "Built Server SSL context for Transport", + sslSettingsManager.sslContextHandler(CertType.TRANSPORT) + .map(SslContextHandler::sslContext) + .map(SslContext::isServer) + .orElse(false) + + ); + assertThat( + "Built Client SSL context for Transport Client", + sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT) + .map(SslContextHandler::sslContext) + .map(SslContext::isClient) + .orElse(false) + + ); + } + + @Test + public void loadConfigurationAndBuildExtendedTransportSslContexts() throws Exception { + writeCertificates( + "ca_server_transport_certificate.pem", + "access_server_transport_certificate.pem", + "access_server_transport_certificate_pk.pem" + ); + writeCertificates( + "ca_client_transport_certificate.pem", + "access_client_transport_certificate.pem", + "access_client_transport_certificate_pk.pem" + ); + + final var securitySettings = new MockSecureSettings(); + securitySettings.setString( + SSL_TRANSPORT_PREFIX + SSL_TRANSPORT_SERVER_EXTENDED_PREFIX + "pemkey_password_secure", + certificatesRule.privateKeyPassword() + ); + securitySettings.setString( + SSL_TRANSPORT_PREFIX + SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX + "pemkey_password_secure", + certificatesRule.privateKeyPassword() + ); + final var sslSettingsManager = new SslSettingsManager( + TestEnvironment.newEnvironment( + defaultSettingsBuilder().put(SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_SERVER_PEMTRUSTEDCAS_FILEPATH, path("ca_server_transport_certificate.pem")) + .put(SECURITY_SSL_TRANSPORT_SERVER_PEMCERT_FILEPATH, path("access_server_transport_certificate.pem")) + .put(SECURITY_SSL_TRANSPORT_SERVER_PEMKEY_FILEPATH, path("access_server_transport_certificate_pk.pem")) + .put(SECURITY_SSL_TRANSPORT_CLIENT_PEMTRUSTEDCAS_FILEPATH, path("ca_client_transport_certificate.pem")) + .put(SECURITY_SSL_TRANSPORT_CLIENT_PEMCERT_FILEPATH, path("access_client_transport_certificate.pem")) + .put(SECURITY_SSL_TRANSPORT_CLIENT_PEMKEY_FILEPATH, path("access_client_transport_certificate_pk.pem")) + .setSecureSettings(securitySettings) + .build() + ) + ); + + assertThat("Didn't load HTTP configuration", sslSettingsManager.sslConfiguration(CertType.HTTP).isEmpty()); + assertThat("Loaded Transport configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT).isPresent()); + assertThat("Loaded Transport Client configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT).isPresent()); + assertThat( + "SSL configuration for Transport and Transport Client is not the same", + sslSettingsManager.sslConfiguration(CertType.TRANSPORT) + .flatMap(t -> sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT).map(tc -> !tc.equals(t))) + .orElse(true) + ); + assertThat("Built HTTP SSL Context", sslSettingsManager.sslContextHandler(CertType.HTTP).isEmpty()); + assertThat("Built Transport SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT).isPresent()); + assertThat("Built Transport Client SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT).isPresent()); + + assertThat( + "Built Server SSL context for Transport", + sslSettingsManager.sslContextHandler(CertType.TRANSPORT) + .map(SslContextHandler::sslContext) + .map(SslContext::isServer) + .orElse(false) + + ); + assertThat( + "Built Client SSL context for Transport Client", + sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT) + .map(SslContextHandler::sslContext) + .map(SslContext::isClient) + .orElse(false) + + ); + } + + private void withTransportSslSettings( + final Settings.Builder settingsBuilder, + final String caFileName, + final String accessFileName, + final String accessPkFileName + ) { + settingsBuilder.put(SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH, path(caFileName)) + .put(SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH, path(accessFileName)) + .put(SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH, path(accessPkFileName)); + } + + private void withHttpSslSettings(final Settings.Builder settingsBuilder) { + settingsBuilder.put(SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SECURITY_SSL_HTTP_ENABLED, true) + .put(SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH, path("ca_http_certificate.pem")) + .put(SECURITY_SSL_HTTP_PEMCERT_FILEPATH, path("access_http_certificate.pem")) + .put(SECURITY_SSL_HTTP_PEMKEY_FILEPATH, path("access_http_certificate_pk.pem")); + } + + Settings.Builder defaultSettingsBuilder() { + return Settings.builder() + .put(Environment.PATH_HOME_SETTING.getKey(), certificatesRule.configRootFolder().toString()) + .put("client.type", "node"); + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/config/CertificateTest.java b/src/test/java/org/opensearch/security/ssl/config/CertificateTest.java new file mode 100644 index 0000000000..5fe2185d44 --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/config/CertificateTest.java @@ -0,0 +1,38 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl.config; + +import java.lang.reflect.Method; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.fail; + +public class CertificateTest { + + @Test + public void testGetObjectMethod() { + try { + final Method method = Certificate.getObjectMethod(); + assertThat("Method should not be null", method, notNullValue()); + assertThat( + "One of the expected methods should be available", + method.getName().equals("getBaseObject") || method.getName().equals("getObject") + ); + } catch (ClassNotFoundException | NoSuchMethodException e) { + fail("Exception should not be thrown: " + e.getMessage()); + } + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/config/JdkSslCertificatesLoaderTest.java b/src/test/java/org/opensearch/security/ssl/config/JdkSslCertificatesLoaderTest.java new file mode 100644 index 0000000000..174f6c0fd5 --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/config/JdkSslCertificatesLoaderTest.java @@ -0,0 +1,318 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl.config; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.junit.Test; + +import org.opensearch.common.collect.Tuple; +import org.opensearch.common.settings.MockSecureSettings; +import org.opensearch.env.TestEnvironment; + +import static java.util.Objects.isNull; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.opensearch.security.ssl.util.SSLConfigConstants.DEFAULT_STORE_PASSWORD; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_TYPE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_KEYSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_TRUSTSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_KEYSTORE_TYPE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_KEYSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_TRUSTSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_TYPE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_HTTP_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_SERVER_EXTENDED_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_TYPE; + +public class JdkSslCertificatesLoaderTest extends SslCertificatesLoaderTest { + + static final Function resolveKeyStoreType = s -> isNull(s) ? KeyStore.getDefaultType() : s; + + static final String SERVER_TRUSTSTORE_ALIAS = "server-truststore-alias"; + + static final String SERVER_KEYSTORE_ALIAS = "server-keystore-alias"; + + static final String CLIENT_TRUSTSTORE_ALIAS = "client-truststore-alias"; + + static final String CLIENT_KEYSTORE_ALIAS = "client-keystore-alias"; + + @Test + public void loadHttpSslConfigurationFromKeyAndTrustStoreFiles() throws Exception { + testJdkBasedSslConfiguration(SSL_HTTP_PREFIX, randomBoolean()); + } + + @Test + public void loadTransportJdkBasedSslConfiguration() throws Exception { + testJdkBasedSslConfiguration(SSL_TRANSPORT_PREFIX, true); + } + + @Test + public void loadTransportJdkBasedSslExtendedConfiguration() throws Exception { + final var clientKeyPair = certificatesRule.generateKeyPair(); + + final var serverCaCertificate = certificatesRule.x509CaCertificate(); + final var clientCaCertificate = certificatesRule.toX509Certificate(certificatesRule.generateCaCertificate(clientKeyPair)); + + final var serverAccessCertificateKey = certificatesRule.accessCertificatePrivateKey(); + final var serverAccessCertificate = certificatesRule.x509AccessCertificate(); + + final var clientAccessCertificateAndKey = certificatesRule.generateAccessCertificate(clientKeyPair); + + final var clientAccessCertificateKey = clientAccessCertificateAndKey.v1(); + final var clientAccessCertificate = certificatesRule.toX509Certificate(clientAccessCertificateAndKey.v2()); + + final var trustStoreType = randomKeyStoreType(); + final var keyStoreType = randomKeyStoreType(); + + final var useSecurePassword = randomBoolean(); + final var trustStorePassword = randomKeyStorePassword(useSecurePassword); + final var keyStorePassword = randomKeyStorePassword(useSecurePassword); + + final var trustStorePath = createTrustStore( + trustStoreType, + trustStorePassword, + Map.of(SERVER_TRUSTSTORE_ALIAS, serverCaCertificate, CLIENT_TRUSTSTORE_ALIAS, clientCaCertificate) + ); + final var keyStorePath = createKeyStore( + keyStoreType, + keyStorePassword, + Map.of( + SERVER_KEYSTORE_ALIAS, + Tuple.tuple(serverAccessCertificateKey, serverAccessCertificate), + CLIENT_KEYSTORE_ALIAS, + Tuple.tuple(clientAccessCertificateKey, clientAccessCertificate) + ) + ); + + final var settingsBuilder = defaultSettingsBuilder().put(SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_TRUSTSTORE_TYPE, trustStoreType) + .put(SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, trustStorePath) + .put(SECURITY_SSL_TRANSPORT_SERVER_TRUSTSTORE_ALIAS, SERVER_TRUSTSTORE_ALIAS) + .put(SECURITY_SSL_TRANSPORT_CLIENT_TRUSTSTORE_ALIAS, CLIENT_TRUSTSTORE_ALIAS) + .put(SECURITY_SSL_TRANSPORT_KEYSTORE_TYPE, keyStoreType) + .put(SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, keyStorePath) + .put(SECURITY_SSL_TRANSPORT_SERVER_KEYSTORE_ALIAS, SERVER_KEYSTORE_ALIAS) + .put(SECURITY_SSL_TRANSPORT_CLIENT_KEYSTORE_ALIAS, CLIENT_KEYSTORE_ALIAS); + + if (useSecurePassword) { + final var securitySettings = new MockSecureSettings(); + securitySettings.setString(SSL_TRANSPORT_PREFIX + "keystore_password_secure", keyStorePassword); + securitySettings.setString(SSL_TRANSPORT_PREFIX + "truststore_password_secure", trustStorePassword); + + securitySettings.setString( + SSL_TRANSPORT_PREFIX + SSL_TRANSPORT_SERVER_EXTENDED_PREFIX + "keystore_keypassword_secure", + certificatesRule.privateKeyPassword() + ); + securitySettings.setString( + SSL_TRANSPORT_PREFIX + SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX + "keystore_keypassword_secure", + certificatesRule.privateKeyPassword() + ); + settingsBuilder.setSecureSettings(securitySettings); + } else { + settingsBuilder.put(SSL_TRANSPORT_PREFIX + "keystore_password", keyStorePassword); + settingsBuilder.put(SSL_TRANSPORT_PREFIX + "truststore_password", trustStorePassword); + + settingsBuilder.put( + SSL_TRANSPORT_PREFIX + SSL_TRANSPORT_SERVER_EXTENDED_PREFIX + "keystore_keypassword", + certificatesRule.privateKeyPassword() + ); + settingsBuilder.put( + SSL_TRANSPORT_PREFIX + SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX + "keystore_keypassword", + certificatesRule.privateKeyPassword() + ); + } + final var settings = settingsBuilder.build(); + + final var serverConfiguration = new SslCertificatesLoader(SSL_TRANSPORT_PREFIX, SSL_TRANSPORT_SERVER_EXTENDED_PREFIX) + .loadConfiguration(TestEnvironment.newEnvironment(settings)); + assertTrustStoreConfiguration( + serverConfiguration.v1(), + trustStorePath, + new Certificate(serverCaCertificate, resolveKeyStoreType.apply(trustStoreType), SERVER_TRUSTSTORE_ALIAS, false) + ); + assertKeyStoreConfiguration( + serverConfiguration.v2(), + List.of(keyStorePath), + new Certificate(serverAccessCertificate, resolveKeyStoreType.apply(keyStoreType), SERVER_KEYSTORE_ALIAS, true) + ); + + final var clientConfiguration = new SslCertificatesLoader(SSL_TRANSPORT_PREFIX, SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX) + .loadConfiguration(TestEnvironment.newEnvironment(settings)); + assertTrustStoreConfiguration( + clientConfiguration.v1(), + trustStorePath, + new Certificate(clientCaCertificate, resolveKeyStoreType.apply(trustStoreType), CLIENT_TRUSTSTORE_ALIAS, false) + ); + assertKeyStoreConfiguration( + clientConfiguration.v2(), + List.of(keyStorePath), + new Certificate(clientAccessCertificate, resolveKeyStoreType.apply(keyStoreType), CLIENT_KEYSTORE_ALIAS, true) + ); + } + + private void testJdkBasedSslConfiguration(final String sslConfigPrefix, final boolean useAuthorityCertificate) throws Exception { + final var useSecurePassword = randomBoolean(); + + final var keyPair = certificatesRule.generateKeyPair(); + final var trustStoreCertificates = Map.of( + "default-truststore-alias", + certificatesRule.x509CaCertificate(), + "another-truststore-alias", + certificatesRule.toX509Certificate(certificatesRule.generateCaCertificate(keyPair)) + ); + + final var keysAndCertificate = certificatesRule.generateAccessCertificate(keyPair); + final var keyStoreCertificates = Map.of( + "default-keystore-alias", + Tuple.tuple(certificatesRule.accessCertificatePrivateKey(), certificatesRule.x509AccessCertificate()), + "another-keystore-alias", + Tuple.tuple(keysAndCertificate.v1(), certificatesRule.toX509Certificate(keysAndCertificate.v2())) + ); + + final var trustStoreAlias = randomFrom(new String[] { "default-truststore-alias", "another-truststore-alias", null }); + final var keyStoreAlias = (String) null;// randomFrom(new String[] { "default-keystore-alias", "another-keystore-alias", null }); + + final var keyStorePassword = randomKeyStorePassword(useSecurePassword); + final var trustStorePassword = randomKeyStorePassword(useSecurePassword); + + final var keyStoreType = randomKeyStoreType(); + final var keyStorePath = createKeyStore(keyStoreType, keyStorePassword, keyStoreCertificates); + + final var trustStoreType = randomKeyStoreType(); + final var trustStorePath = createTrustStore(trustStoreType, trustStorePassword, trustStoreCertificates); + + final var settingsBuilder = defaultSettingsBuilder().put(sslConfigPrefix + ENABLED, true) + .put(sslConfigPrefix + KEYSTORE_FILEPATH, keyStorePath) + .put(sslConfigPrefix + KEYSTORE_ALIAS, keyStoreAlias) + .put(sslConfigPrefix + KEYSTORE_TYPE, keyStoreType); + if (useAuthorityCertificate) { + settingsBuilder.put(sslConfigPrefix + TRUSTSTORE_FILEPATH, trustStorePath) + .put(sslConfigPrefix + TRUSTSTORE_ALIAS, trustStoreAlias) + .put(sslConfigPrefix + TRUSTSTORE_TYPE, trustStoreType); + } + if (useSecurePassword) { + final var securitySettings = new MockSecureSettings(); + securitySettings.setString(sslConfigPrefix + "keystore_password_secure", keyStorePassword); + securitySettings.setString(sslConfigPrefix + "keystore_keypassword_secure", certificatesRule.privateKeyPassword()); + if (useAuthorityCertificate) { + securitySettings.setString(sslConfigPrefix + "truststore_password_secure", trustStorePassword); + } + settingsBuilder.setSecureSettings(securitySettings); + } else { + settingsBuilder.put(sslConfigPrefix + "keystore_password", keyStorePassword); + settingsBuilder.put(sslConfigPrefix + "keystore_keypassword", certificatesRule.privateKeyPassword()); + if (useAuthorityCertificate) { + settingsBuilder.put(sslConfigPrefix + "truststore_password", trustStorePassword); + } + } + + final var configuration = new SslCertificatesLoader(sslConfigPrefix).loadConfiguration( + TestEnvironment.newEnvironment(settingsBuilder.build()) + ); + + if (useAuthorityCertificate) { + final var expectedTrustStoreCertificates = isNull(trustStoreAlias) + ? trustStoreCertificates.entrySet() + .stream() + .map(e -> new Certificate(e.getValue(), resolveKeyStoreType.apply(trustStoreType), e.getKey(), false)) + .toArray(Certificate[]::new) + : trustStoreCertificates.entrySet() + .stream() + .filter(e -> e.getKey().equals(trustStoreAlias)) + .map(e -> new Certificate(e.getValue(), resolveKeyStoreType.apply(trustStoreType), e.getKey(), false)) + .toArray(Certificate[]::new); + assertTrustStoreConfiguration(configuration.v1(), trustStorePath, expectedTrustStoreCertificates); + } else { + assertThat(configuration.v1(), is(TrustStoreConfiguration.EMPTY_CONFIGURATION)); + } + + final var expectedKeyStoreCertificates = isNull(keyStoreAlias) + ? keyStoreCertificates.entrySet() + .stream() + .map(e -> new Certificate(e.getValue().v2(), resolveKeyStoreType.apply(keyStoreType), e.getKey(), true)) + .toArray(Certificate[]::new) + : keyStoreCertificates.entrySet() + .stream() + .filter(e -> e.getKey().equals(keyStoreAlias)) + .map(e -> new Certificate(e.getValue().v2(), resolveKeyStoreType.apply(keyStoreType), e.getKey(), true)) + .toArray(Certificate[]::new); + assertKeyStoreConfiguration(configuration.v2(), List.of(keyStorePath), expectedKeyStoreCertificates); + } + + String randomKeyStoreType() { + return randomFrom(new String[] { "jks", "pkcs12", null }); + } + + String randomKeyStorePassword(final boolean useSecurePassword) { + return useSecurePassword ? randomAsciiAlphanumOfLength(10) : randomFrom(new String[] { randomAsciiAlphanumOfLength(10), null }); + } + + Path createTrustStore(final String type, final String password, Map certificates) throws Exception { + final var keyStore = keyStore(type); + for (final var alias : certificates.keySet()) { + keyStore.setCertificateEntry(alias, certificates.get(alias)); + } + final var trustStorePath = path(String.format("truststore.%s", isNull(type) ? "jsk" : type)); + storeKeyStore(keyStore, trustStorePath, password); + return trustStorePath; + } + + Path createKeyStore(final String type, final String password, final Map> keysAndCertificates) + throws Exception { + final var keyStore = keyStore(type); + final var keyStorePath = path(String.format("keystore.%s", isNull(type) ? "jsk" : type)); + for (final var alias : keysAndCertificates.keySet()) { + final var keyAndCertificate = keysAndCertificates.get(alias); + keyStore.setKeyEntry( + alias, + keyAndCertificate.v1(), + certificatesRule.privateKeyPassword().toCharArray(), + new X509Certificate[] { keyAndCertificate.v2() } + ); + } + storeKeyStore(keyStore, keyStorePath, password); + return keyStorePath; + } + + KeyStore keyStore(final String type) throws Exception { + final var keyStore = KeyStore.getInstance(isNull(type) ? KeyStore.getDefaultType() : type); + keyStore.load(null, null); + return keyStore; + } + + void storeKeyStore(final KeyStore keyStore, final Path path, final String password) throws Exception { + try (final var out = Files.newOutputStream(path)) { + keyStore.store(out, isNull(password) ? DEFAULT_STORE_PASSWORD.toCharArray() : password.toCharArray()); + } + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/config/PemSslCertificatesLoaderTest.java b/src/test/java/org/opensearch/security/ssl/config/PemSslCertificatesLoaderTest.java new file mode 100644 index 0000000000..d03bf9c59d --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/config/PemSslCertificatesLoaderTest.java @@ -0,0 +1,174 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl.config; + +import java.security.SecureRandom; +import java.util.List; + +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.BeforeClass; +import org.junit.Test; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.openssl.PKCS8Generator; +import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder; + +import org.opensearch.common.settings.MockSecureSettings; +import org.opensearch.env.TestEnvironment; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.opensearch.security.ssl.CertificatesUtils.privateKeyToPemObject; +import static org.opensearch.security.ssl.CertificatesUtils.writePemContent; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_CERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_KEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_TRUSTED_CAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_HTTP_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_SERVER_EXTENDED_PREFIX; + +public class PemSslCertificatesLoaderTest extends SslCertificatesLoaderTest { + + final static String PEM_CA_CERTIFICATE_FILE_NAME = "ca_certificate.pem"; + + final static String PEM_KEY_CERTIFICATE_FILE_NAME = "key_certificate.pem"; + + final static String PEM_CERTIFICATE_PRIVATE_KEY_FILE_NAME = "private_key.pem"; + + @BeforeClass + public static void setup() throws Exception { + writePemContent(path(PEM_CA_CERTIFICATE_FILE_NAME), certificatesRule.caCertificateHolder()); + writePemContent(path(PEM_KEY_CERTIFICATE_FILE_NAME), certificatesRule.accessCertificateHolder()); + writePemContent( + path(PEM_CERTIFICATE_PRIVATE_KEY_FILE_NAME), + new PKCS8Generator( + PrivateKeyInfo.getInstance(certificatesRule.accessCertificatePrivateKey().getEncoded()), + new JceOpenSSLPKCS8EncryptorBuilder(PKCS8Generator.PBE_SHA1_3DES).setRandom(new SecureRandom()) + .setPassword(certificatesRule.privateKeyPassword().toCharArray()) + .build() + ).generate() + ); + } + + @Test + public void loadHttpSslConfigurationFromPemFiles() throws Exception { + testLoadPemBasedConfiguration(SSL_HTTP_PREFIX, randomBoolean()); + } + + @Test + public void loadTransportSslConfigurationFromPemFiles() throws Exception { + testLoadPemBasedConfiguration(SSL_HTTP_PREFIX, false); + } + + void testLoadPemBasedConfiguration(final String sslConfigPrefix, final boolean useAuthorityCertificate) throws Exception { + final var settingsBuilder = defaultSettingsBuilder().put(sslConfigPrefix + ENABLED, true) + .put(sslConfigPrefix + PEM_CERT_FILEPATH, path(PEM_KEY_CERTIFICATE_FILE_NAME)) + .put(sslConfigPrefix + PEM_KEY_FILEPATH, path(PEM_CERTIFICATE_PRIVATE_KEY_FILE_NAME)); + if (useAuthorityCertificate) { + settingsBuilder.put(sslConfigPrefix + PEM_TRUSTED_CAS_FILEPATH, path(PEM_CA_CERTIFICATE_FILE_NAME)); + } + if (randomBoolean()) { + final var securitySettings = new MockSecureSettings(); + securitySettings.setString(sslConfigPrefix + "pemkey_password_secure", certificatesRule.privateKeyPassword()); + settingsBuilder.setSecureSettings(securitySettings); + } else { + settingsBuilder.put(sslConfigPrefix + "pemkey_password", certificatesRule.privateKeyPassword()); + } + + final var settings = settingsBuilder.build(); + final var configuration = new SslCertificatesLoader(SSL_HTTP_PREFIX).loadConfiguration(TestEnvironment.newEnvironment(settings)); + if (useAuthorityCertificate) { + assertTrustStoreConfiguration( + configuration.v1(), + path(PEM_CA_CERTIFICATE_FILE_NAME), + new Certificate(certificatesRule.x509CaCertificate(), false) + ); + } else { + assertThat(configuration.v1(), is(TrustStoreConfiguration.EMPTY_CONFIGURATION)); + } + assertKeyStoreConfiguration( + configuration.v2(), + List.of(path(PEM_KEY_CERTIFICATE_FILE_NAME), path(PEM_CERTIFICATE_PRIVATE_KEY_FILE_NAME)), + new Certificate(certificatesRule.x509AccessCertificate(), true) + ); + } + + @Test + public void loadExtendedTransportSslConfigurationFromPemFiles() throws Exception { + final var keyPair = certificatesRule.generateKeyPair(); + final var clientCaCertificate = certificatesRule.generateCaCertificate(keyPair); + final var keyAndCertificate = certificatesRule.generateAccessCertificate(keyPair); + final var clientCaCertificatePath = "client_ca_certificate.pem"; + final var clientKeyCertificatePath = "client_key_certificate.pem"; + final var clientPrivateKeyCertificatePath = "client_private_key_certificate.pem"; + final var clientPrivateKeyPassword = RandomStringUtils.randomAlphabetic(10); + + writePemContent(path(clientCaCertificatePath), clientCaCertificate); + writePemContent(path(clientKeyCertificatePath), keyAndCertificate.v2()); + writePemContent(path(clientPrivateKeyCertificatePath), privateKeyToPemObject(keyAndCertificate.v1(), clientPrivateKeyPassword)); + + final var settingsBuilder = defaultSettingsBuilder().put(SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_SERVER_PEMTRUSTEDCAS_FILEPATH, path(PEM_CA_CERTIFICATE_FILE_NAME)) + .put(SECURITY_SSL_TRANSPORT_SERVER_PEMCERT_FILEPATH, path(PEM_KEY_CERTIFICATE_FILE_NAME)) + .put(SECURITY_SSL_TRANSPORT_SERVER_PEMKEY_FILEPATH, path(PEM_CERTIFICATE_PRIVATE_KEY_FILE_NAME)) + + .put(SECURITY_SSL_TRANSPORT_CLIENT_PEMTRUSTEDCAS_FILEPATH, path(clientCaCertificatePath)) + .put(SECURITY_SSL_TRANSPORT_CLIENT_PEMCERT_FILEPATH, path(clientKeyCertificatePath)) + .put(SECURITY_SSL_TRANSPORT_CLIENT_PEMKEY_FILEPATH, path(clientPrivateKeyCertificatePath)); + if (randomBoolean()) { + final var securitySettings = new MockSecureSettings(); + securitySettings.setString(SSL_TRANSPORT_PREFIX + "server.pemkey_password_secure", certificatesRule.privateKeyPassword()); + securitySettings.setString(SSL_TRANSPORT_PREFIX + "client.pemkey_password_secure", clientPrivateKeyPassword); + settingsBuilder.setSecureSettings(securitySettings); + } else { + settingsBuilder.put(SSL_TRANSPORT_PREFIX + "server.pemkey_password", certificatesRule.privateKeyPassword()); + settingsBuilder.put(SSL_TRANSPORT_PREFIX + "client.pemkey_password", clientPrivateKeyPassword); + } + final var settings = settingsBuilder.build(); + + final var transportServerConfiguration = new SslCertificatesLoader(SSL_TRANSPORT_PREFIX, SSL_TRANSPORT_SERVER_EXTENDED_PREFIX) + .loadConfiguration(TestEnvironment.newEnvironment(settings)); + assertTrustStoreConfiguration( + transportServerConfiguration.v1(), + path(PEM_CA_CERTIFICATE_FILE_NAME), + new Certificate(certificatesRule.x509CaCertificate(), false) + ); + assertKeyStoreConfiguration( + transportServerConfiguration.v2(), + List.of(path(PEM_KEY_CERTIFICATE_FILE_NAME), path(PEM_CERTIFICATE_PRIVATE_KEY_FILE_NAME)), + new Certificate(certificatesRule.x509AccessCertificate(), true) + ); + final var transportClientConfiguration = new SslCertificatesLoader(SSL_TRANSPORT_PREFIX, SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX) + .loadConfiguration(TestEnvironment.newEnvironment(settings)); + assertTrustStoreConfiguration( + transportClientConfiguration.v1(), + path(clientCaCertificatePath), + new Certificate(certificatesRule.toX509Certificate(clientCaCertificate), false) + ); + assertKeyStoreConfiguration( + transportClientConfiguration.v2(), + List.of(path(clientKeyCertificatePath), path(clientPrivateKeyCertificatePath)), + new Certificate(certificatesRule.toX509Certificate(keyAndCertificate.v2()), true) + ); + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/config/SslCertificatesLoaderTest.java b/src/test/java/org/opensearch/security/ssl/config/SslCertificatesLoaderTest.java new file mode 100644 index 0000000000..0dfc02b386 --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/config/SslCertificatesLoaderTest.java @@ -0,0 +1,66 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl.config; + +import java.nio.file.Path; +import java.util.List; + +import com.carrotsearch.randomizedtesting.RandomizedTest; +import org.junit.ClassRule; + +import org.opensearch.common.settings.Settings; +import org.opensearch.env.Environment; +import org.opensearch.security.ssl.CertificatesRule; + +import static java.util.Objects.nonNull; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.notNullValue; + +public abstract class SslCertificatesLoaderTest extends RandomizedTest { + + @ClassRule + public static CertificatesRule certificatesRule = new CertificatesRule(); + + static Path path(final String fileName) { + return certificatesRule.configRootFolder().resolve(fileName); + } + + Settings.Builder defaultSettingsBuilder() throws Exception { + return Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), certificatesRule.caCertificateHolder().toString()); + } + + void assertTrustStoreConfiguration( + final TrustStoreConfiguration trustStoreConfiguration, + final Path expectedFile, + final Certificate... expectedCertificates + ) { + assertThat("Truststore configuration created", nonNull(trustStoreConfiguration)); + assertThat(trustStoreConfiguration.file(), is(expectedFile)); + assertThat(trustStoreConfiguration.loadCertificates(), containsInAnyOrder(expectedCertificates)); + assertThat(trustStoreConfiguration.createTrustManagerFactory(true), is(notNullValue())); + } + + void assertKeyStoreConfiguration( + final KeyStoreConfiguration keyStoreConfiguration, + final List expectedFiles, + final Certificate... expectedCertificates + ) { + assertThat("Keystore configuration created", nonNull(keyStoreConfiguration)); + assertThat(keyStoreConfiguration.files(), contains(expectedFiles.toArray(new Path[0]))); + assertThat(keyStoreConfiguration.loadCertificates(), containsInAnyOrder(expectedCertificates)); + assertThat(keyStoreConfiguration.createKeyManagerFactory(true), is(notNullValue())); + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/config/SslParametersTest.java b/src/test/java/org/opensearch/security/ssl/config/SslParametersTest.java new file mode 100644 index 0000000000..d95c336e15 --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/config/SslParametersTest.java @@ -0,0 +1,90 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl.config; + +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.net.ssl.SSLContext; + +import org.junit.Test; + +import org.opensearch.common.settings.Settings; + +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.SslProvider; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ALLOWED_SSL_CIPHERS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_CLIENTAUTH_MODE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED_CIPHERS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED_PROTOCOLS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED_CIPHERS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED_PROTOCOLS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_HTTP_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_PREFIX; + +public class SslParametersTest { + + @Test + public void testDefaultSslParameters() throws Exception { + final var settings = Settings.EMPTY; + final var httpSslParameters = SslParameters.loader(settings).load(true); + final var transportSslParameters = SslParameters.loader(settings).load(false); + + final var defaultCiphers = List.of(ALLOWED_SSL_CIPHERS); + final var finalDefaultCiphers = Stream.of(SSLContext.getDefault().getDefaultSSLParameters().getCipherSuites()) + .filter(defaultCiphers::contains) + .sorted(String::compareTo) + .collect(Collectors.toList()); + + assertThat(httpSslParameters.provider(), is(SslProvider.JDK)); + assertThat(transportSslParameters.provider(), is(SslProvider.JDK)); + + assertThat(httpSslParameters.allowedProtocols(), is(List.of("TLSv1.3", "TLSv1.2"))); + assertThat(httpSslParameters.allowedCiphers(), is(finalDefaultCiphers)); + + assertThat(transportSslParameters.allowedProtocols(), is(List.of("TLSv1.3", "TLSv1.2"))); + assertThat(transportSslParameters.allowedCiphers(), is(finalDefaultCiphers)); + + assertThat(httpSslParameters.clientAuth(), is(ClientAuth.OPTIONAL)); + assertThat(transportSslParameters.clientAuth(), is(ClientAuth.REQUIRE)); + } + + @Test + public void testCustomSSlParameters() { + final var settings = Settings.builder() + .put(SECURITY_SSL_HTTP_CLIENTAUTH_MODE, ClientAuth.REQUIRE.name().toLowerCase(Locale.ROOT)) + .putList(SECURITY_SSL_HTTP_ENABLED_PROTOCOLS, List.of("TLSv1.2", "TLSv1")) + .putList(SECURITY_SSL_HTTP_ENABLED_CIPHERS, List.of("TLS_AES_256_GCM_SHA384")) + .putList(SECURITY_SSL_TRANSPORT_ENABLED_PROTOCOLS, List.of("TLSv1.3", "TLSv1.2")) + .putList(SECURITY_SSL_TRANSPORT_ENABLED_CIPHERS, List.of("TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384")) + .build(); + final var httpSslParameters = SslParameters.loader(settings.getByPrefix(SSL_HTTP_PREFIX)).load(true); + final var transportSslParameters = SslParameters.loader(settings.getByPrefix(SSL_TRANSPORT_PREFIX)).load(false); + + assertThat(httpSslParameters.provider(), is(SslProvider.JDK)); + assertThat(transportSslParameters.provider(), is(SslProvider.JDK)); + + assertThat(httpSslParameters.allowedProtocols(), is(List.of("TLSv1.2"))); + assertThat(httpSslParameters.allowedCiphers(), is(List.of("TLS_AES_256_GCM_SHA384"))); + + assertThat(transportSslParameters.allowedProtocols(), is(List.of("TLSv1.3", "TLSv1.2"))); + assertThat(transportSslParameters.allowedCiphers(), is(List.of("TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384"))); + + assertThat(httpSslParameters.clientAuth(), is(ClientAuth.REQUIRE)); + assertThat(transportSslParameters.clientAuth(), is(ClientAuth.REQUIRE)); + } + +}