From 4ddf6186a983127c7ca93fd16107d8db4b430b41 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Thu, 11 Jan 2024 15:01:47 -0500 Subject: [PATCH] MLE-4116: Can now configure trust store For SSL and/or certificate authentication. Same approach as for a keystore, just for a truststore. Was able to reuse and improve `TwoWaySSLTest` by modifying it to use the key store as a trust store instead of using a "trust everything" trust manager. --- .../client/DatabaseClientBuilder.java | 43 +++++++++++++++ .../client/DatabaseClientFactory.java | 4 ++ .../impl/DatabaseClientPropertySource.java | 21 ++++++++ .../com/marklogic/client/impl/SSLUtil.java | 2 +- .../client/test/ssl/TwoWaySSLTest.java | 53 +++++++++++++++---- 5 files changed, 112 insertions(+), 11 deletions(-) diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientBuilder.java b/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientBuilder.java index 219a59ab6..0bf7f5561 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientBuilder.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientBuilder.java @@ -301,6 +301,49 @@ public DatabaseClientBuilder withKeyStoreAlgorithm(String algorithm) { props.put(PREFIX + "ssl.keystore.algorithm", algorithm); return this; } + + /** + * Supports constructing an {@code X509TrustManager} based on the given file path, which should point to a Java + * key store or trust store. + * + * @param path + * @return + * @since 6.5.0 + */ + public DatabaseClientBuilder withTrustStorePath(String path) { + props.put(PREFIX + "ssl.truststore.path", path); + return this; + } + + /** + * @param password optional password for a trust store + * @return + * @since 6.5.0 + */ + public DatabaseClientBuilder withTrustStorePassword(String password) { + props.put(PREFIX + "ssl.truststore.password", password); + return this; + } + + /** + * @param type e.g. "JKS" + * @return + * @since 6.5.0 + */ + public DatabaseClientBuilder withTrustStoreType(String type) { + props.put(PREFIX + "ssl.truststore.type", type); + return this; + } + + /** + * @param algorithm e.g. "SunX509" + * @return + * @since 6.5.0 + */ + public DatabaseClientBuilder withTrustStoreAlgorithm(String algorithm) { + props.put(PREFIX + "ssl.truststore.algorithm", algorithm); + return this; + } } diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientFactory.java b/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientFactory.java index 0f01216c6..d158c2580 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientFactory.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientFactory.java @@ -1303,6 +1303,10 @@ public String getCertificatePassword() { *
  • marklogic.client.ssl.keystore.password = must be a String; optional password for a key store; since 6.4.0.
  • *
  • marklogic.client.ssl.keystore.type = must be a String; optional type for a key store, defaults to "JKS"; since 6.4.0.
  • *
  • marklogic.client.ssl.keystore.algorithm = must be a String; optional algorithm for a key store, defaults to "SunX509"; since 6.4.0.
  • + *
  • marklogic.client.ssl.truststore.path = must be a String; specifies a file path for a trust store for SSL and/or certificate authentication; since 6.5.0.
  • + *
  • marklogic.client.ssl.truststore.password = must be a String; optional password for a trust store; since 6.5.0.
  • + *
  • marklogic.client.ssl.truststore.type = must be a String; optional type for a trust store, defaults to "JKS"; since 6.5.0.
  • + *
  • marklogic.client.ssl.truststore.algorithm = must be a String; optional algorithm for a trust store, defaults to "SunX509"; since 6.5.0.
  • * * * @param propertySource diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/DatabaseClientPropertySource.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/DatabaseClientPropertySource.java index b36f0b8aa..8047f93bc 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/DatabaseClientPropertySource.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/DatabaseClientPropertySource.java @@ -23,6 +23,7 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.X509TrustManager; import java.security.KeyManagementException; +import java.security.KeyStore; import java.security.NoSuchAlgorithmException; import java.util.LinkedHashMap; import java.util.Map; @@ -317,9 +318,29 @@ private X509TrustManager getTrustManager() { throw new IllegalArgumentException("Trust manager must be an instanceof " + X509TrustManager.class.getName()); } } + + String path = getNullableStringValue("ssl.truststore.path"); + if (path != null && path.trim().length() > 0) { + return buildTrustManagerFromTrustStorePath(path); + } + return null; } + /** + * Added in 6.5.0 to support configuring a trust manager via properties. + * + * @param path + * @return + */ + private X509TrustManager buildTrustManagerFromTrustStorePath(String path) { + final String password = getNullableStringValue("ssl.truststore.password"); + final String type = getNullableStringValue("ssl.truststore.type", "JKS"); + final String algorithm = getNullableStringValue("ssl.truststore.algorithm", "SunX509"); + KeyStore trustStore = SSLUtil.getKeyStore(path, password != null ? password.toCharArray() : null, type); + return (X509TrustManager) SSLUtil.getTrustManagers(algorithm, trustStore)[0]; + } + private SSLContext getSSLContext() { Object val = propertySource.apply(PREFIX + "sslContext"); if (val != null) { diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/SSLUtil.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/SSLUtil.java index 099b51c38..00b15bc79 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/SSLUtil.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/SSLUtil.java @@ -60,7 +60,7 @@ public static TrustManager[] getDefaultTrustManagers() { * @param trustManagerAlgorithm e.g. "SunX509". * @param optionalKeyStore if not null, used to initialize the TrustManagerFactory constructed based on the * given algorithm. - * @return + * @return an array of at least length 1 where the first instance is an {@code X509TrustManager} */ public static TrustManager[] getTrustManagers(String trustManagerAlgorithm, KeyStore optionalKeyStore) { TrustManagerFactory trustManagerFactory; diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/ssl/TwoWaySSLTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/ssl/TwoWaySSLTest.java index 56274cff0..767661d43 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/test/ssl/TwoWaySSLTest.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/ssl/TwoWaySSLTest.java @@ -12,6 +12,7 @@ import com.marklogic.client.test.junit5.RequireSSLExtension; import com.marklogic.mgmt.ManageClient; import com.marklogic.mgmt.resource.appservers.ServerManager; +import com.marklogic.mgmt.resource.security.CertificateTemplateManager; import com.marklogic.rest.util.Fragment; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -74,6 +75,7 @@ public static void setup() throws Exception { createKeystoreFile(tempDir); keyStoreFile = new File(tempDir.toFile(), "client.jks"); p12File = new File(tempDir.toFile(), "client.p12"); + addServerCertificateToKeyStore(tempDir); } @AfterAll @@ -99,11 +101,15 @@ void digestAuthentication() { DatabaseClient clientWithCert = Common.newClientBuilder() .withKeyStorePath(keyStoreFile.getAbsolutePath()) .withKeyStorePassword(KEYSTORE_PASSWORD) + // Still need this as "common"/"strict" don't work for our temporary server certificate. .withSSLHostnameVerifier(DatabaseClientFactory.SSLHostnameVerifier.ANY) - // This is a reasonable trust manager since it references the temporary server certificate as something - // that it accepts instead of accepting everything. - .withTrustManager(RequireSSLExtension.newSecureTrustManager()) + + // Starting in 6.5.0, we can use a real trust manager as the server certificate is in the keystore. + .withTrustStorePath(keyStoreFile.getAbsolutePath()) + .withTrustStorePassword(KEYSTORE_PASSWORD) + .withTrustStoreType("JKS") + .withTrustStoreAlgorithm("SunX509") .build(); verifyTestDocumentCanBeRead(clientWithCert); @@ -416,11 +422,7 @@ private static void createPkcs12File(Path tempDir) throws Exception { "-name", "my-client", "-passout", "pass:" + KEYSTORE_PASSWORD); - ExecutorService executorService = Executors.newSingleThreadExecutor(); - Process process = builder.start(); - executorService.submit(new StreamGobbler(process.getInputStream(), System.out::println)); - executorService.submit(new StreamGobbler(process.getErrorStream(), System.err::println)); - int exitCode = process.waitFor(); + int exitCode = runProcess(builder); assertEquals(0, exitCode, "Unable to create pkcs12 file using openssl"); } @@ -436,12 +438,43 @@ private static void createKeystoreFile(Path tempDir) throws Exception { "-srcstorepass", KEYSTORE_PASSWORD, "-alias", "my-client"); + int exitCode = runProcess(builder); + assertEquals(0, exitCode, "Unable to create keystore using keytool"); + } + + /** + * Retrieves the server certificate associated with the certificate template for this test and stores it in the + * key store so that the key store can also act as a trust store. + * + * @param tempDir + * @throws Exception + */ + private static void addServerCertificateToKeyStore(Path tempDir) throws Exception { + Fragment xml = new CertificateTemplateManager(Common.newManageClient()).getCertificatesForTemplate("java-unittest-template"); + String serverCertificate = xml.getElementValue("/msec:certificate-list/msec:certificate/msec:pem"); + + File certificateFile = new File(tempDir.toFile(), "server.cert"); + FileCopyUtils.copy(serverCertificate.getBytes(), certificateFile); + + ProcessBuilder builder = new ProcessBuilder(); + builder.directory(tempDir.toFile()); + builder.command("keytool", "-importcert", + "-keystore", keyStoreFile.getAbsolutePath(), + "-storepass", KEYSTORE_PASSWORD, + "-file", certificateFile.getAbsolutePath(), + "-noprompt", + "-alias", "java-unittest-template-certificate"); + + int exitCode = runProcess(builder); + assertEquals(0, exitCode, "Unable to add server public certificate to keystore."); + } + + private static int runProcess(ProcessBuilder builder) throws Exception { Process process = builder.start(); ExecutorService executorService = Executors.newSingleThreadExecutor(); executorService.submit(new StreamGobbler(process.getInputStream(), System.out::println)); executorService.submit(new StreamGobbler(process.getErrorStream(), System.err::println)); - int exitCode = process.waitFor(); - assertEquals(0, exitCode, "Unable to create keystore using keytool"); + return process.waitFor(); } /**