Skip to content

Commit 9ccfadb

Browse files
authored
feat(qwp): support for PEM CA files (#75)
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 9ccfadb

18 files changed

Lines changed: 698 additions & 189 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/ClientTlsConfiguration.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ public class ClientTlsConfiguration {
3333
private final String trustStorePath;
3434

3535
public ClientTlsConfiguration(String trustStorePath, char[] trustStorePassword, int tlsValidationMode) {
36+
if (trustStorePath != null && tlsValidationMode == TLS_VALIDATION_MODE_NONE) {
37+
throw new IllegalArgumentException("custom trust store cannot be combined with disabled TLS validation");
38+
}
3639
this.trustStorePath = trustStorePath;
3740
this.trustStorePassword = trustStorePassword;
3841
this.tlsValidationMode = tlsValidationMode;

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

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,7 @@ enum Transport {
934934
*/
935935
final class LineSenderBuilder {
936936
private static final int AUTO_FLUSH_DISABLED = 0;
937+
private static final String TLS_ROOTS_INSECURE_CONFIG_ERROR = "tls_roots cannot be combined with tls_verify=unsafe_off; remove tls_verify to use custom roots, or remove tls_roots to disable certificate validation";
937938
// close() drain timeout. Default applied at build() time. 0 or -1
938939
// means "fast close" (skip the drain entirely); any positive value
939940
// bounds the wait for ackedFsn to catch up to publishedFsn. Uses
@@ -1415,7 +1416,7 @@ public Sender build() {
14151416
}
14161417
ClientTlsConfiguration tlsConfig = null;
14171418
if (tlsEnabled) {
1418-
assert (trustStorePath == null) == (trustStorePassword == null); //either both null or both non-null
1419+
assert trustStorePassword == null || trustStorePath != null;
14191420
tlsConfig = new ClientTlsConfiguration(trustStorePath, trustStorePassword, tlsValidationMode == TlsValidationMode.DEFAULT ? ClientTlsConfiguration.TLS_VALIDATION_MODE_FULL : ClientTlsConfiguration.TLS_VALIDATION_MODE_NONE);
14201421
}
14211422
return AbstractLineHttpSender.createLineSender(hosts, ports, httpPath, httpClientConfiguration, tlsConfig, actualAutoFlushRows, httpToken,
@@ -1437,7 +1438,7 @@ public Sender build() {
14371438

14381439
ClientTlsConfiguration wsTlsConfig = null;
14391440
if (tlsEnabled) {
1440-
assert (trustStorePath == null) == (trustStorePassword == null);
1441+
assert trustStorePassword == null || trustStorePath != null;
14411442
wsTlsConfig = new ClientTlsConfiguration(
14421443
trustStorePath,
14431444
trustStorePassword,
@@ -3573,13 +3574,12 @@ private LineSenderBuilder fromConfig(CharSequence configurationString) {
35733574
if (hosts.size() == 0) {
35743575
throw new LineSenderException("addr is missing");
35753576
}
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) {
3577+
if (trustStorePath == null && trustStorePassword != null) {
35813578
throw new LineSenderException("tls_roots_password was configured, but tls_roots is missing");
35823579
}
3580+
if (trustStorePath != null && tlsValidationMode == TlsValidationMode.INSECURE) {
3581+
throw new LineSenderException(TLS_ROOTS_INSECURE_CONFIG_ERROR);
3582+
}
35833583
if (protocol == PROTOCOL_HTTP || protocol == PROTOCOL_WEBSOCKET) {
35843584
if (user != null) {
35853585
httpUsernamePassword(user, password);
@@ -3836,8 +3836,11 @@ static void validateWsConfig(ConfigView view, boolean tls) {
38363836
if (!tls && (tlsVerify != null || tlsRoots != null || tlsRootsPassword != null)) {
38373837
throw new IllegalArgumentException("tls_verify/tls_roots/tls_roots_password require the wss:: schema");
38383838
}
3839-
if ((tlsRoots == null) != (tlsRootsPassword == null)) {
3840-
throw new IllegalArgumentException("tls_roots and tls_roots_password must be provided together");
3839+
if (tlsRoots == null && tlsRootsPassword != null) {
3840+
throw new IllegalArgumentException("tls_roots_password requires tls_roots");
3841+
}
3842+
if (tlsRoots != null && "unsafe_off".equals(tlsVerify)) {
3843+
throw new IllegalArgumentException(TLS_ROOTS_INSECURE_CONFIG_ERROR);
38413844
}
38423845
}
38433846

@@ -3975,6 +3978,9 @@ private void validateParameters() {
39753978
if (!tlsEnabled && tlsValidationMode != TlsValidationMode.DEFAULT) {
39763979
throw new LineSenderException("TLS validation disabled, but TLS was not enabled");
39773980
}
3981+
if (trustStorePath != null && tlsValidationMode == TlsValidationMode.INSECURE) {
3982+
throw new LineSenderException("custom trust store cannot be combined with disabled TLS validation");
3983+
}
39783984
if (keyId != null && bufferCapacity < MIN_BUFFER_SIZE) {
39793985
throw new LineSenderException("Requested buffer too small ")
39803986
.put("[minimalCapacity=").put(MIN_BUFFER_SIZE)
@@ -4133,26 +4139,51 @@ private void websocket() {
41334139

41344140
public class AdvancedTlsSettings {
41354141
/**
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.
4142+
* Configure a PEM file containing one or more custom root certificates.
4143+
* This is only needed when using {@link #enableTls()} and the default
4144+
* trust store does not contain the certificate chain used by a server.
4145+
* Most users should not need it.
41384146
* <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.
4147+
* The path can be on the local filesystem, or it can use the
4148+
* {@code classpath:} prefix.
4149+
*
4150+
* @param pemRootsPath a path to a PEM certificate file or bundle
4151+
* @return an instance of LineSenderBuilder for further configuration
4152+
*/
4153+
public LineSenderBuilder customTrustStore(String pemRootsPath) {
4154+
return setCustomTrustStore(pemRootsPath, null);
4155+
}
4156+
4157+
/**
4158+
* Configure a password-protected JKS or PKCS#12 trust store. This is
4159+
* only needed when using {@link #enableTls()} and the default trust
4160+
* store does not contain the certificate chain used by a server.
4161+
* Most users should not need it.
4162+
* <br>
4163+
* The path can be on the local filesystem, or it can use the
4164+
* {@code classpath:} prefix.
41414165
*
41424166
* @param trustStorePath a path to a trust store.
4143-
* @param trustStorePassword a password to for the truststore
4167+
* @param trustStorePassword the trust store password
41444168
* @return an instance of LineSenderBuilder for further configuration
41454169
*/
41464170
public LineSenderBuilder customTrustStore(String trustStorePath, char[] trustStorePassword) {
4171+
if (trustStorePassword == null) {
4172+
throw new LineSenderException("trust store password cannot be null");
4173+
}
4174+
return setCustomTrustStore(trustStorePath, trustStorePassword);
4175+
}
4176+
4177+
private LineSenderBuilder setCustomTrustStore(String trustStorePath, char[] trustStorePassword) {
41474178
if (LineSenderBuilder.this.trustStorePath != null) {
41484179
throw new LineSenderException("custom trust store was already configured ")
41494180
.put("[path=").put(LineSenderBuilder.this.trustStorePath).put("]");
41504181
}
41514182
if (Chars.isBlank(trustStorePath)) {
41524183
throw new LineSenderException("trust store path cannot be empty nor null");
41534184
}
4154-
if (trustStorePassword == null) {
4155-
throw new LineSenderException("trust store password cannot be null");
4185+
if (tlsValidationMode == TlsValidationMode.INSECURE) {
4186+
throw new LineSenderException("custom trust store cannot be configured when TLS validation is disabled");
41564187
}
41574188

41584189
LineSenderBuilder.this.trustStorePath = trustStorePath;
@@ -4165,12 +4196,16 @@ public LineSenderBuilder customTrustStore(String trustStorePath, char[] trustSto
41654196
* This is suitable when testing self-signed certificate. It's inherently insecure and should
41664197
* never be used in a production.
41674198
* <br>
4168-
* If you cannot use trusted certificate then you should prefer {@link #customTrustStore(String, char[])}
4169-
* over disabling validation.
4199+
* If you cannot use a certificate in the default trust store then
4200+
* you should prefer {@link #customTrustStore(String)} or
4201+
* {@link #customTrustStore(String, char[])} over disabling validation.
41704202
*
41714203
* @return an instance of LineSenderBuilder for further configuration
41724204
*/
41734205
public LineSenderBuilder disableCertificateValidation() {
4206+
if (LineSenderBuilder.this.trustStorePath != null) {
4207+
throw new LineSenderException("TLS validation cannot be disabled when a custom trust store is configured");
4208+
}
41744209
LineSenderBuilder.this.tlsValidationMode = TlsValidationMode.INSECURE;
41754210
return LineSenderBuilder.this;
41764211
}

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+
}

0 commit comments

Comments
 (0)