Skip to content

Commit f2c37e6

Browse files
committed
[TLS Registry] provide a TLS configuration called javax.net.ssl having truststore set in the same way as default SunJSSE provider, fix #45175
1 parent ec6bf3c commit f2c37e6

File tree

5 files changed

+340
-26
lines changed

5 files changed

+340
-26
lines changed

docs/src/main/asciidoc/tls-registry-reference.adoc

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,41 @@ certificate. Dynamic clients are `@Dependent` scoped, so you should
183183
inject them into components with an appropriate scope.
184184
====
185185

186+
=== Referencing the default truststore of SunJSSE
187+
188+
JDK distributions typically contain a truststore in the `$JAVA_HOME/lib/security/cacerts` file.
189+
It is used as a default truststore by SunJSSE, the default implementation of Java Secure Socket Extension (JSSE).
190+
SSL/TLS capabilities provided by SunJSSE are leveraged by various Java Runtime components,
191+
such as `javax.net.ssl.HttpsURLConnection` and others.
192+
193+
Although Quarkus extensions typically do not honor the default truststore of SunJSSE,
194+
it might still be practical to use it in some situations - be it migration from legacy technologies
195+
or when running on a Linux distribution where the SunJSSE truststore is synchronized with the operating system truststore.
196+
197+
To make the use of SunJSSE truststore easier, Quarkus TLS Registry provides a TLS configuration
198+
under the name `javax.net.ssl` that mimics the default behavior of SunJSSE:
199+
200+
. If the `javax.net.ssl.trustStore` system property is defined, then its value is honored as a truststore
201+
. Otherwise, the paths `$JAVA_HOME/lib/security/jssecacerts` and `$JAVA_HOME/lib/security/cacerts` are checked
202+
and the first existing file is used as a truststore
203+
. Otherwise an `IllegalStateException` is thrown.
204+
205+
The password for opening the truststore is taken from the `javax.net.ssl.trustStorePassword` system property.
206+
If it is not set, the default password `changeit` is used.
207+
208+
`javax.net.ssl` can be used as a value for various `*.tls-configuration-name` properties, for example:
209+
210+
.Example configuration for a gRPC client:
211+
[source,properties]
212+
----
213+
quarkus.grpc.clients.hello.tls-configuration-name=javax.net.ssl
214+
----
215+
216+
[WARNING]
217+
====
218+
The `javax.net.ssl` TLS configuration can be neither customized nor overridden.
219+
====
220+
186221
== Configuring TLS
187222

188223
TLS configuration primarily involves managing keystores and truststores.
@@ -248,15 +283,15 @@ quarkus.tls.http.key-store.pem.password=password
248283
PKCS12 keystores are single files that contain the certificate and the private key.
249284

250285
To configure a PKCS12 keystore:
251-
286+
252287
[source,properties]
253288
----
254289
quarkus.tls.key-store.p12.path=server-keystore.p12
255290
quarkus.tls.key-store.p12.password=secret
256291
----
257-
292+
258293
`.p12` files are password-protected, so you need to provide the password to open the keystore.
259-
294+
260295
These files can include more than one certificate and private key.
261296
If this is the case, take either of the following actions:
262297

@@ -292,11 +327,11 @@ To configure a JKS keystore:
292327
quarkus.tls.key-store.jks.path=server-keystore.jks
293328
quarkus.tls.key-store.jks.password=secret
294329
----
295-
330+
296331
`.jks` files are password-protected, so you need to provide the password to open the keystore.
297332
Also, they can include more than one certificate and private key.
298333
If this is the case:
299-
334+
300335
* Provide and configure the alias of the certificate and the private key you want to use:
301336
+
302337
[source,properties]
@@ -317,12 +352,12 @@ Server Name Indication (SNI) is a TLS extension that makes it possible for a cli
317352
SNI enables a server to present different TLS certificates for multiple domains on a single IP address, which facilitates secure communication for virtual hosting scenarios.
318353

