Skip to content

Commit 496c5b4

Browse files
authored
Merge pull request #2873 from ClickHouse/06/10/26/certs_as_string
SSL Certificates input as string
2 parents 8353658 + d49e274 commit 496c5b4

5 files changed

Lines changed: 196 additions & 18 deletions

File tree

clickhouse-client/src/main/java/com/clickhouse/client/config/ClickHouseDefaultSslContextProvider.java

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
package com.clickhouse.client.config;
22

3-
import java.io.*;
3+
import com.clickhouse.client.ClickHouseConfig;
4+
import com.clickhouse.client.ClickHouseSslContextProvider;
5+
import com.clickhouse.data.ClickHouseUtils;
6+
7+
import javax.net.ssl.KeyManager;
8+
import javax.net.ssl.KeyManagerFactory;
9+
import javax.net.ssl.SSLContext;
10+
import javax.net.ssl.SSLException;
11+
import javax.net.ssl.TrustManager;
12+
import javax.net.ssl.TrustManagerFactory;
13+
import javax.net.ssl.X509TrustManager;
14+
import java.io.BufferedReader;
15+
import java.io.ByteArrayInputStream;
16+
import java.io.IOException;
17+
import java.io.InputStream;
18+
import java.io.InputStreamReader;
19+
import java.nio.charset.StandardCharsets;
420
import java.security.KeyFactory;
521
import java.security.KeyManagementException;
622
import java.security.KeyStore;
@@ -18,24 +34,30 @@
1834
import java.util.Base64;
1935
import java.util.Optional;
2036

