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);