Repository: EventStore/EventStoreDB-Client-Java (java)

Issue:
Interface for Certificate Input
The Java client only supports using the default JVM truststore.

On Containers, it can be cumbersome to load certificates into the trust store. Can it be made possible to provide an interface for streaming the certificates and keys into the client application?

Source Ticket: 110542

Pull request description:
Support certificate file input in connection string and builder.
Added: Support certificate file input in connection string and builder.

Fixes #198 

Patch:
diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/ConnectionSettingsBuilder.java b/db-client-java/src/main/java/com/eventstore/dbclient/ConnectionSettingsBuilder.java
index 48d4d9e..2578fed 100644
--- a/db-client-java/src/main/java/com/eventstore/dbclient/ConnectionSettingsBuilder.java
+++ b/db-client-java/src/main/java/com/eventstore/dbclient/ConnectionSettingsBuilder.java
@@ -11,8 +11,6 @@ import java.net.URL;
 import java.net.URLDecoder;
 import java.util.*;
 
-import static java.lang.Integer.parseInt;
-
 /**
  * Utility to create client settings programmatically.
  */
@@ -31,6 +29,7 @@ public class ConnectionSettingsBuilder {
     private long _keepAliveInterval = Consts.DEFAULT_KEEP_ALIVE_INTERVAL_IN_MS;
     private Long _defaultDeadline = null;
     private List<ClientInterceptor> _interceptors = new ArrayList<>();
+    private String _tlsCaFile = null;
 
     ConnectionSettingsBuilder() {}
 
@@ -52,7 +51,8 @@ public class ConnectionSettingsBuilder {
                 _keepAliveTimeout,
                 _keepAliveInterval,
                 _defaultDeadline,
-                _interceptors);
+                _interceptors,
+                _tlsCaFile);
     }
 
     /**
@@ -183,6 +183,16 @@ public class ConnectionSettingsBuilder {
         return this;
     }
 
+    /**
+     * Client certificate for secure connection. Not required for enabling secure connection. Useful for self-signed
+     * certificate that are not installed on the system trust store.
+     * @param filepath path to a certificate file.
+     */
+    public ConnectionSettingsBuilder tlsCaFile(String filepath) {
+        this._tlsCaFile = filepath;
+        return this;
+    }
+
     void parseGossipSeed(String host) {
         String[] hostParts =  host.split(":");
 
@@ -377,6 +387,13 @@ public class ConnectionSettingsBuilder {
                     }
                     break;
 
+                case "tlscafile":
+                    if (entry[1].isEmpty())
+                        invalidParamFormat(entry[0], entry[1]);
+
+                    builder._tlsCaFile = entry[1];
+                    break;
+
                 default:
                     logger.warn(String.format("Unknown setting '%s' is ignored", entry[0]));
                     break;
diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/ConnectionState.java b/db-client-java/src/main/java/com/eventstore/dbclient/ConnectionState.java
index 72b0324..f111541 100644
--- a/db-client-java/src/main/java/com/eventstore/dbclient/ConnectionState.java
+++ b/db-client-java/src/main/java/com/eventstore/dbclient/ConnectionState.java
@@ -10,6 +10,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.net.ssl.SSLException;
+import java.io.File;
 import java.net.InetSocketAddress;
 import java.util.concurrent.TimeUnit;
 
@@ -28,9 +29,11 @@ class ConnectionState {
             try {
                 SslContextBuilder sslContextBuilder = GrpcSslContexts.forClient();
 
-                if (!settings.isTlsVerifyCert()) {
+                if (settings.getTlsCaFile() != null)
+                    sslContextBuilder.trustManager(new File(settings.getTlsCaFile()));
+
+                if (!settings.isTlsVerifyCert())
                     sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE);
-                }
 
                 this.sslContext = sslContextBuilder.build();
             } catch (SSLException e) {
diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBClientSettings.java b/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBClientSettings.java
index 428e168..fc3ba3c 100644
--- a/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBClientSettings.java
+++ b/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBClientSettings.java
@@ -37,6 +37,7 @@ public class EventStoreDBClientSettings {
     private final long keepAliveInterval;
     private final Long defaultDeadline;
     private final List<ClientInterceptor> interceptors;
+    private final String tlsCaFile;
 
     /**
      * If the dns discovery is enabled.
@@ -139,6 +140,14 @@ public class EventStoreDBClientSettings {
         return interceptors;
     }
 
+    /**
+     * Client certificate for secure connection.
+     * @return tls CA file if it was provided, otherwise null.
+     */
+    public String getTlsCaFile() {
+        return tlsCaFile;
+    }
+
     EventStoreDBClientSettings(
             boolean dnsDiscover,
             int maxDiscoverAttempts,
@@ -152,7 +161,8 @@ public class EventStoreDBClientSettings {
             long keepAliveTimeout,
             long keepAliveInterval,
             Long defaultDeadline,
-            List<ClientInterceptor> interceptors
+            List<ClientInterceptor> interceptors,
+            String tlsCaFile
     ) {
         this.dnsDiscover = dnsDiscover;
         this.maxDiscoverAttempts = maxDiscoverAttempts;
@@ -167,6 +177,7 @@ public class EventStoreDBClientSettings {
         this.keepAliveInterval = keepAliveInterval;
         this.defaultDeadline = defaultDeadline;
         this.interceptors = interceptors;
+        this.tlsCaFile = tlsCaFile;
     }
 
     /**


Test patch:
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/misc/ParseValidConnectionStringTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/misc/ParseValidConnectionStringTests.java
index aed8b7d..2bdba08 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/misc/ParseValidConnectionStringTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/misc/ParseValidConnectionStringTests.java
@@ -102,6 +102,10 @@ public class ParseValidConnectionStringTests {
                 Arguments.of(
                         "esdb://127.0.0.1:21573?defaultDeadline=60000",
                         "{\"dnsDiscover\":false,\"maxDiscoverAttempts\":3,\"discoveryInterval\":500,\"gossipTimeout\":3000,\"nodePreference\":\"leader\",\"tls\":true,\"tlsVerifyCert\":true,\"throwOnAppendFailure\":true,\"hosts\":[{\"address\":\"127.0.0.1\",\"port\":21573}], \"defaultDeadline\": 60000}"
+                ),
+                Arguments.of(
+                        "esdb://127.0.0.1:21573?tlsCaFile=/path/to/cert",
+                        "{\"dnsDiscover\":false,\"maxDiscoverAttempts\":3,\"discoveryInterval\":500,\"gossipTimeout\":3000,\"nodePreference\":\"leader\",\"tls\":true,\"tlsVerifyCert\":true,\"throwOnAppendFailure\":true,\"hosts\":[{\"address\":\"127.0.0.1\",\"port\":21573}], \"tlsCaFile\": \"/path/to/cert\"}"
                 )
         );
     }
@@ -117,6 +121,7 @@ public class ParseValidConnectionStringTests {
         Assertions.assertEquals(settings.getKeepAliveTimeout(), other.getKeepAliveTimeout());
         Assertions.assertEquals(settings.getKeepAliveInterval(), other.getKeepAliveInterval());
         Assertions.assertEquals(settings.getDefaultDeadline(), other.getDefaultDeadline());
+        Assertions.assertEquals(settings.getTlsCaFile(), other.getTlsCaFile());
 
         Assertions.assertEquals(settings.getHosts().length, other.getHosts().length);
         IntStream.range(0, settings.getHosts().length).forEach((i) -> {
@@ -190,6 +195,9 @@ public class ParseValidConnectionStringTests {
             builder.defaultDeadline(tree.get("defaultDeadline").asLong());
         }
 
+        if (tree.get("tlsCaFile") != null)
+            builder.tlsCaFile(tree.get("tlsCaFile").asText());
+
         tree.get("hosts").elements().forEachRemaining((host) -> {
             builder.addHost(new InetSocketAddress(host.get("address").asText(), host.get("port").asInt()));
         });