Skip to content

Commit 1d53750

Browse files
Support TLS cipher suite selection in client-v2 and jdbc-v2
Neither client could restrict the negotiated TLS cipher suites (#2882): the SSL configuration exposed trust store, CA cert, client cert/key, SNI and mode, but nothing for cipher suites, so applications could not enforce a stronger or compliance-mandated set. client-v2: - New ClientConfigProperties.SSL_CIPHER_SUITES ("ssl_cipher_suites"), a comma-separated list parsed into a List<String>. - Client.Builder.setSSLCipherSuites(String...) to set it programmatically. - HttpAPIClientHelper applies the configured suites to the SSL socket via the connection socket factory. CustomSSLConnectionFactory gains a constructor that passes supportedCipherSuites to the base SSLConnectionSocketFactory. Existing hostname-verification behavior is unchanged: verification is skipped only for TRUST/VERIFY_CA or when a custom SNI is set, and the default verifier is kept otherwise (so STRICT + cipher suites still verifies the hostname). jdbc-v2 needs no code change: ssl_cipher_suites is forwarded to client-v2 as a regular string property (via URL or Properties). Tests: client-v2 ClientBuilderTest (builder + comma-separated string parse to a list, and an HTTPS client builds with cipher suites configured) and jdbc-v2 JdbcConfigurationTest (ssl_cipher_suites forwarded via Properties and URL). Examples (client-v2 and jdbc SSLExamples) and docs/features.md updated. Fixes: #2882 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 8ce79b1 commit 1d53750

8 files changed

Lines changed: 193 additions & 4 deletions

File tree

client-v2/src/main/java/com/clickhouse/client/api/Client.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import java.time.ZoneId;
6060
import java.time.temporal.ChronoUnit;
6161
import java.util.ArrayList;
62+
import java.util.Arrays;
6263
import java.util.Collection;
6364
import java.util.Collections;
6465
import java.util.HashMap;
@@ -785,6 +786,21 @@ public Builder setSSLMode(SSLMode sslMode) {
785786
return this;
786787
}
787788

789+
/**
790+
* Restricts the TLS cipher suites the client may negotiate on secure connections. When set, only
791+
* the listed cipher suites are enabled on the SSL socket (subject to what the JVM and the server
792+
* support); when not set, the JVM defaults are used. Suite names use the standard JSSE names, for
793+
* example {@code TLS_AES_256_GCM_SHA384}.
794+
*
795+
* @param cipherSuites cipher suite names to enable
796+
* @return same instance of the builder
797+
*/
798+
public Builder setSSLCipherSuites(String... cipherSuites) {
799+
this.configuration.put(ClientConfigProperties.SSL_CIPHER_SUITES.getKey(),
800+
ClientConfigProperties.commaSeparated(Arrays.asList(cipherSuites)));
801+
return this;
802+
}
803+
788804
/**
789805
* Configure client to use server timezone for date/datetime columns. Default is true.
790806
* If this options is selected then server timezone should be set as well.

client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,13 @@ public enum ClientConfigProperties {
118118

119119
SSL_MODE("ssl_mode", SSLMode.class, SSLMode.STRICT.name()),
120120

121+
/**
122+
* Comma-separated list of TLS cipher suites the client is allowed to negotiate on secure connections.
123+
* When set, only these cipher suites are enabled on the SSL socket (subject to what the JVM and server
124+
* support); when unset, the JVM defaults are used.
125+
*/
126+
SSL_CIPHER_SUITES("ssl_cipher_suites", List.class),
127+
121128
RETRY_ON_FAILURE("retry", Integer.class, "3"),
122129

123130
INPUT_OUTPUT_FORMAT("format", ClickHouseFormat.class),

client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,8 +288,17 @@ public CloseableHttpClient createHttpClient(boolean initSslContext, Map<String,
288288
// Trust and VerifyCa skip hostname verification. The same applies when a custom SNI is
289289
// set because the connection hostname will not match the certificate.
290290
boolean trustAllHostnames = sslMode == SSLMode.TRUST || sslMode == SSLMode.VERIFY_CA;
291-
if (socketSNI != null && !socketSNI.trim().isEmpty() || trustAllHostnames) {
292-
sslConnectionSocketFactory = new CustomSSLConnectionFactory(socketSNI, sslContext, (hostname, session) -> true);
291+
boolean hasSNI = socketSNI != null && !socketSNI.trim().isEmpty();
292+
List<String> cipherSuites = ClientConfigProperties.SSL_CIPHER_SUITES.getOrDefault(configuration);
293+
String[] enabledCipherSuites = cipherSuites == null || cipherSuites.isEmpty()
294+
? null : cipherSuites.toArray(new String[0]);
295+
if (hasSNI || trustAllHostnames || enabledCipherSuites != null) {
296+
// Skip hostname verification only for trust-all modes or when a custom SNI is used (the
297+
// connection hostname would not match the certificate); otherwise a null verifier makes the
298+
// factory fall back to the JDK/HttpClient default verifier, keeping STRICT verification.
299+
HostnameVerifier hostnameVerifier = trustAllHostnames || hasSNI ? (hostname, session) -> true : null;
300+
sslConnectionSocketFactory = new CustomSSLConnectionFactory(socketSNI, sslContext, hostnameVerifier,
301+
enabledCipherSuites);
293302
} else {
294303
sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext);
295304
}
@@ -1066,7 +1075,15 @@ public static class CustomSSLConnectionFactory extends SSLConnectionSocketFactor
10661075
private final SNIHostName defaultSNI;
10671076

10681077
public CustomSSLConnectionFactory(String defaultSNI, SSLContext sslContext, HostnameVerifier hostnameVerifier) {
1069-
super(sslContext, hostnameVerifier);
1078+
this(defaultSNI, sslContext, hostnameVerifier, null);
1079+
}
1080+
1081+
public CustomSSLConnectionFactory(String defaultSNI, SSLContext sslContext, HostnameVerifier hostnameVerifier,
1082+
String[] supportedCipherSuites) {
1083+
// supportedProtocols is left as null (JDK defaults are used); supportedCipherSuites, when
1084+
// provided, restricts the cipher suites the base factory enables on each socket. A null
1085+
// hostnameVerifier makes the base factory fall back to its default verifier.
1086+
super(sslContext, null, supportedCipherSuites, hostnameVerifier);
10701087
this.defaultSNI = defaultSNI == null || defaultSNI.trim().isEmpty() ? null : new SNIHostName(defaultSNI);
10711088
}
10721089

client-v2/src/test/java/com/clickhouse/client/api/ClientBuilderTest.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
import org.testng.annotations.Test;
77

88
import java.lang.reflect.Field;
9+
import java.util.Arrays;
910
import java.util.List;
11+
import java.util.Map;
1012

1113
public class ClientBuilderTest {
1214

@@ -86,6 +88,57 @@ public void testSslModeInvalidValueRejected() {
8688
.build());
8789
}
8890

91+
@Test
92+
public void testSetSSLCipherSuitesStoredInConfiguration() throws Exception {
93+
try (Client client = new Client.Builder()
94+
.addEndpoint("https://localhost:8443")
95+
.setUsername("default")
96+
.setPassword("")
97+
.setSSLCipherSuites("TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256")
98+
.build()) {
99+
Assert.assertEquals(extractConfiguration(client).get(ClientConfigProperties.SSL_CIPHER_SUITES.getKey()),
100+
Arrays.asList("TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256"),
101+
"Cipher suites set via the builder should be stored as a parsed list");
102+
}
103+
}
104+
105+
@Test
106+
public void testSSLCipherSuitesViaSetOptionParsedAsList() throws Exception {
107+
// The comma-separated string form is the path used by URL/JDBC properties.
108+
try (Client client = new Client.Builder()
109+
.addEndpoint("https://localhost:8443")
110+
.setUsername("default")
111+
.setPassword("")
112+
.setOption(ClientConfigProperties.SSL_CIPHER_SUITES.getKey(),
113+
"TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256")
114+
.build()) {
115+
Assert.assertEquals(extractConfiguration(client).get(ClientConfigProperties.SSL_CIPHER_SUITES.getKey()),
116+
Arrays.asList("TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256"),
117+
"Comma-separated cipher suites should be parsed into a list");
118+
}
119+
}
120+
121+
@Test
122+
public void testClientBuildsWithCipherSuitesOverHttps() {
123+
// Exercises the HTTPS connection-socket-factory path with cipher suites configured (STRICT mode,
124+
// no SNI): the client must build without error.
125+
try (Client client = new Client.Builder()
126+
.addEndpoint("https://localhost:8443")
127+
.setUsername("default")
128+
.setPassword("")
129+
.setSSLCipherSuites("TLS_AES_256_GCM_SHA384")
130+
.build()) {
131+
Assert.assertNotNull(client);
132+
}
133+
}
134+
135+
@SuppressWarnings("unchecked")
136+
private static Map<String, Object> extractConfiguration(Client client) throws Exception {
137+
Field configField = Client.class.getDeclaredField("configuration");
138+
configField.setAccessible(true);
139+
return (Map<String, Object>) configField.get(client);
140+
}
141+
89142
private static String extractFirstEndpointUri(Client client) throws Exception {
90143
Field endpointsField = Client.class.getDeclaredField("endpoints");
91144
endpointsField.setAccessible(true);

docs/features.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ This document lists stable, user-visible behavior in `client-v2` and `jdbc-v2` t
77
- HTTP and HTTPS connectivity: Connects to ClickHouse over HTTP(S), supports endpoint paths, and exposes a basic `ping` health check.
88
- 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.
99
- 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` (accept any server certificate and skip hostname verification; a configured trust store or CA certificate is ignored with a warning, while a client certificate/key is still applied for mTLS if configured), `VERIFY_CA` (validate the certificate chain but skip hostname verification), and `STRICT` (full chain and hostname verification, default).
10+
- TLS cipher suite selection: `Client.Builder.setSSLCipherSuites(String...)` (or the comma-separated `ssl_cipher_suites` property) restricts the cipher suites enabled on secure connections. When set, only the listed suites are enabled on the SSL socket (subject to JVM and server support); when unset, the JVM defaults apply. Cipher-suite selection is independent of the trust configuration and `ssl_mode`.
1011
- Authentication modes: Supports username/password credentials, ClickHouse auth headers, bearer tokens, and optional HTTP Basic authentication.
1112
- Runtime credential updates: Existing `Client` instances can update username/password or bearer-token credentials for subsequent requests without rebuilding the client.
1213
- Proxy support: Can send requests through configured HTTP proxies, including proxy credentials.
@@ -53,7 +54,7 @@ Compatibility-sensitive traits:
5354

5455
- JDBC driver registration: Registers through the standard JDBC service mechanism and is available through `DriverManager`.
5556
- JDBC URL parsing: Accepts `jdbc:clickhouse:` and `jdbc:ch:` URLs with host, port, optional HTTP path, optional database, and query parameters.
56-
- 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`, `verify_ca`, `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.
57+
- 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`, `verify_ca`, `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. The `ssl_cipher_suites` property (a comma-separated list) restricts the negotiated TLS cipher suites and is forwarded to the underlying `client-v2` transport.
5758
- Driver and client properties: Separates JDBC-specific properties from passthrough client options used by the underlying `client-v2` transport.
5859
- DataSource support: Provides a JDBC `DataSource` implementation backed by the same driver configuration model.
5960
- Connection lifecycle: Supports connection close, validity checks, ping-based health checks, and network timeout management.

examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/SSLExamples.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
* <li>Connecting to a server with a self-signed certificate without any trust material -
2727
* {@link SSLMode#TRUST} accepts any server certificate and skips hostname verification.
2828
* Use it only for testing or in fully trusted environments.</li>
29+
* <li>Restricting the negotiated TLS cipher suites with
30+
* {@link Client.Builder#setSSLCipherSuites(String...)} - useful to enforce a stronger or
31+
* compliance-mandated set of cipher suites instead of the JVM defaults.</li>
2932
* </ul>
3033
*
3134
* <p>More SSL examples (mTLS, trust stores, SNI) will be added to this class later.</p>
@@ -70,6 +73,7 @@ public static void main(String[] args) {
7073
if (rootCert != null) {
7174
connectWithCustomRootCertificate(endpoint, database, user, password, rootCert);
7275
connectWithRootCertificateAsString(endpoint, database, user, password, rootCert);
76+
connectWithCipherSuites(endpoint, database, user, password, rootCert);
7377
} else {
7478
log.info("chRootCert is not set - skipping the custom CA certificate examples. "
7579
+ "Pass the path to the CA certificate (PEM) that signed the server certificate to run them.");
@@ -88,6 +92,8 @@ public static void main(String[] args) {
8892
SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
8993
connectWithRootCertificateAsString(server.getEndpoint(), database,
9094
SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
95+
connectWithCipherSuites(server.getEndpoint(), database,
96+
SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
9197
} catch (Exception e) {
9298
log.error("Failed to run the SSL example against a local Docker server", e);
9399
}
@@ -192,6 +198,38 @@ static void connectWithRootCertificateAsString(String endpoint, String database,
192198
}
193199
}
194200

201+
/**
202+
* Connects while restricting the TLS cipher suites the client is allowed to negotiate, using
203+
* {@link Client.Builder#setSSLCipherSuites(String...)}. Only the listed suites are enabled on the
204+
* socket (subject to what the JVM and the server support); this is useful to enforce a stronger or
205+
* compliance-mandated set of cipher suites rather than relying on the JVM defaults.
206+
*
207+
* <p>The CA certificate is still used to verify the server, and hostname verification stays enabled -
208+
* cipher-suite selection is independent of the trust configuration and the SSL mode. The suites below
209+
* cover TLS 1.3 and TLS 1.2; keep at least one suite the server actually supports, or the handshake
210+
* fails.</p>
211+
*/
212+
static void connectWithCipherSuites(String endpoint, String database, String user, String password,
213+
String rootCert) {
214+
log.info("Connecting to {} with a restricted set of TLS cipher suites", endpoint);
215+
try (Client client = new Client.Builder()
216+
.addEndpoint(endpoint)
217+
.setUsername(user)
218+
.setPassword(password)
219+
.setDefaultDatabase(database)
220+
.setRootCertificate(rootCert)
221+
// Restrict negotiation to these cipher suites (TLS 1.3 and TLS 1.2).
222+
.setSSLCipherSuites("TLS_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384")
223+
.build()) {
224+
225+
List<GenericRecord> rows = client.queryAll("SELECT currentUser() AS user, version() AS version");
226+
log.info("Connected securely (restricted cipher suites) as '{}' to ClickHouse {}",
227+
rows.get(0).getString("user"), rows.get(0).getString("version"));
228+
} catch (Exception e) {
229+
log.error("Secure connection with restricted cipher suites failed", e);
230+
}
231+
}
232+
195233
private static String trimToNull(String value) {
196234
if (value == null) {
197235
return null;

examples/jdbc/src/main/java/com/clickhouse/examples/jdbc/SSLExamples.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
* the {@code ssl_mode=trust} connection property accepts any server certificate and skips
3232
* hostname verification ({@code ssl_mode=none} is accepted as an alias). Use it only for
3333
* testing or in fully trusted environments.</li>
34+
* <li>Restricting the negotiated TLS cipher suites with the {@code ssl_cipher_suites} connection
35+
* property (a comma-separated list) - useful to enforce a stronger or compliance-mandated set of
36+
* cipher suites instead of the JVM defaults.</li>
3437
* </ul>
3538
*
3639
* <p>More SSL examples (mTLS, trust stores, SNI) will be added to this class later.</p>
@@ -72,6 +75,7 @@ public static void main(String[] args) {
7275
if (rootCert != null) {
7376
connectWithCustomRootCertificate(url, user, password, rootCert);
7477
connectWithRootCertificateAsString(url, user, password, rootCert);
78+
connectWithCipherSuites(url, user, password, rootCert);
7579
} else {
7680
log.info("chRootCert is not set - skipping the custom CA certificate examples. "
7781
+ "Pass the path to the CA certificate (PEM) that signed the server certificate to run them.");
@@ -93,6 +97,8 @@ public static void main(String[] args) {
9397
SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
9498
connectWithRootCertificateAsString(server.getJdbcUrl(),
9599
SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
100+
connectWithCipherSuites(server.getJdbcUrl(),
101+
SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
96102
} catch (Exception e) {
97103
log.error("Failed to run the SSL example against a local Docker server", e);
98104
Runtime.getRuntime().exit(-1);
@@ -196,6 +202,39 @@ static void connectWithRootCertificateAsString(String url, String user, String p
196202
}
197203
}
198204

205+
/**
206+
* Connects while restricting the TLS cipher suites the driver is allowed to negotiate, using the
207+
* {@code ssl_cipher_suites} connection property (a comma-separated list). Only the listed suites are
208+
* enabled on the socket (subject to what the JVM and the server support); this is useful to enforce a
209+
* stronger or compliance-mandated set of cipher suites rather than relying on the JVM defaults.
210+
*
211+
* <p>The CA certificate is still used to verify the server and hostname verification stays enabled -
212+
* cipher-suite selection is independent of the trust configuration and {@code ssl_mode}. Keep at least
213+
* one suite the server actually supports, or the handshake fails.</p>
214+
*/
215+
static void connectWithCipherSuites(String url, String user, String password, String rootCert)
216+
throws SQLException {
217+
log.info("Connecting to {} with a restricted set of TLS cipher suites", url);
218+
219+
Properties properties = new Properties();
220+
properties.setProperty(ClientConfigProperties.USER.getKey(), user); // user
221+
properties.setProperty(ClientConfigProperties.PASSWORD.getKey(), password); // password
222+
properties.setProperty("ssl", "true"); // enable TLS even if the URL has no https scheme
223+
properties.setProperty(ClientConfigProperties.CA_CERTIFICATE.getKey(), rootCert); // sslrootcert
224+
// Restrict negotiation to these cipher suites (TLS 1.3 and TLS 1.2), comma-separated.
225+
properties.setProperty(ClientConfigProperties.SSL_CIPHER_SUITES.getKey(),
226+
"TLS_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"); // ssl_cipher_suites
227+
228+
try (Connection connection = DriverManager.getConnection(url, properties);
229+
Statement stmt = connection.createStatement();
230+
ResultSet rs = stmt.executeQuery("SELECT currentUser() AS user, version() AS version")) {
231+
if (rs.next()) {
232+
log.info("Connected securely (restricted cipher suites) as '{}' to ClickHouse {}",
233+
rs.getString("user"), rs.getString("version"));
234+
}
235+
}
236+
}
237+
199238
private static String trimToNull(String value) {
200239
if (value == null) {
201240
return null;

0 commit comments

Comments
 (0)