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();
}
/**