Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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();
Expand Down
28 changes: 28 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 @@ -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;
Expand Down Expand Up @@ -755,6 +756,33 @@ public Builder setClientKey(String path) {
return this;
}

/**
* Defines how strictly the client verifies a server identity on secure connections.
*
* <p>Supported modes:</p>
* <ul>
* <li>{@link SSLMode#Disabled} - SSL is not used; only meaningful with plain protocols</li>
* <li>{@link SSLMode#Trust} - encrypt, but accept any server certificate and skip
* hostname verification</li>
* <li>{@link SSLMode#VerifyCa} - validate the server certificate chain, but skip
* hostname verification</li>
* <li>{@link SSLMode#Strict} - full verification of the certificate chain and the
* hostname (default)</li>
* </ul>
*
* <p>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 <b>not</b> make the client use
* encryption on a plain HTTP endpoint: the endpoint scheme always decides whether the
* connection is encrypted.</p>
*
* @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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>The mode affects only connections that are already using a secure transport (for example,
* an {@code https://} endpoint). It does <b>not</b> enable encryption for plain protocols - an
* {@code http://} endpoint stays unencrypted whatever the mode is.</p>
*
* <p>Modes from the least to the most strict:</p>
* <ul>
* <li>{@link #Disabled} - SSL is not used. Plain protocols only.</li>
* <li>{@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.</li>
* <li>{@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.</li>
* <li>{@link #Strict} - full verification (default): certificate chain is validated and the
* hostname must match the certificate.</li>
* </ul>
*/
public enum SSLMode {

/**
* SSL is not used. Connection is not encrypted.
*/
Disabled,

Check failure on line 28 in client-v2/src/main/java/com/clickhouse/client/api/enums/SSLMode.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this constant name to match the regular expression '^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$'.

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AZ61JW3HZBZf2NUkYWje&open=AZ61JW3HZBZf2NUkYWje&pullRequest=2874

/**
* Encryption without verification: any server certificate is accepted and
* the hostname is not verified.
*/
Trust,

Check failure on line 34 in client-v2/src/main/java/com/clickhouse/client/api/enums/SSLMode.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this constant name to match the regular expression '^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$'.

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AZ61JW3HZBZf2NUkYWjf&open=AZ61JW3HZBZf2NUkYWjf&pullRequest=2874

/**
* Server certificate chain is validated, but the hostname is not verified.
*/
VerifyCa,

Check failure on line 39 in client-v2/src/main/java/com/clickhouse/client/api/enums/SSLMode.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this constant name to match the regular expression '^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$'.

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AZ61JW3HZBZf2NUkYWjg&open=AZ61JW3HZBZf2NUkYWjg&pullRequest=2874

/**
* Full verification: certificate chain is validated and the hostname must match
* the certificate. Default mode.
*/
Strict;

Check failure on line 45 in client-v2/src/main/java/com/clickhouse/client/api/enums/SSLMode.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this constant name to match the regular expression '^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$'.

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AZ61JW3HZBZf2NUkYWjh&open=AZ61JW3HZBZf2NUkYWjh&pullRequest=2874

/**
* 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 + "'");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -165,11 +167,33 @@
} 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) {

// 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.
try {
sslContext = sslContextProvider.getSslContextFromCerts(ClickHouseSslMode.NONE,

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this use of "ClickHouseSslMode"; it is deprecated.

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AZ61JW22ZBZf2NUkYWjc&open=AZ61JW22ZBZf2NUkYWjc&pullRequest=2874

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this use of "ClickHouseSslMode"; it is deprecated.

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AZ61JW22ZBZf2NUkYWjd&open=AZ61JW22ZBZf2NUkYWjd&pullRequest=2874
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,
Expand Down Expand Up @@ -272,7 +296,11 @@
LayeredConnectionSocketFactory sslConnectionSocketFactory;
if (sslContext != null) {
String socketSNI = (String)configuration.get(ClientConfigProperties.SSL_SOCKET_SNI.getKey());
if (socketSNI != null && !socketSNI.trim().isEmpty()) {
SSLMode sslMode = ClientConfigProperties.SSL_MODE.getOrDefault(configuration);
// 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.VerifyCa;
if (socketSNI != null && !socketSNI.trim().isEmpty() || trustAllHostnames) {
sslConnectionSocketFactory = new CustomSSLConnectionFactory(socketSNI, sslContext, (hostname, session) -> true);
} else {
sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -365,7 +365,7 @@ public void testDefaultSettings() {
.setSocketSndbuf(100000)
.build()) {
Map<String, String> 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");
Expand All @@ -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");
}
}

Expand Down Expand Up @@ -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.
}
}

Expand Down
Loading
Loading