319354
To enable SNI:
320-
355+
321356
[source,properties]
322357
----
323358
quarkus.tls.key-store.sni=true # Disabled by default
324359
----
325-
360+
326361
With SNI enabled, the client indicates the server name during the TLS handshake, which allows the server to select the appropriate certificate:
327362

328363
* When configuring the keystore with PEM files, multiple certificate (CRT) and key files must be provided.
@@ -390,7 +425,7 @@ quarkus.tls.trust-store.p12.path=client-truststore.p12
390425
quarkus.tls.trust-store.p12.password=password
391426
quarkus.tls.trust-store.p12.alias=my-alias
392427
----
393-
428+
394429
`.p12` files are password-protected, so you need to provide the password to open the truststore.
395430
However, unlike keystores, the alias does not require a password because it contains a public certificate, not a private key.
396431

@@ -408,7 +443,7 @@ quarkus.tls.trust-store.jks.path=client-truststore.jks
408443
quarkus.tls.trust-store.jks.password=password
409444
quarkus.tls.trust-store.jks.alias=my-alias
410445
----
411-
446+
412447
`.jks` files are password-protected, so you need to provide the password to open the truststore.
413448
However, unlike keystores, the alias does not require a password because it contains a public certificate, not a private key.
414449

@@ -432,7 +467,7 @@ quarkus.tls.trust-store.credentials-provider.bean-name=my-credentials-provider
432467
# The key used to retrieve the truststore password, `password` by default
433468
quarkus.tls.trust-store.credentials-provider.password-key=password
434469
----
435-
470+
436471
IMPORTANT: The credential provider can only be used with PKCS12 and JKS truststores.
437472

438473
=== Other properties
@@ -562,7 +597,7 @@ While extensions automatically use the TLS registry, you can also access the TLS
562597

563598
To access the TLS configuration, inject the `TlsConfigurationRegistry` bean.
564599
You can retrieve a named TLS configuration by calling `get("<NAME>")` or the default configuration by calling `getDefault()`.
565-
600+
566601
[source,java]
567602
----
568603
@Inject
@@ -572,7 +607,7 @@ TlsConfiguration def = certificates.getDefault().orElseThrow();
572607
TlsConfiguration named = certificates.get("name").orElseThrow();
573608
//...
574609
----
575-
610+
576611
The `TlsConfiguration` object contains the keystores, truststores, cipher suites, protocols, and other properties.
577612
It also provides a way to create an `SSLContext` from the configuration.
578613

@@ -591,9 +626,9 @@ To register a certificate in the TLS registry by using the extension, the _proce
591626
TlsCertificateBuildItem item = new TlsCertificateBuildItem("named",
592627
new MyCertificateSupplier());
593628
----
594-
629+
595630
The certificate supplier is a runtime object generally retrieved by using a recorder method.
596-
631+
597632
.An example of a certificate supplier:
598633
[source,java]
599634
----
@@ -937,7 +972,7 @@ Ensure that the path matches the one used in the configuration (here `/etc/tls`)
937972
. Deploy your application to use the certificate generated by OpenShift.
938973
This will make the service available over HTTPS.
939974

940-
[NOTE]
975+
[NOTE]
941976
====
942977
By setting the `quarkus.tls.key-store.pem.acme.cert` and `quarkus.tls.key-store.pem.acme.key` variables or their environment variable variant, the TLS registry will use the certificate and private key from the secret.
943978
@@ -1209,7 +1244,7 @@ Even if the Quarkus Development CA is installed, you can generate a self-signed
12091244
----
12101245
quarkus tls generate-certificate --name my-cert --self-signed
12111246
----
1212-
1247+
12131248
This generates a self-signed certificate that the Quarkus Development CA does not sign.
12141249

