Skip to content

Commit

Permalink
[TLS Registry] provide a TLS configuration called javax.net.ssl havin…
Browse files Browse the repository at this point in the history
…g truststore set in the same way as default SunJSSE provider, fix quarkusio#45175
  • Loading branch information
ppalaga committed Dec 18, 2024
1 parent 969b378 commit a4c5028
Show file tree
Hide file tree
Showing 5 changed files with 346 additions and 26 deletions.
67 changes: 51 additions & 16 deletions docs/src/main/asciidoc/tls-registry-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,41 @@ certificate. Dynamic clients are `@Dependent` scoped, so you should
inject them into components with an appropriate scope.
====

=== Referencing the default truststore of SunJSSE

JDK distributions typically contain a truststore in the `$JAVA_HOME/lib/security/cacerts` file.
It is used as a default truststore by SunJSSE, the default implementation of Java Secure Socket Extension (JSSE).
SSL/TLS capabilities provided by SunJSSE are leveraged by various Java Runtime components,
such as `javax.net.ssl.HttpsURLConnection` and others.

Although Quarkus extensions typically do not honor the default truststore of SunJSSE,
it might still be practical to use it in some situations - be it migration from legacy technologies
or when running on a Linux distribution where the SunJSSE truststore is synchronized with the operating system truststore.

To make the use of SunJSSE truststore easier, Quarkus TLS Registry provides a TLS configuration
under the name `javax.net.ssl` that mimics the default behavior of SunJSSE:

. If the `javax.net.ssl.trustStore` system property is defined, then its value is honored as a truststore
. Otherwise, the paths `$JAVA_HOME/lib/security/jssecacerts` and `$JAVA_HOME/lib/security/cacerts` are checked
and the first existing file is used as a truststore
. Otherwise an `IllegalStateException` is thrown.

The password for opening the truststore is taken from the `javax.net.ssl.trustStorePassword` system property.
If it is not set, the default password `changeit` is used.

`javax.net.ssl` can be used as a value for various `*.tls-configuration-name` properties, for example:

.Example configuration for a gRPC client:
[source,properties]
----
quarkus.grpc.clients.hello.tls-configuration-name=javax.net.ssl
----

[WARNING]
====
The `javax.net.ssl` TLS configuration can be neither customized nor overridden.
====

== Configuring TLS

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

To configure a PKCS12 keystore:

[source,properties]
----
quarkus.tls.key-store.p12.path=server-keystore.p12
quarkus.tls.key-store.p12.password=secret
----

`.p12` files are password-protected, so you need to provide the password to open the keystore.

These files can include more than one certificate and private key.
If this is the case, take either of the following actions:

Expand Down Expand Up @@ -292,11 +327,11 @@ To configure a JKS keystore:
quarkus.tls.key-store.jks.path=server-keystore.jks
quarkus.tls.key-store.jks.password=secret
----

`.jks` files are password-protected, so you need to provide the password to open the keystore.
Also, they can include more than one certificate and private key.
If this is the case:

* Provide and configure the alias of the certificate and the private key you want to use:
+
[source,properties]
Expand All @@ -317,12 +352,12 @@ Server Name Indication (SNI) is a TLS extension that makes it possible for a cli
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.

To enable SNI:

[source,properties]
----
quarkus.tls.key-store.sni=true # Disabled by default
----

With SNI enabled, the client indicates the server name during the TLS handshake, which allows the server to select the appropriate certificate:

* When configuring the keystore with PEM files, multiple certificate (CRT) and key files must be provided.
Expand Down Expand Up @@ -390,7 +425,7 @@ quarkus.tls.trust-store.p12.path=client-truststore.p12
quarkus.tls.trust-store.p12.password=password
quarkus.tls.trust-store.p12.alias=my-alias
----

`.p12` files are password-protected, so you need to provide the password to open the truststore.
However, unlike keystores, the alias does not require a password because it contains a public certificate, not a private key.

