From 0b5776dc15bfdb9cb7f5208fd2948980a88ab230 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Wed, 10 Jun 2026 21:58:43 -0700 Subject: [PATCH 1/3] Added ssl_mode to client-v2 and jdbc-v2 --- .../ClickHouseDefaultSslContextProvider.java | 59 ++++++--- .../com/clickhouse/client/api/Client.java | 28 +++++ .../client/api/ClientConfigProperties.java | 3 + .../api/internal/HttpAPIClientHelper.java | 25 +++- .../clickhouse/client/HttpTransportTests.java | 118 ++++++++++++++++++ .../jdbc/internal/JdbcConfiguration.java | 20 ++- .../com/clickhouse/jdbc/ConnectionTest.java | 72 +++++++++++ .../jdbc/internal/JdbcConfigurationTest.java | 51 ++++++++ 8 files changed, 353 insertions(+), 23 deletions(-) diff --git a/clickhouse-client/src/main/java/com/clickhouse/client/config/ClickHouseDefaultSslContextProvider.java b/clickhouse-client/src/main/java/com/clickhouse/client/config/ClickHouseDefaultSslContextProvider.java index cf114cb26..9b0a2a12b 100644 --- a/clickhouse-client/src/main/java/com/clickhouse/client/config/ClickHouseDefaultSslContextProvider.java +++ b/clickhouse-client/src/main/java/com/clickhouse/client/config/ClickHouseDefaultSslContextProvider.java @@ -154,14 +154,38 @@ public SSLContext getJavaSslContext(ClickHouseConfig config) throws SSLException } public SSLContext getSslContextFromCerts(String clientCert, String clientKey, String sslRootCert) throws SSLException { - return getSslContextImpl(ClickHouseSslMode.STRICT, - clientCert, clientKey, sslRootCert, null, null, KeyStore.getDefaultType()); + return getSslContextFromCerts(ClickHouseSslMode.STRICT, clientCert, clientKey, sslRootCert); + } + + /** + * Creates an SSL context from certificates with an explicit SSL mode. + * With {@link ClickHouseSslMode#NONE} the server certificate is not validated, while client + * certificate and key are still used (if provided) so that mTLS keeps working. + * + * @param sslMode ssl mode + * @param clientCert client certificate for mTLS, file path or PEM content; may be null + * @param clientKey client private key for mTLS, file path or PEM content; may be null + * @param sslRootCert CA certificate to validate the server certificate, file path or PEM content; may be null + * @return SSL context + * @throws SSLException when the context cannot be created + */ + public SSLContext getSslContextFromCerts(ClickHouseSslMode sslMode, String clientCert, String clientKey, + String sslRootCert) throws SSLException { + return getSslContextImpl(sslMode, clientCert, clientKey, sslRootCert, null, null, KeyStore.getDefaultType()); } public SSLContext getSslContextFromKeyStore(String truststorePath, String truststorePassword, String keyStoreType) throws SSLException { return getSslContextImpl(ClickHouseSslMode.STRICT, null, null, null, truststorePath, truststorePassword, keyStoreType); } + private KeyManager[] getKeyManagers(String clientCert, String clientKey) + throws NoSuchAlgorithmException, InvalidKeySpecException, IOException, CertificateException, + KeyStoreException, UnrecoverableKeyException { + KeyManagerFactory factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + factory.init(getKeyStore(clientCert, clientKey), null); + return factory.getKeyManagers(); + } + private SSLContext getSslContextImpl(ClickHouseSslMode sslMode, String clientCert, String clientKey, String sslRootCert, String truststorePath, String truststorePassword, String keyStoreType) throws SSLException { SSLContext ctx; try { @@ -172,34 +196,29 @@ private SSLContext getSslContextImpl(ClickHouseSslMode sslMode, String clientCer if (sslMode == ClickHouseSslMode.NONE) { tms = new TrustManager[]{new NonValidatingTrustManager()}; - kms = new KeyManager[0]; + // client certificate and key are independent from server verification - keep mTLS working + kms = clientCert != null && !clientCert.isEmpty() ? getKeyManagers(clientCert, clientKey) + : new KeyManager[0]; sr = new SecureRandom(); } else if (sslMode == ClickHouseSslMode.STRICT) { - if (truststorePath != null && !truststorePath.isEmpty()) { + if (clientCert != null && !clientCert.isEmpty()) { + kms = getKeyManagers(clientCert, clientKey); + } + if (truststorePath != null && !truststorePath.isEmpty()) { try (InputStream in = ClickHouseUtils.getFileInputStream(truststorePath)) { KeyStore myTrustStore = KeyStore.getInstance(keyStoreType); - myTrustStore.load(in, truststorePassword.toCharArray()); + myTrustStore.load(in, truststorePassword == null ? null : truststorePassword.toCharArray()); TrustManagerFactory factory = TrustManagerFactory .getInstance(TrustManagerFactory.getDefaultAlgorithm()); factory.init(myTrustStore); tms = factory.getTrustManagers(); - - } - } else { - if (clientCert != null && !clientCert.isEmpty()) { - KeyManagerFactory factory = KeyManagerFactory - .getInstance(KeyManagerFactory.getDefaultAlgorithm()); - factory.init(getKeyStore(clientCert, clientKey), null); - kms = factory.getKeyManagers(); - } - - if (sslRootCert != null && !sslRootCert.isEmpty()) { - TrustManagerFactory factory = TrustManagerFactory - .getInstance(TrustManagerFactory.getDefaultAlgorithm()); - factory.init(getKeyStore(sslRootCert, null)); - tms = factory.getTrustManagers(); } + } else if (sslRootCert != null && !sslRootCert.isEmpty()) { + TrustManagerFactory factory = TrustManagerFactory + .getInstance(TrustManagerFactory.getDefaultAlgorithm()); + factory.init(getKeyStore(sslRootCert, null)); + tms = factory.getTrustManagers(); } sr = new SecureRandom(); diff --git a/client-v2/src/main/java/com/clickhouse/client/api/Client.java b/client-v2/src/main/java/com/clickhouse/client/api/Client.java index 58d94da9b..2df5ced77 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/Client.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/Client.java @@ -12,6 +12,7 @@ import com.clickhouse.client.api.data_formats.internal.ProcessParser; import com.clickhouse.client.api.enums.Protocol; import com.clickhouse.client.api.enums.ProxyType; +import com.clickhouse.client.api.enums.SSLMode; import com.clickhouse.client.api.http.ClickHouseHttpProto; import com.clickhouse.client.api.insert.InsertResponse; import com.clickhouse.client.api.insert.InsertSettings; @@ -755,6 +756,33 @@ public Builder setClientKey(String path) { return this; } + /** + * Defines how strictly the client verifies a server identity on secure connections. + * + *

Supported modes:

+ * + * + *

The mode applies only when a secure protocol is in use - for the HTTP transport that + * means an {@code https://} endpoint. Setting any mode does not make the client use + * encryption on a plain HTTP endpoint: the endpoint scheme always decides whether the + * connection is encrypted.

+ * + * @param sslMode ssl mode + * @return same instance of the builder + */ + public Builder setSSLMode(SSLMode sslMode) { + this.configuration.put(ClientConfigProperties.SSL_MODE.getKey(), sslMode.name()); + return this; + } + /** * Configure client to use server timezone for date/datetime columns. Default is true. * If this options is selected then server timezone should be set as well. diff --git a/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java b/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java index e548a90f9..68b86b694 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java @@ -1,6 +1,7 @@ package com.clickhouse.client.api; import com.clickhouse.client.api.data_formats.internal.AbstractBinaryFormatReader; +import com.clickhouse.client.api.enums.SSLMode; import com.clickhouse.client.api.internal.ClickHouseLZ4OutputStream; import com.clickhouse.data.ClickHouseDataType; import com.clickhouse.data.ClickHouseFormat; @@ -115,6 +116,8 @@ public enum ClientConfigProperties { SSL_CERTIFICATE("sslcert", String.class), + SSL_MODE("ssl_mode", SSLMode.class, SSLMode.Strict.name()), + RETRY_ON_FAILURE("retry", Integer.class, "3"), INPUT_OUTPUT_FORMAT("format", ClickHouseFormat.class), diff --git a/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java b/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java index 5ae4730b7..67122b36b 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java @@ -11,6 +11,8 @@ import com.clickhouse.client.api.DataTransferException; import com.clickhouse.client.api.ServerException; import com.clickhouse.client.api.enums.ProxyType; +import com.clickhouse.client.api.enums.SSLMode; +import com.clickhouse.client.config.ClickHouseSslMode; import com.clickhouse.client.api.http.ClickHouseHttpProto; import com.clickhouse.client.api.transport.Endpoint; import com.clickhouse.client.config.ClickHouseDefaultSslContextProvider; @@ -165,11 +167,26 @@ public SSLContext createSSLContext(Map configuration) { } catch (NoSuchAlgorithmException e) { throw new ClientException("Failed to create default SSL context", e); } + final SSLMode sslMode = ClientConfigProperties.SSL_MODE.getOrDefault(configuration); final String trustStorePath = (String) configuration.get(ClientConfigProperties.SSL_TRUST_STORE.getKey()); final String caCertificate = (String) configuration.get(ClientConfigProperties.CA_CERTIFICATE.getKey()); final String sslCertificate = (String) configuration.get(ClientConfigProperties.SSL_CERTIFICATE.getKey()); final String sslKey = (String) configuration.get(ClientConfigProperties.SSL_KEY.getKey()); - if (trustStorePath != null) { + + if (sslMode == SSLMode.Trust) { + // Server certificate is not validated. Trust material (trust store or CA certificate) + // is not needed, but client certificate and key are still applied for mTLS. + try { + sslContext = sslContextProvider.getSslContextFromCerts(ClickHouseSslMode.NONE, + sslCertificate, sslKey, null); + } catch (SSLException e) { + throw new ClientMisconfigurationException("Failed to create SSL context for the Trust SSL mode", e); + } + } else if (trustStorePath != null) { + if (caCertificate != null) { + throw new ClientMisconfigurationException("CA certificate cannot be used together with a trust store." + + " The CA certificate should be imported into the trust store instead."); + } try { sslContext = sslContextProvider.getSslContextFromKeyStore( trustStorePath, @@ -272,7 +289,11 @@ public CloseableHttpClient createHttpClient(boolean initSslContext, Map true); } else { sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext); diff --git a/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java b/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java index 5c6384a60..c91e1f55c 100644 --- a/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java @@ -14,6 +14,7 @@ import com.clickhouse.client.api.data_formats.ClickHouseBinaryFormatReader; import com.clickhouse.client.api.enums.Protocol; import com.clickhouse.client.api.enums.ProxyType; +import com.clickhouse.client.api.enums.SSLMode; import com.clickhouse.client.api.http.ClickHouseHttpProto; import com.clickhouse.client.api.insert.InsertResponse; import com.clickhouse.client.api.insert.InsertSettings; @@ -291,6 +292,123 @@ public void testSecureConnection() { } } + @Test(groups = { "integration" }) + public void testSSLModeTrust() { + if (isCloud()) { + return; // test uses self-signed cert + } + + ClickHouseNode secureServer = getSecureServer(ClickHouseProtocol.HTTP); + + // Default mode (Strict) without any trust material - the self-signed certificate must be rejected + try (Client client = new Client.Builder() + .addEndpoint("https://localhost:" + secureServer.getPort()) + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()) + .build()) { + Assert.expectThrows(Exception.class, () -> client.queryAll("SELECT 1")); + } + + // Trust mode - the same certificate is accepted without any trust material + try (Client client = new Client.Builder() + .addEndpoint("https://localhost:" + secureServer.getPort()) + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()) + .setOption(ClientConfigProperties.SSL_MODE.getKey(), SSLMode.Trust.name()) + .build()) { + List records = client.queryAll("SELECT timezone()"); + Assert.assertEquals(records.get(0).getString(1), "UTC"); + } catch (Exception e) { + Assert.fail("Trust SSL mode should accept a self-signed certificate", e); + } + } + + @Test(groups = { "integration" }) + public void testSSLModeVerifyCa() { + if (isCloud()) { + return; // test uses self-signed cert + } + + ClickHouseNode secureServer = getSecureServer(ClickHouseProtocol.HTTP); + // server certificate has CN=localhost, so connecting via 127.0.0.1 fails hostname verification + final String endpointByIp = "https://127.0.0.1:" + secureServer.getPort(); + final String serverCertificate = "containers/clickhouse-server/certs/localhost.crt"; + + // Strict mode (default): certificate chain is trusted, but the hostname does not match + try (Client client = new Client.Builder() + .addEndpoint(endpointByIp) + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()) + .setRootCertificate(serverCertificate) + .build()) { + Assert.expectThrows(Exception.class, () -> client.queryAll("SELECT 1")); + } + + // VerifyCa mode: certificate chain is validated, hostname mismatch is ignored + try (Client client = new Client.Builder() + .addEndpoint(endpointByIp) + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()) + .setRootCertificate(serverCertificate) + .setSSLMode(SSLMode.VerifyCa) + .build()) { + List records = client.queryAll("SELECT timezone()"); + Assert.assertEquals(records.get(0).getString(1), "UTC"); + } catch (Exception e) { + Assert.fail("VerifyCa SSL mode should ignore hostname mismatch", e); + } + + // VerifyCa mode still validates the certificate chain - without the CA it must fail + try (Client client = new Client.Builder() + .addEndpoint(endpointByIp) + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()) + .setSSLMode(SSLMode.VerifyCa) + .build()) { + Assert.expectThrows(Exception.class, () -> client.queryAll("SELECT 1")); + } + } + + @Test(groups = { "integration" }) + public void testSSLModeDisabled() { + if (isCloud()) { + return; // plain HTTP is not available in cloud + } + + ClickHouseNode server = getServer(ClickHouseProtocol.HTTP); + + // Disabled mode with a plain HTTP endpoint - SSL is simply not used + try (Client client = new Client.Builder() + .addEndpoint("http://" + server.getHost() + ":" + server.getPort()) + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()) + .setSSLMode(SSLMode.Disabled) + .build()) { + List records = client.queryAll("SELECT timezone()"); + Assert.assertEquals(records.get(0).getString(1), "UTC"); + } catch (Exception e) { + Assert.fail("Disabled SSL mode should work with a plain HTTP endpoint", e); + } + } + + @Test(groups = { "integration" }) + public void testSSLModeStrictWithTrustStoreAndCaCertificate() { + if (isCloud()) { + return; + } + + ClickHouseNode secureServer = getSecureServer(ClickHouseProtocol.HTTP); + + // CA certificate cannot be combined with a trust store - it should be in the trust store already + Assert.expectThrows(ClientMisconfigurationException.class, () -> new Client.Builder() + .addEndpoint("https://localhost:" + secureServer.getPort()) + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()) + .setSSLTrustStore("containers/clickhouse-server/certs/KeyStore.jks") + .setRootCertificate("containers/clickhouse-server/certs/localhost.crt") + .build()); + } + @Test(groups = { "integration" }, dataProvider = "NoResponseFailureProvider") public void testInsertAndNoHttpResponseFailure(String body, int maxRetries, ThrowingFunction function, boolean shouldFail) { diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java index eca74aa53..0dd15e2da 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java @@ -2,6 +2,7 @@ import com.clickhouse.client.api.Client; import com.clickhouse.client.api.ClientConfigProperties; +import com.clickhouse.client.api.enums.SSLMode; import com.clickhouse.client.api.http.ClickHouseHttpProto; import com.clickhouse.data.ClickHouseDataType; import com.clickhouse.jdbc.Driver; @@ -332,7 +333,8 @@ private Map parseUrl(String url) throws SQLException { * @param urlProperties - properties parsed from URL * @param providedProperties - properties object provided by application */ - private void buildFinalProperties(Map urlProperties, Properties providedProperties) { + private void buildFinalProperties(Map urlProperties, Properties providedProperties) + throws SQLException { // Copy provided properties Map props = new HashMap<>(); @@ -379,6 +381,22 @@ private void buildFinalProperties(Map urlProperties, Properties } } + String sslMode = clientProperties.get(ClientConfigProperties.SSL_MODE.getKey()); + if (sslMode != null) { + if ("none".equalsIgnoreCase(sslMode)) { + // JDBC drivers traditionally use 'none' for the no-verification SSL mode - alias it to 'trust' + clientProperties.put(ClientConfigProperties.SSL_MODE.getKey(), SSLMode.Trust.name()); + } else { + try { + // values are case-insensitive in JDBC - normalize before passing to the client + clientProperties.put(ClientConfigProperties.SSL_MODE.getKey(), SSLMode.fromValue(sslMode).name()); + } catch (IllegalArgumentException e) { + throw new SQLException("Unknown value '" + sslMode + "' for property '" + + ClientConfigProperties.SSL_MODE.getKey() + "'", e); + } + } + } + // Fill list of client properties information, add not specified properties (doesn't affect client properties) for (ClientConfigProperties clientProp : ClientConfigProperties.values()) { DriverPropertyInfo propertyInfo = propertyInfos.get(clientProp.getKey()); diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java index 0ce66c84c..8972ff834 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java @@ -763,6 +763,78 @@ public void testSecureConnection() throws Exception { } } + @Test(groups = { "integration" }) + public void testSSLModeTrust() throws Exception { + if (isCloud()) { + return; // this test uses self-signed cert + } + ClickHouseNode secureServer = getSecureServer(ClickHouseProtocol.HTTP); + String jdbcUrl = "jdbc:clickhouse:" + secureServer.getBaseUri(); + + Properties properties = new Properties(); + properties.put(ClientConfigProperties.USER.getKey(), "default"); + properties.put(ClientConfigProperties.PASSWORD.getKey(), ClickHouseServerForTest.getPassword()); + + // Default mode (strict) without any trust material - the self-signed certificate must be rejected + Assert.expectThrows(Exception.class, () -> { + try (Connection conn = new ConnectionImpl(jdbcUrl, properties); + Statement stmt = conn.createStatement()) { + stmt.executeQuery("SELECT 1"); + } + }); + + // 'none' is a JDBC alias for the 'trust' mode + for (String mode : new String[] { "none", "trust", "Trust" }) { + Properties trustProperties = new Properties(); + trustProperties.putAll(properties); + trustProperties.put(ClientConfigProperties.SSL_MODE.getKey(), mode); + + try (Connection conn = new ConnectionImpl(jdbcUrl, trustProperties); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT number FROM system.numbers LIMIT 10")) { + + int count = 0; + while (rs.next()) { count++; } + Assert.assertEquals(count, 10, "Failed for ssl_mode '" + mode + "'"); + } + } + } + + @Test(groups = { "integration" }) + public void testSSLModeVerifyCa() throws Exception { + if (isCloud()) { + return; // this test uses self-signed cert + } + ClickHouseNode secureServer = getSecureServer(ClickHouseProtocol.HTTP); + // server certificate has CN=localhost, so connecting via 127.0.0.1 fails hostname verification + String jdbcUrl = "jdbc:clickhouse://127.0.0.1:" + secureServer.getPort() + "/"; + + Properties properties = new Properties(); + properties.put(ClientConfigProperties.USER.getKey(), "default"); + properties.put(ClientConfigProperties.PASSWORD.getKey(), ClickHouseServerForTest.getPassword()); + properties.put(DriverProperties.SECURE_CONNECTION.getKey(), "true"); + properties.put(ClientConfigProperties.CA_CERTIFICATE.getKey(), "containers/clickhouse-server/certs/localhost.crt"); + + // Default mode (strict): certificate chain is trusted, but the hostname does not match + Assert.expectThrows(Exception.class, () -> { + try (Connection conn = new ConnectionImpl(jdbcUrl, properties); + Statement stmt = conn.createStatement()) { + stmt.executeQuery("SELECT 1"); + } + }); + + // verify_ca: certificate chain is validated, hostname mismatch is ignored + properties.put(ClientConfigProperties.SSL_MODE.getKey(), "verifyca"); + try (Connection conn = new ConnectionImpl(jdbcUrl, properties); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT number FROM system.numbers LIMIT 10")) { + + int count = 0; + while (rs.next()) { count++; } + Assert.assertEquals(count, 10); + } + } + @Test(groups = { "integration" }) public void testSelectingDatabase() throws Exception { if (isCloud()) { diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcConfigurationTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcConfigurationTest.java index 9e0abe8bc..08d80748f 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcConfigurationTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcConfigurationTest.java @@ -2,6 +2,7 @@ import com.clickhouse.client.api.Client; import com.clickhouse.client.api.ClientConfigProperties; +import com.clickhouse.client.api.enums.SSLMode; import com.clickhouse.data.ClickHouseDataType; import com.clickhouse.jdbc.DriverProperties; @@ -17,6 +18,8 @@ import java.util.Map; import java.util.Objects; import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; @@ -169,6 +172,54 @@ public void testConfigurationProperties() throws Exception { assertEquals(p.value, "default1"); } + @DataProvider(name = "sslModeValues") + public Object[][] sslModeValues() { + return new Object[][] { + // input value, expected client property value + { "none", SSLMode.Trust.name() }, // JDBC alias for the no-verification mode + { "NONE", SSLMode.Trust.name() }, + { "disabled", SSLMode.Disabled.name() }, + { "Disabled", SSLMode.Disabled.name() }, + { "trust", SSLMode.Trust.name() }, + { "Trust", SSLMode.Trust.name() }, + { "verifyca", SSLMode.VerifyCa.name() }, + { "VERIFYCA", SSLMode.VerifyCa.name() }, + { "strict", SSLMode.Strict.name() }, + { "Strict", SSLMode.Strict.name() }, + }; + } + + @Test + public void testSSLModeDatasetCoversAllModes() { + Set covered = Arrays.stream(sslModeValues()) + .map(row -> (String) row[1]) + .collect(Collectors.toSet()); + Set allModes = Arrays.stream(SSLMode.values()) + .map(Enum::name) + .collect(Collectors.toSet()); + assertEquals(covered, allModes, + "SSLMode constants changed - update the 'sslModeValues' dataset and the ssl_mode handling in JdbcConfiguration"); + } + + @Test(dataProvider = "sslModeValues") + public void testSSLModeProperty(String value, String expected) throws Exception { + // passed via Properties + Properties properties = new Properties(); + properties.setProperty(ClientConfigProperties.SSL_MODE.getKey(), value); + JdbcConfiguration configuration = new JdbcConfiguration("jdbc:clickhouse://localhost:8123/", properties); + assertEquals(configuration.getClientProperties().get(ClientConfigProperties.SSL_MODE.getKey()), expected); + + // passed as a URL parameter + configuration = new JdbcConfiguration("jdbc:clickhouse://localhost:8123/?ssl_mode=" + value, new Properties()); + assertEquals(configuration.getClientProperties().get(ClientConfigProperties.SSL_MODE.getKey()), expected); + } + + @Test + public void testSSLModeInvalidValue() { + assertThrows(SQLException.class, + () -> new JdbcConfiguration("jdbc:clickhouse://localhost:8123/?ssl_mode=insecure", new Properties())); + } + @DataProvider(name = "typeMappingsPropertyKey") public Object[][] typeMappingsPropertyKey() { return new Object[][] { From 767fc605a889d5c33d0abd3d009323a008f65804 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Wed, 10 Jun 2026 22:11:13 -0700 Subject: [PATCH 2/3] Added examples --- .../clickhouse/client/api/enums/SSLMode.java | 62 +++++++++++++++++++ examples/client-v2/README.md | 16 +++-- .../examples/client_v2/SSLExamples.java | 53 +++++++++++++--- examples/jdbc/README.md | 17 +++-- .../clickhouse/examples/jdbc/SSLExamples.java | 57 ++++++++++++++--- 5 files changed, 178 insertions(+), 27 deletions(-) create mode 100644 client-v2/src/main/java/com/clickhouse/client/api/enums/SSLMode.java diff --git a/client-v2/src/main/java/com/clickhouse/client/api/enums/SSLMode.java b/client-v2/src/main/java/com/clickhouse/client/api/enums/SSLMode.java new file mode 100644 index 000000000..03a3bacae --- /dev/null +++ b/client-v2/src/main/java/com/clickhouse/client/api/enums/SSLMode.java @@ -0,0 +1,62 @@ +package com.clickhouse.client.api.enums; + +/** + * Defines how strictly the client verifies a server identity when a secure protocol is used. + * + *

The mode affects only connections that are already using a secure transport (for example, + * an {@code https://} endpoint). It does not enable encryption for plain protocols - an + * {@code http://} endpoint stays unencrypted whatever the mode is.

+ * + *

Modes from the least to the most strict:

+ *
    + *
  • {@link #Disabled} - SSL is not used. Plain protocols only.
  • + *
  • {@link #Trust} - encryption is used, but the server certificate chain is not validated + * and the hostname is not verified. Susceptible to MITM attacks - use only for testing or in + * fully trusted environments.
  • + *
  • {@link #VerifyCa} - the server certificate chain is validated against the trust material + * (default JVM trust store, configured trust store, or a CA certificate), but the hostname is + * not checked against the certificate.
  • + *
  • {@link #Strict} - full verification (default): certificate chain is validated and the + * hostname must match the certificate.
  • + *
+ */ +public enum SSLMode { + + /** + * SSL is not used. Connection is not encrypted. + */ + Disabled, + + /** + * Encryption without verification: any server certificate is accepted and + * the hostname is not verified. + */ + Trust, + + /** + * Server certificate chain is validated, but the hostname is not verified. + */ + VerifyCa, + + /** + * Full verification: certificate chain is validated and the hostname must match + * the certificate. Default mode. + */ + Strict; + + /** + * Case-insensitive variant of {@link #valueOf(String)}. + * + * @param value mode name in any case + * @return matching mode + * @throws IllegalArgumentException when the value does not match any mode + */ + public static SSLMode fromValue(String value) { + for (SSLMode mode : values()) { + if (mode.name().equalsIgnoreCase(value)) { + return mode; + } + } + throw new IllegalArgumentException("Unknown SSL mode '" + value + "'"); + } +} diff --git a/examples/client-v2/README.md b/examples/client-v2/README.md index b1a688b96..6ebbdd3cc 100644 --- a/examples/client-v2/README.md +++ b/examples/client-v2/README.md @@ -78,10 +78,15 @@ Notes: ## SSL Examples -`com.clickhouse.examples.client_v2.SSLExamples` shows how to connect securely to a server whose -certificate is signed by a custom (private) CA. Only the CA certificate is passed to the client -with `Client.Builder.setRootCertificate()` - no trust store configuration is required, and the JVM -default trust store stays untouched. +`com.clickhouse.examples.client_v2.SSLExamples` shows how to connect securely to a server: + +- **Custom CA certificate** - the server certificate is signed by a custom (private) CA. Only the + CA certificate is passed to the client with `Client.Builder.setRootCertificate()` (as a file path + or directly as a PEM string) - no trust store configuration is required, and the JVM default + trust store stays untouched. +- **Self-signed certificate without verification** - `Client.Builder.setSSLMode(SSLMode.Trust)` + accepts any server certificate and skips hostname verification. The connection is encrypted, but + the server identity is not verified - use it only for testing or in fully trusted environments. The example runs in one of two modes. @@ -113,7 +118,8 @@ mvn exec:java -Dexec.mainClass="com.clickhouse.examples.client_v2.SSLExamples" \ -DchRootCert="/path/to/ca.crt" ``` -`-DchRootCert` is required in this mode and must point to the CA certificate in PEM format. +`-DchRootCert` must point to the CA certificate in PEM format. When it is omitted, only the +self-signed (`SSLMode.Trust`) example runs - useful when you do not have the CA certificate at hand. ### Setting up a Docker dev instance with a self-signed certificate manually diff --git a/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/SSLExamples.java b/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/SSLExamples.java index 5d47235f4..307c5adff 100644 --- a/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/SSLExamples.java +++ b/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/SSLExamples.java @@ -1,6 +1,7 @@ package com.clickhouse.examples.client_v2; import com.clickhouse.client.api.Client; +import com.clickhouse.client.api.enums.SSLMode; import com.clickhouse.client.api.query.GenericRecord; import lombok.extern.slf4j.Slf4j; @@ -22,6 +23,9 @@ *
  • Passing the CA certificate as a PEM string instead of a file path - useful when the * certificate comes from an environment variable or a secret manager (typical for * Kubernetes/cloud deployments) and you do not want to write it to disk.
  • + *
  • Connecting to a server with a self-signed certificate without any trust material - + * {@link SSLMode#Trust} accepts any server certificate and skips hostname verification. + * Use it only for testing or in fully trusted environments.
  • * * *

    More SSL examples (mTLS, trust stores, SNI) will be added to this class later.

    @@ -41,7 +45,8 @@ *
  • {@code chPort} - ClickHouse HTTPS port, default {@code 8443}
  • *
  • {@code chDatabase} - database name, default {@code default}
  • *
  • {@code chUser} and {@code chPassword} - credentials (standalone mode)
  • - *
  • {@code chRootCert} - path to the root CA certificate in PEM format (required in standalone mode)
  • + *
  • {@code chRootCert} - path to the root CA certificate in PEM format. When omitted in + * standalone mode, only the self-signed (Trust) example runs
  • *
  • {@code chImage} - Docker image for local mode, default {@code clickhouse/clickhouse-server:latest}
  • * */ @@ -58,16 +63,17 @@ public static void main(String[] args) { final String user = System.getProperty("chUser", "default"); final String password = System.getProperty("chPassword", ""); final String rootCert = trimToNull(System.getProperty("chRootCert")); - if (rootCert == null) { - log.error("chRootCert is required when chHost is set. " - + "Pass the path to the CA certificate (PEM) that signed the server certificate."); - return; - } log.info("Running in standalone mode against {}:{}", host, port); String endpoint = "https://" + host + ":" + port; - connectWithCustomRootCertificate(endpoint, database, user, password, rootCert); - connectWithRootCertificateAsString(endpoint, database, user, password, rootCert); + connectToSelfSignedServer(endpoint, database, user, password); + if (rootCert != null) { + connectWithCustomRootCertificate(endpoint, database, user, password, rootCert); + connectWithRootCertificateAsString(endpoint, database, user, password, rootCert); + } else { + log.info("chRootCert is not set - skipping the custom CA certificate examples. " + + "Pass the path to the CA certificate (PEM) that signed the server certificate to run them."); + } return; } @@ -76,6 +82,8 @@ public static void main(String[] args) { final String image = System.getProperty("chImage", "clickhouse/clickhouse-server:latest"); log.info("Running in local mode (set -DchHost to verify your own server)"); try (SecureServerSupport server = SecureServerSupport.start(image)) { + connectToSelfSignedServer(server.getEndpoint(), database, + SecureServerSupport.USER, SecureServerSupport.PASSWORD); connectWithCustomRootCertificate(server.getEndpoint(), database, SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath()); connectWithRootCertificateAsString(server.getEndpoint(), database, @@ -88,6 +96,35 @@ public static void main(String[] args) { Runtime.getRuntime().exit(0); } + /** + * Connects to a ClickHouse server with a self-signed certificate without providing + * any trust material. {@link SSLMode#Trust} makes the client accept any server + * certificate and skip hostname verification. + * + *

    Warning: the connection is encrypted, but the server identity is NOT verified, + * which makes it susceptible to man-in-the-middle attacks. Use this mode only for testing + * or in fully trusted environments. Prefer {@link Client.Builder#setRootCertificate(String)} + * with the signing CA certificate whenever possible.

    + */ + static void connectToSelfSignedServer(String endpoint, String database, String user, String password) { + log.info("Connecting to {} accepting any server certificate (SSLMode.Trust)", endpoint); + try (Client client = new Client.Builder() + .addEndpoint(endpoint) + .setUsername(user) + .setPassword(password) + .setDefaultDatabase(database) + // Accept the self-signed certificate and skip hostname verification. + .setSSLMode(SSLMode.Trust) + .build()) { + + List rows = client.queryAll("SELECT currentUser() AS user, version() AS version"); + log.info("Connected (server certificate not verified) as '{}' to ClickHouse {}", + rows.get(0).getString("user"), rows.get(0).getString("version")); + } catch (Exception e) { + log.error("Connection with SSLMode.Trust failed", e); + } + } + /** * Connects to a ClickHouse server using a custom root CA certificate. * Use this when the server certificate is signed by a private CA (corporate CA, diff --git a/examples/jdbc/README.md b/examples/jdbc/README.md index 51d63327a..b6dd1c88a 100644 --- a/examples/jdbc/README.md +++ b/examples/jdbc/README.md @@ -24,10 +24,16 @@ Addition options can be passed to the application: ## SSL Examples -`com.clickhouse.examples.jdbc.SSLExamples` shows how to connect securely to a server whose -certificate is signed by a custom (private) CA. Only the CA certificate is passed with the -`sslrootcert` connection property - no trust store configuration is required, and the JVM default -trust store stays untouched. +`com.clickhouse.examples.jdbc.SSLExamples` shows how to connect securely to a server: + +- **Custom CA certificate** - the server certificate is signed by a custom (private) CA. Only the + CA certificate is passed with the `sslrootcert` connection property (as a file path or directly + as a PEM string) - no trust store configuration is required, and the JVM default trust store + stays untouched. +- **Self-signed certificate without verification** - the `ssl_mode=trust` connection property + (`ssl_mode=none` is accepted as an alias) accepts any server certificate and skips hostname + verification. The connection is encrypted, but the server identity is not verified - use it only + for testing or in fully trusted environments. The example runs in one of two modes. @@ -57,7 +63,8 @@ mvn exec:java -Dexec.mainClass="com.clickhouse.examples.jdbc.SSLExamples" \ -DchRootCert="/path/to/ca.crt" ``` -`-DchRootCert` is required in this mode and must point to the CA certificate in PEM format. +`-DchRootCert` must point to the CA certificate in PEM format. When it is omitted, only the +self-signed (`ssl_mode=trust`) example runs - useful when you do not have the CA certificate at hand. ### Setting up a Docker dev instance with a self-signed certificate manually diff --git a/examples/jdbc/src/main/java/com/clickhouse/examples/jdbc/SSLExamples.java b/examples/jdbc/src/main/java/com/clickhouse/examples/jdbc/SSLExamples.java index 11a022fb5..f6a1cef1f 100644 --- a/examples/jdbc/src/main/java/com/clickhouse/examples/jdbc/SSLExamples.java +++ b/examples/jdbc/src/main/java/com/clickhouse/examples/jdbc/SSLExamples.java @@ -27,6 +27,10 @@ *
  • Passing the CA certificate as a PEM string instead of a file path - useful when the * certificate comes from an environment variable or a secret manager (typical for * Kubernetes/cloud deployments) and you do not want to write it to disk.
  • + *
  • Connecting to a server with a self-signed certificate without any trust material - + * the {@code ssl_mode=trust} connection property accepts any server certificate and skips + * hostname verification ({@code ssl_mode=none} is accepted as an alias). Use it only for + * testing or in fully trusted environments.
  • * * *

    More SSL examples (mTLS, trust stores, SNI) will be added to this class later.

    @@ -45,7 +49,8 @@ *
  • {@code chUrl} - ClickHouse JDBC URL, e.g. {@code jdbc:clickhouse://my-host:8443/default}. * When set, standalone mode is used
  • *
  • {@code chUser} and {@code chPassword} - credentials (standalone mode)
  • - *
  • {@code chRootCert} - path to the root CA certificate in PEM format (required in standalone mode)
  • + *
  • {@code chRootCert} - path to the root CA certificate in PEM format. When omitted in + * standalone mode, only the self-signed (trust) example runs
  • *
  • {@code chImage} - Docker image for local mode, default {@code clickhouse/clickhouse-server:latest}
  • * */ @@ -60,18 +65,19 @@ public static void main(String[] args) { final String user = System.getProperty("chUser", "default"); final String password = System.getProperty("chPassword", ""); final String rootCert = trimToNull(System.getProperty("chRootCert")); - if (rootCert == null) { - log.error("chRootCert is required when chUrl is set. " - + "Pass the path to the CA certificate (PEM) that signed the server certificate."); - return; - } log.info("Running in standalone mode against {}", url); try { - connectWithCustomRootCertificate(url, user, password, rootCert); - connectWithRootCertificateAsString(url, user, password, rootCert); + connectToSelfSignedServer(url, user, password); + if (rootCert != null) { + connectWithCustomRootCertificate(url, user, password, rootCert); + connectWithRootCertificateAsString(url, user, password, rootCert); + } else { + log.info("chRootCert is not set - skipping the custom CA certificate examples. " + + "Pass the path to the CA certificate (PEM) that signed the server certificate to run them."); + } } catch (SQLException | IOException e) { - log.error("Secure connection with a custom root CA certificate failed", e); + log.error("Secure connection failed", e); } return; } @@ -81,6 +87,8 @@ public static void main(String[] args) { final String image = System.getProperty("chImage", "clickhouse/clickhouse-server:latest"); log.info("Running in local mode (set -DchUrl to verify your own server)"); try (SecureServerSupport server = SecureServerSupport.start(image)) { + connectToSelfSignedServer(server.getJdbcUrl(), + SecureServerSupport.USER, SecureServerSupport.PASSWORD); connectWithCustomRootCertificate(server.getJdbcUrl(), SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath()); connectWithRootCertificateAsString(server.getJdbcUrl(), @@ -93,6 +101,37 @@ public static void main(String[] args) { Runtime.getRuntime().exit(0); } + /** + * Connects to a ClickHouse server with a self-signed certificate without providing + * any trust material. The {@code ssl_mode=trust} connection property makes the driver + * accept any server certificate and skip hostname verification. The traditional JDBC + * value {@code ssl_mode=none} is accepted as an alias. + * + *

    Warning: the connection is encrypted, but the server identity is NOT verified, + * which makes it susceptible to man-in-the-middle attacks. Use this mode only for testing + * or in fully trusted environments. Prefer {@code sslrootcert} with the signing CA + * certificate whenever possible.

    + */ + static void connectToSelfSignedServer(String url, String user, String password) throws SQLException { + log.info("Connecting to {} accepting any server certificate (ssl_mode=trust)", url); + + Properties properties = new Properties(); + properties.setProperty(ClientConfigProperties.USER.getKey(), user); // user + properties.setProperty(ClientConfigProperties.PASSWORD.getKey(), password); // password + properties.setProperty("ssl", "true"); // enable TLS even if the URL has no https scheme + // Accept the self-signed certificate and skip hostname verification. + properties.setProperty(ClientConfigProperties.SSL_MODE.getKey(), "trust"); // ssl_mode + + try (Connection connection = DriverManager.getConnection(url, properties); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT currentUser() AS user, version() AS version")) { + if (rs.next()) { + log.info("Connected (server certificate not verified) as '{}' to ClickHouse {}", + rs.getString("user"), rs.getString("version")); + } + } + } + /** * Connects to a ClickHouse server using a custom root CA certificate. * Use this when the server certificate is signed by a private CA (corporate CA, From 12e3b7415a2c16089b389b05dfac9f263eb8b88d Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Mon, 15 Jun 2026 11:14:22 -0700 Subject: [PATCH 3/3] updated tests and docs --- .../client/api/internal/HttpAPIClientHelper.java | 7 +++++++ .../src/test/java/com/clickhouse/client/ClientTests.java | 7 ++++--- .../java/com/clickhouse/client/HttpTransportTests.java | 9 +++++++++ docs/features.md | 8 ++++++-- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java b/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java index 67122b36b..4f5698537 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java @@ -173,6 +173,13 @@ public SSLContext createSSLContext(Map configuration) { final String sslCertificate = (String) configuration.get(ClientConfigProperties.SSL_CERTIFICATE.getKey()); final String sslKey = (String) configuration.get(ClientConfigProperties.SSL_KEY.getKey()); + // This method is only reached when a secure (https) endpoint is configured, so SSLMode.Disabled + // contradicts the endpoint scheme. The mode does not turn encryption off - the scheme decides it. + if (sslMode == SSLMode.Disabled) { + throw new ClientMisconfigurationException("SSL mode '" + SSLMode.Disabled + + "' cannot be used with a secure (https) endpoint. Use SSLMode.Trust to trust all certificates or use plain HTTP"); + } + if (sslMode == SSLMode.Trust) { // Server certificate is not validated. Trust material (trust store or CA certificate) // is not needed, but client certificate and key are still applied for mTLS. diff --git a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java index eaa675349..862dcb12a 100644 --- a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java @@ -332,7 +332,7 @@ public void testDefaultSettings() { Assert.assertEquals(config.get(p.getKey()), p.getDefaultValue(), "Default value doesn't match"); } } - Assert.assertEquals(config.size(), 34); // to check everything is set. Increment when new added. + Assert.assertEquals(config.size(), 35); // to check everything is set. Increment when new added. } try (Client client = new Client.Builder() @@ -365,7 +365,7 @@ public void testDefaultSettings() { .setSocketSndbuf(100000) .build()) { Map config = client.getConfiguration(); - Assert.assertEquals(config.size(), 35); // to check everything is set. Increment when new added. + Assert.assertEquals(config.size(), 36); // to check everything is set. Increment when new added. Assert.assertEquals(config.get(ClientConfigProperties.DATABASE.getKey()), "mydb"); Assert.assertEquals(config.get(ClientConfigProperties.MAX_EXECUTION_TIME.getKey()), "10"); Assert.assertEquals(config.get(ClientConfigProperties.COMPRESSION_LZ4_UNCOMPRESSED_BUF_SIZE.getKey()), "300000"); @@ -389,6 +389,7 @@ public void testDefaultSettings() { Assert.assertEquals(config.get(ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.getKey()), "20000"); Assert.assertEquals(config.get(ClientConfigProperties.SOCKET_RCVBUF_OPT.getKey()), "100000"); Assert.assertEquals(config.get(ClientConfigProperties.SOCKET_SNDBUF_OPT.getKey()), "100000"); + Assert.assertEquals(config.get(ClientConfigProperties.SSL_MODE.getKey()), "Strict"); } } @@ -432,7 +433,7 @@ public void testWithOldDefaults() { Assert.assertEquals(config.get(p.getKey()), p.getDefaultValue(), "Default value doesn't match"); } } - Assert.assertEquals(config.size(), 34); // to check everything is set. Increment when new added. + Assert.assertEquals(config.size(), 35); // to check everything is set. Increment when new added. } } diff --git a/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java b/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java index c91e1f55c..7b2bdb569 100644 --- a/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java @@ -389,6 +389,15 @@ public void testSSLModeDisabled() { } catch (Exception e) { Assert.fail("Disabled SSL mode should work with a plain HTTP endpoint", e); } + + ClickHouseNode secureServer = getSecureServer(ClickHouseProtocol.HTTP); + // Disabled mode contradicts a secure (https) endpoint - the scheme decides encryption, not the mode + Assert.expectThrows(ClientMisconfigurationException.class, () -> new Client.Builder() + .addEndpoint("https://localhost:" + secureServer.getPort()) + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()) + .setSSLMode(SSLMode.Disabled) + .build()); } @Test(groups = { "integration" }) diff --git a/docs/features.md b/docs/features.md index be63e9f99..13bb50807 100644 --- a/docs/features.md +++ b/docs/features.md @@ -5,7 +5,8 @@ This document lists stable, user-visible behavior in `client-v2` and `jdbc-v2` t ## `client-v2` - HTTP and HTTPS connectivity: Connects to ClickHouse over HTTP(S), supports endpoint paths, and exposes a basic `ping` health check. -- TLS configuration: Supports trust stores, client certificates/keys, SSL certificate authentication, and SNI for HTTPS connections. +- TLS configuration: Supports trust stores, client certificates/keys, SSL certificate authentication, and SNI for HTTPS connections. Trust material (root CA and client certificate/key) can be supplied either as a file path or directly as PEM content. +- SSL verification modes: `Client.Builder.setSSLMode(SSLMode)` (or the `ssl_mode` property) controls how strictly the server identity is verified on secure connections: `Disabled` (SSL not used; plain protocols only), `Trust` (encrypt but accept any server certificate and skip hostname verification, while still applying a client certificate/key for mTLS if configured), `VerifyCa` (validate the certificate chain but skip hostname verification), and `Strict` (full chain and hostname verification, default). - Authentication modes: Supports username/password credentials, ClickHouse auth headers, bearer tokens, and optional HTTP Basic authentication. - Runtime credential updates: Existing `Client` instances can update username/password or bearer-token credentials for subsequent requests without rebuilding the client. - Proxy support: Can send requests through configured HTTP proxies, including proxy credentials. @@ -41,13 +42,15 @@ Compatibility-sensitive traits: - `Geometry` handling is shape-sensitive: supported values are 1D through 4D Java arrays representing the nested geometry variants, and unsupported shapes or non-array values are rejected during serialization. - `Geometry` write inference is dimension-based rather than fully type-specific: point, ring/line string, polygon/multi-line string, and multi-polygon are selected from array depth, so writing `Geometry` cannot currently distinguish `Ring` from `LineString` or `Polygon` from `MultiLineString`. - Session precedence is part of the contract: client session defaults apply to each request, operation settings may override them, and only the client `session_id` is mutable at runtime while other client session properties remain fixed for the lifetime of the client. +- SSL mode behavior is compatibility-sensitive: the default is `Strict`. `ssl_mode` does not enable or disable encryption - the endpoint scheme decides that. `Disabled` is only valid with a plain `http://` endpoint; combining it with an `https://` endpoint throws `ClientMisconfigurationException`. A CA certificate and a trust store cannot be configured together (the CA certificate must be imported into the trust store), and that combination also throws `ClientMisconfigurationException`. When reading the `ssl_mode` value through the client configuration map, enum names are matched case-sensitively (`Disabled`, `Trust`, `VerifyCa`, `Strict`). +- Certificate-as-content support is compatibility-sensitive: any certificate or key value containing a PEM begin marker (`-----BEGIN`) is treated as inline PEM content, otherwise it is treated as a file path (also searched in the home directory and on the classpath). ## `jdbc-v2` - JDBC driver registration: Registers through the standard JDBC service mechanism and is available through `DriverManager`. - JDBC URL parsing: Accepts `jdbc:clickhouse:` and `jdbc:ch:` URLs with host, port, optional HTTP path, optional database, and query parameters. -- SSL URL support: Supports HTTPS connections through URL and property configuration, including default protocol and port handling. +- SSL URL support: Supports HTTPS connections through URL and property configuration, including default protocol and port handling. The `ssl_mode` property selects the verification strictness (`disabled`, `trust`, `verifyca`, `strict`); values are case-insensitive and the traditional JDBC value `none` is accepted as an alias for `trust`. Root CA and client certificate/key may be supplied as a file path or as inline PEM content. - Driver and client properties: Separates JDBC-specific properties from passthrough client options used by the underlying `client-v2` transport. - DataSource support: Provides a JDBC `DataSource` implementation backed by the same driver configuration model. - Connection lifecycle: Supports connection close, validity checks, ping-based health checks, and network timeout management. @@ -85,4 +88,5 @@ Compatibility-sensitive traits: - `getString()` formatting for temporal values is stable output: `Date` uses `yyyy-MM-dd`, `DateTime` uses `yyyy-MM-dd HH:mm:ss`, and `DateTime64` preserves fractional precision, all interpreted in server timezone context where applicable. - Date and timestamp setters with `Calendar` are timezone-sensitive by design. Preserving the current day-shift and instant-preserving behavior is important for compatibility. - `setObject()` temporal behavior is specific and should not drift: `LocalDateTime` and `Instant` are rendered through `fromUnixTimestamp64Nano(...)`, while `Timestamp` and `Date` use quoted textual forms. +- JDBC `ssl_mode` handling is compatibility-sensitive: values are case-insensitive, `none` is aliased to `trust` (the no-verification mode), and an unrecognized value throws `SQLException` during connection configuration. The normalized canonical mode name is forwarded to the underlying `client-v2` transport. - INSERT result semantics depend on server-side `async_insert` and `wait_for_async_insert`. The driver does not override these settings, so it follows whatever the server profile or user configuration sets. When `async_insert=1` and `wait_for_async_insert=0`, `Statement.executeUpdate(...)` and `PreparedStatement.executeUpdate(...)` may return `0` (or an under-counted value), and parsing/data errors in the INSERT body may not be reported synchronously as a `SQLException`. Set `async_insert=0` (or `wait_for_async_insert=1`) per connection or statement to restore synchronous row counts and error reporting.