From 816fd16f1c223eacebddef5be982617332f5b651 Mon Sep 17 00:00:00 2001 From: jcarranzan Date: Wed, 26 Feb 2025 11:26:33 +0100 Subject: [PATCH] Adding test coverage to support encrypted PEM Keys These tests will be added: - Https communication using encrypted Pem, ensure you can communicate with Quarkus REST endpoint using HTTPS (no client-side authentication). - Certificate reloading, validate with newly generated certificate, it works for encrypted PEMs as well . - Injecting TLS registry configuration and can see the private key decrypted (so you can see keystore and check some x509 attributes). - FIPS compatibility, it works under FIPS enable environment. --- .../io/quarkus/qe/TlsRegistryResource.java | 42 ++++++++++++++++ ...tpsEncryptedPEMCertificateReloadingIT.java | 49 +++++++++++++++++++ .../io/quarkus/qe/HttpsEncryptedPemIT.java | 31 ++++++++++++ .../quarkus/qe/TlsRegistryDecryptedKeyIT.java | 35 +++++++++++++ .../security/certificate/Certificate.java | 3 ++ 5 files changed, 160 insertions(+) create mode 100644 examples/https/src/main/java/io/quarkus/qe/TlsRegistryResource.java create mode 100644 examples/https/src/test/java/io/quarkus/qe/HttpsEncryptedPEMCertificateReloadingIT.java create mode 100644 examples/https/src/test/java/io/quarkus/qe/HttpsEncryptedPemIT.java create mode 100644 examples/https/src/test/java/io/quarkus/qe/TlsRegistryDecryptedKeyIT.java diff --git a/examples/https/src/main/java/io/quarkus/qe/TlsRegistryResource.java b/examples/https/src/main/java/io/quarkus/qe/TlsRegistryResource.java new file mode 100644 index 000000000..36f9c0734 --- /dev/null +++ b/examples/https/src/main/java/io/quarkus/qe/TlsRegistryResource.java @@ -0,0 +1,42 @@ +package io.quarkus.qe; + +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.tls.TlsConfiguration; +import io.quarkus.tls.TlsConfigurationRegistry; + +@Path("/tls-registry") +public class TlsRegistryResource { + + private static final String CERT_EXAMPLE = "dummy-entry-0"; + + @Inject + TlsConfigurationRegistry tlsConfigurationRegistry; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String tlsRegistryInspection() throws KeyStoreException, CertificateParsingException { + TlsConfiguration tlsConfiguration = tlsConfigurationRegistry.getDefault().orElseThrow(); + + KeyStore keyStore = tlsConfiguration.getKeyStore(); + if (keyStore == null) { + return "No KeyStore found in default TLS configuration."; + } + X509Certificate x509Certificate = (X509Certificate) keyStore.getCertificate(CERT_EXAMPLE); + if (x509Certificate == null) { + return "No certificate found with alias " + CERT_EXAMPLE; + } + return "Subject X500 : " + x509Certificate.getSubjectX500Principal().getName() + + "\nSubject Alternative names (SANs) : " + x509Certificate.getSubjectAlternativeNames(); + } + +} diff --git a/examples/https/src/test/java/io/quarkus/qe/HttpsEncryptedPEMCertificateReloadingIT.java b/examples/https/src/test/java/io/quarkus/qe/HttpsEncryptedPEMCertificateReloadingIT.java new file mode 100644 index 000000000..e5859cff0 --- /dev/null +++ b/examples/https/src/test/java/io/quarkus/qe/HttpsEncryptedPEMCertificateReloadingIT.java @@ -0,0 +1,49 @@ +package io.quarkus.qe; + +import static io.quarkus.qe.HttpsTlsRegistryNamedConfigIT.CLIENT_CN_1; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.security.certificate.CertificateBuilder; +import io.quarkus.test.security.certificate.ClientCertificateRequest; +import io.quarkus.test.services.Certificate; +import io.quarkus.test.services.QuarkusApplication; +import io.quarkus.test.utils.AwaitilityUtils; + +@QuarkusScenario +public class HttpsEncryptedPEMCertificateReloadingIT { + + private static final String NEW_CLIENT_CN = "my-new-client"; + private static final String CERT_PREFIX = "reload-cert"; + + @QuarkusApplication(ssl = true, certificates = @Certificate(clientCertificates = { + @Certificate.ClientCertificate(cnAttribute = CLIENT_CN_1) + }, configureTruststore = true, configureHttpServer = true, configureKeystore = true, prefix = CERT_PREFIX, format = Certificate.Format.ENCRYPTED_PEM)) + static final RestService app = new RestService() + .withProperty("quarkus.http.ssl.client-auth", "request") + .withProperty("quarkus.http.insecure-requests", "DISABLED") + .withProperty("quarkus.tls.reload-period", "2s"); + + @Test + public void encryptedPEMCertificateReloadingTest() { + var path = "/greeting/mutual-tls"; + var response = app.mutinyHttps(CLIENT_CN_1).get(path).sendAndAwait(); + assertEquals(HttpStatus.SC_OK, response.statusCode()); + assertEquals("Hello CN=%s!".formatted(CLIENT_CN_1), response.bodyAsString()); + + var clientReq = new ClientCertificateRequest(NEW_CLIENT_CN, false); + app + . getPropertyFromContext(CertificateBuilder.INSTANCE_KEY) + .regenerateCertificate(CERT_PREFIX, certReq -> certReq.withClientRequests(clientReq)); + + AwaitilityUtils.untilAsserted(() -> { + var response1 = app.mutinyHttps(NEW_CLIENT_CN).get(path).sendAndAwait(); + assertEquals(HttpStatus.SC_OK, response1.statusCode()); + assertEquals("Hello CN=%s!".formatted(NEW_CLIENT_CN), response1.bodyAsString()); + }); + } +} diff --git a/examples/https/src/test/java/io/quarkus/qe/HttpsEncryptedPemIT.java b/examples/https/src/test/java/io/quarkus/qe/HttpsEncryptedPemIT.java new file mode 100644 index 000000000..3a1470639 --- /dev/null +++ b/examples/https/src/test/java/io/quarkus/qe/HttpsEncryptedPemIT.java @@ -0,0 +1,31 @@ +package io.quarkus.qe; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.Certificate; +import io.quarkus.test.services.QuarkusApplication; + +@QuarkusScenario +public class HttpsEncryptedPemIT { + + private static final String DUMMY_CLIENT_CN_1 = "my-dummy--1"; + @QuarkusApplication(ssl = true, certificates = @Certificate(format = Certificate.Format.ENCRYPTED_PEM, configureHttpServer = true, clientCertificates = { + @Certificate.ClientCertificate(cnAttribute = DUMMY_CLIENT_CN_1) + })) + static final RestService app = new RestService() + .withProperty("quarkus.http.insecure-requests", "disabled") + .withProperty("quarkus.http.ssl.client-auth", "none"); + + @Test + public void testSimpleHttpsCommunication() { + var response = app.mutinyHttps(DUMMY_CLIENT_CN_1).get("/greeting").sendAndAwait(); + assertEquals(HttpStatus.SC_OK, response.statusCode()); + assertEquals("Hello World!", response.bodyAsString(), + "Response is not the expected on that endpoint"); + } +} diff --git a/examples/https/src/test/java/io/quarkus/qe/TlsRegistryDecryptedKeyIT.java b/examples/https/src/test/java/io/quarkus/qe/TlsRegistryDecryptedKeyIT.java new file mode 100644 index 000000000..dcb67fab6 --- /dev/null +++ b/examples/https/src/test/java/io/quarkus/qe/TlsRegistryDecryptedKeyIT.java @@ -0,0 +1,35 @@ +package io.quarkus.qe; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.Certificate; +import io.quarkus.test.services.QuarkusApplication; + +@QuarkusScenario +public class TlsRegistryDecryptedKeyIT { + private static final String CERT_EXAMPLE = "dummy-entry-0"; + + @QuarkusApplication(ssl = true, certificates = @Certificate(format = Certificate.Format.ENCRYPTED_PEM, configureHttpServer = true, clientCertificates = @Certificate.ClientCertificate(cnAttribute = CERT_EXAMPLE))) + static final RestService app = new RestService() + .withProperty("quarkus.http.insecure-requests", "disabled") + .withProperty("quarkus.http.ssl.client-auth", "none"); + + @Test + public void testInspectDecryptedKey() { + var response = app.mutinyHttps(CERT_EXAMPLE).get("/tls-registry").sendAndAwait(); + assertEquals(HttpStatus.SC_OK, response.statusCode(), "Expected 200 but got: " + + response.statusCode()); + + String body = response.bodyAsString(); + assertTrue(body.contains("Subject X500 : CN=dummy-entry-0"), + "Response from /tls-inspection does not contain subject info: " + body); + assertTrue(body.contains("localhost") || body.contains(" [[2, localhost], [2, 0.0.0.0]]"), + "Response from /tls-inspection does not mention Subject Alternative names (SANs) : localhost or [[2, localhost], [2, 0.0.0.0]]"); + } +} diff --git a/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/Certificate.java b/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/Certificate.java index 3e46b7b15..b335b638c 100644 --- a/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/Certificate.java +++ b/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/Certificate.java @@ -123,6 +123,9 @@ private static Certificate.PemCertificate of(CertificateOptions o) { boolean withClientCerts = cnAttrs.length > 0; String cn = withClientCerts ? cnAttrs[0] : "localhost"; final CertificateRequest request = createCertificateRequest(o.prefix(), o.format(), o.password(), withClientCerts, cn); + if (o.format() == ENCRYPTED_PEM && !withClientCerts) { + throw new RuntimeException("You configured ENCRYPTED_PEM but didn't provide any client certificates. "); + } try { var certFile = generator.generate(request).get(0); if (certFile instanceof Pkcs12CertificateFiles pkcs12CertFile) {