Skip to content
Merged
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
Expand Up @@ -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;
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1303,6 +1303,10 @@ public String getCertificatePassword() {
* <li>marklogic.client.ssl.keystore.password = must be a String; optional password for a key store; since 6.4.0.</li>
* <li>marklogic.client.ssl.keystore.type = must be a String; optional type for a key store, defaults to "JKS"; since 6.4.0.</li>
* <li>marklogic.client.ssl.keystore.algorithm = must be a String; optional algorithm for a key store, defaults to "SunX509"; since 6.4.0.</li>
* <li>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.</li>
* <li>marklogic.client.ssl.truststore.password = must be a String; optional password for a trust store; since 6.5.0.</li>
* <li>marklogic.client.ssl.truststore.type = must be a String; optional type for a trust store, defaults to "JKS"; since 6.5.0.</li>
* <li>marklogic.client.ssl.truststore.algorithm = must be a String; optional algorithm for a trust store, defaults to "SunX509"; since 6.5.0.</li>
* </ol>
*
* @param propertySource
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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");
}

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

/**
Expand Down