21-
import javax.net.ssl.KeyManager;
22-
import javax.net.ssl.KeyManagerFactory;
23-
import javax.net.ssl.SSLContext;
24-
import javax.net.ssl.SSLException;
25-
import javax.net.ssl.TrustManager;
26-
import javax.net.ssl.TrustManagerFactory;
27-
import javax.net.ssl.X509TrustManager;
28-
29-
import com.clickhouse.client.ClickHouseConfig;
30-
import com.clickhouse.client.ClickHouseSslContextProvider;
31-
import com.clickhouse.data.ClickHouseUtils;
32-
3337
@Deprecated
3438
public class ClickHouseDefaultSslContextProvider implements ClickHouseSslContextProvider {
3539
static final String PEM_HEADER_PREFIX = "---BEGIN ";
3640
static final String PEM_HEADER_SUFFIX = " PRIVATE KEY---";
3741
static final String PEM_FOOTER_PREFIX = "---END ";
3842

43+
/** Standard PEM encapsulation boundary (RFC 7468). Present in any PEM content, never in a file path. */
44+
static final String PEM_BEGIN_MARKER = "-----BEGIN";
45+
46+
/**
47+
* Opens a stream over PEM material that may be supplied either as a file path (also searched in the home
48+
* directory and on the classpath) or directly as PEM content.
49+
*
50+
* @param certOrContent file path or PEM content of a certificate or a private key
51+
* @return stream over the PEM content
52+
* @throws IOException when the value is a path and the file cannot be opened
53+
*/
54+
static InputStream getCertificateInputStream(String certOrContent) throws IOException {
55+
if (certOrContent.contains(PEM_BEGIN_MARKER)) {
56+
return new ByteArrayInputStream(certOrContent.getBytes(StandardCharsets.US_ASCII));
57+
}
58+
return ClickHouseUtils.getFileInputStream(certOrContent);
59+
}
60+
3961
/**
4062
* An insecure {@link javax.net.ssl.TrustManager}, that don't validate the
4163
* certificate.
@@ -71,7 +93,7 @@ public static PrivateKey getPrivateKey(String keyFile)
7193
String algorithm = (String) ClickHouseDefaults.SSL_KEY_ALGORITHM.getEffectiveDefaultValue();
7294
StringBuilder builder = new StringBuilder();
7395
try (BufferedReader reader = new BufferedReader(
74-
new InputStreamReader(ClickHouseUtils.getFileInputStream(keyFile)))) {
96+
new InputStreamReader(getCertificateInputStream(keyFile)))) {
7597
String line = reader.readLine();
7698
if (line != null) {
7799
algorithm = getAlgorithm(line, algorithm);
@@ -102,7 +124,7 @@ public KeyStore getKeyStore(String cert, String key) throws NoSuchAlgorithmExcep
102124
ClickHouseUtils.format("%s KeyStore not available", KeyStore.getDefaultType()));
103125
}
104126

105-
try (InputStream in = ClickHouseUtils.getFileInputStream(cert)) {
127+
try (InputStream in = getCertificateInputStream(cert)) {
106128
CertificateFactory factory = CertificateFactory
107129
.getInstance((String) ClickHouseDefaults.SSL_CERTIFICATE_TYPE.getEffectiveDefaultValue());
108130
if (key == null || key.isEmpty()) {

clickhouse-client/src/test/java/com/clickhouse/client/config/ClickHouseDefaultSslContextProviderTest.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
package com.clickhouse.client.config;
22

3+
import com.clickhouse.data.ClickHouseUtils;
34
import org.testng.Assert;
45
import org.testng.annotations.Test;
56

7+
import java.io.FileNotFoundException;
8+
import java.io.InputStream;
9+
import java.nio.charset.StandardCharsets;
10+
import java.security.KeyStore;
11+
import java.security.PrivateKey;
12+
613
public class ClickHouseDefaultSslContextProviderTest {
14+
static String readTestResource(String name) throws Exception {
15+
try (InputStream in = ClickHouseUtils.getFileInputStream(name)) {
16+
return new String(in.readAllBytes(), StandardCharsets.US_ASCII);
17+
}
18+
}
19+
720
@Test(groups = { "unit" })
821
public void testGetAlgorithm() {
922
Assert.assertEquals(ClickHouseDefaultSslContextProvider.getAlgorithm("", null), null);
@@ -19,4 +32,44 @@ public void testGetPrivateKey() throws Exception {
1932
// openssl genpkey -out pkey4test.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048
2033
Assert.assertNotNull(ClickHouseDefaultSslContextProvider.getPrivateKey("pkey4test.pem"));
2134
}
35+
36+
@Test(groups = { "unit" })
37+
public void testGetCertificateInputStream() throws Exception {
38+
String pemContent = readTestResource("client.crt");
39+
try (InputStream in = ClickHouseDefaultSslContextProvider.getCertificateInputStream(pemContent)) {
40+
byte[] buffer = new byte[pemContent.length()];
41+
int read = in.read(buffer);
42+
Assert.assertEquals(new String(buffer, 0, read, StandardCharsets.US_ASCII), pemContent);
43+
}
44+
45+
try (InputStream in = ClickHouseDefaultSslContextProvider.getCertificateInputStream("client.crt")) {
46+
Assert.assertTrue(in.read() != -1);
47+
}
48+
49+
Assert.assertThrows(FileNotFoundException.class,
50+
() -> ClickHouseDefaultSslContextProvider.getCertificateInputStream("non-existent.crt"));
51+
}
52+
53+
@Test(groups = { "unit" })
54+
public void testGetPrivateKeyFromPemContent() throws Exception {
55+
PrivateKey fromFile = ClickHouseDefaultSslContextProvider.getPrivateKey("pkey4test.pem");
56+
PrivateKey fromContent = ClickHouseDefaultSslContextProvider
57+
.getPrivateKey(readTestResource("pkey4test.pem"));
58+
Assert.assertEquals(fromContent, fromFile);
59+
Assert.assertEquals(fromContent.getAlgorithm(), fromFile.getAlgorithm());
60+
Assert.assertEquals(fromContent.getFormat(), fromFile.getFormat());
61+
Assert.assertEquals(fromContent.getEncoded(), fromFile.getEncoded());
62+
}
63+
64+
@Test(groups = { "unit" })
65+
public void testGetKeyStoreFromPemContent() throws Exception {
66+
ClickHouseDefaultSslContextProvider provider = new ClickHouseDefaultSslContextProvider();
67+
68+
KeyStore trustStore = provider.getKeyStore(readTestResource("client.crt"), null);
69+
Assert.assertNotNull(trustStore.getCertificate("cert1"));
70+
71+
KeyStore keyStore = provider.getKeyStore(readTestResource("some_user.crt"),
72+
readTestResource("some_user.key"));
73+
Assert.assertNotNull(keyStore.getKey("key", null));
74+
}
2275
}

client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2186,7 +2186,7 @@ protected Client.Builder newClient() {
21862186
@DataProvider(name = "testCustomCaCertificateProvider")
21872187
public static Object[][] testCustomCaCertificateProvider() {
21882188
return new Object[][]{
2189-
// TODO: decide if we need to support certificates via string {true},
2189+
{true},
21902190
{false}};
21912191
}
21922192

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

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
import com.clickhouse.client.api.query.GenericRecord;
55
import lombok.extern.slf4j.Slf4j;
66

7+
import java.io.IOException;
8+
import java.nio.charset.StandardCharsets;
9+
import java.nio.file.Files;
10+
import java.nio.file.Paths;
711
import java.util.List;
812

913
/**
@@ -15,6 +19,9 @@
1519
* the CA certificate is passed with {@link Client.Builder#setRootCertificate(String)}.
1620
* No trust store configuration is needed: the certificate is added to a trust store
1721
* used only by this client, so the JVM default trust store stays untouched.</li>
22+
* <li>Passing the CA certificate as a PEM string instead of a file path - useful when the
23+
* certificate comes from an environment variable or a secret manager (typical for
24+
* Kubernetes/cloud deployments) and you do not want to write it to disk.</li>
1825
* </ul>
1926
*
2027
* <p>More SSL examples (mTLS, trust stores, SNI) will be added to this class later.</p>
@@ -58,7 +65,9 @@ public static void main(String[] args) {
5865
}
5966

6067
log.info("Running in standalone mode against {}:{}", host, port);
61-
connectWithCustomRootCertificate("https://" + host + ":" + port, database, user, password, rootCert);
68+
String endpoint = "https://" + host + ":" + port;
69+
connectWithCustomRootCertificate(endpoint, database, user, password, rootCert);
70+
connectWithRootCertificateAsString(endpoint, database, user, password, rootCert);
6271
return;
6372
}
6473

@@ -69,6 +78,8 @@ public static void main(String[] args) {
6978
try (SecureServerSupport server = SecureServerSupport.start(image)) {
7079
connectWithCustomRootCertificate(server.getEndpoint(), database,
7180
SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
81+
connectWithRootCertificateAsString(server.getEndpoint(), database,
82+
SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
7283
} catch (Exception e) {
7384
log.error("Failed to run the SSL example against a local Docker server", e);
7485
}
@@ -100,6 +111,50 @@ static void connectWithCustomRootCertificate(String endpoint, String database, S
100111
}
101112
}
102113

114+
/**
115+
* Same as {@link #connectWithCustomRootCertificate}, but the CA certificate is passed as PEM
116+
* content instead of a file path. {@link Client.Builder#setRootCertificate(String)} accepts both:
117+
* any value containing a {@code -----BEGIN ...-----} block is treated as PEM content.
118+
*
119+
* <p>This is handy when the certificate is delivered through an environment variable or
120+
* a secret manager (e.g. a Kubernetes secret projected into {@code CLICKHOUSE_CA_CERT}),
121+
* so the application never has to write it to disk:</p>
122+
*
123+
* <pre>{@code
124+
* String caPem = System.getenv("CLICKHOUSE_CA_CERT");
125+
* Client client = new Client.Builder().setRootCertificate(caPem)...
126+
* }</pre>
127+
*/
128+
static void connectWithRootCertificateAsString(String endpoint, String database, String user, String password,
129+
String rootCertPath) {
130+
final String rootCertPem;
131+
try {
132+
// In a real application the PEM content would typically come from an env variable
133+
// or a secret manager; here we simply read the file generated for this example.
134+
rootCertPem = new String(Files.readAllBytes(Paths.get(rootCertPath)), StandardCharsets.US_ASCII);
135+
} catch (IOException e) {
136+
log.error("Failed to read the CA certificate from {}", rootCertPath, e);
137+
return;
138+
}
139+
140+
log.info("Connecting to {} using root CA certificate passed as a PEM string", endpoint);
141+
try (Client client = new Client.Builder()
142+
.addEndpoint(endpoint)
143+
.setUsername(user)
144+
.setPassword(password)
145+
.setDefaultDatabase(database)
146+
// PEM content, not a path - detected by the "-----BEGIN" marker.
147+
.setRootCertificate(rootCertPem)
148+
.build()) {
149+
150+
List<GenericRecord> rows = client.queryAll("SELECT currentUser() AS user, version() AS version");
151+
log.info("Connected securely (CA cert as string) as '{}' to ClickHouse {}",
152+
rows.get(0).getString("user"), rows.get(0).getString("version"));
153+
} catch (Exception e) {
154+
log.error("Secure connection with a CA certificate passed as a string failed", e);
155+
}
156+
}
157+
103158
private static String trimToNull(String value) {
104159
if (value == null) {
105160
return null;

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

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
import org.slf4j.Logger;
55
import org.slf4j.LoggerFactory;
66

7+
import java.io.IOException;
8+
import java.nio.charset.StandardCharsets;
9+
import java.nio.file.Files;
10+
import java.nio.file.Paths;
711
import java.sql.Connection;
812
import java.sql.DriverManager;
913
import java.sql.ResultSet;
@@ -20,6 +24,9 @@
2024
* the CA certificate is passed with the {@code sslrootcert} connection property.
2125
* No trust store configuration is needed: the certificate is added to a trust store
2226
* used only by this connection, so the JVM default trust store stays untouched.</li>
27+
* <li>Passing the CA certificate as a PEM string instead of a file path - useful when the
28+
* certificate comes from an environment variable or a secret manager (typical for
29+
* Kubernetes/cloud deployments) and you do not want to write it to disk.</li>
2330
* </ul>
2431
*
2532
* <p>More SSL examples (mTLS, trust stores, SNI) will be added to this class later.</p>
@@ -62,7 +69,8 @@ public static void main(String[] args) {
6269
log.info("Running in standalone mode against {}", url);
6370
try {
6471
connectWithCustomRootCertificate(url, user, password, rootCert);
65-
} catch (SQLException e) {
72+
connectWithRootCertificateAsString(url, user, password, rootCert);
73+
} catch (SQLException | IOException e) {
6674
log.error("Secure connection with a custom root CA certificate failed", e);
6775
}
6876
return;
@@ -75,6 +83,8 @@ public static void main(String[] args) {
7583
try (SecureServerSupport server = SecureServerSupport.start(image)) {
7684
connectWithCustomRootCertificate(server.getJdbcUrl(),
7785
SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
86+
connectWithRootCertificateAsString(server.getJdbcUrl(),
87+
SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
7888
} catch (Exception e) {
7989
log.error("Failed to run the SSL example against a local Docker server", e);
8090
Runtime.getRuntime().exit(-1);
@@ -109,6 +119,44 @@ static void connectWithCustomRootCertificate(String url, String user, String pas
109119
}
110120
}
111121

122+
/**
123+
* Same as {@link #connectWithCustomRootCertificate}, but the CA certificate is passed as PEM
124+
* content instead of a file path. The {@code sslrootcert} property accepts both: any value
125+
* containing a {@code -----BEGIN ...-----} block is treated as PEM content.
126+
*
127+
* <p>This is handy when the certificate is delivered through an environment variable or
128+
* a secret manager (e.g. a Kubernetes secret projected into {@code CLICKHOUSE_CA_CERT}),
129+
* so the application never has to write it to disk:</p>
130+
*
131+
* <pre>{@code
132+
* properties.setProperty("sslrootcert", System.getenv("CLICKHOUSE_CA_CERT"));
133+
* }</pre>
134+
*/
135+
static void connectWithRootCertificateAsString(String url, String user, String password, String rootCertPath)
136+
throws SQLException, IOException {
137+
// In a real application the PEM content would typically come from an env variable
138+
// or a secret manager; here we simply read the file generated for this example.
139+
String rootCertPem = new String(Files.readAllBytes(Paths.get(rootCertPath)), StandardCharsets.US_ASCII);
140+
141+
log.info("Connecting to {} using root CA certificate passed as a PEM string", url);
142+
143+
Properties properties = new Properties();
144+
properties.setProperty(ClientConfigProperties.USER.getKey(), user); // user
145+
properties.setProperty(ClientConfigProperties.PASSWORD.getKey(), password); // password
146+
properties.setProperty("ssl", "true"); // enable TLS even if the URL has no https scheme
147+
// PEM content, not a path - detected by the "-----BEGIN" marker.
148+
properties.setProperty(ClientConfigProperties.CA_CERTIFICATE.getKey(), rootCertPem); // sslrootcert
149+
150+
try (Connection connection = DriverManager.getConnection(url, properties);
151+
Statement stmt = connection.createStatement();
152+
ResultSet rs = stmt.executeQuery("SELECT currentUser() AS user, version() AS version")) {
153+
if (rs.next()) {
154+
log.info("Connected securely (CA cert as string) as '{}' to ClickHouse {}",
155+
rs.getString("user"), rs.getString("version"));
156+
}
157+
}
158+
}
159+
112160
private static String trimToNull(String value) {
113161
if (value == null) {
114162
return null;

0 commit comments

Comments
 (0)