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
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
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
package com.clickhouse.client.config;

import com.clickhouse.data.ClickHouseUtils;
import org.testng.Assert;
import org.testng.annotations.Test;

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

public class ClickHouseDefaultSslContextProviderTest {
static String readTestResource(String name) throws Exception {
try (InputStream in = ClickHouseUtils.getFileInputStream(name)) {
return new String(in.readAllBytes(), StandardCharsets.US_ASCII);
}
}

@Test(groups = { "unit" })
public void testGetAlgorithm() {
Assert.assertEquals(ClickHouseDefaultSslContextProvider.getAlgorithm("", null), null);
Expand All @@ -19,4 +32,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 39 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=AZ608VFschQzNLXlEKnz&open=AZ608VFschQzNLXlEKnz&pullRequest=2873
byte[] buffer = new byte[pemContent.length()];
int read = in.read(buffer);
Assert.assertEquals(new String(buffer, 0, read, StandardCharsets.US_ASCII), pemContent);
}
Comment on lines +39 to +43

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

Check warning on line 45 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=AZ608VFschQzNLXlEKn0&open=AZ608VFschQzNLXlEKn0&pullRequest=2873
Assert.assertTrue(in.read() != -1);
}

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

Check warning on line 50 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=AZ608VFschQzNLXlEKn1&open=AZ608VFschQzNLXlEKn1&pullRequest=2873
}

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

Check warning on line 55 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=AZ608VFschQzNLXlEKn2&open=AZ608VFschQzNLXlEKn2&pullRequest=2873
PrivateKey fromContent = ClickHouseDefaultSslContextProvider

