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
@@ -1,6 +1,22 @@
package com.clickhouse.client.config;

import java.io.*;
import com.clickhouse.client.ClickHouseConfig;
import com.clickhouse.client.ClickHouseSslContextProvider;
import com.clickhouse.data.ClickHouseUtils;

import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.KeyManagementException;
import java.security.KeyStore;
Expand All @@ -18,24 +34,30 @@
import java.util.Base64;
import java.util.Optional;

import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import com.clickhouse.client.ClickHouseConfig;
import com.clickhouse.client.ClickHouseSslContextProvider;
import com.clickhouse.data.ClickHouseUtils;

@Deprecated
public class ClickHouseDefaultSslContextProvider implements ClickHouseSslContextProvider {
static final String PEM_HEADER_PREFIX = "---BEGIN ";
static final String PEM_HEADER_SUFFIX = " PRIVATE KEY---";
static final String PEM_FOOTER_PREFIX = "---END ";

/** Standard PEM encapsulation boundary (RFC 7468). Present in any PEM content, never in a file path. */
static final String PEM_BEGIN_MARKER = "-----BEGIN";

/**
* Opens a stream over PEM material that may be supplied either as a file path (also searched in the home
* directory and on the classpath) or directly as PEM content.
*
* @param certOrContent file path or PEM content of a certificate or a private key
* @return stream over the PEM content
* @throws IOException when the value is a path and the file cannot be opened
*/
static InputStream getCertificateInputStream(String certOrContent) throws IOException {
if (certOrContent.contains(PEM_BEGIN_MARKER)) {
return new ByteArrayInputStream(certOrContent.getBytes(StandardCharsets.US_ASCII));
}
return ClickHouseUtils.getFileInputStream(certOrContent);
}

/**
* An insecure {@link javax.net.ssl.TrustManager}, that don't validate the
* certificate.
Expand Down Expand Up @@ -71,7 +93,7 @@ public static PrivateKey getPrivateKey(String keyFile)
String algorithm = (String) ClickHouseDefaults.SSL_KEY_ALGORITHM.getEffectiveDefaultValue();
StringBuilder builder = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(ClickHouseUtils.getFileInputStream(keyFile)))) {
new InputStreamReader(getCertificateInputStream(keyFile)))) {
String line = reader.readLine();
if (line != null) {
algorithm = getAlgorithm(line, algorithm);
Expand Down Expand Up @@ -102,7 +124,7 @@ public KeyStore getKeyStore(String cert, String key) throws NoSuchAlgorithmExcep
ClickHouseUtils.format("%s KeyStore not available", KeyStore.getDefaultType()));
}

try (InputStream in = ClickHouseUtils.getFileInputStream(cert)) {
try (InputStream in = getCertificateInputStream(cert)) {
CertificateFactory factory = CertificateFactory
.getInstance((String) ClickHouseDefaults.SSL_CERTIFICATE_TYPE.getEffectiveDefaultValue());
if (key == null || key.isEmpty()) {
Expand Down Expand Up @@ -132,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 @@ -150,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
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
package com.clickhouse.client.config;

import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.PrivateKey;

import com.clickhouse.data.ClickHouseUtils;

import org.testng.Assert;
import org.testng.annotations.Test;

public class ClickHouseDefaultSslContextProviderTest {
static String readTestResource(String name) throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (InputStream in = ClickHouseUtils.getFileInputStream(name)) {
byte[] buffer = new byte[2048];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
}
return new String(out.toByteArray(), StandardCharsets.US_ASCII);
}

@Test(groups = { "unit" })
public void testGetAlgorithm() {
Assert.assertEquals(ClickHouseDefaultSslContextProvider.getAlgorithm("", null), null);
Expand All @@ -19,4 +40,41 @@
// openssl genpkey -out pkey4test.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048
Assert.assertNotNull(ClickHouseDefaultSslContextProvider.getPrivateKey("pkey4test.pem"));
}

@Test(groups = { "unit" })
public void testGetCertificateInputStream() throws Exception {
String pemContent = readTestResource("client.crt");
try (InputStream in = ClickHouseDefaultSslContextProvider.getCertificateInputStream(pemContent)) {

Check warning on line 47 in clickhouse-client/src/test/java/com/clickhouse/client/config/ClickHouseDefaultSslContextProviderTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AZ61JW0AZBZf2NUkYWjP&open=AZ61JW0AZBZf2NUkYWjP&pullRequest=2874
byte[] buffer = new byte[pemContent.length()];
int read = in.read(buffer);
Assert.assertEquals(new String(buffer, 0, read, StandardCharsets.US_ASCII), pemContent);
}

try (InputStream in = ClickHouseDefaultSslContextProvider.getCertificateInputStream("client.crt")) {

Check warning on line 53 in clickhouse-client/src/test/java/com/clickhouse/client/config/ClickHouseDefaultSslContextProviderTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AZ61JW0AZBZf2NUkYWjQ&open=AZ61JW0AZBZf2NUkYWjQ&pullRequest=2874
Assert.assertTrue(in.read() != -1);
}

Assert.assertThrows(FileNotFoundException.class,
() -> ClickHouseDefaultSslContextProvider.getCertificateInputStream("non-existent.crt"));

Check warning on line 58 in clickhouse-client/src/test/java/com/clickhouse/client/config/ClickHouseDefaultSslContextProviderTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

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

@Test(groups = { "unit" })
public void testGetPrivateKeyFromPemContent() throws Exception {
PrivateKey fromFile = ClickHouseDefaultSslContextProvider.getPrivateKey("pkey4test.pem");

Check warning on line 63 in clickhouse-client/src/test/java/com/clickhouse/client/config/ClickHouseDefaultSslContextProviderTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

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

Check warning on line 64 in clickhouse-client/src/test/java/com/clickhouse/client/config/ClickHouseDefaultSslContextProviderTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AZ61JW0AZBZf2NUkYWjT&open=AZ61JW0AZBZf2NUkYWjT&pullRequest=2874
.getPrivateKey(readTestResource("pkey4test.pem"));
Assert.assertEquals(fromContent, fromFile);
}

@Test(groups = { "unit" })
public void testGetKeyStoreFromPemContent() throws Exception {
ClickHouseDefaultSslContextProvider provider = new ClickHouseDefaultSslContextProvider();

Check warning on line 71 in clickhouse-client/src/test/java/com/clickhouse/client/config/ClickHouseDefaultSslContextProviderTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

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

Check warning on line 71 in clickhouse-client/src/test/java/com/clickhouse/client/config/ClickHouseDefaultSslContextProviderTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

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

KeyStore trustStore = provider.getKeyStore(readTestResource("client.crt"), null);
Assert.assertNotNull(trustStore.getCertificate("cert1"));

KeyStore keyStore = provider.getKeyStore(readTestResource("some_user.crt"),
readTestResource("some_user.key"));
Assert.assertNotNull(keyStore.getKey("key", null));
}
}
12 changes: 12 additions & 0 deletions client-v2/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,18 @@
<version>1.5.7-6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.84</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>1.84</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
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 + "'");
}
}
Loading
Loading