From 95ca6bf48ee28d664b83483f356c4f5eef60f3b6 Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Fri, 13 Dec 2024 15:02:26 +0100 Subject: [PATCH] TLS - Enable Policy Configuration for Expired or Not Yet Valid Certificates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces the ability to configure a policy for handling expired or not-yet-valid certificates presented during TLS handshakes (anywhere in the certificate chain). Previously, the trust store could not be configured to reject or warn about such certificates. While surprising, this behavior aligns with RFC 3280 and related specifications. With this change, users can now define the desired behavior using the following options: * IGNORE – Matches the previous behavior, allowing expired or not-yet-valid certificates without any warning. * WARN – Logs a warning message when such certificates are detected in the chain (new default). * REJECT – Rejects the handshake entirely if an expired or not-yet-valid certificate is encountered. --- .../quarkus/tls/ExpiredJKSTrustStoreTest.java | 100 ++++++++++++ .../quarkus/tls/ExpiredP12TrustStoreTest.java | 100 ++++++++++++ .../quarkus/tls/ExpiredPemTrustStoreTest.java | 98 ++++++++++++ ...stStoreWithMTLSAndServerRejectionTest.java | 87 +++++++++++ .../tls/ExpiredTrustStoreWithMTLSTest.java | 124 +++++++++++++++ .../tls/runtime/config/TrustStoreConfig.java | 27 ++++ .../runtime/keystores/ExpiryTrustOptions.java | 147 ++++++++++++++++++ .../tls/runtime/keystores/JKSKeyStores.java | 8 +- .../tls/runtime/keystores/P12KeyStores.java | 7 +- .../tls/runtime/keystores/PemKeyStores.java | 9 +- 10 files changed, 703 insertions(+), 4 deletions(-) create mode 100644 extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/ExpiredJKSTrustStoreTest.java create mode 100644 extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/ExpiredP12TrustStoreTest.java create mode 100644 extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/ExpiredPemTrustStoreTest.java create mode 100644 extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/ExpiredTrustStoreWithMTLSAndServerRejectionTest.java create mode 100644 extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/ExpiredTrustStoreWithMTLSTest.java create mode 100644 extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/keystores/ExpiryTrustOptions.java diff --git a/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/ExpiredJKSTrustStoreTest.java b/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/ExpiredJKSTrustStoreTest.java new file mode 100644 index 00000000000000..b421a4cec129ca --- /dev/null +++ b/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/ExpiredJKSTrustStoreTest.java @@ -0,0 +1,100 @@ +package io.quarkus.tls; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.concurrent.CountDownLatch; + +import javax.net.ssl.SSLHandshakeException; + +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +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; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.ext.web.client.WebClient; +import io.vertx.ext.web.client.WebClientOptions; + +@Certificates(baseDir = "target/certs", certificates = { + @Certificate(name = "expired-test-formats", password = "password", formats = { Format.JKS, Format.PEM, + Format.PKCS12 }, duration = -5) +}) +public class ExpiredJKSTrustStoreTest { + + private static final String configuration = """ + # Server + quarkus.tls.key-store.jks.path=target/certs/expired-test-formats-keystore.jks + quarkus.tls.key-store.jks.password=password + + # Clients + quarkus.tls.warn.trust-store.jks.path=target/certs/expired-test-formats-truststore.jks + quarkus.tls.warn.trust-store.jks.password=password + quarkus.tls.warn.trust-store.certificate-expiration-policy=warn + + quarkus.tls.reject.trust-store.jks.path=target/certs/expired-test-formats-truststore.jks + quarkus.tls.reject.trust-store.jks.password=password + quarkus.tls.reject.trust-store.certificate-expiration-policy=reject + """; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .add(new StringAsset(configuration), "application.properties")); + + @Inject + TlsConfigurationRegistry certificates; + + @Inject + Vertx vertx; + + @Test + void testWarn() throws InterruptedException { + TlsConfiguration cf = certificates.get("warn").orElseThrow(); + assertThat(cf.getTrustStoreOptions()).isNotNull(); + + WebClient client = WebClient.create(vertx, new WebClientOptions() + .setSsl(true) + .setTrustOptions(cf.getTrustStoreOptions())); + + vertx.createHttpServer(new HttpServerOptions() + .setSsl(true) + .setKeyCertOptions(certificates.getDefault().orElseThrow().getKeyStoreOptions())) + .requestHandler(rc -> rc.response().end("Hello")).listen(8081).toCompletionStage().toCompletableFuture().join(); + + CountDownLatch latch = new CountDownLatch(1); + client.get(8081, "localhost", "/").send(ar -> { + assertThat(ar.succeeded()).isTrue(); + assertThat(ar.result().bodyAsString()).isEqualTo("Hello"); + latch.countDown(); + }); + + assertThat(latch.await(10, java.util.concurrent.TimeUnit.SECONDS)).isTrue(); + } + + @Test + void testReject() { + TlsConfiguration cf = certificates.get("reject").orElseThrow(); + assertThat(cf.getTrustStoreOptions()).isNotNull(); + + WebClient client = WebClient.create(vertx, new WebClientOptions() + .setSsl(true) + .setTrustOptions(cf.getTrustStoreOptions())); + + vertx.createHttpServer(new HttpServerOptions() + .setSsl(true) + .setKeyCertOptions(certificates.getDefault().orElseThrow().getKeyStoreOptions())) + .requestHandler(rc -> rc.response().end("Hello")).listen(8081).toCompletionStage().toCompletableFuture().join(); + + assertThatThrownBy(() -> client.get(8081, "localhost", "/") + .send().toCompletionStage().toCompletableFuture().join()).hasCauseInstanceOf(SSLHandshakeException.class); + } +} diff --git a/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/ExpiredP12TrustStoreTest.java b/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/ExpiredP12TrustStoreTest.java new file mode 100644 index 00000000000000..dafb2dce66bd59 --- /dev/null +++ b/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/ExpiredP12TrustStoreTest.java @@ -0,0 +1,100 @@ +package io.quarkus.tls; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.concurrent.CountDownLatch; + +import javax.net.ssl.SSLHandshakeException; + +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +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; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.ext.web.client.WebClient; +import io.vertx.ext.web.client.WebClientOptions; + +@Certificates(baseDir = "target/certs", certificates = { + @Certificate(name = "expired-test-formats", password = "password", formats = { Format.JKS, Format.PEM, + Format.PKCS12 }, duration = -5) +}) +public class ExpiredP12TrustStoreTest { + + private static final String configuration = """ + # Server + quarkus.tls.key-store.p12.path=target/certs/expired-test-formats-keystore.p12 + quarkus.tls.key-store.p12.password=password + + # Clients + quarkus.tls.warn.trust-store.p12.path=target/certs/expired-test-formats-truststore.p12 + quarkus.tls.warn.trust-store.p12.password=password + quarkus.tls.warn.trust-store.certificate-expiration-policy=warn + + quarkus.tls.reject.trust-store.p12.path=target/certs/expired-test-formats-truststore.p12 + quarkus.tls.reject.trust-store.p12.password=password + quarkus.tls.reject.trust-store.certificate-expiration-policy=reject + """; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .add(new StringAsset(configuration), "application.properties")); + + @Inject + TlsConfigurationRegistry certificates; + + @Inject + Vertx vertx; + + @Test + void testWarn() throws InterruptedException { + TlsConfiguration cf = certificates.get("warn").orElseThrow(); + assertThat(cf.getTrustStoreOptions()).isNotNull(); + + WebClient client = WebClient.create(vertx, new WebClientOptions() + .setSsl(true) + .setTrustOptions(cf.getTrustStoreOptions())); + + vertx.createHttpServer(new HttpServerOptions() + .setSsl(true) + .setKeyCertOptions(certificates.getDefault().orElseThrow().getKeyStoreOptions())) + .requestHandler(rc -> rc.response().end("Hello")).listen(8081).toCompletionStage().toCompletableFuture().join(); + + CountDownLatch latch = new CountDownLatch(1); + client.get(8081, "localhost", "/").send(ar -> { + assertThat(ar.succeeded()).isTrue(); + assertThat(ar.result().bodyAsString()).isEqualTo("Hello"); + latch.countDown(); + }); + + assertThat(latch.await(10, java.util.concurrent.TimeUnit.SECONDS)).isTrue(); + } + + @Test + void testReject() { + TlsConfiguration cf = certificates.get("reject").orElseThrow(); + assertThat(cf.getTrustStoreOptions()).isNotNull(); + + WebClient client = WebClient.create(vertx, new WebClientOptions() + .setSsl(true) + .setTrustOptions(cf.getTrustStoreOptions())); + + vertx.createHttpServer(new HttpServerOptions() + .setSsl(true) + .setKeyCertOptions(certificates.getDefault().orElseThrow().getKeyStoreOptions())) + .requestHandler(rc -> rc.response().end("Hello")).listen(8081).toCompletionStage().toCompletableFuture().join(); + + assertThatThrownBy(() -> client.get(8081, "localhost", "/") + .send().toCompletionStage().toCompletableFuture().join()).hasCauseInstanceOf(SSLHandshakeException.class); + } +} diff --git a/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/ExpiredPemTrustStoreTest.java b/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/ExpiredPemTrustStoreTest.java new file mode 100644 index 00000000000000..7324667f18623a --- /dev/null +++ b/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/ExpiredPemTrustStoreTest.java @@ -0,0 +1,98 @@ +package io.quarkus.tls; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.concurrent.CountDownLatch; + +import javax.net.ssl.SSLHandshakeException; + +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +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; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.ext.web.client.WebClient; +import io.vertx.ext.web.client.WebClientOptions; + +@Certificates(baseDir = "target/certs", certificates = { + @Certificate(name = "expired-test-formats", password = "password", formats = { Format.JKS, Format.PEM, + Format.PKCS12 }, duration = -5) +}) +public class ExpiredPemTrustStoreTest { + + private static final String configuration = """ + # Server + quarkus.tls.key-store.p12.path=target/certs/expired-test-formats-keystore.p12 + quarkus.tls.key-store.p12.password=password + + # Clients + quarkus.tls.warn.trust-store.pem.certs=target/certs/expired-test-formats.crt + quarkus.tls.warn.trust-store.certificate-expiration-policy=warn + + quarkus.tls.reject.trust-store.pem.certs=target/certs/expired-test-formats.crt + quarkus.tls.reject.trust-store.certificate-expiration-policy=reject + """; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .add(new StringAsset(configuration), "application.properties")); + + @Inject + TlsConfigurationRegistry certificates; + + @Inject + Vertx vertx; + + @Test + void testWarn() throws InterruptedException { + TlsConfiguration cf = certificates.get("warn").orElseThrow(); + assertThat(cf.getTrustStoreOptions()).isNotNull(); + + WebClient client = WebClient.create(vertx, new WebClientOptions() + .setSsl(true) + .setTrustOptions(cf.getTrustStoreOptions())); + + vertx.createHttpServer(new HttpServerOptions() + .setSsl(true) + .setKeyCertOptions(certificates.getDefault().orElseThrow().getKeyStoreOptions())) + .requestHandler(rc -> rc.response().end("Hello")).listen(8081).toCompletionStage().toCompletableFuture().join(); + + CountDownLatch latch = new CountDownLatch(1); + client.get(8081, "localhost", "/").send(ar -> { + assertThat(ar.succeeded()).isTrue(); + assertThat(ar.result().bodyAsString()).isEqualTo("Hello"); + latch.countDown(); + }); + + assertThat(latch.await(10, java.util.concurrent.TimeUnit.SECONDS)).isTrue(); + } + + @Test + void testReject() { + TlsConfiguration cf = certificates.get("reject").orElseThrow(); + assertThat(cf.getTrustStoreOptions()).isNotNull(); + + WebClient client = WebClient.create(vertx, new WebClientOptions() + .setSsl(true) + .setTrustOptions(cf.getTrustStoreOptions())); + + vertx.createHttpServer(new HttpServerOptions() + .setSsl(true) + .setKeyCertOptions(certificates.getDefault().orElseThrow().getKeyStoreOptions())) + .requestHandler(rc -> rc.response().end("Hello")).listen(8081).toCompletionStage().toCompletableFuture().join(); + + assertThatThrownBy(() -> client.get(8081, "localhost", "/") + .send().toCompletionStage().toCompletableFuture().join()).hasCauseInstanceOf(SSLHandshakeException.class); + } +} diff --git a/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/ExpiredTrustStoreWithMTLSAndServerRejectionTest.java b/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/ExpiredTrustStoreWithMTLSAndServerRejectionTest.java new file mode 100644 index 00000000000000..5cbd873342efc4 --- /dev/null +++ b/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/ExpiredTrustStoreWithMTLSAndServerRejectionTest.java @@ -0,0 +1,87 @@ +package io.quarkus.tls; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.AfterEach; +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; +import io.vertx.core.Vertx; +import io.vertx.core.http.ClientAuth; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.ext.web.client.WebClient; +import io.vertx.ext.web.client.WebClientOptions; + +@Certificates(baseDir = "target/certs", certificates = { + @Certificate(name = "expired-mtls", password = "password", formats = { Format.PKCS12 }, duration = -5, client = true) +}) +public class ExpiredTrustStoreWithMTLSAndServerRejectionTest { + + private static final String configuration = """ + # Server + quarkus.tls.key-store.p12.path=target/certs/expired-mtls-keystore.p12 + quarkus.tls.key-store.p12.password=password + quarkus.tls.trust-store.p12.path=target/certs/expired-mtls-server-truststore.p12 + quarkus.tls.trust-store.p12.password=password + quarkus.tls.trust-store.certificate-expiration-policy=reject + + # Client + quarkus.tls.warn.trust-store.p12.path=target/certs/expired-mtls-client-truststore.p12 + quarkus.tls.warn.trust-store.p12.password=password + quarkus.tls.warn.trust-store.certificate-expiration-policy=ignore + quarkus.tls.warn.key-store.p12.path=target/certs/expired-mtls-client-keystore.p12 + quarkus.tls.warn.key-store.p12.password=password + """; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .add(new StringAsset(configuration), "application.properties")); + + @Inject + TlsConfigurationRegistry certificates; + + @Inject + Vertx vertx; + + private HttpServer server; + + @AfterEach + void cleanup() { + if (server != null) { + server.close().toCompletionStage().toCompletableFuture().join(); + } + } + + @Test + void testServerRejection() throws InterruptedException { + TlsConfiguration cf = certificates.get("warn").orElseThrow(); + assertThat(cf.getTrustStoreOptions()).isNotNull(); + + WebClient client = WebClient.create(vertx, new WebClientOptions() + .setSsl(true) + .setKeyCertOptions(cf.getKeyStoreOptions()) + .setTrustOptions(cf.getTrustStoreOptions())); + + server = vertx.createHttpServer(new HttpServerOptions() + .setSsl(true) + .setClientAuth(ClientAuth.REQUIRED) + .setTrustOptions(certificates.getDefault().orElseThrow().getTrustStoreOptions()) + .setKeyCertOptions(certificates.getDefault().orElseThrow().getKeyStoreOptions())) + .requestHandler(rc -> rc.response().end("Hello")).listen(8081).toCompletionStage().toCompletableFuture().join(); + + assertThatThrownBy(() -> client.get(8081, "localhost", "/").send().toCompletionStage().toCompletableFuture().join()) + .hasMessageContaining("SSLHandshakeException"); + } +} diff --git a/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/ExpiredTrustStoreWithMTLSTest.java b/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/ExpiredTrustStoreWithMTLSTest.java new file mode 100644 index 00000000000000..a56fd7af490933 --- /dev/null +++ b/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/ExpiredTrustStoreWithMTLSTest.java @@ -0,0 +1,124 @@ +package io.quarkus.tls; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.concurrent.CountDownLatch; + +import javax.net.ssl.SSLHandshakeException; + +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.AfterEach; +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; +import io.vertx.core.Vertx; +import io.vertx.core.http.ClientAuth; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.ext.web.client.WebClient; +import io.vertx.ext.web.client.WebClientOptions; + +@Certificates(baseDir = "target/certs", certificates = { + @Certificate(name = "expired-mtls", password = "password", formats = { Format.PKCS12 }, duration = -5, client = true) +}) +public class ExpiredTrustStoreWithMTLSTest { + + private static final String configuration = """ + # Server + quarkus.tls.key-store.p12.path=target/certs/expired-mtls-keystore.p12 + quarkus.tls.key-store.p12.password=password + quarkus.tls.trust-store.p12.path=target/certs/expired-mtls-server-truststore.p12 + quarkus.tls.trust-store.p12.password=password + # The server will ignore the expired client certificates + + # Clients + quarkus.tls.warn.trust-store.p12.path=target/certs/expired-mtls-client-truststore.p12 + quarkus.tls.warn.trust-store.p12.password=password + quarkus.tls.warn.trust-store.certificate-expiration-policy=warn + quarkus.tls.warn.key-store.p12.path=target/certs/expired-mtls-client-keystore.p12 + quarkus.tls.warn.key-store.p12.password=password + + quarkus.tls.reject.trust-store.p12.path=target/certs/expired-mtls-client-truststore.p12 + quarkus.tls.reject.trust-store.p12.password=password + quarkus.tls.reject.trust-store.certificate-expiration-policy=reject + quarkus.tls.reject.key-store.p12.path=target/certs/expired-mtls-client-keystore.p12 + quarkus.tls.reject.key-store.p12.password=password + """; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .add(new StringAsset(configuration), "application.properties")); + + @Inject + TlsConfigurationRegistry certificates; + + @Inject + Vertx vertx; + + private HttpServer server; + + @AfterEach + void cleanup() { + if (server != null) { + server.close().toCompletionStage().toCompletableFuture().join(); + } + } + + @Test + void testWarn() throws InterruptedException { + TlsConfiguration cf = certificates.get("warn").orElseThrow(); + assertThat(cf.getTrustStoreOptions()).isNotNull(); + + WebClient client = WebClient.create(vertx, new WebClientOptions() + .setSsl(true) + .setKeyCertOptions(cf.getKeyStoreOptions()) + .setTrustOptions(cf.getTrustStoreOptions())); + + server = vertx.createHttpServer(new HttpServerOptions() + .setSsl(true) + .setClientAuth(ClientAuth.REQUIRED) + .setTrustOptions(certificates.getDefault().orElseThrow().getTrustStoreOptions()) + .setKeyCertOptions(certificates.getDefault().orElseThrow().getKeyStoreOptions())) + .requestHandler(rc -> rc.response().end("Hello")).listen(8081).toCompletionStage().toCompletableFuture().join(); + + CountDownLatch latch = new CountDownLatch(1); + client.get(8081, "localhost", "/").send(ar -> { + assertThat(ar.succeeded()).isTrue(); + assertThat(ar.result().bodyAsString()).isEqualTo("Hello"); + latch.countDown(); + }); + + assertThat(latch.await(10, java.util.concurrent.TimeUnit.SECONDS)).isTrue(); + } + + @Test + void testReject() { + TlsConfiguration cf = certificates.get("reject").orElseThrow(); + assertThat(cf.getTrustStoreOptions()).isNotNull(); + + WebClient client = WebClient.create(vertx, new WebClientOptions() + .setSsl(true) + .setKeyCertOptions(cf.getKeyStoreOptions()) + .setTrustOptions(cf.getTrustStoreOptions())); + + server = vertx.createHttpServer(new HttpServerOptions() + .setSsl(true) + .setClientAuth(ClientAuth.REQUIRED) + .setTrustOptions(certificates.getDefault().orElseThrow().getTrustStoreOptions()) + .setKeyCertOptions(certificates.getDefault().orElseThrow().getKeyStoreOptions())) + .requestHandler(rc -> rc.response().end("Hello")).listen(8081).toCompletionStage().toCompletableFuture().join(); + + assertThatThrownBy(() -> client.get(8081, "localhost", "/") + .send().toCompletionStage().toCompletableFuture().join()).hasCauseInstanceOf(SSLHandshakeException.class); + } +} diff --git a/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/config/TrustStoreConfig.java b/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/config/TrustStoreConfig.java index 7d7d75df0ffc08..4c81d27bb04b7b 100644 --- a/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/config/TrustStoreConfig.java +++ b/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/config/TrustStoreConfig.java @@ -3,6 +3,7 @@ import java.util.Optional; import io.quarkus.runtime.annotations.ConfigGroup; +import io.smallrye.config.WithDefault; @ConfigGroup public interface TrustStoreConfig { @@ -22,6 +23,32 @@ public interface TrustStoreConfig { */ Optional jks(); + /** + * Enforce certificate expiration. + * When enables, the certificate expiration date is verified and the certificate (or any certificate in the chain) + * is rejected if it is expired. + */ + @WithDefault("WARN") + CertificateExpiryPolicy certificateExpirationPolicy(); + + /** + * The policy to apply when a certificate is expired. + */ + enum CertificateExpiryPolicy { + /** + * Ignore the expiration date. + */ + IGNORE, + /** + * Log a warning when the certificate is expired. + */ + WARN, + /** + * Reject the certificate if it is expired. + */ + REJECT + } + /** * The credential provider configuration for the trust store. * A credential provider offers a way to retrieve the trust store password. diff --git a/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/keystores/ExpiryTrustOptions.java b/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/keystores/ExpiryTrustOptions.java new file mode 100644 index 00000000000000..6f26790bf15ec8 --- /dev/null +++ b/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/keystores/ExpiryTrustOptions.java @@ -0,0 +1,147 @@ +package io.quarkus.tls.runtime.keystores; + +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.util.function.Function; + +import javax.net.ssl.ManagerFactoryParameters; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.TrustManagerFactorySpi; +import javax.net.ssl.X509TrustManager; + +import org.jboss.logging.Logger; + +import io.quarkus.tls.runtime.config.TrustStoreConfig; +import io.smallrye.mutiny.unchecked.Unchecked; +import io.smallrye.mutiny.unchecked.UncheckedFunction; +import io.vertx.core.Vertx; +import io.vertx.core.net.TrustOptions; + +/** + * A trust options that verify for the certificate expiration date and reject the certificate if it is expired. + */ +public class ExpiryTrustOptions implements TrustOptions { + + private final TrustOptions delegate; + private final TrustStoreConfig.CertificateExpiryPolicy policy; + + private static final Logger LOGGER = Logger.getLogger(ExpiryTrustOptions.class); + + public ExpiryTrustOptions(TrustOptions delegate, TrustStoreConfig.CertificateExpiryPolicy certificateExpiryPolicy) { + this.delegate = delegate; + this.policy = certificateExpiryPolicy; + } + + @Override + public TrustOptions copy() { + return this; + } + + @Override + public TrustManagerFactory getTrustManagerFactory(Vertx vertx) throws Exception { + var tmf = delegate.getTrustManagerFactory(vertx); + return new TrustManagerFactory(new TrustManagerFactorySpi() { + @Override + protected void engineInit(KeyStore ks) throws KeyStoreException { + tmf.init(ks); + } + + @Override + protected void engineInit(ManagerFactoryParameters spec) throws InvalidAlgorithmParameterException { + tmf.init(spec); + } + + @Override + protected TrustManager[] engineGetTrustManagers() { + var managers = tmf.getTrustManagers(); + return getWrappedTrustManagers(managers); + } + }, tmf.getProvider(), tmf.getAlgorithm()) { + // Empty - we use this pattern to have access to the protected constructor + }; + } + + @Override + public Function trustManagerMapper(Vertx vertx) { + return Unchecked.function(new UncheckedFunction() { + @Override + public TrustManager[] apply(String s) throws Exception { + TrustManager[] tms = delegate.trustManagerMapper(vertx).apply(s); + return ExpiryTrustOptions.this.getWrappedTrustManagers(tms); + } + }); + } + + private TrustManager[] getWrappedTrustManagers(TrustManager[] tms) { + var wrapped = new TrustManager[tms.length]; + for (int i = 0; i < tms.length; i++) { + var manager = tms[i]; + if (!(manager instanceof X509TrustManager)) { + wrapped[i] = manager; + } else { + wrapped[i] = new ExpiryAwareX509TrustManager((X509TrustManager) manager); + } + } + return wrapped; + } + + private class ExpiryAwareX509TrustManager implements X509TrustManager { + + final X509TrustManager tm; + + private ExpiryAwareX509TrustManager(X509TrustManager tm) { + this.tm = tm; + } + + @Override + public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) + throws CertificateException { + verifyExpiration(chain); + tm.checkClientTrusted(chain, authType); + } + + private void verifyExpiration(X509Certificate[] chain) + throws CertificateExpiredException, CertificateNotYetValidException { + // Verify if there is any expired certificate in the chain - if so, throw an exception + for (X509Certificate cert : chain) { + try { + cert.checkValidity(); + } catch (CertificateExpiredException e) { + // Ignore has been handled before, so, no need to check for this value. + if (policy == TrustStoreConfig.CertificateExpiryPolicy.REJECT) { + LOGGER.error("A certificate has expired - rejecting", e); + throw e; + } else { // WARN + LOGGER.warn("A certificate has expired", e); + } + } catch (CertificateNotYetValidException e) { + // Ignore has been handled before, so, no need to check for this value. + if (policy == TrustStoreConfig.CertificateExpiryPolicy.REJECT) { + LOGGER.error("A certificate is not yet valid - rejecting", e); + throw e; + } else { // WARN + LOGGER.warn("A certificate is not yet valid", e); + } + } + } + } + + @Override + public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) + throws CertificateException { + verifyExpiration(chain); + tm.checkServerTrusted(chain, authType); + } + + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return tm.getAcceptedIssuers(); + } + } +} diff --git a/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/keystores/JKSKeyStores.java b/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/keystores/JKSKeyStores.java index 347127541b1f91..2fc073b2e0eaad 100644 --- a/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/keystores/JKSKeyStores.java +++ b/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/keystores/JKSKeyStores.java @@ -43,7 +43,13 @@ public static TrustStoreAndTrustOptions verifyJKSTrustStoreStore(TrustStoreConfi JksOptions options = toOptions(jksConfig, config.credentialsProvider(), name); KeyStore ks = loadKeyStore(vertx, name, options, "trust"); verifyTrustStoreAlias(options, name, ks); - return new TrustStoreAndTrustOptions(ks, options); + if (config.certificateExpirationPolicy() == TrustStoreConfig.CertificateExpiryPolicy.IGNORE) { + return new TrustStoreAndTrustOptions(ks, options); + } else { + var wrapped = new ExpiryTrustOptions(options, config.certificateExpirationPolicy()); + return new TrustStoreAndTrustOptions(ks, wrapped); + } + } private static JksOptions toOptions(JKSKeyStoreConfig config, diff --git a/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/keystores/P12KeyStores.java b/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/keystores/P12KeyStores.java index 70f875be2568d8..3d205261c836dd 100644 --- a/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/keystores/P12KeyStores.java +++ b/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/keystores/P12KeyStores.java @@ -43,7 +43,12 @@ public static TrustStoreAndTrustOptions verifyP12TrustStoreStore(TrustStoreConfi PfxOptions options = toOptions(p12Config, config.credentialsProvider(), name); KeyStore ks = loadKeyStore(vertx, name, options, "trust"); verifyTrustStoreAlias(p12Config.alias(), name, ks); - return new TrustStoreAndTrustOptions(ks, options); + if (config.certificateExpirationPolicy() == TrustStoreConfig.CertificateExpiryPolicy.IGNORE) { + return new TrustStoreAndTrustOptions(ks, options); + } else { + var wrapped = new ExpiryTrustOptions(options, config.certificateExpirationPolicy()); + return new TrustStoreAndTrustOptions(ks, wrapped); + } } private static PfxOptions toOptions(P12KeyStoreConfig config, KeyStoreCredentialProviderConfig pc, String name) { diff --git a/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/keystores/PemKeyStores.java b/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/keystores/PemKeyStores.java index 47a3e104e357ca..e0a21c360b6dfd 100644 --- a/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/keystores/PemKeyStores.java +++ b/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/keystores/PemKeyStores.java @@ -43,8 +43,13 @@ public static TrustStoreAndTrustOptions verifyPEMTrustStoreStore(TrustStoreConfi } try { var options = config.toOptions(); - KeyStore keyStore = options.loadKeyStore(vertx); - return new TrustStoreAndTrustOptions(keyStore, options); + KeyStore ks = options.loadKeyStore(vertx); + if (tsc.certificateExpirationPolicy() == TrustStoreConfig.CertificateExpiryPolicy.IGNORE) { + return new TrustStoreAndTrustOptions(ks, options); + } else { + var wrapped = new ExpiryTrustOptions(options, tsc.certificateExpirationPolicy()); + return new TrustStoreAndTrustOptions(ks, wrapped); + } } catch (UncheckedIOException e) { throw new IllegalStateException("Invalid PEM trusted certificates configuration for certificate '" + name + "' - cannot read the PEM certificate files", e);