Check warning on line 56 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=AZ608VFschQzNLXlEKn3&open=AZ608VFschQzNLXlEKn3&pullRequest=2873
.getPrivateKey(readTestResource("pkey4test.pem"));
Assert.assertEquals(fromContent, fromFile);
}

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

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=AZ608VFschQzNLXlEKn4&open=AZ608VFschQzNLXlEKn4&pullRequest=2873

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=AZ608VFschQzNLXlEKn5&open=AZ608VFschQzNLXlEKn5&pullRequest=2873

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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2186,7 +2186,7 @@ protected Client.Builder newClient() {
@DataProvider(name = "testCustomCaCertificateProvider")
public static Object[][] testCustomCaCertificateProvider() {
return new Object[][]{
// TODO: decide if we need to support certificates via string {true},
{true},
{false}};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
import com.clickhouse.client.api.query.GenericRecord;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

/**
Expand All @@ -15,6 +19,9 @@
* the CA certificate is passed with {@link Client.Builder#setRootCertificate(String)}.
* No trust store configuration is needed: the certificate is added to a trust store
* used only by this client, so the JVM default trust store stays untouched.</li>
* <li>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.</li>
* </ul>
*
* <p>More SSL examples (mTLS, trust stores, SNI) will be added to this class later.</p>
Expand Down Expand Up @@ -58,7 +65,9 @@ public static void main(String[] args) {
}

log.info("Running in standalone mode against {}:{}", host, port);
connectWithCustomRootCertificate("https://" + host + ":" + port, database, user, password, rootCert);
String endpoint = "https://" + host + ":" + port;
connectWithCustomRootCertificate(endpoint, database, user, password, rootCert);
connectWithRootCertificateAsString(endpoint, database, user, password, rootCert);
return;
}

Expand All @@ -69,6 +78,8 @@ public static void main(String[] args) {
try (SecureServerSupport server = SecureServerSupport.start(image)) {
connectWithCustomRootCertificate(server.getEndpoint(), database,
SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
connectWithRootCertificateAsString(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 @@ -100,6 +111,50 @@ static void connectWithCustomRootCertificate(String endpoint, String database, S
}
}

/**
* Same as {@link #connectWithCustomRootCertificate}, but the CA certificate is passed as PEM
* content instead of a file path. {@link Client.Builder#setRootCertificate(String)} accepts both:
* any value containing a {@code -----BEGIN ...-----} block is treated as PEM content.
*
* <p>This is handy when the certificate is delivered through an environment variable or
* a secret manager (e.g. a Kubernetes secret projected into {@code CLICKHOUSE_CA_CERT}),
* so the application never has to write it to disk:</p>
*
* <pre>{@code
* String caPem = System.getenv("CLICKHOUSE_CA_CERT");
* Client client = new Client.Builder().setRootCertificate(caPem)...
* }</pre>
*/
static void connectWithRootCertificateAsString(String endpoint, String database, String user, String password,
String rootCertPath) {
final String rootCertPem;
try {
// In a real application the PEM content would typically come from an env variable
// or a secret manager; here we simply read the file generated for this example.
rootCertPem = new String(Files.readAllBytes(Paths.get(rootCertPath)), StandardCharsets.US_ASCII);
} catch (IOException e) {
log.error("Failed to read the CA certificate from {}", rootCertPath, e);
return;
}

log.info("Connecting to {} using root CA certificate passed as a PEM string", endpoint);
try (Client client = new Client.Builder()
.addEndpoint(endpoint)
.setUsername(user)
.setPassword(password)
.setDefaultDatabase(database)
// PEM content, not a path - detected by the "-----BEGIN" marker.
.setRootCertificate(rootCertPem)
.build()) {

List<GenericRecord> rows = client.queryAll("SELECT currentUser() AS user, version() AS version");
log.info("Connected securely (CA cert as string) as '{}' to ClickHouse {}",
rows.get(0).getString("user"), rows.get(0).getString("version"));
} catch (Exception e) {
log.error("Secure connection with a CA certificate passed as a string 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 @@ -4,6 +4,10 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
Expand All @@ -20,6 +24,9 @@
* the CA certificate is passed with the {@code sslrootcert} connection property.
* No trust store configuration is needed: the certificate is added to a trust store
* used only by this connection, so the JVM default trust store stays untouched.</li>
* <li>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.</li>
* </ul>
*
* <p>More SSL examples (mTLS, trust stores, SNI) will be added to this class later.</p>
Expand Down Expand Up @@ -62,7 +69,8 @@ public static void main(String[] args) {
log.info("Running in standalone mode against {}", url);
try {
connectWithCustomRootCertificate(url, user, password, rootCert);
} catch (SQLException e) {
connectWithRootCertificateAsString(url, user, password, rootCert);
} catch (SQLException | IOException e) {
log.error("Secure connection with a custom root CA certificate failed", e);
}
return;
Expand All @@ -75,6 +83,8 @@ public static void main(String[] args) {
try (SecureServerSupport server = SecureServerSupport.start(image)) {
connectWithCustomRootCertificate(server.getJdbcUrl(),
SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
connectWithRootCertificateAsString(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 @@ -109,6 +119,44 @@ static void connectWithCustomRootCertificate(String url, String user, String pas
}
}

/**
* Same as {@link #connectWithCustomRootCertificate}, but the CA certificate is passed as PEM
* content instead of a file path. The {@code sslrootcert} property accepts both: any value
* containing a {@code -----BEGIN ...-----} block is treated as PEM content.
*
* <p>This is handy when the certificate is delivered through an environment variable or
* a secret manager (e.g. a Kubernetes secret projected into {@code CLICKHOUSE_CA_CERT}),
* so the application never has to write it to disk:</p>
*
* <pre>{@code
* properties.setProperty("sslrootcert", System.getenv("CLICKHOUSE_CA_CERT"));
* }</pre>
*/
static void connectWithRootCertificateAsString(String url, String user, String password, String rootCertPath)
throws SQLException, IOException {
// In a real application the PEM content would typically come from an env variable
// or a secret manager; here we simply read the file generated for this example.
String rootCertPem = new String(Files.readAllBytes(Paths.get(rootCertPath)), StandardCharsets.US_ASCII);

log.info("Connecting to {} using root CA certificate passed as a PEM string", 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
// PEM content, not a path - detected by the "-----BEGIN" marker.
properties.setProperty(ClientConfigProperties.CA_CERTIFICATE.getKey(), rootCertPem); // sslrootcert

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 (CA cert as string) as '{}' to ClickHouse {}",
rs.getString("user"), rs.getString("version"));
}
}
}

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