From 884c8968b45bbbcca9e851742d092161931e18a7 Mon Sep 17 00:00:00 2001 From: jcarranzan Date: Thu, 27 Feb 2025 12:41:22 +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. --- .../GrpcMtlsEncryptedPemTlsRegistryIT.java | 43 +++++++++++++++ .../security/https/TlsRegistryResource.java | 45 ++++++++++++++++ .../https/secured/HttpsEncryptedPemIT.java | 29 ++++++++++ .../TlsRegistryCertificateReloadingIT.java | 53 +++++++++++++++++++ .../secured/TlsRegistryDecryptedKeyIT.java | 33 ++++++++++++ .../ts/security/https/utils/Certificates.java | 1 + 6 files changed, 204 insertions(+) create mode 100644 http/grpc/src/test/java/io/quarkus/ts/http/grpc/GrpcMtlsEncryptedPemTlsRegistryIT.java create mode 100644 security/https/src/main/java/io/quarkus/ts/security/https/TlsRegistryResource.java create mode 100644 security/https/src/test/java/io/quarkus/ts/security/https/secured/HttpsEncryptedPemIT.java create mode 100644 security/https/src/test/java/io/quarkus/ts/security/https/secured/TlsRegistryCertificateReloadingIT.java create mode 100644 security/https/src/test/java/io/quarkus/ts/security/https/secured/TlsRegistryDecryptedKeyIT.java diff --git a/http/grpc/src/test/java/io/quarkus/ts/http/grpc/GrpcMtlsEncryptedPemTlsRegistryIT.java b/http/grpc/src/test/java/io/quarkus/ts/http/grpc/GrpcMtlsEncryptedPemTlsRegistryIT.java new file mode 100644 index 000000000..333de848a --- /dev/null +++ b/http/grpc/src/test/java/io/quarkus/ts/http/grpc/GrpcMtlsEncryptedPemTlsRegistryIT.java @@ -0,0 +1,43 @@ +package io.quarkus.ts.http.grpc; + +import static io.quarkus.test.services.Certificate.Format.ENCRYPTED_PEM; +import static org.junit.Assert.assertEquals; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.bootstrap.GrpcService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.Certificate; +import io.quarkus.test.services.Certificate.ClientCertificate; +import io.quarkus.test.services.QuarkusApplication; +import io.quarkus.ts.grpc.GreeterGrpc; +import io.quarkus.ts.grpc.HelloReply; +import io.quarkus.ts.grpc.HelloRequest; + +@QuarkusScenario +public class GrpcMtlsEncryptedPemTlsRegistryIT { + + private static final String CERT_PREFIX = "grpc-mtls-separate-server"; + private static final String CLIENT_CN_NAME = "mtls-client-name"; + @QuarkusApplication(grpc = true, ssl = true, certificates = @Certificate(prefix = CERT_PREFIX, clientCertificates = { + @ClientCertificate(cnAttribute = CLIENT_CN_NAME) }, format = ENCRYPTED_PEM, configureKeystore = true, configureTruststore = true, tlsConfigName = "mtls-server", configureHttpServer = true) + + ) + static final GrpcService app = (GrpcService) new GrpcService() + .withProperty("quarkus.grpc.server.use-separate-server", "false") + .withProperty("quarkus.http.insecure-requests", "disabled") + .withProperty("quarkus.http.ssl.client-auth", "request") + .withProperty("quarkus.http.auth.permission.perm-1.policy", "authenticated") + .withProperty("quarkus.http.auth.permission.perm-1.paths", "*") + .withProperty("quarkus.http.auth.permission.perm-1.auth-mechanism", "X509"); + + @Test + public void testMutualTlsCommunicationWithHelloService() { + try (var channel = app.securedGrpcChannel()) { + HelloRequest request = HelloRequest.newBuilder().setName(CLIENT_CN_NAME).build(); + HelloReply response = GreeterGrpc.newBlockingStub(channel).sayHello(request); + assertEquals("Hello " + CLIENT_CN_NAME, response.getMessage()); + } + } + +} diff --git a/security/https/src/main/java/io/quarkus/ts/security/https/TlsRegistryResource.java b/security/https/src/main/java/io/quarkus/ts/security/https/TlsRegistryResource.java new file mode 100644 index 000000000..17d5a8e6c --- /dev/null +++ b/security/https/src/main/java/io/quarkus/ts/security/https/TlsRegistryResource.java @@ -0,0 +1,45 @@ +package io.quarkus.ts.security.https; + +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.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import io.quarkus.tls.TlsConfiguration; +import io.quarkus.tls.TlsConfigurationRegistry; + +@Path("/tls-registry") +public class TlsRegistryResource { + + public static final String DUMMY_ENTRY_CERT = "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) { + throw new WebApplicationException("No KeyStore found in default TLS configuration.", + Response.Status.INTERNAL_SERVER_ERROR); + } + X509Certificate x509Certificate = (X509Certificate) keyStore.getCertificate(DUMMY_ENTRY_CERT); + if (x509Certificate == null) { + throw new WebApplicationException("No certificate found with alias " + DUMMY_ENTRY_CERT, + Response.Status.INTERNAL_SERVER_ERROR); + } + return "Subject X500 : " + x509Certificate.getSubjectX500Principal().getName() + + "\nSubject Alternative names (SANs) : " + x509Certificate.getSubjectAlternativeNames(); + } +} diff --git a/security/https/src/test/java/io/quarkus/ts/security/https/secured/HttpsEncryptedPemIT.java b/security/https/src/test/java/io/quarkus/ts/security/https/secured/HttpsEncryptedPemIT.java new file mode 100644 index 000000000..429ceaaeb --- /dev/null +++ b/security/https/src/test/java/io/quarkus/ts/security/https/secured/HttpsEncryptedPemIT.java @@ -0,0 +1,29 @@ +package io.quarkus.ts.security.https.secured; + +import static io.quarkus.ts.security.https.utils.Certificates.CLIENT_CN; +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 { + @QuarkusApplication(ssl = true, certificates = @Certificate(format = Certificate.Format.ENCRYPTED_PEM, configureHttpServer = true, clientCertificates = { + @Certificate.ClientCertificate(cnAttribute = CLIENT_CN) })) + static final RestService app = new RestService() + .withProperty("quarkus.http.insecure-requests", "disabled") + .withProperty("quarkus.http.ssl.client-auth", "none"); + + @Test + public void simpleHttpsCommunicationEncryptedPem() { + var response = app.mutinyHttps(CLIENT_CN).get("/hello/simple").sendAndAwait(); + assertEquals(HttpStatus.SC_OK, response.statusCode()); + assertEquals("Hello, use SSL true", response.bodyAsString(), + "Response is not the expected on that endpoint"); + } +} diff --git a/security/https/src/test/java/io/quarkus/ts/security/https/secured/TlsRegistryCertificateReloadingIT.java b/security/https/src/test/java/io/quarkus/ts/security/https/secured/TlsRegistryCertificateReloadingIT.java new file mode 100644 index 000000000..8cf5d5ab8 --- /dev/null +++ b/security/https/src/test/java/io/quarkus/ts/security/https/secured/TlsRegistryCertificateReloadingIT.java @@ -0,0 +1,53 @@ +package io.quarkus.ts.security.https.secured; + +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 TlsRegistryCertificateReloadingIT { + + private static final String MTLS_PATH = "/secured/mtls"; + private static final String CERT_PREFIX = "qe-test"; + private static final String CLIENT_CN_1 = "client-cn-1"; + private static final String NEW_CLIENT_CN = "my-new-client"; + private static final String TLS_CONFIG_NAME = "mtls-http"; + + @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, tlsConfigName = "mtls-http")) + static final RestService app = new RestService() + .withProperty("quarkus.http.ssl.client-auth", "request") + .withProperty("quarkus.management.ssl.client-auth", "required") + .withProperty("quarkus.http.insecure-requests", "disabled") + .withProperty("quarkus.tls." + TLS_CONFIG_NAME + ".reload-period", "2s"); + + @Test + public void testCertificateReload() { + var response = app.mutinyHttps(CLIENT_CN_1).get("/reload-mtls-certificates").sendAndAwait(); + assertEquals(HttpStatus.SC_OK, response.statusCode()); + assertEquals("Certificates reloaded.", response.bodyAsString()); + + var clientReq = new ClientCertificateRequest(NEW_CLIENT_CN, false); + app + . getPropertyFromContext(CertificateBuilder.INSTANCE_KEY) + .regenerateCertificate(CERT_PREFIX, certRequest -> certRequest.withClientRequests(clientReq)); + + AwaitilityUtils.untilAsserted(() -> { + var httpResponse = app.mutinyHttps(NEW_CLIENT_CN).get(MTLS_PATH).sendAndAwait(); + assertEquals(HttpStatus.SC_OK, httpResponse.statusCode()); + assertEquals("Client certificate: CN=" + NEW_CLIENT_CN, httpResponse.bodyAsString()); + + }); + } + +} diff --git a/security/https/src/test/java/io/quarkus/ts/security/https/secured/TlsRegistryDecryptedKeyIT.java b/security/https/src/test/java/io/quarkus/ts/security/https/secured/TlsRegistryDecryptedKeyIT.java new file mode 100644 index 000000000..0f7af876b --- /dev/null +++ b/security/https/src/test/java/io/quarkus/ts/security/https/secured/TlsRegistryDecryptedKeyIT.java @@ -0,0 +1,33 @@ +package io.quarkus.ts.security.https.secured; + +import static io.quarkus.ts.security.https.utils.Certificates.DUMMY_ENTRY_CERT; +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 { + @QuarkusApplication(ssl = true, certificates = @Certificate(format = Certificate.Format.ENCRYPTED_PEM, configureHttpServer = true, clientCertificates = @Certificate.ClientCertificate(cnAttribute = DUMMY_ENTRY_CERT))) + 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(DUMMY_ENTRY_CERT).get("/tls-registry").sendAndAwait(); + assertEquals(HttpStatus.SC_OK, 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/security/https/src/test/java/io/quarkus/ts/security/https/utils/Certificates.java b/security/https/src/test/java/io/quarkus/ts/security/https/utils/Certificates.java index 4ec422b72..8006e1410 100644 --- a/security/https/src/test/java/io/quarkus/ts/security/https/utils/Certificates.java +++ b/security/https/src/test/java/io/quarkus/ts/security/https/utils/Certificates.java @@ -3,6 +3,7 @@ public final class Certificates { public static final String CLIENT_CN = "client"; + public static final String DUMMY_ENTRY_CERT = "dummy-entry-0"; public static final String GUEST_CLIENT_CN = "guest-client"; public static final String UNKNOWN_CLIENT_CN = "unknown"; public static final String CLIENT_PASSWORD = "client-password";