Skip to content

Commit 28bf04f

Browse files
committed
feat(tls): support for PEM CA files
tls_roots now accepts a PEM certificate file: QuestDB.connect("wss::addr=db.example.com:9000;tls_roots=/etc/ssl/ca.pem;") The file may contain one certificate or a bundle, and no longer has to be imported into a password-protected JKS with keytool first. Existing JKS/PKCS#12 trust stores keep working as before by supplying tls_roots_password, and PKCS#12 now also loads on Java 8.
1 parent f47859b commit 28bf04f

16 files changed

Lines changed: 588 additions & 192 deletions

File tree

README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,19 @@ try (QuestDB db = QuestDB.connect("wss::addr=localhost:9000;username=admin;passw
364364
}
365365
```
366366

367+
**Custom PEM certificate authority:**
368+
369+
```java
370+
try (QuestDB db = QuestDB.connect(
371+
"wss::addr=localhost:9000;tls_roots=/path/to/ca.pem;")) {
372+
// ... use db ...
373+
}
374+
```
375+
376+
`tls_roots` accepts a PEM certificate or bundle directly. For an existing JKS
377+
or PKCS#12 trust store, also set `tls_roots_password`; the password switches the
378+
file interpretation from PEM to a Java trust store.
379+
367380
**Disable certificate validation (not for production):**
368381

369382
```java
@@ -412,8 +425,8 @@ schema::key1=value1;key2=value2;
412425
| `password` / `pass` | | Basic-auth password |
413426
| `token` | | Bearer token (sent as an `Authorization` header on the WS upgrade) |
414427
| `tls_verify` | `on` | TLS certificate validation (`on` or `unsafe_off`) |
415-
| `tls_roots` | | Path to a custom truststore |
416-
| `tls_roots_password` | | Truststore password |
428+
| `tls_roots` | | Path to a PEM certificate/bundle, or a JKS/PKCS#12 trust store |
429+
| `tls_roots_password` | | Optional JKS/PKCS#12 password; omit when `tls_roots` is PEM |
417430
| `connect_timeout` | _(OS)_ | TCP connect + TLS handshake timeout, in milliseconds |
418431
| `auth_timeout_ms` | `15000` | Authentication/upgrade request timeout, in milliseconds |
419432

core/src/main/java/io/questdb/client/Sender.java

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1415,7 +1415,7 @@ public Sender build() {
14151415
}
14161416
ClientTlsConfiguration tlsConfig = null;
14171417
if (tlsEnabled) {
1418-
assert (trustStorePath == null) == (trustStorePassword == null); //either both null or both non-null
1418+
assert trustStorePassword == null || trustStorePath != null;
14191419
tlsConfig = new ClientTlsConfiguration(trustStorePath, trustStorePassword, tlsValidationMode == TlsValidationMode.DEFAULT ? ClientTlsConfiguration.TLS_VALIDATION_MODE_FULL : ClientTlsConfiguration.TLS_VALIDATION_MODE_NONE);
14201420
}
14211421
return AbstractLineHttpSender.createLineSender(hosts, ports, httpPath, httpClientConfiguration, tlsConfig, actualAutoFlushRows, httpToken,
@@ -1437,7 +1437,7 @@ public Sender build() {
14371437

14381438
ClientTlsConfiguration wsTlsConfig = null;
14391439
if (tlsEnabled) {
1440-
assert (trustStorePath == null) == (trustStorePassword == null);
1440+
assert trustStorePassword == null || trustStorePath != null;
14411441
wsTlsConfig = new ClientTlsConfiguration(
14421442
trustStorePath,
14431443
trustStorePassword,
@@ -3573,11 +3573,7 @@ private LineSenderBuilder fromConfig(CharSequence configurationString) {
35733573
if (hosts.size() == 0) {
35743574
throw new LineSenderException("addr is missing");
35753575
}
3576-
if (trustStorePath != null) {
3577-
if (trustStorePassword == null) {
3578-
throw new LineSenderException("tls_roots was configured, but tls_roots_password is missing");
3579-
}
3580-
} else if (trustStorePassword != null) {
3576+
if (trustStorePath == null && trustStorePassword != null) {
35813577
throw new LineSenderException("tls_roots_password was configured, but tls_roots is missing");
35823578
}
35833579
if (protocol == PROTOCOL_HTTP || protocol == PROTOCOL_WEBSOCKET) {
@@ -3836,8 +3832,8 @@ static void validateWsConfig(ConfigView view, boolean tls) {
38363832
if (!tls && (tlsVerify != null || tlsRoots != null || tlsRootsPassword != null)) {
38373833
throw new IllegalArgumentException("tls_verify/tls_roots/tls_roots_password require the wss:: schema");
38383834
}
3839-
if ((tlsRoots == null) != (tlsRootsPassword == null)) {
3840-
throw new IllegalArgumentException("tls_roots and tls_roots_password must be provided together");
3835+
if (tlsRoots == null && tlsRootsPassword != null) {
3836+
throw new IllegalArgumentException("tls_roots_password requires tls_roots");
38413837
}
38423838
}
38433839

@@ -4133,27 +4129,49 @@ private void websocket() {
41334129

41344130
public class AdvancedTlsSettings {
41354131
/**
4136-
* Configure a custom truststore. This is only needed when using {@link #enableTls()} when your default
4137-
* truststore does not contain certificate chain used by a server. Most users should not need it.
4132+
* Configure a PEM file containing one or more custom root certificates.
4133+
* This is only needed when using {@link #enableTls()} and the default
4134+
* trust store does not contain the certificate chain used by a server.
4135+
* Most users should not need it.
4136+
* <br>
4137+
* The path can be on the local filesystem, or it can use the
4138+
* {@code classpath:} prefix.
4139+
*
4140+
* @param pemRootsPath a path to a PEM certificate file or bundle
4141+
* @return an instance of LineSenderBuilder for further configuration
4142+
*/
4143+
public LineSenderBuilder customTrustStore(String pemRootsPath) {
4144+
return setCustomTrustStore(pemRootsPath, null);
4145+
}
4146+
4147+
/**
4148+
* Configure a password-protected JKS or PKCS#12 trust store. This is
4149+
* only needed when using {@link #enableTls()} and the default trust
4150+
* store does not contain the certificate chain used by a server.
4151+
* Most users should not need it.
41384152
* <br>
4139-
* The path can be either a path on a local filesystem. Or you can prefix it with "classpath:" to instruct
4140-
* the Sender to load a trust store from a classpath.
4153+
* The path can be on the local filesystem, or it can use the
4154+
* {@code classpath:} prefix.
41414155
*
41424156
* @param trustStorePath a path to a trust store.
4143-
* @param trustStorePassword a password to for the truststore
4157+
* @param trustStorePassword the trust store password
41444158
* @return an instance of LineSenderBuilder for further configuration
41454159
*/
41464160
public LineSenderBuilder customTrustStore(String trustStorePath, char[] trustStorePassword) {
4161+
if (trustStorePassword == null) {
4162+
throw new LineSenderException("trust store password cannot be null");
4163+
}
4164+
return setCustomTrustStore(trustStorePath, trustStorePassword);
4165+
}
4166+
4167+
private LineSenderBuilder setCustomTrustStore(String trustStorePath, char[] trustStorePassword) {
41474168
if (LineSenderBuilder.this.trustStorePath != null) {
41484169
throw new LineSenderException("custom trust store was already configured ")
41494170
.put("[path=").put(LineSenderBuilder.this.trustStorePath).put("]");
41504171
}
41514172
if (Chars.isBlank(trustStorePath)) {
41524173
throw new LineSenderException("trust store path cannot be empty nor null");
41534174
}
4154-
if (trustStorePassword == null) {
4155-
throw new LineSenderException("trust store password cannot be null");
4156-
}
41574175

41584176
LineSenderBuilder.this.trustStorePath = trustStorePath;
41594177
LineSenderBuilder.this.trustStorePassword = trustStorePassword;
@@ -4165,8 +4183,9 @@ public LineSenderBuilder customTrustStore(String trustStorePath, char[] trustSto
41654183
* This is suitable when testing self-signed certificate. It's inherently insecure and should
41664184
* never be used in a production.
41674185
* <br>
4168-
* If you cannot use trusted certificate then you should prefer {@link #customTrustStore(String, char[])}
4169-
* over disabling validation.
4186+
* If you cannot use a certificate in the default trust store then
4187+
* you should prefer {@link #customTrustStore(String)} or
4188+
* {@link #customTrustStore(String, char[])} over disabling validation.
41704189
*
41714190
* @return an instance of LineSenderBuilder for further configuration
41724191
*/

core/src/main/java/io/questdb/client/cutlass/line/tcp/DelegatingTlsChannel.java

Lines changed: 9 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -27,46 +27,24 @@
2727
import io.questdb.client.Sender;
2828
import io.questdb.client.cutlass.line.LineChannel;
2929
import io.questdb.client.cutlass.line.LineSenderException;
30+
import io.questdb.client.network.TlsTrustStore;
3031
import io.questdb.client.std.MemoryTag;
3132
import io.questdb.client.std.Misc;
3233
import io.questdb.client.std.Unsafe;
3334
import io.questdb.client.std.Vect;
3435
import org.slf4j.Logger;
3536
import org.slf4j.LoggerFactory;
3637

37-
import javax.net.ssl.SSLContext;
3838
import javax.net.ssl.SSLEngine;
3939
import javax.net.ssl.SSLEngineResult;
4040
import javax.net.ssl.SSLException;
41-
import javax.net.ssl.SSLParameters;
42-
import javax.net.ssl.TrustManager;
43-
import javax.net.ssl.TrustManagerFactory;
44-
import javax.net.ssl.X509TrustManager;
45-
import java.io.FileInputStream;
46-
import java.io.FileNotFoundException;
47-
import java.io.InputStream;
4841
import java.lang.reflect.Field;
4942
import java.nio.Buffer;
5043
import java.nio.ByteBuffer;
51-
import java.security.KeyStore;
52-
import java.security.SecureRandom;
53-
import java.security.cert.X509Certificate;
5444

5545
public final class DelegatingTlsChannel implements LineChannel {
5646
private static final long ADDRESS_FIELD_OFFSET;
5747
private static final int AFTER_HANDSHAKE = 1;
58-
private static final TrustManager[] BLIND_TRUST_MANAGERS = new TrustManager[]{new X509TrustManager() {
59-
public void checkClientTrusted(X509Certificate[] certs, String t) {
60-
}
61-
62-
public void checkServerTrusted(X509Certificate[] certs, String t) {
63-
}
64-
65-
public X509Certificate[] getAcceptedIssuers() {
66-
return null;
67-
}
68-
}};
69-
7048
private static final long CAPACITY_FIELD_OFFSET;
7149
private static final int CLOSED = 3;
7250
private static final int CLOSING = 2;
@@ -202,44 +180,14 @@ private static long allocateMemoryAndResetBuffer(ByteBuffer buffer, int capacity
202180
}
203181

204182
private static SSLEngine createSslEngine(String trustStorePath, char[] trustStorePassword, Sender.TlsValidationMode validationMode, String peerHost) {
205-
assert trustStorePath == null || validationMode == Sender.TlsValidationMode.DEFAULT;
206183
try {
207-
SSLContext sslContext;
208-
if (trustStorePath != null) {
209-
sslContext = SSLContext.getInstance("TLS");
210-
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
211-
KeyStore jks = KeyStore.getInstance("JKS");
212-
try (InputStream trustStoreStream = openTruststoreStream(trustStorePath)) {
213-
jks.load(trustStoreStream, trustStorePassword);
214-
}
215-
tmf.init(jks);
216-
TrustManager[] trustManagers = tmf.getTrustManagers();
217-
sslContext.init(null, trustManagers, new SecureRandom());
218-
} else if (validationMode == Sender.TlsValidationMode.INSECURE) {
219-
sslContext = SSLContext.getInstance("TLS");
220-
sslContext.init(null, BLIND_TRUST_MANAGERS, new SecureRandom());
221-
} else {
222-
sslContext = SSLContext.getDefault();
223-
}
224-
225-
// SSLEngine needs to know hostname during TLS handshake to validate a server certificate was issued
226-
// for the server we are connecting to. For details see the comment below.
227-
// Hostname validation does not use port at all hence we can get away with a dummy value -1
228-
SSLEngine sslEngine = sslContext.createSSLEngine(peerHost, -1);
229-
if (validationMode != Sender.TlsValidationMode.INSECURE) {
230-
SSLParameters sslParameters = sslEngine.getSSLParameters();
231-
// The https validation algorithm? That looks confusing! After all we are not using any
232-
// https here at so what does it mean?
233-
// It's actually simple: It just instructs the SSLEngine to perform the same hostname validation
234-
// as it does during HTTPS connections. SSLEngine does not do hostname validation by default. Without
235-
// this option SSLEngine would happily accept any certificate as long as it's signed by a trusted CA.
236-
// This option will make sure certificates are accepted only if they were issued for the
237-
// server we are connecting to.
238-
sslParameters.setEndpointIdentificationAlgorithm("https");
239-
sslEngine.setSSLParameters(sslParameters);
240-
}
241-
sslEngine.setUseClientMode(true);
242-
return sslEngine;
184+
return TlsTrustStore.createSslEngine(
185+
trustStorePath,
186+
trustStorePassword,
187+
validationMode == Sender.TlsValidationMode.INSECURE,
188+
peerHost,
189+
DelegatingTlsChannel.class
190+
);
243191
} catch (Throwable t) {
244192
if (t instanceof LineSenderException) {
245193
throw (LineSenderException) t;
@@ -256,20 +204,6 @@ private static long expandBuffer(ByteBuffer buffer, long oldAddress) {
256204
return newAddress;
257205
}
258206

259-
private static InputStream openTruststoreStream(String trustStorePath) throws FileNotFoundException {
260-
InputStream trustStoreStream;
261-
if (trustStorePath.startsWith("classpath:")) {
262-
String adjustedPath = trustStorePath.substring("classpath:".length());
263-
trustStoreStream = DelegatingTlsChannel.class.getResourceAsStream(adjustedPath);
264-
if (trustStoreStream == null) {
265-
throw new LineSenderException("configured trust store is unavailable ")
266-
.put("[path=").put(trustStorePath).put("]");
267-
}
268-
return trustStoreStream;
269-
}
270-
return new FileInputStream(trustStorePath);
271-
}
272-
273207
private static void resetBufferToPointer(ByteBuffer buffer, long ptr, int len) {
274208
assert buffer.isDirect();
275209
Unsafe.getUnsafe().putLong(buffer, ADDRESS_FIELD_OFFSET, ptr);
@@ -437,4 +371,4 @@ private void writeToUpstreamAndClear() {
437371
LIMIT_FIELD_OFFSET = Unsafe.getUnsafe().objectFieldOffset(limitField);
438372
CAPACITY_FIELD_OFFSET = Unsafe.getUnsafe().objectFieldOffset(capacityField);
439373
}
440-
}
374+
}

core/src/main/java/io/questdb/client/cutlass/qwp/client/QwpQueryClient.java

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -345,9 +345,10 @@ private QwpQueryClient(String host, int port) {
345345
* <li>{@code tls_verify=on|unsafe_off} -- TLS certificate validation. Default is {@code on}.
346346
* Only allowed with the {@code wss::} schema. {@code unsafe_off} disables hostname and
347347
* certificate chain validation; use only for testing.</li>
348-
* <li>{@code tls_roots=<path>} -- path to a custom trust store (PKCS12 or JKS). Must be
349-
* paired with {@code tls_roots_password}. Only allowed with {@code wss::}.</li>
350-
* <li>{@code tls_roots_password=<secret>} -- password for the custom trust store.</li>
348+
* <li>{@code tls_roots=<path>} -- path to a PEM certificate file or bundle. Only allowed
349+
* with {@code wss::}.</li>
350+
* <li>{@code tls_roots_password=<secret>} -- optional password. When present,
351+
* {@code tls_roots} is read as a JKS or PKCS#12 trust store instead of PEM.</li>
351352
* <li>{@code zone=<id>} -- client zone identifier (opaque, case-insensitive;
352353
* e.g. {@code eu-west-1a}). When set with {@code target=any|replica},
353354
* failover prefers endpoints whose server-advertised {@code zone_id}
@@ -462,7 +463,11 @@ public static QwpQueryClient fromConfig(CharSequence configurationString) {
462463
client.withCompression(compression, compressionLevel);
463464
if (tls) {
464465
if (tlsRoots != null) {
465-
client.withTrustStore(tlsRoots, tlsRootsPassword.toCharArray());
466+
if (tlsRootsPassword == null) {
467+
client.withTrustStore(tlsRoots);
468+
} else {
469+
client.withTrustStore(tlsRoots, tlsRootsPassword.toCharArray());
470+
}
466471
} else if (tlsValidation != null && tlsValidation == ClientTlsConfiguration.TLS_VALIDATION_MODE_NONE) {
467472
client.withInsecureTls();
468473
} else {
@@ -542,8 +547,8 @@ public static void validateConfig(ConfigView view, boolean tls) {
542547
throw new IllegalArgumentException(
543548
"tls_verify/tls_roots/tls_roots_password require the wss:: schema");
544549
}
545-
if ((tlsRoots == null) != (tlsRootsPassword == null)) {
546-
throw new IllegalArgumentException("tls_roots and tls_roots_password must be provided together");
550+
if (tlsRoots == null && tlsRootsPassword != null) {
551+
throw new IllegalArgumentException("tls_roots_password requires tls_roots");
547552
}
548553
// Mirror fromConfig's effective values: a missing bound takes its
549554
// default, so the ordering is enforced even when only one key is set
@@ -1215,7 +1220,8 @@ public QwpQueryClient withInitialCredit(long bytes) {
12151220

12161221
/**
12171222
* Enables TLS with certificate validation disabled. Intended for testing only --
1218-
* production code should use {@link #withTls} or {@link #withTrustStore}.
1223+
* production code should use {@link #withTls} or
1224+
* {@link #withTrustStore(String)}.
12191225
* Must be called before {@link #connect}.
12201226
*/
12211227
public QwpQueryClient withInsecureTls() {
@@ -1286,7 +1292,27 @@ public QwpQueryClient withTls() {
12861292
}
12871293

12881294
/**
1289-
* Enables TLS with full certificate validation against the given custom trust store.
1295+
* Enables TLS with full certificate validation against the custom root
1296+
* certificates in the given PEM file or bundle.
1297+
* Must be called before {@link #connect}.
1298+
*
1299+
* @param pemRootsPath filesystem path to a PEM certificate file or bundle
1300+
*/
1301+
public QwpQueryClient withTrustStore(String pemRootsPath) {
1302+
checkPreConnect("withTrustStore");
1303+
if (pemRootsPath == null) {
1304+
throw new IllegalArgumentException("pemRootsPath must not be null");
1305+
}
1306+
this.tlsEnabled = true;
1307+
this.tlsValidationMode = ClientTlsConfiguration.TLS_VALIDATION_MODE_FULL;
1308+
this.trustStorePath = pemRootsPath;
1309+
this.trustStorePassword = null;
1310+
return this;
1311+
}
1312+
1313+
/**
1314+
* Enables TLS with full certificate validation against the given
1315+
* password-protected JKS or PKCS#12 trust store.
12901316
* Must be called before {@link #connect}.
12911317
*
12921318
* @param trustStorePath filesystem path to a PKCS12 or JKS trust store

0 commit comments

Comments
 (0)