Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions client-v2/src/main/java/com/clickhouse/client/api/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
Expand Down Expand Up @@ -785,6 +786,21 @@ public Builder setSSLMode(SSLMode sslMode) {
return this;
}

/**
* Restricts the TLS cipher suites the client may negotiate on secure connections. When set, only
* the listed cipher suites are enabled on the SSL socket (subject to what the JVM and the server
* support); when not set, the JVM defaults are used. Suite names use the standard JSSE names, for
* example {@code TLS_AES_256_GCM_SHA384}.
*
* @param cipherSuites cipher suite names to enable
* @return same instance of the builder
*/
public Builder setSSLCipherSuites(String... cipherSuites) {
this.configuration.put(ClientConfigProperties.SSL_CIPHER_SUITES.getKey(),
ClientConfigProperties.commaSeparated(Arrays.asList(cipherSuites)));
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,13 @@ public enum ClientConfigProperties {

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

/**
* Comma-separated list of TLS cipher suites the client is allowed to negotiate on secure connections.
* When set, only these cipher suites are enabled on the SSL socket (subject to what the JVM and server
* support); when unset, the JVM defaults are used.
*/
SSL_CIPHER_SUITES("ssl_cipher_suites", List.class),

RETRY_ON_FAILURE("retry", Integer.class, "3"),

INPUT_OUTPUT_FORMAT("format", ClickHouseFormat.class),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@
return phccm;
}

public CloseableHttpClient createHttpClient(boolean initSslContext, Map<String, Object> configuration) {

Check warning on line 280 in client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

A "Brain Method" was detected. Refactor it to reduce at least one of the following metrics: LOC from 68 to 64, Complexity from 23 to 14, Nesting Level from 3 to 2, Number of Variables from 27 to 6.

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AZ9HuUVmAixZi3ET3ZHa&open=AZ9HuUVmAixZi3ET3ZHa&pullRequest=2919
// Top Level builders
HttpClientBuilder clientBuilder = HttpClientBuilder.create();
SSLContext sslContext = initSslContext ? createSSLContext(configuration) : null;
Expand All @@ -288,8 +288,17 @@
// Trust and VerifyCa skip hostname verification. The same applies when a custom SNI is
// set because the connection hostname will not match the certificate.
boolean trustAllHostnames = sslMode == SSLMode.TRUST || sslMode == SSLMode.VERIFY_CA;
if (socketSNI != null && !socketSNI.trim().isEmpty() || trustAllHostnames) {
sslConnectionSocketFactory = new CustomSSLConnectionFactory(socketSNI, sslContext, (hostname, session) -> true);
boolean hasSNI = socketSNI != null && !socketSNI.trim().isEmpty();
List<String> cipherSuites = ClientConfigProperties.SSL_CIPHER_SUITES.getOrDefault(configuration);
String[] enabledCipherSuites = cipherSuites == null || cipherSuites.isEmpty()
? null : cipherSuites.toArray(new String[0]);
if (hasSNI || trustAllHostnames || enabledCipherSuites != null) {
// Skip hostname verification only for trust-all modes or when a custom SNI is used (the
// connection hostname would not match the certificate); otherwise a null verifier makes the
// factory fall back to the JDK/HttpClient default verifier, keeping STRICT verification.
HostnameVerifier hostnameVerifier = trustAllHostnames || hasSNI ? (hostname, session) -> true : null;

Check failure on line 299 in client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Enable server hostname verification on this SSL/TLS connection.

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AZ9HuUVmAixZi3ET3ZHZ&open=AZ9HuUVmAixZi3ET3ZHZ&pullRequest=2919
sslConnectionSocketFactory = new CustomSSLConnectionFactory(socketSNI, sslContext, hostnameVerifier,
enabledCipherSuites);
} else {
sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext);
}
Expand Down Expand Up @@ -1066,7 +1075,15 @@
private final SNIHostName defaultSNI;

public CustomSSLConnectionFactory(String defaultSNI, SSLContext sslContext, HostnameVerifier hostnameVerifier) {
super(sslContext, hostnameVerifier);
this(defaultSNI, sslContext, hostnameVerifier, null);
}

public CustomSSLConnectionFactory(String defaultSNI, SSLContext sslContext, HostnameVerifier hostnameVerifier,
String[] supportedCipherSuites) {
// supportedProtocols is left as null (JDK defaults are used); supportedCipherSuites, when
// provided, restricts the cipher suites the base factory enables on each socket. A null
// hostnameVerifier makes the base factory fall back to its default verifier.
super(sslContext, null, supportedCipherSuites, hostnameVerifier);
this.defaultSNI = defaultSNI == null || defaultSNI.trim().isEmpty() ? null : new SNIHostName(defaultSNI);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
import org.testng.annotations.Test;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

public class ClientBuilderTest {

Expand Down Expand Up @@ -86,6 +88,57 @@ public void testSslModeInvalidValueRejected() {
.build());
}

@Test
public void testSetSSLCipherSuitesStoredInConfiguration() throws Exception {
try (Client client = new Client.Builder()
.addEndpoint("https://localhost:8443")
.setUsername("default")
.setPassword("")
.setSSLCipherSuites("TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256")
.build()) {
Assert.assertEquals(extractConfiguration(client).get(ClientConfigProperties.SSL_CIPHER_SUITES.getKey()),
Arrays.asList("TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256"),
"Cipher suites set via the builder should be stored as a parsed list");
}
}

@Test
public void testSSLCipherSuitesViaSetOptionParsedAsList() throws Exception {
// The comma-separated string form is the path used by URL/JDBC properties.
try (Client client = new Client.Builder()
.addEndpoint("https://localhost:8443")
.setUsername("default")
.setPassword("")
.setOption(ClientConfigProperties.SSL_CIPHER_SUITES.getKey(),
"TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256")
.build()) {
Assert.assertEquals(extractConfiguration(client).get(ClientConfigProperties.SSL_CIPHER_SUITES.getKey()),
Arrays.asList("TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256"),
"Comma-separated cipher suites should be parsed into a list");
}
}

@Test
public void testClientBuildsWithCipherSuitesOverHttps() {
// Exercises the HTTPS connection-socket-factory path with cipher suites configured (STRICT mode,
// no SNI): the client must build without error.
try (Client client = new Client.Builder()
.addEndpoint("https://localhost:8443")
.setUsername("default")
.setPassword("")
.setSSLCipherSuites("TLS_AES_256_GCM_SHA384")
.build()) {
Assert.assertNotNull(client);
}
}

@SuppressWarnings("unchecked")
private static Map<String, Object> extractConfiguration(Client client) throws Exception {
Field configField = Client.class.getDeclaredField("configuration");
configField.setAccessible(true);
return (Map<String, Object>) configField.get(client);
}

private static String extractFirstEndpointUri(Client client) throws Exception {
Field endpointsField = Client.class.getDeclaredField("endpoints");
endpointsField.setAccessible(true);
Expand Down
3 changes: 2 additions & 1 deletion docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This document lists stable, user-visible behavior in `client-v2` and `jdbc-v2` t
- 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. 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` (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).
- 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`.
- 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.
Expand Down Expand Up @@ -53,7 +54,7 @@ Compatibility-sensitive traits:

- 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. 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.
- 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.
- 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
* <li>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.</li>
* <li>Restricting the negotiated TLS cipher suites with
* {@link Client.Builder#setSSLCipherSuites(String...)} - useful to enforce a stronger or
* compliance-mandated set of cipher suites instead of the JVM defaults.</li>
* </ul>
*
* <p>More SSL examples (mTLS, trust stores, SNI) will be added to this class later.</p>
Expand Down Expand Up @@ -70,6 +73,7 @@ public static void main(String[] args) {
if (rootCert != null) {
connectWithCustomRootCertificate(endpoint, database, user, password, rootCert);
connectWithRootCertificateAsString(endpoint, database, user, password, rootCert);
connectWithCipherSuites(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.");
Expand All @@ -88,6 +92,8 @@ public static void main(String[] args) {
SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
connectWithRootCertificateAsString(server.getEndpoint(), database,
SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
connectWithCipherSuites(server.getEndpoint(), database,
SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
} catch (Exception e) {
log.error("Failed to run the SSL example against a local Docker server", e);
}
Expand Down Expand Up @@ -192,6 +198,38 @@ static void connectWithRootCertificateAsString(String endpoint, String database,
}
}

/**
* Connects while restricting the TLS cipher suites the client is allowed to negotiate, using
* {@link Client.Builder#setSSLCipherSuites(String...)}. Only the listed suites are enabled on the
* socket (subject to what the JVM and the server support); this is useful to enforce a stronger or
* compliance-mandated set of cipher suites rather than relying on the JVM defaults.
*
* <p>The CA certificate is still used to verify the server, and hostname verification stays enabled -
* cipher-suite selection is independent of the trust configuration and the SSL mode. The suites below
* cover TLS 1.3 and TLS 1.2; keep at least one suite the server actually supports, or the handshake
* fails.</p>
*/
static void connectWithCipherSuites(String endpoint, String database, String user, String password,
String rootCert) {
log.info("Connecting to {} with a restricted set of TLS cipher suites", endpoint);
try (Client client = new Client.Builder()
.addEndpoint(endpoint)
.setUsername(user)
.setPassword(password)
.setDefaultDatabase(database)
.setRootCertificate(rootCert)
// Restrict negotiation to these cipher suites (TLS 1.3 and TLS 1.2).
.setSSLCipherSuites("TLS_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384")
.build()) {

List<GenericRecord> rows = client.queryAll("SELECT currentUser() AS user, version() AS version");
log.info("Connected securely (restricted cipher suites) as '{}' to ClickHouse {}",
rows.get(0).getString("user"), rows.get(0).getString("version"));
} catch (Exception e) {
log.error("Secure connection with restricted cipher suites failed", e);
}
}

private static String trimToNull(String value) {
if (value == null) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
* 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.</li>
* <li>Restricting the negotiated TLS cipher suites with the {@code ssl_cipher_suites} connection
* property (a comma-separated list) - useful to enforce a stronger or compliance-mandated set of
* cipher suites instead of the JVM defaults.</li>
* </ul>
*
* <p>More SSL examples (mTLS, trust stores, SNI) will be added to this class later.</p>
Expand Down Expand Up @@ -72,6 +75,7 @@ public static void main(String[] args) {
if (rootCert != null) {
connectWithCustomRootCertificate(url, user, password, rootCert);
connectWithRootCertificateAsString(url, user, password, rootCert);
connectWithCipherSuites(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.");
Expand All @@ -93,6 +97,8 @@ public static void main(String[] args) {
SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
connectWithRootCertificateAsString(server.getJdbcUrl(),
SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
connectWithCipherSuites(server.getJdbcUrl(),
SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
} catch (Exception e) {
log.error("Failed to run the SSL example against a local Docker server", e);
Runtime.getRuntime().exit(-1);
Expand Down Expand Up @@ -196,6 +202,39 @@ static void connectWithRootCertificateAsString(String url, String user, String p
}
}

/**
* Connects while restricting the TLS cipher suites the driver is allowed to negotiate, using the
* {@code ssl_cipher_suites} connection property (a comma-separated list). Only the listed suites are
* enabled on the socket (subject to what the JVM and the server support); this is useful to enforce a
* stronger or compliance-mandated set of cipher suites rather than relying on the JVM defaults.
*
* <p>The CA certificate is still used to verify the server and hostname verification stays enabled -
* cipher-suite selection is independent of the trust configuration and {@code ssl_mode}. Keep at least
* one suite the server actually supports, or the handshake fails.</p>
*/
static void connectWithCipherSuites(String url, String user, String password, String rootCert)
throws SQLException {
log.info("Connecting to {} with a restricted set of TLS cipher suites", 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
properties.setProperty(ClientConfigProperties.CA_CERTIFICATE.getKey(), rootCert); // sslrootcert
// Restrict negotiation to these cipher suites (TLS 1.3 and TLS 1.2), comma-separated.
properties.setProperty(ClientConfigProperties.SSL_CIPHER_SUITES.getKey(),
"TLS_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"); // ssl_cipher_suites

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 securely (restricted cipher suites) as '{}' to ClickHouse {}",
rs.getString("user"), rs.getString("version"));
}
}
}

private static String trimToNull(String value) {
if (value == null) {
return null;
Expand Down
Loading
Loading