Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
/*
* 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.FileInputStream;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.security.cert.CertificateException;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import com.marklogic.client.DatabaseClient;
Expand All @@ -23,70 +27,56 @@
/**
* SSLClientCreator illustrates the basic approach for creating a client using SSL for database access.
*
* Note: to run this example, you must modify the REST server by specifying a SSL certificate template.
* <p>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.</p>
*
* <p>Note: to run this example, you must also modify the REST server by specifying an SSL certificate template.</p>
*/
public class SSLClientCreator {
public static void main(String[] args) throws IOException, KeyManagementException, NoSuchAlgorithmException {
public static void main(String[] args) throws IOException, KeyManagementException, NoSuchAlgorithmException,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should instead use the support in DatabaseClientFactory to configure trusted SSL via properties so that the user doesn't have to deal with all this boilerplate. Check out the public static DatabaseClient newClient(Function<String, Object> propertySource) { method. It handles all of this plumbing.

KeyStoreException, CertificateException {
run(Util.loadProperties());
}

public static void run(ExampleProperties props) throws NoSuchAlgorithmException, KeyManagementException {
public static void run(ExampleProperties props) throws NoSuchAlgorithmException, KeyManagementException,
KeyStoreException, CertificateException, IOException {
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) {
}
// Load the truststore containing the server's CA certificate.
// 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.");
}
KeyStore trustStore = KeyStore.getInstance("JKS");
try (FileInputStream fis = new FileInputStream(props.trustStorePath)) {
trustStore.load(fis, props.trustStorePassword.toCharArray());
}

@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
};
// Initialise a TrustManagerFactory from the truststore so that the SSLContext
// will validate the server's certificate against the trusted CAs it contains.
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
TrustManager[] trustManagers = tmf.getTrustManagers();
X509TrustManager trustManager = (X509TrustManager) trustManagers[0];

// create an SSL context
// Create an SSL context backed by the real trust manager.
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
* <java-home>/lib/security/cacerts file for trusted root certificates, if
* javax.net.ssl.trustStore system property is not set and
* <java-home>/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);
sslContext.init(null, trustManagers, null);
Comment on lines +57 to +71

// create the client
// (note: a real application should use a COMMON, STRICT, or implemented hostname verifier)
// Create the client using STRICT hostname verification so that the server's
// certificate CN/SANs are checked against the host being connected to.
DatabaseClient client = DatabaseClientFactory.newClient(
props.host, props.port,
new DigestAuthContext(props.writerUser, props.writerPassword)
.withSSLContext(sslContext, (X509TrustManager) naiveTrustMgr[0])
.withSSLHostnameVerifier(SSLHostnameVerifier.ANY));
.withSSLContext(sslContext, trustManager)
.withSSLHostnameVerifier(SSLHostnameVerifier.STRICT));

// make use of the client connection
TextDocumentManager docMgr = client.newTextDocumentManager();
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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"));
Expand All @@ -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");
Comment on lines 45 to +49
}
}

Expand Down
6 changes: 6 additions & 0 deletions examples/src/main/resources/Example.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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);

Expand All @@ -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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing here - try reworking this to use the support in DatabaseClientFactory for doing all of this for the user.

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);
Comment on lines +808 to +811
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;
}
Expand Down Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -24,7 +24,9 @@

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 static org.junit.jupiter.api.Assertions.*;

Expand Down Expand Up @@ -185,6 +187,36 @@ 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.
*
* <p>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.</p>
*/
@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 = Common.newClientBuilder()
.withSSLContext(sslContext)
.withTrustManager((X509TrustManager) trustManagers[0])
.withSSLHostnameVerifier(DatabaseClientFactory.SSLHostnameVerifier.ANY)
.build();

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

DatabaseClient buildTrustAllClientWithSSLProtocol(String sslProtocol) {
return Common.newClientBuilder()
.withSSLProtocol(sslProtocol)
Expand Down
Loading