diff --git a/docs/src/main/asciidoc/tls-registry-reference.adoc b/docs/src/main/asciidoc/tls-registry-reference.adoc index d9f9fe505f84d..8ef99143c6cb2 100644 --- a/docs/src/main/asciidoc/tls-registry-reference.adoc +++ b/docs/src/main/asciidoc/tls-registry-reference.adoc @@ -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. @@ -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: @@ -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] @@ -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. @@ -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. @@ -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. @@ -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 @@ -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("")` or the default configuration by calling `getDefault()`. - + [source,java] ---- @Inject @@ -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. @@ -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] ---- @@ -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. @@ -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 diff --git a/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/JavaNetSslTlsBucketConfigTest.java b/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/JavaNetSslTlsBucketConfigTest.java new file mode 100644 index 0000000000000..a625b00562a9f --- /dev/null +++ b/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/JavaNetSslTlsBucketConfigTest.java @@ -0,0 +1,85 @@ +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; + +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 defaultTrustManagers = Stream.of(trustManagerFactory.getTrustManagers()) + .filter(m -> m instanceof X509TrustManager) + .map(m -> (X509TrustManager) m) + .collect(Collectors.toList()); + assertThat(defaultTrustManagers).hasSizeGreaterThan(0); + + final List 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; + } + } + } +} diff --git a/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/CertificateRecorder.java b/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/CertificateRecorder.java index fb3912bc267f3..4343a2db0c049 100644 --- a/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/CertificateRecorder.java +++ b/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/CertificateRecorder.java @@ -27,6 +27,7 @@ public class CertificateRecorder implements TlsConfigurationRegistry { private final Map certificates = new ConcurrentHashMap<>(); private volatile TlsCertificateUpdater reloader; + private volatile Vertx vertx; /** * Validate the certificate configuration. @@ -38,6 +39,7 @@ public class CertificateRecorder implements TlsConfigurationRegistry { * @param vertx the Vert.x instance */ public void validateCertificates(TlsConfig config, RuntimeValue vertx, ShutdownContext shutdownContext) { + this.vertx = vertx.getValue(); // Verify the default config if (config.defaultCertificateConfig().isPresent()) { verifyCertificateConfig(config.defaultCertificateConfig().get(), vertx.getValue(), TlsConfig.DEFAULT_NAME); @@ -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; @@ -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) { @@ -131,6 +142,13 @@ public static TrustStoreAndTrustOptions verifyTrustStore(TrustStoreConfig config @Override public Optional 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)); } @@ -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 "); } + 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"); } diff --git a/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/JavaNetSslTlsBucketConfig.java b/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/JavaNetSslTlsBucketConfig.java new file mode 100644 index 0000000000000..5a1be00cfb4fa --- /dev/null +++ b/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/JavaNetSslTlsBucketConfig.java @@ -0,0 +1,170 @@ +package io.quarkus.tls.runtime; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.time.Duration; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.Set; + +import org.jboss.logging.Logger; + +import io.quarkus.tls.runtime.config.JKSTrustStoreConfig; +import io.quarkus.tls.runtime.config.KeyStoreConfig; +import io.quarkus.tls.runtime.config.P12TrustStoreConfig; +import io.quarkus.tls.runtime.config.PemCertsConfig; +import io.quarkus.tls.runtime.config.TlsBucketConfig; +import io.quarkus.tls.runtime.config.TrustStoreConfig; +import io.quarkus.tls.runtime.config.TrustStoreConfig.CertificateExpiryPolicy; +import io.quarkus.tls.runtime.config.TrustStoreCredentialProviderConfig; + +/** + * A {@link TlsBucketConfig} mimicking the way how SunJSSE locates the default truststore: + *
    + *
  1. If the {@code javax.net.ssl.trustStore} property is defined, then it is honored + *
  2. If the {@code $JAVA_HOME/lib/security/jssecacerts} is a regular file, then it is used + *
  3. If the {@code $JAVA_HOME/lib/security/cacerts} is a regular file, then it is used + *
  4. Otherwise an {@link IllegalStateException} is thrown. + *
+ * + * @since 3.18.0 + */ +class JavaNetSslTlsBucketConfig implements TlsBucketConfig { + + private static final Logger log = Logger.getLogger(JavaNetSslTlsBucketConfig.class); + + JavaNetSslTlsBucketConfig() { + } + + @Override + public Optional keyStore() { + return Optional.empty(); + } + + @Override + public Optional trustStore() { + final Path tsPath = defaultTrustStorePath(); + final Optional jksConfig; + final Optional p12Config; + final String tsType = System.getProperty("javax.net.ssl.trustStoreType", KeyStore.getDefaultType()) + .toLowerCase(Locale.US); + final Optional password = Optional + .ofNullable(System.getProperty("javax.net.ssl.trustStorePassword", "changeit")); + switch (tsType) { + case "pkcs12": { + p12Config = Optional.of(new JavaNetSslStoreConfig( + tsPath, + password, + Optional.empty(), + null)); + jksConfig = Optional.empty(); + break; + } + case "jks": { + p12Config = Optional.empty(); + jksConfig = Optional.of(new JavaNetSslStoreConfig( + tsPath, + password, + Optional.empty(), + null)); + break; + } + default: + throw new IllegalArgumentException("Unexpected javax.net.ssl.trustStoreType: " + tsType); + } + final TrustStoreConfig tsCfg = new JavaNetSslTrustStoreConfig(p12Config, jksConfig, CertificateExpiryPolicy.WARN); + return Optional.of(tsCfg); + } + + static Path defaultTrustStorePath() { + final String rawTsPath = System.getProperty("javax.net.ssl.trustStore"); + if (rawTsPath != null && !rawTsPath.isEmpty()) { + log.debugf("Honoring javax.net.ssl.trustStore property value: %s", rawTsPath); + return Path.of(rawTsPath); + } + final String javaHome = System.getProperty("java.home"); + if (javaHome == null || javaHome.isEmpty()) { + throw new IllegalStateException( + "Could not locate the default Java truststore because the 'java.home' property is not set"); + } + final Path javaHomePath = Path.of(javaHome); + if (!Files.isDirectory(javaHomePath)) { + throw new IllegalStateException("Could not locate the default Java truststore because the 'java.home' path '" + + javaHome + "' is not a directory"); + } + final Path jssecacerts = javaHomePath.resolve("lib/security/jssecacerts"); + if (Files.isRegularFile(jssecacerts)) { + log.debugf("Using %s as a truststore", jssecacerts); + return jssecacerts; + } + final Path cacerts = javaHomePath.resolve("lib/security/cacerts"); + if (Files.isRegularFile(cacerts)) { + log.debugf("Using %s as a truststore", cacerts); + return cacerts; + } + throw new IllegalStateException( + "Could not locate the default Java truststore. Tried javax.net.ssl.trustStore system property, " + jssecacerts + + " and " + cacerts); + } + + @Override + public Optional> cipherSuites() { + return Optional.empty(); + } + + @Override + public Set protocols() { + return Set.of("TLSv1.3", "TLSv1.2"); + } + + @Override + public Duration handshakeTimeout() { + return Duration.parse("10S"); + } + + @Override + public boolean alpn() { + return true; + } + + @Override + public Optional> certificateRevocationList() { + return Optional.empty(); + } + + @Override + public boolean trustAll() { + return false; + } + + @Override + public Optional hostnameVerificationAlgorithm() { + return Optional.empty(); + } + + @Override + public Optional reloadPeriod() { + return Optional.empty(); + } + + static record JavaNetSslStoreConfig(Path path, Optional password, Optional alias, + Optional provider) implements P12TrustStoreConfig, JKSTrustStoreConfig { + } + + static record JavaNetSslTrustStoreConfig(Optional p12, Optional jks, + CertificateExpiryPolicy certificateExpirationPolicy) implements TrustStoreConfig { + + @Override + public Optional pem() { + return Optional.empty(); + } + + @Override + public TrustStoreCredentialProviderConfig credentialsProvider() { + return null; + } + + } +} \ No newline at end of file diff --git a/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/config/TlsConfig.java b/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/config/TlsConfig.java index ab0a77134bcb5..3b05594b791c7 100644 --- a/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/config/TlsConfig.java +++ b/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/config/TlsConfig.java @@ -14,6 +14,7 @@ public interface TlsConfig { String DEFAULT_NAME = ""; + String JAVA_NET_SSL_TLS_CONFIGURATION_NAME = "javax.net.ssl"; /** * The default TLS bucket configuration