Skip to content

Commit d37662a

Browse files
authored
Merge pull request #1614 from marklogic/feature/585-2way-ssl
DEVEXP-585 Can now configure 2-way SSL
2 parents 2e968cb + 0c245f9 commit d37662a

File tree

6 files changed

+284
-62
lines changed

6 files changed

+284
-62
lines changed

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
group=com.marklogic
2-
version=6.3-SNAPSHOT
2+
version=6.4-SNAPSHOT
33
describedName=MarkLogic Java Client API
44
publishUrl=file:../marklogic-java/releases
55

marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientBuilder.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,48 @@ public DatabaseClientBuilder withGzippedResponsesDisabled() {
259259
props.put(PREFIX + "disableGzippedResponses", true);
260260
return this;
261261
}
262+
263+
/**
264+
* Enables 2-way SSL by creating an SSL context based on the given key store path.
265+
*
266+
* @param path
267+
* @return
268+
* @since 6.4.0
269+
*/
270+
public DatabaseClientBuilder withKeyStorePath(String path) {
271+
props.put(PREFIX + "ssl.keystore.path", path);
272+
return this;
273+
}
274+
275+
/**
276+
* @param password optional password for a key store
277+
* @return
278+
* @since 6.4.0
279+
*/
280+
public DatabaseClientBuilder withKeyStorePassword(String password) {
281+
props.put(PREFIX + "ssl.keystore.password", password);
282+
return this;
283+
}
284+
285+
/**
286+
* @param type e.g. "JKS"
287+
* @return
288+
* @since 6.4.0
289+
*/
290+
public DatabaseClientBuilder withKeyStoreType(String type) {
291+
props.put(PREFIX + "ssl.keystore.type", type);
292+
return this;
293+
}
294+
295+
/**
296+
* @param algorithm e.g. "SunX509"
297+
* @return
298+
* @since 6.4.0
299+
*/
300+
public DatabaseClientBuilder withKeyStoreAlgorithm(String algorithm) {
301+
props.put(PREFIX + "ssl.keystore.algorithm", algorithm);
302+
return this;
303+
}
262304
}
263305

264306

marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientFactory.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1299,6 +1299,10 @@ public String getCertificatePassword() {
12991299
* a String with a value of either "any", "common", or "strict"</li>
13001300
* <li>marklogic.client.trustManager = must be an instance of {@code javax.net.ssl.X509TrustManager};
13011301
* if not specified and an SSL context is configured, an attempt will be made to use the JVM's default trust manager</li>
1302+
* <li>marklogic.client.ssl.keystore.path = must be a String; enables 2-way SSL if set; since 6.4.0.</li>
1303+
* <li>marklogic.client.ssl.keystore.password = must be a String; optional password for a key store; since 6.4.0.</li>
1304+
* <li>marklogic.client.ssl.keystore.type = must be a String; optional type for a key store, defaults to "JKS"; since 6.4.0.</li>
1305+
* <li>marklogic.client.ssl.keystore.algorithm = must be a String; optional algorithm for a key store, defaults to "SunX509"; since 6.4.0.</li>
13021306
* </ol>
13031307
*
13041308
* @param propertySource

marklogic-client-api/src/main/java/com/marklogic/client/impl/DatabaseClientPropertySource.java

Lines changed: 37 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ private DatabaseClientFactory.SecurityContext newSecurityContext() {
151151
}
152152
final String authType = (String) typeValue;
153153

154-
final SSLInputs sslInputs = buildSSLInputs(authType);
154+
final SSLUtil.SSLInputs sslInputs = buildSSLInputs(authType);
155155
DatabaseClientFactory.SecurityContext securityContext = newSecurityContext(authType, sslInputs);
156156
if (sslInputs.getSslContext() != null) {
157157
securityContext.withSSLContext(sslInputs.getSslContext(), sslInputs.getTrustManager());
@@ -160,7 +160,7 @@ private DatabaseClientFactory.SecurityContext newSecurityContext() {
160160
return securityContext;
161161
}
162162

163-
private DatabaseClientFactory.SecurityContext newSecurityContext(String type, SSLInputs sslInputs) {
163+
private DatabaseClientFactory.SecurityContext newSecurityContext(String type, SSLUtil.SSLInputs sslInputs) {
164164
switch (type.toLowerCase()) {
165165
case DatabaseClientBuilder.AUTH_TYPE_BASIC:
166166
return newBasicAuthContext();
@@ -188,11 +188,15 @@ private String getRequiredStringValue(String propertyName) {
188188
}
189189

190190
private String getNullableStringValue(String propertyName) {
191+
return getNullableStringValue(propertyName, null);
192+
}
193+
194+
private String getNullableStringValue(String propertyName, String defaultValue) {
191195
Object value = propertySource.apply(PREFIX + propertyName);
192196
if (value != null && !(value instanceof String)) {
193197
throw new IllegalArgumentException(propertyName + " must be of type String");
194198
}
195-
return (String) value;
199+
return value != null ? (String) value : defaultValue;
196200
}
197201

198202
private DatabaseClientFactory.SecurityContext newBasicAuthContext() {
@@ -221,7 +225,7 @@ private DatabaseClientFactory.SecurityContext newCloudAuthContext() {
221225
return new DatabaseClientFactory.MarkLogicCloudAuthContext(apiKey, duration);
222226
}
223227

224-
private DatabaseClientFactory.SecurityContext newCertificateAuthContext(SSLInputs sslInputs) {
228+
private DatabaseClientFactory.SecurityContext newCertificateAuthContext(SSLUtil.SSLInputs sslInputs) {
225229
String file = getNullableStringValue("certificate.file");
226230
String password = getNullableStringValue("certificate.password");
227231
if (file != null && file.trim().length() > 0) {
@@ -234,6 +238,9 @@ private DatabaseClientFactory.SecurityContext newCertificateAuthContext(SSLInput
234238
throw new RuntimeException("Unable to create CertificateAuthContext; cause " + e.getMessage(), e);
235239
}
236240
}
241+
if (sslInputs.getSslContext() == null) {
242+
throw new RuntimeException("An SSLContext is required for certificate authentication.");
243+
}
237244
return new DatabaseClientFactory.CertificateAuthContext(sslInputs.getSslContext(), sslInputs.getTrustManager());
238245
}
239246

@@ -271,28 +278,34 @@ private DatabaseClientFactory.SSLHostnameVerifier determineHostnameVerifier() {
271278
* case the user does not define their own SSLContext or SSL protocol
272279
* @return
273280
*/
274-
private SSLInputs buildSSLInputs(String authType) {
281+
private SSLUtil.SSLInputs buildSSLInputs(String authType) {
275282
X509TrustManager userTrustManager = getTrustManager();
276283

277284
// Approach 1 - user provides an SSLContext object, in which case there's nothing further to check.
278285
SSLContext sslContext = getSSLContext();
279286
if (sslContext != null) {
280-
return new SSLInputs(sslContext, userTrustManager);
287+
return new SSLUtil.SSLInputs(sslContext, userTrustManager);
281288
}
282289

283-
// Approaches 2 and 3 - user defines an SSL protocol.
284-
// Approach 2 - "default" is a convenience for using the JVM's default SSLContext.
285-
// Approach 3 - create a new SSLContext, and initialize it if the user-provided TrustManager is not null.
290+
// Approach 2 - user wants two-way SSL via a keystore.
291+
final String keyStorePath = getNullableStringValue("ssl.keystore.path");
292+
if (keyStorePath != null && keyStorePath.trim().length() > 0) {
293+
return useKeyStoreForTwoWaySSL(keyStorePath, userTrustManager);
294+
}
295+
296+
// Approaches 3 and 4 - user defines an SSL protocol.
297+
// Approach 3 - "default" is a convenience for using the JVM's default SSLContext.
298+
// Approach 4 - create a new SSLContext, and initialize it if the user-provided TrustManager is not null.
286299
final String sslProtocol = getSSLProtocol(authType);
287300
if (sslProtocol != null) {
288301
return "default".equalsIgnoreCase(sslProtocol) ?
289302
useDefaultSSLContext(userTrustManager) :
290303
useNewSSLContext(sslProtocol, userTrustManager);
291304
}
292305

293-
// Approach 4 - still return the user-defined TrustManager as that may be needed for certificate authentication,
306+
// Approach 5 - still return the user-defined TrustManager as that may be needed for certificate authentication,
294307
// which has its own way of constructing an SSLContext from a PKCS12 file.
295-
return new SSLInputs(null, userTrustManager);
308+
return new SSLUtil.SSLInputs(null, userTrustManager);
296309
}
297310

298311
private X509TrustManager getTrustManager() {
@@ -332,27 +345,36 @@ private String getSSLProtocol(String authType) {
332345
return sslProtocol;
333346
}
334347

348+
private SSLUtil.SSLInputs useKeyStoreForTwoWaySSL(String keyStorePath, X509TrustManager userTrustManager) {
349+
final String password = getNullableStringValue("ssl.keystore.password");
350+
final String keyStoreType = getNullableStringValue("ssl.keystore.type", "JKS");
351+
final String algorithm = getNullableStringValue("ssl.keystore.algorithm", "SunX509");
352+
final char[] charPassword = password != null ? password.toCharArray() : null;
353+
final String sslProtocol = getNullableStringValue("sslProtocol", "TLSv1.2");
354+
return SSLUtil.createSSLContextFromKeyStore(keyStorePath, charPassword, keyStoreType, algorithm, sslProtocol, userTrustManager);
355+
}
356+
335357
/**
336358
* Uses the JVM's default SSLContext. Because OkHttp requires a separate TrustManager, this approach will either
337359
* user the user-provided TrustManager or it will assume that the JVM's default TrustManager should be used.
338360
*/
339-
private SSLInputs useDefaultSSLContext(X509TrustManager userTrustManager) {
361+
private SSLUtil.SSLInputs useDefaultSSLContext(X509TrustManager userTrustManager) {
340362
SSLContext sslContext;
341363
try {
342364
sslContext = SSLContext.getDefault();
343365
} catch (NoSuchAlgorithmException e) {
344366
throw new RuntimeException("Unable to obtain default SSLContext; cause: " + e.getMessage(), e);
345367
}
346368
X509TrustManager trustManager = userTrustManager != null ? userTrustManager : SSLUtil.getDefaultTrustManager();
347-
return new SSLInputs(sslContext, trustManager);
369+
return new SSLUtil.SSLInputs(sslContext, trustManager);
348370
}
349371

350372
/**
351373
* Constructs a new SSLContext based on the given protocol (e.g. TLSv1.2). The SSLContext will be initialized if
352374
* the user's TrustManager is not null. Otherwise, OkHttpUtil will eventually initialize the SSLContext using the
353375
* JVM's default TrustManager.
354376
*/
355-
private SSLInputs useNewSSLContext(String sslProtocol, X509TrustManager userTrustManager) {
377+
private SSLUtil.SSLInputs useNewSSLContext(String sslProtocol, X509TrustManager userTrustManager) {
356378
SSLContext sslContext;
357379
try {
358380
sslContext = SSLContext.getInstance(sslProtocol);
@@ -368,27 +390,6 @@ private SSLInputs useNewSSLContext(String sslProtocol, X509TrustManager userTrus
368390
sslProtocol, e.getMessage()), e);
369391
}
370392
}
371-
return new SSLInputs(sslContext, userTrustManager);
372-
}
373-
374-
/**
375-
* Captures the inputs provided by the caller that pertain to constructing an SSLContext.
376-
*/
377-
private static class SSLInputs {
378-
private final SSLContext sslContext;
379-
private final X509TrustManager trustManager;
380-
381-
public SSLInputs(SSLContext sslContext, X509TrustManager trustManager) {
382-
this.sslContext = sslContext;
383-
this.trustManager = trustManager;
384-
}
385-
386-
public SSLContext getSslContext() {
387-
return sslContext;
388-
}
389-
390-
public X509TrustManager getTrustManager() {
391-
return trustManager;
392-
}
393+
return new SSLUtil.SSLInputs(sslContext, userTrustManager);
393394
}
394395
}

0 commit comments

Comments
 (0)