Skip to content

Commit f2ac4b0

Browse files
committed
implemented accepting certificates as plain strings
1 parent 26caa45 commit f2ac4b0

5 files changed

Lines changed: 201 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: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,30 @@
11
package com.clickhouse.client.config;
22

3+
import java.io.ByteArrayOutputStream;
4+
import java.io.FileNotFoundException;
5+
import java.io.InputStream;
6+
import java.nio.charset.StandardCharsets;
7+
import java.security.KeyStore;
8+
import java.security.PrivateKey;
9+
10+
import com.clickhouse.data.ClickHouseUtils;
11+
312
import org.testng.Assert;
413
import org.testng.annotations.Test;
514

615
public class ClickHouseDefaultSslContextProviderTest {
16+
static String readTestResource(String name) throws Exception {
17+
ByteArrayOutputStream out = new ByteArrayOutputStream();
18+
try (InputStream in = ClickHouseUtils.getFileInputStream(name)) {
19+
byte[] buffer = new byte[2048];
20+
int read;
21+
while ((read = in.read(buffer)) != -1) {
22+
out.write(buffer, 0, read);
23+
}
24+
}
25+
return new String(out.toByteArray(), StandardCharsets.US_ASCII);
26+
}
27+
728
@Test(groups = { "unit" })
829
public void testGetAlgorithm() {
930
Assert.assertEquals(ClickHouseDefaultSslContextProvider.getAlgorithm("", null), null);
@@ -19,4 +40,41 @@ public void testGetPrivateKey() throws Exception {
1940
// openssl genpkey -out pkey4test.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048
2041
Assert.assertNotNull(ClickHouseDefaultSslContextProvider.getPrivateKey("pkey4test.pem"));
2142
}
43+
44+
@Test(groups = { "unit" })
45+
public void testGetCertificateInputStream() throws Exception {
46+
String pemContent = readTestResource("client.crt");
47+
try (InputStream in = ClickHouseDefaultSslContextProvider.getCertificateInputStream(pemContent)) {
48+
byte[] buffer = new byte[pemContent.length()];
49+
int read = in.read(buffer);
50+
Assert.assertEquals(new String(buffer, 0, read, StandardCharsets.US_ASCII), pemContent);
51+
}
52+
53+
try (InputStream in = ClickHouseDefaultSslContextProvider.getCertificateInputStream("client.crt")) {
54+
Assert.assertTrue(in.read() != -1);
55+
}
56+
57+
Assert.assertThrows(FileNotFoundException.class,
58+
() -> ClickHouseDefaultSslContextProvider.getCertificateInputStream("non-existent.crt"));
59+
}
60+
61+
@Test(groups = { "unit" })
62+
public void testGetPrivateKeyFromPemContent() throws Exception {
63+
PrivateKey fromFile = ClickHouseDefaultSslContextProvider.getPrivateKey("pkey4test.pem");
64+
PrivateKey fromContent = ClickHouseDefaultSslContextProvider
65+
.getPrivateKey(readTestResource("pkey4test.pem"));
66+
Assert.assertEquals(fromContent, fromFile);
67+
}
68+
69+
@Test(groups = { "unit" })
70+
public void testGetKeyStoreFromPemContent() throws Exception {
71+
ClickHouseDefaultSslContextProvider provider = new ClickHouseDefaultSslContextProvider();
72+
73+
KeyStore trustStore = provider.getKeyStore(readTestResource("client.crt"), null);
74+
Assert.assertNotNull(trustStore.getCertificate("cert1"));
75+
76+
KeyStore keyStore = provider.getKeyStore(readTestResource("some_user.crt"),
77+
readTestResource("some_user.key"));
78+
Assert.assertNotNull(keyStore.getKey("key", null));
79+
}
2280
}

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
Runtime.getRuntime().exit(-1);
@@ -103,6 +114,50 @@ static void connectWithCustomRootCertificate(String endpoint, String database, S
103114
}
104115
}
105116

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