Expand All @@ -408,7 +443,7 @@ quarkus.tls.trust-store.jks.path=client-truststore.jks
quarkus.tls.trust-store.jks.password=password
quarkus.tls.trust-store.jks.alias=my-alias
----

`.jks` files are password-protected, so you need to provide the password to open the truststore.
However, unlike keystores, the alias does not require a password because it contains a public certificate, not a private key.

Expand All @@ -432,7 +467,7 @@ quarkus.tls.trust-store.credentials-provider.bean-name=my-credentials-provider
# The key used to retrieve the truststore password, `password` by default
quarkus.tls.trust-store.credentials-provider.password-key=password
----

IMPORTANT: The credential provider can only be used with PKCS12 and JKS truststores.

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

To access the TLS configuration, inject the `TlsConfigurationRegistry` bean.
You can retrieve a named TLS configuration by calling `get("<NAME>")` or the default configuration by calling `getDefault()`.

[source,java]
----
@Inject
Expand All @@ -572,7 +607,7 @@ TlsConfiguration def = certificates.getDefault().orElseThrow();
TlsConfiguration named = certificates.get("name").orElseThrow();
//...
----

The `TlsConfiguration` object contains the keystores, truststores, cipher suites, protocols, and other properties.
It also provides a way to create an `SSLContext` from the configuration.

Expand All @@ -591,9 +626,9 @@ To register a certificate in the TLS registry by using the extension, the _proce
TlsCertificateBuildItem item = new TlsCertificateBuildItem("named",
new MyCertificateSupplier());
----

The certificate supplier is a runtime object generally retrieved by using a recorder method.

.An example of a certificate supplier:
[source,java]
----
Expand Down Expand Up @@ -937,7 +972,7 @@ Ensure that the path matches the one used in the configuration (here `/etc/tls`)
. Deploy your application to use the certificate generated by OpenShift.
This will make the service available over HTTPS.

[NOTE]
[NOTE]
====
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.
Expand Down Expand Up @@ -1209,7 +1244,7 @@ Even if the Quarkus Development CA is installed, you can generate a self-signed
----
quarkus tls generate-certificate --name my-cert --self-signed
----

This generates a self-signed certificate that the Quarkus Development CA does not sign.

=== Uninstalling the Quarkus Development CA
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package io.quarkus.tls;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import jakarta.inject.Inject;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.smallrye.certs.Format;
import io.smallrye.certs.junit5.Certificate;
import io.smallrye.certs.junit5.Certificates;

@Certificates(baseDir = "target/certs", certificates = {
@Certificate(name = "test-formats", password = "password", formats = { Format.JKS, Format.PEM, Format.PKCS12 })
})
public class JavaNetSslTlsBucketConfigTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer(
() -> ShrinkWrap.create(JavaArchive.class));

@Inject
TlsConfigurationRegistry certificates;

@Test
void test() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
TlsConfiguration def = certificates.get("javax.net.ssl").orElseThrow();

assertThat(def.getTrustStoreOptions()).isNotNull();
final KeyStore actualTs = def.getTrustStore();
assertThat(actualTs).isNotNull();

/*
* Get the default trust managers, one of which should be SunJSSE based,
* which in turn should use the same default trust store lookup algo
* like we do in io.quarkus.tls.runtime.JavaNetSslTlsBucketConfig.defaultTrustStorePath()
*/
final TrustManagerFactory trustManagerFactory = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
final List<X509TrustManager> defaultTrustManagers = Stream.of(trustManagerFactory.getTrustManagers())
.filter(m -> m instanceof X509TrustManager)
.map(m -> (X509TrustManager) m)
.collect(Collectors.toList());
assertThat(defaultTrustManagers).hasSizeGreaterThan(0);

final List<String> actualAliases = Collections.list(actualTs.aliases());
assertThat(actualAliases).hasSizeGreaterThan(0);

