diff --git a/gradle.properties b/gradle.properties
index c468e05ee..2315a868c 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,5 +1,5 @@
group=com.marklogic
-version=6.3-SNAPSHOT
+version=6.4-SNAPSHOT
describedName=MarkLogic Java Client API
publishUrl=file:../marklogic-java/releases
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 1d8606fc2..219a59ab6 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
@@ -259,6 +259,48 @@ public DatabaseClientBuilder withGzippedResponsesDisabled() {
props.put(PREFIX + "disableGzippedResponses", true);
return this;
}
+
+ /**
+ * Enables 2-way SSL by creating an SSL context based on the given key store path.
+ *
+ * @param path
+ * @return
+ * @since 6.4.0
+ */
+ public DatabaseClientBuilder withKeyStorePath(String path) {
+ props.put(PREFIX + "ssl.keystore.path", path);
+ return this;
+ }
+
+ /**
+ * @param password optional password for a key store
+ * @return
+ * @since 6.4.0
+ */
+ public DatabaseClientBuilder withKeyStorePassword(String password) {
+ props.put(PREFIX + "ssl.keystore.password", password);
+ return this;
+ }
+
+ /**
+ * @param type e.g. "JKS"
+ * @return
+ * @since 6.4.0
+ */
+ public DatabaseClientBuilder withKeyStoreType(String type) {
+ props.put(PREFIX + "ssl.keystore.type", type);
+ return this;
+ }
+
+ /**
+ * @param algorithm e.g. "SunX509"
+ * @return
+ * @since 6.4.0
+ */
+ public DatabaseClientBuilder withKeyStoreAlgorithm(String algorithm) {
+ props.put(PREFIX + "ssl.keystore.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 cf18e6b26..0f01216c6 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
@@ -1299,6 +1299,10 @@ public String getCertificatePassword() {
* a String with a value of either "any", "common", or "strict"
*
marklogic.client.trustManager = must be an instance of {@code javax.net.ssl.X509TrustManager};
* if not specified and an SSL context is configured, an attempt will be made to use the JVM's default trust manager
+ * marklogic.client.ssl.keystore.path = must be a String; enables 2-way SSL if set; since 6.4.0.
+ * 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.
*
*
* @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 112bb304a..b36f0b8aa 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
@@ -151,7 +151,7 @@ private DatabaseClientFactory.SecurityContext newSecurityContext() {
}
final String authType = (String) typeValue;
- final SSLInputs sslInputs = buildSSLInputs(authType);
+ final SSLUtil.SSLInputs sslInputs = buildSSLInputs(authType);
DatabaseClientFactory.SecurityContext securityContext = newSecurityContext(authType, sslInputs);
if (sslInputs.getSslContext() != null) {
securityContext.withSSLContext(sslInputs.getSslContext(), sslInputs.getTrustManager());
@@ -160,7 +160,7 @@ private DatabaseClientFactory.SecurityContext newSecurityContext() {
return securityContext;
}
- private DatabaseClientFactory.SecurityContext newSecurityContext(String type, SSLInputs sslInputs) {
+ private DatabaseClientFactory.SecurityContext newSecurityContext(String type, SSLUtil.SSLInputs sslInputs) {
switch (type.toLowerCase()) {
case DatabaseClientBuilder.AUTH_TYPE_BASIC:
return newBasicAuthContext();
@@ -188,11 +188,15 @@ private String getRequiredStringValue(String propertyName) {
}
private String getNullableStringValue(String propertyName) {
+ return getNullableStringValue(propertyName, null);
+ }
+
+ private String getNullableStringValue(String propertyName, String defaultValue) {
Object value = propertySource.apply(PREFIX + propertyName);
if (value != null && !(value instanceof String)) {
throw new IllegalArgumentException(propertyName + " must be of type String");
}
- return (String) value;
+ return value != null ? (String) value : defaultValue;
}
private DatabaseClientFactory.SecurityContext newBasicAuthContext() {
@@ -221,7 +225,7 @@ private DatabaseClientFactory.SecurityContext newCloudAuthContext() {
return new DatabaseClientFactory.MarkLogicCloudAuthContext(apiKey, duration);
}
- private DatabaseClientFactory.SecurityContext newCertificateAuthContext(SSLInputs sslInputs) {
+ private DatabaseClientFactory.SecurityContext newCertificateAuthContext(SSLUtil.SSLInputs sslInputs) {
String file = getNullableStringValue("certificate.file");
String password = getNullableStringValue("certificate.password");
if (file != null && file.trim().length() > 0) {
@@ -234,6 +238,9 @@ private DatabaseClientFactory.SecurityContext newCertificateAuthContext(SSLInput
throw new RuntimeException("Unable to create CertificateAuthContext; cause " + e.getMessage(), e);
}
}
+ if (sslInputs.getSslContext() == null) {
+ throw new RuntimeException("An SSLContext is required for certificate authentication.");
+ }
return new DatabaseClientFactory.CertificateAuthContext(sslInputs.getSslContext(), sslInputs.getTrustManager());
}
@@ -271,18 +278,24 @@ private DatabaseClientFactory.SSLHostnameVerifier determineHostnameVerifier() {
* case the user does not define their own SSLContext or SSL protocol
* @return
*/
- private SSLInputs buildSSLInputs(String authType) {
+ private SSLUtil.SSLInputs buildSSLInputs(String authType) {
X509TrustManager userTrustManager = getTrustManager();
// Approach 1 - user provides an SSLContext object, in which case there's nothing further to check.
SSLContext sslContext = getSSLContext();
if (sslContext != null) {
- return new SSLInputs(sslContext, userTrustManager);
+ return new SSLUtil.SSLInputs(sslContext, userTrustManager);
}
- // Approaches 2 and 3 - user defines an SSL protocol.
- // Approach 2 - "default" is a convenience for using the JVM's default SSLContext.
- // Approach 3 - create a new SSLContext, and initialize it if the user-provided TrustManager is not null.
+ // Approach 2 - user wants two-way SSL via a keystore.
+ final String keyStorePath = getNullableStringValue("ssl.keystore.path");
+ if (keyStorePath != null && keyStorePath.trim().length() > 0) {
+ return useKeyStoreForTwoWaySSL(keyStorePath, userTrustManager);
+ }
+
+ // Approaches 3 and 4 - user defines an SSL protocol.
+ // Approach 3 - "default" is a convenience for using the JVM's default SSLContext.
+ // Approach 4 - create a new SSLContext, and initialize it if the user-provided TrustManager is not null.
final String sslProtocol = getSSLProtocol(authType);
if (sslProtocol != null) {
return "default".equalsIgnoreCase(sslProtocol) ?
@@ -290,9 +303,9 @@ private SSLInputs buildSSLInputs(String authType) {
useNewSSLContext(sslProtocol, userTrustManager);
}
- // Approach 4 - still return the user-defined TrustManager as that may be needed for certificate authentication,
+ // Approach 5 - still return the user-defined TrustManager as that may be needed for certificate authentication,
// which has its own way of constructing an SSLContext from a PKCS12 file.
- return new SSLInputs(null, userTrustManager);
+ return new SSLUtil.SSLInputs(null, userTrustManager);
}
private X509TrustManager getTrustManager() {
@@ -332,11 +345,20 @@ private String getSSLProtocol(String authType) {
return sslProtocol;
}
+ private SSLUtil.SSLInputs useKeyStoreForTwoWaySSL(String keyStorePath, X509TrustManager userTrustManager) {
+ final String password = getNullableStringValue("ssl.keystore.password");
+ final String keyStoreType = getNullableStringValue("ssl.keystore.type", "JKS");
+ final String algorithm = getNullableStringValue("ssl.keystore.algorithm", "SunX509");
+ final char[] charPassword = password != null ? password.toCharArray() : null;
+ final String sslProtocol = getNullableStringValue("sslProtocol", "TLSv1.2");
+ return SSLUtil.createSSLContextFromKeyStore(keyStorePath, charPassword, keyStoreType, algorithm, sslProtocol, userTrustManager);
+ }
+
/**
* Uses the JVM's default SSLContext. Because OkHttp requires a separate TrustManager, this approach will either
* user the user-provided TrustManager or it will assume that the JVM's default TrustManager should be used.
*/
- private SSLInputs useDefaultSSLContext(X509TrustManager userTrustManager) {
+ private SSLUtil.SSLInputs useDefaultSSLContext(X509TrustManager userTrustManager) {
SSLContext sslContext;
try {
sslContext = SSLContext.getDefault();
@@ -344,7 +366,7 @@ private SSLInputs useDefaultSSLContext(X509TrustManager userTrustManager) {
throw new RuntimeException("Unable to obtain default SSLContext; cause: " + e.getMessage(), e);
}
X509TrustManager trustManager = userTrustManager != null ? userTrustManager : SSLUtil.getDefaultTrustManager();
- return new SSLInputs(sslContext, trustManager);
+ return new SSLUtil.SSLInputs(sslContext, trustManager);
}
/**
@@ -352,7 +374,7 @@ private SSLInputs useDefaultSSLContext(X509TrustManager userTrustManager) {
* the user's TrustManager is not null. Otherwise, OkHttpUtil will eventually initialize the SSLContext using the
* JVM's default TrustManager.
*/
- private SSLInputs useNewSSLContext(String sslProtocol, X509TrustManager userTrustManager) {
+ private SSLUtil.SSLInputs useNewSSLContext(String sslProtocol, X509TrustManager userTrustManager) {
SSLContext sslContext;
try {
sslContext = SSLContext.getInstance(sslProtocol);
@@ -368,27 +390,6 @@ private SSLInputs useNewSSLContext(String sslProtocol, X509TrustManager userTrus
sslProtocol, e.getMessage()), e);
}
}
- return new SSLInputs(sslContext, userTrustManager);
- }
-
- /**
- * Captures the inputs provided by the caller that pertain to constructing an SSLContext.
- */
- private static class SSLInputs {
- private final SSLContext sslContext;
- private final X509TrustManager trustManager;
-
- public SSLInputs(SSLContext sslContext, X509TrustManager trustManager) {
- this.sslContext = sslContext;
- this.trustManager = trustManager;
- }
-
- public SSLContext getSslContext() {
- return sslContext;
- }
-
- public X509TrustManager getTrustManager() {
- return trustManager;
- }
+ return new SSLUtil.SSLInputs(sslContext, userTrustManager);
}
}
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 217f850dd..099b51c38 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
@@ -18,20 +18,31 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.TrustManagerFactory;
-import javax.net.ssl.X509TrustManager;
+import javax.net.ssl.*;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
-public interface SSLUtil {
+/**
+ * SSL convenience methods that are stored in the "impl" package, but we may eventually want to make these officially
+ * public, particular for reuse in connectors.
+ */
+public abstract class SSLUtil {
- static X509TrustManager getDefaultTrustManager() {
+ /**
+ * @return an X509TrustManager based on the JVM's default trust manager algorithm. How this is constructed can vary
+ * based on the JVM type and version. One common approach is for the JVM to constructs this based on its
+ * ./jre/lib/security/cacerts file.
+ */
+ public static X509TrustManager getDefaultTrustManager() {
X509TrustManager trustManager = (X509TrustManager) getDefaultTrustManagers()[0];
Logger logger = LoggerFactory.getLogger(SSLUtil.class);
if (logger.isDebugEnabled() && trustManager.getAcceptedIssuers() != null) {
- logger.debug("Count of accepted issuers in default trust manager: {}", trustManager.getAcceptedIssuers().length);
+ logger.debug("Count of accepted issuers in default trust manager: {}",
+ trustManager.getAcceptedIssuers().length);
}
return trustManager;
}
@@ -40,29 +51,140 @@ static X509TrustManager getDefaultTrustManager() {
* @return a non-empty array of TrustManager instances based on the JVM's default trust manager algorithm, with the
* first trust manager guaranteed to be an instance of X509TrustManager.
*/
- static TrustManager[] getDefaultTrustManagers() {
+ public static TrustManager[] getDefaultTrustManagers() {
final String defaultAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
+ return getTrustManagers(defaultAlgorithm, null);
+ }
+
+ /**
+ * @param trustManagerAlgorithm e.g. "SunX509".
+ * @param optionalKeyStore if not null, used to initialize the TrustManagerFactory constructed based on the
+ * given algorithm.
+ * @return
+ */
+ public static TrustManager[] getTrustManagers(String trustManagerAlgorithm, KeyStore optionalKeyStore) {
TrustManagerFactory trustManagerFactory;
try {
- trustManagerFactory = TrustManagerFactory.getInstance(defaultAlgorithm);
+ trustManagerFactory = TrustManagerFactory.getInstance(trustManagerAlgorithm);
} catch (NoSuchAlgorithmException e) {
- throw new RuntimeException("Unable to obtain trust manager factory using JVM's default trust manager algorithm: " + defaultAlgorithm, e);
+ throw new RuntimeException(
+ "Unable to obtain trust manager factory using algorithm: " + trustManagerAlgorithm, e);
}
try {
- trustManagerFactory.init((KeyStore) null);
+ trustManagerFactory.init(optionalKeyStore);
} catch (KeyStoreException e) {
- throw new RuntimeException("Unable to initialize trust manager factory obtained using JVM's default trust manager algorithm: " + defaultAlgorithm
- + "; cause: " + e.getMessage(), e);
+ throw new RuntimeException(String.format(
+ "Unable to initialize trust manager factory obtained using algorithm: %s; cause: %s",
+ trustManagerAlgorithm, e.getMessage()), e);
}
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers == null || trustManagers.length == 0) {
- throw new RuntimeException("No trust managers found using the JVM's default trust manager algorithm: " + defaultAlgorithm);
+ throw new RuntimeException("No trust managers found using algorithm: " + trustManagerAlgorithm);
}
if (!(trustManagers[0] instanceof X509TrustManager)) {
throw new RuntimeException("Default trust manager is not an X509TrustManager: " + trustManagers[0]);
}
return trustManagers;
}
+
+ /**
+ * Captures the oft-repeated boilerplate Java code for creating an SSLContext based on a key store.
+ *
+ * @param keyStorePath required path to a key store file
+ * @param keyStorePassword optional password, can be null
+ * @param keyStoreType type of key store, e.g. "JKS"
+ * @param algorithm key store algorithm, e.g. "SunX509"
+ * @param sslProtocol e.g. "TLSv1.2"
+ * @param userProvidedTrustManager optional trust manager provided by a user; if not null, will be used to
+ * initialize the SSLContext instead of using the key store as a trust manager.
+ * @return
+ */
+ static SSLInputs createSSLContextFromKeyStore(String keyStorePath, char[] keyStorePassword, String keyStoreType,
+ String algorithm, String sslProtocol,
+ X509TrustManager userProvidedTrustManager) {
+
+ KeyStore keyStore = getKeyStore(keyStorePath, keyStorePassword, keyStoreType);
+ KeyManagerFactory keyManagerFactory = newKeyManagerFactory(keyStore, keyStorePassword, algorithm);
+ SSLContext sslContext = newSSLContext(sslProtocol);
+
+ TrustManager[] trustManagers = userProvidedTrustManager != null
+ ? new X509TrustManager[]{userProvidedTrustManager}
+ : getTrustManagers(algorithm, keyStore);
+
+ try {
+ sslContext.init(keyManagerFactory.getKeyManagers(), trustManagers, null);
+ } catch (KeyManagementException ex) {
+ throw new RuntimeException("Unable to initialize SSL context", ex);
+ }
+
+ return new SSLInputs(sslContext, (X509TrustManager) trustManagers[0]);
+ }
+
+ /**
+ * @return a Java KeyStore based on the given inputs.
+ */
+ public static KeyStore getKeyStore(String keyStorePath, char[] keyStorePassword, String keyStoreType) {
+ KeyStore keyStore;
+ try {
+ keyStore = KeyStore.getInstance(keyStoreType);
+ } catch (KeyStoreException ex) {
+ throw new RuntimeException("Unable to get instance of key store with type: " + keyStoreType, ex);
+ }
+
+ try (InputStream inputStream = new FileInputStream(keyStorePath)) {
+ keyStore.load(inputStream, keyStorePassword);
+ return keyStore;
+ } catch (Exception ex) {
+ throw new RuntimeException("Unable to read from key store at path: " + keyStorePath, ex);
+ }
+ }
+
+ private static KeyManagerFactory newKeyManagerFactory(KeyStore keyStore, char[] keyStorePassword, String algorithm) {
+ KeyManagerFactory keyManagerFactory;
+ try {
+ keyManagerFactory = KeyManagerFactory.getInstance(algorithm);
+ } catch (NoSuchAlgorithmException ex) {
+ throw new RuntimeException("Unable to create key manager factory with algorithm: " + algorithm, ex);
+ }
+
+ try {
+ keyManagerFactory.init(keyStore, keyStorePassword);
+ } catch (Exception ex) {
+ throw new RuntimeException("Unable to initialize key manager factory", ex);
+ }
+ return keyManagerFactory;
+ }
+
+ private static SSLContext newSSLContext(String sslProtocol) {
+ try {
+ return SSLContext.getInstance(sslProtocol);
+ } catch (Exception ex) {
+ throw new RuntimeException("Unable to create SSL context using protocol: " + sslProtocol, ex);
+ }
+ }
+
+ /**
+ * Captures the inputs needed by the Java Client for establishing an SSL connection. The need for a separate
+ * X509TrustManager arose from the switch from Apache's HttpClient to OkHttp, where the latter needs access to a
+ * X509TrustManager (as opposed to relying on any trust managers within an SSLContext).
+ */
+ public static class SSLInputs {
+ private final SSLContext sslContext;
+ private final X509TrustManager trustManager;
+
+ public SSLInputs(SSLContext sslContext, X509TrustManager trustManager) {
+ this.sslContext = sslContext;
+ this.trustManager = trustManager;
+ }
+
+ public SSLContext getSslContext() {
+ return sslContext;
+ }
+
+ public X509TrustManager getTrustManager() {
+ return trustManager;
+ }
+ }
}
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 375a7e211..e454a56c4 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
@@ -23,22 +23,16 @@
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.X509TrustManager;
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
+import java.io.*;
import java.nio.file.Path;
import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(RequireSSLExtension.class)
public class TwoWaySSLTest {
@@ -96,15 +90,19 @@ public static void teardown() {
* SSLContext can connect to the app server.
*/
@Test
- void digestAuthentication() throws Exception {
+ void digestAuthentication() {
if (Common.USE_REVERSE_PROXY_SERVER) {
return;
}
// This client uses our Java KeyStore file with a client certificate in it, so it should work.
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)
- .withSSLContext(createSSLContextWithClientCertificate(keyStoreFile))
+ // 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())
.build();
@@ -129,6 +127,51 @@ void digestAuthentication() throws Exception {
"Unexpected exception: " + userException.getMessage());
}
+ @Test
+ void invalidKeyStoreType() {
+ RuntimeException ex = assertThrows(RuntimeException.class, () -> Common.newClientBuilder()
+ .withKeyStoreType("Not a valid type!")
+ .withKeyStorePath("doesn't matter for this test")
+ .build());
+
+ assertEquals("Unable to get instance of key store with type: Not a valid type!", ex.getMessage());
+ assertTrue(ex.getCause() instanceof KeyStoreException);
+ }
+
+ @Test
+ void invalidKeyStorePath() {
+ RuntimeException ex = assertThrows(RuntimeException.class, () -> Common.newClientBuilder()
+ .withKeyStorePath("/no/keystore/here.txt").build());
+
+ assertEquals("Unable to read from key store at path: /no/keystore/here.txt", ex.getMessage());
+ assertTrue(ex.getCause() instanceof FileNotFoundException);
+ }
+
+ @Test
+ void invalidKeyStorePassword() {
+ RuntimeException ex = assertThrows(RuntimeException.class, () -> Common.newClientBuilder()
+ .withKeyStorePath(keyStoreFile.getAbsolutePath())
+ .withKeyStorePassword("wrong password!")
+ .build());
+
+ assertTrue(ex.getMessage().startsWith("Unable to read from key store at path:"),
+ "Unexpected message: " + ex.getMessage());
+ assertTrue(ex.getCause() instanceof IOException);
+ }
+
+ @Test
+ void invalidKeyStoreAlgorithm() {
+ RuntimeException ex = assertThrows(RuntimeException.class, () -> Common.newClientBuilder()
+ .withKeyStorePath(keyStoreFile.getAbsolutePath())
+ .withKeyStorePassword(KEYSTORE_PASSWORD)
+ .withKeyStoreAlgorithm("Not a valid algorithm!")
+ .build());
+
+ assertEquals("Unable to create key manager factory with algorithm: Not a valid algorithm!", ex.getMessage());
+ assertTrue(ex.getCause() instanceof NoSuchAlgorithmException);
+ }
+
+
/**
* Verifies certificate authentication when a user provides their own SSLContext.
*/
@@ -176,6 +219,16 @@ void certificateAuthenticationWithCertificateFileAndPassword() {
}
}
+ @Test
+ void certificateAuthenticationWithNoSSLContextOrFileAndPassword() {
+ RuntimeException ex = assertThrows(RuntimeException.class, () -> Common.newClientBuilder()
+ .withCertificateAuth(null, RequireSSLExtension.newSecureTrustManager())
+ .withSSLHostnameVerifier(DatabaseClientFactory.SSLHostnameVerifier.ANY)
+ .build());
+
+ assertEquals("An SSLContext is required for certificate authentication.", ex.getMessage());
+ }
+
private void setAuthenticationToCertificate() {
new ServerManager(manageClient)
.save(Common.newServerPayload().put("authentication", "certificate").toString());