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 7984bf3 commit 5841b9e
Show file tree
Hide file tree
Showing 4 changed files with 282 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -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<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,19 @@ public void run() {
}

public void verifyCertificateConfig(TlsBucketConfig config, Vertx vertx, String name) {
final TlsConfiguration tlsConfig = createTlsConfiguration(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 createTlsConfiguration(TlsBucketConfig config, Vertx vertx, String name) {
// Verify the key store
KeyStoreAndKeyCertOptions ks = null;
boolean sni;
Expand Down Expand Up @@ -90,16 +105,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 +137,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 createTlsConfiguration(new JavaNetSslTlsBucketConfig(), vertx,
TlsConfig.JAVA_NET_SSL_TLS_CONFIGURATION_NAME);
});
return Optional.ofNullable(result);
}
return Optional.ofNullable(certificates.get(name));
}

Expand All @@ -147,6 +160,10 @@ 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 name of the TLS configuration to register cannot be " + TlsConfig.JAVA_NET_SSL_TLS_CONFIGURATION_NAME);
}
if (configuration == null) {
throw new IllegalArgumentException("The TLS configuration to register cannot be null");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
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:
* <ol>
* <li>If the {@code javax.net.ssl.trustStore} property is defined, then it is honored
* <li>If the {@code $JAVA_HOME/lib/security/jssecacerts} is a regular file, then it is used
* <li>If the {@code $JAVA_HOME/lib/security/cacerts} is a regular file, then it is used
* <li>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<KeyStoreConfig> keyStore() {
return Optional.empty();
}

@Override
public Optional<TrustStoreConfig> trustStore() {
final Path tsPath = defaultTrustStorePath();
final Optional<JKSTrustStoreConfig> jksConfig;
final Optional<P12TrustStoreConfig> p12Config;
final String tsType = System.getProperty("javax.net.ssl.trustStoreType", KeyStore.getDefaultType())
.toLowerCase(Locale.US);
final Optional<String> 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<List<String>> cipherSuites() {
return Optional.empty();
}

@Override
public Set<String> 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<List<Path>> certificateRevocationList() {
return Optional.empty();
}

@Override
public boolean trustAll() {
return false;
}

@Override
public Optional<String> hostnameVerificationAlgorithm() {
return Optional.empty();
}

@Override
public Optional<Duration> reloadPeriod() {
return Optional.empty();
}

static record JavaNetSslStoreConfig(Path path, Optional<String> password, Optional<String> alias,
Optional<String> provider) implements P12TrustStoreConfig, JKSTrustStoreConfig {
}

static record JavaNetSslTrustStoreConfig(Optional<P12TrustStoreConfig> p12, Optional<JKSTrustStoreConfig> jks,
CertificateExpiryPolicy certificateExpirationPolicy) implements TrustStoreConfig {

@Override
public Optional<PemCertsConfig> pem() {
return Optional.empty();
}

@Override
public TrustStoreCredentialProviderConfig credentialsProvider() {
return null;
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
public interface TlsConfig {

String DEFAULT_NAME = "<default>";
String JAVA_NET_SSL_TLS_CONFIGURATION_NAME = "javax.net.ssl";

/**
* The default TLS bucket configuration
Expand Down

0 comments on commit 5841b9e

Please sign in to comment.