for (String alias : actualAliases) {
/*
* Get the certs from the trust store loaded by us from $JAVA_HOME/lib/security/cacerts or similar
* and validate those against the default trust managers.
* In that way we make sure indirectly that we have loaded some valid trust material.
*/
final X509Certificate cert = (X509Certificate) actualTs.getCertificate(alias);
CertificateException lastException = null;
boolean passed = false;
for (X509TrustManager tm : defaultTrustManagers) {
try {
tm.checkServerTrusted(new X509Certificate[] { cert }, "RSA");
passed = true;
break;
} catch (CertificateException e) {
lastException = e;
}
}
if (!passed && lastException != null) {
throw lastException;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public class CertificateRecorder implements TlsConfigurationRegistry {

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

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

public void verifyCertificateConfig(TlsBucketConfig config, Vertx vertx, String name) {
if (name.equals(TlsConfig.JAVA_NET_SSL_TLS_CONFIGURATION_NAME)) {
throw new IllegalArgumentException(
"The TLS configuration name " + TlsConfig.JAVA_NET_SSL_TLS_CONFIGURATION_NAME
+ " is reserved for providing access to default SunJSSE keystore; neither Quarkus extensions nor end users can adjust of override it");
}
final TlsConfiguration tlsConfig = verifyCertificateConfigInternal(config, vertx, name);
certificates.put(name, tlsConfig);

// Handle reloading if needed
if (config.reloadPeriod().isPresent()) {
if (reloader == null) {
reloader = new TlsCertificateUpdater(vertx);
}
reloader.add(name, certificates.get(name), config.reloadPeriod().get());
}
}

private static TlsConfiguration verifyCertificateConfigInternal(TlsBucketConfig config, Vertx vertx, String name) {
// Verify the key store
KeyStoreAndKeyCertOptions ks = null;
boolean sni;
Expand Down Expand Up @@ -90,16 +110,7 @@ public void verifyCertificateConfig(TlsBucketConfig config, Vertx vertx, String
} else if (config.trustAll()) {
ts = new TrustStoreAndTrustOptions(null, TrustAllOptions.INSTANCE);
}

certificates.put(name, new VertxCertificateHolder(vertx, name, config, ks, ts));

// Handle reloading if needed
if (config.reloadPeriod().isPresent()) {
if (reloader == null) {
reloader = new TlsCertificateUpdater(vertx);
}
reloader.add(name, certificates.get(name), config.reloadPeriod().get());
}
return new VertxCertificateHolder(vertx, name, config, ks, ts);
}

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

@Override
public Optional<TlsConfiguration> get(String name) {
if (TlsConfig.JAVA_NET_SSL_TLS_CONFIGURATION_NAME.equals(name)) {
final TlsConfiguration result = certificates.computeIfAbsent(TlsConfig.JAVA_NET_SSL_TLS_CONFIGURATION_NAME, k -> {
return verifyCertificateConfigInternal(new JavaNetSslTlsBucketConfig(), vertx,
TlsConfig.JAVA_NET_SSL_TLS_CONFIGURATION_NAME);
});
return Optional.ofNullable(result);
}
return Optional.ofNullable(certificates.get(name));
}

Expand All @@ -147,6 +165,11 @@ public void register(String name, TlsConfiguration configuration) {
if (name.equals(TlsConfig.DEFAULT_NAME)) {
throw new IllegalArgumentException("The name of the TLS configuration to register cannot be <default>");
}
if (name.equals(TlsConfig.JAVA_NET_SSL_TLS_CONFIGURATION_NAME)) {
throw new IllegalArgumentException(
"The TLS configuration name " + TlsConfig.JAVA_NET_SSL_TLS_CONFIGURATION_NAME
+ " is reserved for providing access to default SunJSSE keystore; neither Quarkus extensions nor end users can adjust of override it");
}
if (configuration == null) {
throw new IllegalArgumentException("The TLS configuration to register cannot be null");
}
Expand Down
Loading

0 comments on commit a4c5028

Please sign in to comment.