12151250
=== Uninstalling the Quarkus Development CA
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package io.quarkus.tls;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.io.IOException;
6+
import java.security.KeyStore;
7+
import java.security.KeyStoreException;
8+
import java.security.NoSuchAlgorithmException;
9+
import java.security.cert.CertificateException;
10+
import java.security.cert.X509Certificate;
11+
import java.util.Collections;
12+
import java.util.List;
13+
import java.util.stream.Collectors;
14+
import java.util.stream.Stream;
15+
16+
import javax.net.ssl.TrustManagerFactory;
17+
import javax.net.ssl.X509TrustManager;
18+
19+
import jakarta.inject.Inject;
20+
21+
import org.jboss.shrinkwrap.api.ShrinkWrap;
22+
import org.jboss.shrinkwrap.api.spec.JavaArchive;
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.api.extension.RegisterExtension;
25+
26+
import io.quarkus.test.QuarkusUnitTest;
27+
28+
public class JavaNetSslTlsBucketConfigTest {
29+
30+
@RegisterExtension
31+
static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer(
32+
() -> ShrinkWrap.create(JavaArchive.class));
33+
34+
@Inject
35+
TlsConfigurationRegistry certificates;
36+
37+
@Test
38+
void test() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
39+
TlsConfiguration def = certificates.get("javax.net.ssl").orElseThrow();
40+
41+
assertThat(def.getTrustStoreOptions()).isNotNull();
42+
final KeyStore actualTs = def.getTrustStore();
43+
assertThat(actualTs).isNotNull();
44+
45+
/*
46+
* Get the default trust managers, one of which should be SunJSSE based,
47+
* which in turn should use the same default trust store lookup algo
48+
* like we do in io.quarkus.tls.runtime.JavaNetSslTlsBucketConfig.defaultTrustStorePath()
49+
*/
50+
final TrustManagerFactory trustManagerFactory = TrustManagerFactory
51+
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
52+
trustManagerFactory.init((KeyStore) null);
53+
final List<X509TrustManager> defaultTrustManagers = Stream.of(trustManagerFactory.getTrustManagers())
54+
.filter(m -> m instanceof X509TrustManager)
55+
.map(m -> (X509TrustManager) m)
56+
.collect(Collectors.toList());
57+
assertThat(defaultTrustManagers).hasSizeGreaterThan(0);
58+
59+
final List<String> actualAliases = Collections.list(actualTs.aliases());
60+
assertThat(actualAliases).hasSizeGreaterThan(0);
61+
62+
for (String alias : actualAliases) {
63+
/*
64+
* Get the certs from the trust store loaded by us from $JAVA_HOME/lib/security/cacerts or similar
65+
* and validate those against the default trust managers.
66+
* In that way we make sure indirectly that we have loaded some valid trust material.
67+
*/
68+
final X509Certificate cert = (X509Certificate) actualTs.getCertificate(alias);
69+
CertificateException lastException = null;
70+
boolean passed = false;
71+
for (X509TrustManager tm : defaultTrustManagers) {
72+
try {
73+
tm.checkServerTrusted(new X509Certificate[] { cert }, "RSA");
74+
passed = true;
75+
break;
76+
} catch (CertificateException e) {
77+
lastException = e;
78+
}
79+
}
80+
if (!passed && lastException != null) {
81+
throw lastException;
82+
}
83+
}
84+
}
85+
}

extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/CertificateRecorder.java

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public class CertificateRecorder implements TlsConfigurationRegistry {
2727

2828
private final Map<String, TlsConfiguration> certificates = new ConcurrentHashMap<>();
2929
private volatile TlsCertificateUpdater reloader;
30+
private volatile Vertx vertx;
3031

3132
/**
3233
* Validate the certificate configuration.
@@ -38,6 +39,7 @@ public class CertificateRecorder implements TlsConfigurationRegistry {
3839
* @param vertx the Vert.x instance
3940
*/
4041
public void validateCertificates(TlsConfig config, RuntimeValue<Vertx> vertx, ShutdownContext shutdownContext) {
42+
this.vertx = vertx.getValue();
4143
// Verify the default config
4244
if (config.defaultCertificateConfig().isPresent()) {
4345
verifyCertificateConfig(config.defaultCertificateConfig().get(), vertx.getValue(), TlsConfig.DEFAULT_NAME);
@@ -59,6 +61,24 @@ public void run() {
5961
}
6062

6163
public void verifyCertificateConfig(TlsBucketConfig config, Vertx vertx, String name) {
64+
if (name.equals(TlsConfig.JAVA_NET_SSL_TLS_CONFIGURATION_NAME)) {
65+
throw new IllegalArgumentException(
66+
"The TLS configuration name " + TlsConfig.JAVA_NET_SSL_TLS_CONFIGURATION_NAME
67+
+ " is reserved for providing access to default SunJSSE keystore; neither Quarkus extensions nor end users can adjust of override it");
68+
}
69+
final TlsConfiguration tlsConfig = verifyCertificateConfigInternal(config, vertx, name);
70+
certificates.put(name, tlsConfig);
71+
72+
// Handle reloading if needed
73+
if (config.reloadPeriod().isPresent()) {
74+
if (reloader == null) {
75+
reloader = new TlsCertificateUpdater(vertx);
76+
}
77+
reloader.add(name, certificates.get(name), config.reloadPeriod().get());
78+
}
79+
}
80+
81+
private static TlsConfiguration verifyCertificateConfigInternal(TlsBucketConfig config, Vertx vertx, String name) {
6282
// Verify the key store
6383
KeyStoreAndKeyCertOptions ks = null;
6484
boolean sni;
@@ -90,16 +110,7 @@ public void verifyCertificateConfig(TlsBucketConfig config, Vertx vertx, String
90110
} else if (config.trustAll()) {
91111
ts = new TrustStoreAndTrustOptions(null, TrustAllOptions.INSTANCE);
92112
}
93-
94-
certificates.put(name, new VertxCertificateHolder(vertx, name, config, ks, ts));
95-
96-
// Handle reloading if needed
97-
if (config.reloadPeriod().isPresent()) {
98-
if (reloader == null) {
99-
reloader = new TlsCertificateUpdater(vertx);
100-
}
101-
reloader.add(name, certificates.get(name), config.reloadPeriod().get());
102-
}
113+
return new VertxCertificateHolder(vertx, name, config, ks, ts);
103114
}
104115

105116
public static KeyStoreAndKeyCertOptions verifyKeyStore(KeyStoreConfig config, Vertx vertx, String name) {
@@ -131,6 +142,13 @@ public static TrustStoreAndTrustOptions verifyTrustStore(TrustStoreConfig config
131142

132143
@Override
133144
public Optional<TlsConfiguration> get(String name) {
145+
if (TlsConfig.JAVA_NET_SSL_TLS_CONFIGURATION_NAME.equals(name)) {
146+
final TlsConfiguration result = certificates.computeIfAbsent(TlsConfig.JAVA_NET_SSL_TLS_CONFIGURATION_NAME, k -> {
147+
return verifyCertificateConfigInternal(new JavaNetSslTlsBucketConfig(), vertx,
148+
TlsConfig.JAVA_NET_SSL_TLS_CONFIGURATION_NAME);
149+
});
150+
return Optional.ofNullable(result);
151+
}
134152
return Optional.ofNullable(certificates.get(name));
135153
}
136154

@@ -147,6 +165,11 @@ public void register(String name, TlsConfiguration configuration) {
147165
if (name.equals(TlsConfig.DEFAULT_NAME)) {
148166
throw new IllegalArgumentException("The name of the TLS configuration to register cannot be <default>");
149167
}
168+
if (name.equals(TlsConfig.JAVA_NET_SSL_TLS_CONFIGURATION_NAME)) {
169+
throw new IllegalArgumentException(
170+
"The TLS configuration name " + TlsConfig.JAVA_NET_SSL_TLS_CONFIGURATION_NAME
171+
+ " is reserved for providing access to default SunJSSE keystore; neither Quarkus extensions nor end users can adjust of override it");
172+
}
150173
if (configuration == null) {
151174
throw new IllegalArgumentException("The TLS configuration to register cannot be null");
152175
}

0 commit comments

Comments
 (0)