diff --git a/examples/src/main/java/com/marklogic/client/example/cookbook/SSLClientCreator.java b/examples/src/main/java/com/marklogic/client/example/cookbook/SSLClientCreator.java
index 170c3bf1e..c42c6e75b 100644
--- a/examples/src/main/java/com/marklogic/client/example/cookbook/SSLClientCreator.java
+++ b/examples/src/main/java/com/marklogic/client/example/cookbook/SSLClientCreator.java
@@ -1,107 +1,86 @@
/*
- * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
+ * Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
package com.marklogic.client.example.cookbook;
import java.io.IOException;
-import java.security.KeyManagementException;
-import java.security.NoSuchAlgorithmException;
-import java.security.cert.X509Certificate;
-
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.X509TrustManager;
import com.marklogic.client.DatabaseClient;
import com.marklogic.client.DatabaseClientFactory;
-import com.marklogic.client.DatabaseClientFactory.DigestAuthContext;
-import com.marklogic.client.DatabaseClientFactory.SSLHostnameVerifier;
import com.marklogic.client.document.TextDocumentManager;
import com.marklogic.client.example.cookbook.Util.ExampleProperties;
import com.marklogic.client.io.StringHandle;
/**
- * SSLClientCreator illustrates the basic approach for creating a client using SSL for database access.
+ * SSLClientCreator illustrates the basic approach for creating a client using
+ * SSL for database access.
+ *
+ *
+ * A JKS or PKCS12 truststore containing the server's CA certificate must be
+ * configured via
+ * {@code example.truststore.path} and {@code example.truststore.password} in
+ * {@code Example.properties}
+ * (or the equivalent system properties) before running this example.
+ *
*
- * Note: to run this example, you must modify the REST server by specifying a SSL certificate template.
+ *
+ * Note: to run this example, you must also modify the REST server by specifying
+ * an SSL certificate template.
+ *
*/
public class SSLClientCreator {
- public static void main(String[] args) throws IOException, KeyManagementException, NoSuchAlgorithmException {
+ public static void main(String[] args) throws IOException {
run(Util.loadProperties());
}
- public static void run(ExampleProperties props) throws NoSuchAlgorithmException, KeyManagementException {
- System.out.println("example: "+SSLClientCreator.class.getName());
-
- // create a trust manager
- // (note: a real application should verify certificates. This
- // naive trust manager which accepts all the certificates should be replaced
- // by a valid trust manager or get a system default trust manager
- // which would validate whether the remote authentication credentials
- // should be trusted or not.)
- TrustManager naiveTrustMgr[] = new X509TrustManager[] {
- new X509TrustManager() {
- @Override
- public void checkClientTrusted(X509Certificate[] chain, String authType) {
- }
-
- @Override
- public void checkServerTrusted(X509Certificate[] chain, String authType) {
- }
-
- @Override
- public X509Certificate[] getAcceptedIssuers() {
- return new X509Certificate[0];
- }
- }
- };
+ public static void run(ExampleProperties props) {
+ System.out.println("example: " + SSLClientCreator.class.getName());
- // create an SSL context
- SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
- /*
- * Here, we use a naive TrustManager which would accept any certificate
- * which the server produces. But in a real application, there should be a
- * TrustManager which is initialized with a Keystore which would determine
- * whether the remote authentication credentials should be trusted or not.
- *
- * If we init the sslContext with null TrustManager, it would use the
- * /lib/security/cacerts file for trusted root certificates, if
- * javax.net.ssl.trustStore system property is not set and
- * /lib/security/jssecacerts is not present. See this link for
- * more information on TrustManagers -
- * http://docs.oracle.com/javase/7/docs/technotes/guides/security/jsse/
- * JSSERefGuide.html
- *
- * If self signed certificates, signed by CAs created internally are used,
- * then the internal CA's root certificate should be added to the keystore.
- * See this link -
- * https://docs.oracle.com/cd/E19226-01/821-0027/geygn/index.html for adding
- * a root certificate in the keystore.
- */
- sslContext.init(null, naiveTrustMgr, null);
+ // Configure example.truststore.path and example.truststore.password in
+ // Example.properties.
+ if (props.trustStorePath == null || props.trustStorePath.isEmpty()) {
+ throw new IllegalStateException(
+ "example.truststore.path is not configured. Set it in Example.properties to the path of a JKS or "
+ + "PKCS12 truststore containing the server's CA certificate.");
+ }
+ if (props.trustStorePassword == null) {
+ throw new IllegalStateException(
+ "example.truststore.password is not configured. Set it in Example.properties.");
+ }
- // create the client
- // (note: a real application should use a COMMON, STRICT, or implemented hostname verifier)
- DatabaseClient client = DatabaseClientFactory.newClient(
- props.host, props.port,
- new DigestAuthContext(props.writerUser, props.writerPassword)
- .withSSLContext(sslContext, (X509TrustManager) naiveTrustMgr[0])
- .withSSLHostnameVerifier(SSLHostnameVerifier.ANY));
+ // Create the client using the property-source API. SSL is configured
+ // declaratively via the
+ // truststore path and password so that the client validates the server
+ // certificate against
+ // the trusted CAs in that store. STRICT hostname verification ensures the
+ // server certificate
+ // CN/SANs are checked against the connected host.
+ try (DatabaseClient client = DatabaseClientFactory.newClient(propertyName -> switch (propertyName) {
+ case "marklogic.client.host" -> props.host;
+ case "marklogic.client.port" -> props.port;
+ case "marklogic.client.authType" -> "digest";
+ case "marklogic.client.username" -> props.writerUser;
+ case "marklogic.client.password" -> props.writerPassword;
+ case "marklogic.client.sslProtocol" -> "TLSv1.3";
+ case "marklogic.client.ssl.truststore.path" -> props.trustStorePath;
+ case "marklogic.client.ssl.truststore.password" -> props.trustStorePassword;
+ case "marklogic.client.sslHostnameVerifier" -> DatabaseClientFactory.SSLHostnameVerifier.STRICT;
+ default -> null;
+ })) {
- // make use of the client connection
- TextDocumentManager docMgr = client.newTextDocumentManager();
- String docId = "/example/text.txt";
- StringHandle handle = new StringHandle();
- handle.set("A simple text document");
- docMgr.write(docId, handle);
+ // make use of the client connection
+ TextDocumentManager docMgr = client.newTextDocumentManager();
+ String docId = "/example/text.txt";
+ StringHandle handle = new StringHandle();
+ handle.set("A simple text document");
+ docMgr.write(docId, handle);
- System.out.println(
- "Connected by SSL to "+props.host+":"+props.port+" as "+props.writerUser);
+ System.out.println(
+ "Connected by SSL to " + props.host + ":" + props.port + " as " + props.writerUser);
- // clean up the written document
- docMgr.delete(docId);
+ // clean up the written document
+ docMgr.delete(docId);
- // release the client
- client.release();
+ }
}
}
diff --git a/examples/src/main/java/com/marklogic/client/example/cookbook/Util.java b/examples/src/main/java/com/marklogic/client/example/cookbook/Util.java
index 11abb4294..015bdf91b 100644
--- a/examples/src/main/java/com/marklogic/client/example/cookbook/Util.java
+++ b/examples/src/main/java/com/marklogic/client/example/cookbook/Util.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
+ * Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
package com.marklogic.client.example.cookbook;
@@ -29,6 +29,8 @@ static public class ExampleProperties {
public String jdbcUrl;
public String jdbcUser;
public String jdbcPassword;
+ public String trustStorePath;
+ public String trustStorePassword;
public ExampleProperties(Properties props) {
super();
host = System.getProperty("EXAMPLE_HOST", props.getProperty("example.host"));
@@ -43,6 +45,8 @@ public ExampleProperties(Properties props) {
jdbcUrl = props.getProperty("example.jdbc.url");
jdbcUser = props.getProperty("example.jdbc.user");
jdbcPassword = props.getProperty("example.jdbc.password");
+ trustStorePath = props.getProperty("example.truststore.path");
+ trustStorePassword = props.getProperty("example.truststore.password");
}
}
diff --git a/examples/src/main/resources/Example.properties b/examples/src/main/resources/Example.properties
index 3628fa400..017251f6a 100644
--- a/examples/src/main/resources/Example.properties
+++ b/examples/src/main/resources/Example.properties
@@ -11,3 +11,9 @@ example.port=8000
example.jdbc.url=jdbc:hsqldb:hsql://localhost:9002/employees
example.jdbc.user=sa
example.jdbc.password=
+
+# Path to a JKS or PKCS12 truststore containing the server's CA certificate.
+# Required by SSLClientCreator when the server uses a certificate that is not
+# present in the JVM default trust store (e.g. a self-signed or internal CA cert).
+# example.truststore.path=/path/to/truststore.jks
+# example.truststore.password=changeit
diff --git a/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/functionaltest/ConnectedRESTQA.java b/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/functionaltest/ConnectedRESTQA.java
index 1020c7bd7..7d9084342 100644
--- a/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/functionaltest/ConnectedRESTQA.java
+++ b/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/functionaltest/ConnectedRESTQA.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
+ * Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
package com.marklogic.client.functionaltest;
@@ -40,7 +40,6 @@
import java.io.InputStream;
import java.security.*;
import java.security.cert.CertificateException;
-import java.security.cert.X509Certificate;
import java.util.*;
import static org.junit.jupiter.api.Assertions.fail;
@@ -71,6 +70,8 @@ public abstract class ConnectedRESTQA {
private static String admin_password = null;
private static String ml_certificate_password = null;
private static String ml_certificate_file = null;
+ private static String ml_truststore_file = null;
+ private static String ml_truststore_password = null;
private static String mlDataConfigDirPath = null;
private static Boolean isLBHost = false;
@@ -782,23 +783,6 @@ public static void setPathRangeIndexInDatabase(String dbName, JsonNode jnode) {
*/
public static SSLContext getSslContext() throws IOException, NoSuchAlgorithmException, KeyManagementException,
KeyStoreException, CertificateException, UnrecoverableKeyException {
- // create a trust manager
- // (note: a real application should verify certificates)
-
- TrustManager tm = new X509TrustManager() {
- public void checkClientTrusted(X509Certificate[] x509Certificates, String s) {
- // nothing to do
- }
-
- public void checkServerTrusted(X509Certificate[] x509Certificates, String s) {
- // nothing to do
- }
-
- public X509Certificate[] getAcceptedIssuers() {
- return new X509Certificate[0];
- }
- };
-
// get the client certificate. In case we need to modify path.
String mlCertFile = new String(ml_certificate_file);
@@ -816,9 +800,32 @@ public X509Certificate[] getAcceptedIssuers() {
keyManagerFactory.init(keyStore, ml_certificate_password.toCharArray());
KeyManager[] keyMgr = keyManagerFactory.getKeyManagers();
+ // Build a trust manager that validates the server's certificate.
+ // When ml_truststore_file is configured, a TrustManagerFactory is initialised
+ // from that truststore (which must contain the MarkLogic server's CA certificate).
+ // Otherwise the JVM's default trust managers are used as a safe fallback.
+ final TrustManager[] trustManagers;
+ if (ml_truststore_file != null && !ml_truststore_file.trim().isEmpty()) {
+ KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ char[] tsPassword = ml_truststore_password != null ? ml_truststore_password.toCharArray() : new char[0];
+ InputStream tsInput = property.getClass().getResourceAsStream(ml_truststore_file);
+ if (tsInput == null) {
+ throw new IOException("Truststore resource not found on classpath: " + ml_truststore_file
+ + ". Ensure the file exists under the test resources directory.");
+ }
+ try {
+ trustStore.load(tsInput, tsPassword);
+ } finally {
+ tsInput.close();
+ }
+ trustManagers = SSLUtil.getTrustManagers(TrustManagerFactory.getDefaultAlgorithm(), trustStore);
+ } else {
+ trustManagers = SSLUtil.getDefaultTrustManagers();
+ }
+
// create an SSL context
SSLContext mlsslContext = SSLContext.getInstance(SSLUtil.DEFAULT_PROTOCOL);
- mlsslContext.init(keyMgr, new TrustManager[] { tm }, null);
+ mlsslContext.init(keyMgr, trustManagers, null);
return mlsslContext;
}
@@ -991,6 +998,8 @@ public static void loadGradleProperties() {
ssl_enabled = properties.getProperty("restSSLset");
ml_certificate_password = properties.getProperty("ml_certificate_password");
ml_certificate_file = properties.getProperty("ml_certificate_file");
+ ml_truststore_file = properties.getProperty("ml_truststore_file");
+ ml_truststore_password = properties.getProperty("ml_truststore_password");
mlDataConfigDirPath = properties.getProperty("mlDataConfigDirPath");
isLBHost = "gateway".equalsIgnoreCase(properties.getProperty("marklogic.client.connectionType"));
PROPERTY_WAIT = Integer.parseInt(isLBHost ? "15000" : "0");
diff --git a/marklogic-client-api-functionaltests/src/test/resources/test.properties b/marklogic-client-api-functionaltests/src/test/resources/test.properties
index 29d3a7c76..8d0476e59 100644
--- a/marklogic-client-api-functionaltests/src/test/resources/test.properties
+++ b/marklogic-client-api-functionaltests/src/test/resources/test.properties
@@ -24,4 +24,10 @@ httpsPort=8013
ml_certificate_password=welcome
ml_certificate_file=user.p12
+# Path to a JKS truststore (as a classpath resource) containing the MarkLogic
+# server's CA certificate. Required when restSSLset=true; leave commented out
+# for non-SSL test runs.
+# ml_truststore_file=/truststore.jks
+# ml_truststore_password=
+
mlDataConfigDirPath=src/test/java/com/marklogic/client/functionaltest
diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/ssl/OneWaySSLTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/ssl/OneWaySSLTest.java
index 4118b5d7a..29059d2c8 100644
--- a/marklogic-client-api/src/test/java/com/marklogic/client/test/ssl/OneWaySSLTest.java
+++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/ssl/OneWaySSLTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
+ * Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
package com.marklogic.client.test.ssl;
@@ -24,7 +24,11 @@
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
@@ -73,11 +77,11 @@ void trustAllManager() throws Exception {
SSLContext sslContext = SSLContext.getInstance(SSLUtil.DEFAULT_PROTOCOL);
sslContext.init(null, new TrustManager[]{Common.TRUST_ALL_MANAGER}, null);
- DatabaseClient client = Common.newClientBuilder()
- .withSSLContext(sslContext)
- .withTrustManager(Common.TRUST_ALL_MANAGER)
- .withSSLHostnameVerifier(DatabaseClientFactory.SSLHostnameVerifier.ANY)
- .build();
+ DatabaseClient client = newSslClient(Map.of(
+ "marklogic.client.sslContext", sslContext,
+ "marklogic.client.trustManager", Common.TRUST_ALL_MANAGER,
+ "marklogic.client.sslHostnameVerifier", DatabaseClientFactory.SSLHostnameVerifier.ANY
+ ));
DatabaseClient.ConnectionResult result = client.checkConnection();
assertEquals(0, result.getStatusCode(), "A value of zero implies that a connection was successfully made, " +
@@ -91,11 +95,11 @@ void trustAllManager() throws Exception {
*/
@Test
void trustManagerThatOnlyTrustsTheCertificateFromTheCertificateTemplate() {
- DatabaseClient client = Common.newClientBuilder()
- .withSSLProtocol(SSLUtil.DEFAULT_PROTOCOL)
- .withTrustManager(RequireSSLExtension.newSecureTrustManager())
- .withSSLHostnameVerifier(DatabaseClientFactory.SSLHostnameVerifier.ANY)
- .build();
+ DatabaseClient client = newSslClient(Map.of(
+ "marklogic.client.sslProtocol", SSLUtil.DEFAULT_PROTOCOL,
+ "marklogic.client.trustManager", RequireSSLExtension.newSecureTrustManager(),
+ "marklogic.client.sslHostnameVerifier", DatabaseClientFactory.SSLHostnameVerifier.ANY
+ ));
DatabaseClient.ConnectionResult result = client.checkConnection();
assertEquals(0, result.getStatusCode());
@@ -104,11 +108,11 @@ void trustManagerThatOnlyTrustsTheCertificateFromTheCertificateTemplate() {
@Test
void defaultSslContext() throws Exception {
- DatabaseClient client = Common.newClientBuilder()
- .withSSLContext(SSLContext.getDefault())
- .withTrustManager(Common.TRUST_ALL_MANAGER)
- .withSSLHostnameVerifier(DatabaseClientFactory.SSLHostnameVerifier.ANY)
- .build();
+ DatabaseClient client = newSslClient(Map.of(
+ "marklogic.client.sslContext", SSLContext.getDefault(),
+ "marklogic.client.trustManager", Common.TRUST_ALL_MANAGER,
+ "marklogic.client.sslHostnameVerifier", DatabaseClientFactory.SSLHostnameVerifier.ANY
+ ));
MarkLogicIOException ex = assertThrows(MarkLogicIOException.class, () -> client.checkConnection(),
"The connection should fail because the JVM's default SSL Context does not have a CA certificate that " +
@@ -119,7 +123,7 @@ void defaultSslContext() throws Exception {
@ExtendWith(RequiresML11OrLower.class)
@Test
void noSslContext() {
- DatabaseClient client = Common.newClientBuilder().build();
+ DatabaseClient client = newSslClient(Map.of());
DatabaseClient.ConnectionResult result = client.checkConnection();
assertEquals("Forbidden", result.getErrorMessage(), "MarkLogic is expected to return a 403 Forbidden when the " +
@@ -142,7 +146,7 @@ void noSslContext() {
@ExtendWith(RequiresML12.class)
@Test
void noSslContextWithMarkLogic12() {
- DatabaseClient client = Common.newClientBuilder().build();
+ DatabaseClient client = newSslClient(Map.of());
MarkLogicIOException ex = assertThrows(MarkLogicIOException.class, () -> client.checkConnection());
assertTrue(ex.getMessage().contains("unexpected end of stream"), "Per MLE-17505, a change in the openssl " +
@@ -185,11 +189,59 @@ void tLS12ClientWithTLS13ServerShouldFail() {
assertEquals(expected, ex.getMessage());
}
+ /**
+ * Verifies that a {@link DatabaseClient} backed by a truststore that does NOT contain
+ * the server's CA certificate fails with an {@link SSLHandshakeException}. This confirms
+ * that proper certificate validation is active — i.e. the trust manager is not a no-op.
+ *
+ * The JVM default CA bundle is used as the trust store. It contains real certificates
+ * but not the test-only MarkLogic CA, so the TLS handshake starts and then fails with
+ * {@link SSLHandshakeException} when the server's certificate cannot be verified.
+ */
+ @Test
+ void untrustedCertificateThrowsSSLHandshakeException() throws Exception {
+ // Use the JVM default trust managers (standard CA bundle). The MarkLogic
+ // test certificate is issued by a test CA that is not present in that bundle,
+ // so the TLS handshake will fail with SSLHandshakeException.
+ TrustManager[] trustManagers = SSLUtil.getDefaultTrustManagers();
+ SSLContext sslContext = SSLContext.getInstance(SSLUtil.DEFAULT_PROTOCOL);
+ sslContext.init(null, trustManagers, null);
+
+ DatabaseClient client = newSslClient(Map.of(
+ "marklogic.client.sslContext", sslContext,
+ "marklogic.client.trustManager", (X509TrustManager) trustManagers[0],
+ "marklogic.client.sslHostnameVerifier", DatabaseClientFactory.SSLHostnameVerifier.ANY
+ ));
+
+ MarkLogicIOException ex = assertThrows(MarkLogicIOException.class, () -> client.checkConnection(),
+ "Connection must fail because the JVM default trust store does not contain the test-only MarkLogic CA certificate");
+ assertTrue(ex.getCause() instanceof SSLHandshakeException,
+ "Expected SSLHandshakeException caused by an untrusted server certificate; actual cause: " + ex.getCause());
+ }
+
+ private DatabaseClient newSslClient(Map sslProps) {
+ return DatabaseClientFactory.newClient(propertyName -> {
+ if (sslProps.containsKey(propertyName)) {
+ return sslProps.get(propertyName);
+ }
+ return switch (propertyName) {
+ case "marklogic.client.host" -> Common.HOST;
+ case "marklogic.client.port" -> Common.PORT;
+ case "marklogic.client.basePath" -> Common.BASE_PATH;
+ case "marklogic.client.authType" -> Common.AUTH_TYPE;
+ case "marklogic.client.username" -> Common.USER;
+ case "marklogic.client.password" -> Common.PASS;
+ case "marklogic.client.connectionType" -> Common.CONNECTION_TYPE;
+ default -> null;
+ };
+ });
+ }
+
DatabaseClient buildTrustAllClientWithSSLProtocol(String sslProtocol) {
- return Common.newClientBuilder()
- .withSSLProtocol(sslProtocol)
- .withTrustManager(Common.TRUST_ALL_MANAGER)
- .withSSLHostnameVerifier(DatabaseClientFactory.SSLHostnameVerifier.ANY)
- .build();
+ return newSslClient(Map.of(
+ "marklogic.client.sslProtocol", sslProtocol,
+ "marklogic.client.trustManager", Common.TRUST_ALL_MANAGER,
+ "marklogic.client.sslHostnameVerifier", DatabaseClientFactory.SSLHostnameVerifier.ANY
+ ));
}
}