diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ssl/SslServerWithJksTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ssl/SslServerWithJksTest.java index 462663c6ed5c4..bcd8f2527995d 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ssl/SslServerWithJksTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ssl/SslServerWithJksTest.java @@ -3,7 +3,6 @@ import static org.hamcrest.core.Is.is; import java.io.File; -import java.net.MalformedURLException; import java.net.URL; import javax.enterprise.context.ApplicationScoped; @@ -18,11 +17,15 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; import io.restassured.RestAssured; import io.vertx.ext.web.Router; public class SslServerWithJksTest { + @TestHTTPResource(value = "/ssl", ssl = true) + URL url; + @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) @@ -41,8 +44,7 @@ public static void restoreRestAssured() { } @Test - public void testSslServerWithJKS() throws MalformedURLException { - URL url = new URL("https://localhost:8444/ssl"); + public void testSslServerWithJKS() { RestAssured.get(url).then().statusCode(200).body(is("ssl")); } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ssl/SslServerWithP12Test.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ssl/SslServerWithP12Test.java index 42fbe50c14c96..5cf9f63c0983c 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ssl/SslServerWithP12Test.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ssl/SslServerWithP12Test.java @@ -3,7 +3,6 @@ import static org.hamcrest.core.Is.is; import java.io.File; -import java.net.MalformedURLException; import java.net.URL; import javax.enterprise.context.ApplicationScoped; @@ -18,11 +17,15 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; import io.restassured.RestAssured; import io.vertx.ext.web.Router; public class SslServerWithP12Test { + @TestHTTPResource(value = "/ssl", ssl = true) + URL url; + @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) @@ -41,8 +44,7 @@ public static void restoreRestAssured() { } @Test - public void testSslServerWithPkcs12() throws MalformedURLException { - URL url = new URL("https://localhost:8444/ssl"); + public void testSslServerWithPkcs12() { RestAssured.get(url).then().statusCode(200).body(is("ssl")); } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ssl/SslServerWithPemTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ssl/SslServerWithPemTest.java index b6b24dc0f279f..c91cbd06b305a 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ssl/SslServerWithPemTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ssl/SslServerWithPemTest.java @@ -3,7 +3,6 @@ import static org.hamcrest.core.Is.is; import java.io.File; -import java.net.MalformedURLException; import java.net.URL; import javax.enterprise.context.ApplicationScoped; @@ -18,11 +17,15 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; import io.restassured.RestAssured; import io.vertx.ext.web.Router; public class SslServerWithPemTest { + @TestHTTPResource(value = "/ssl", ssl = true) + URL url; + @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) @@ -42,8 +45,7 @@ public static void restoreRestAssured() { } @Test - public void testSslServerWithPem() throws MalformedURLException { - URL url = new URL("https://localhost:8444/ssl"); + public void testSslServerWithPem() { RestAssured.get(url).then().statusCode(200).body(is("ssl")); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CertificateConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CertificateConfig.java index b98701ae25330..a465eef97e34e 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CertificateConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CertificateConfig.java @@ -41,4 +41,23 @@ public class CertificateConfig { */ @ConfigItem(defaultValue = "password") public String keyStorePassword; + + /** + * An optional trust store which holds the certificate information of the certificates to trust + */ + @ConfigItem + public Optional trustStoreFile; + + /** + * An optional parameter to specify type of the trust store file. If not given, the type is automatically detected + * based on the file name. + */ + @ConfigItem + public Optional trustStoreFileType; + + /** + * A parameter to specify the password of the trust store file. + */ + @ConfigItem + public Optional trustStorePassword; } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ServerSslConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ServerSslConfig.java index 526ba667bac60..08ec9669bbf49 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ServerSslConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ServerSslConfig.java @@ -5,6 +5,7 @@ import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.DefaultConverter; +import io.vertx.core.http.ClientAuth; /** * Shared configuration for setting up server-side SSL. @@ -29,4 +30,11 @@ public class ServerSslConfig { @ConfigItem(defaultValue = "TLSv1.3,TLSv1.2") public List protocols; + /** + * Configures the engine to require/request client authentication. + * NONE, REQUEST, REQUIRED + */ + @ConfigItem(defaultValue = "NONE") + public ClientAuth clientAuth; + } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java index 2083218ece802..bb70fcaa2ba6d 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java @@ -296,6 +296,8 @@ private static HttpServerOptions createSslOptions(HttpConfiguration httpConfigur final Optional keyFile = sslConfig.certificate.keyFile; final Optional keyStoreFile = sslConfig.certificate.keyStoreFile; final String keystorePassword = sslConfig.certificate.keyStorePassword; + final Optional trustStoreFile = sslConfig.certificate.trustStoreFile; + final Optional trustStorePassword = sslConfig.certificate.trustStorePassword; final HttpServerOptions serverOptions = new HttpServerOptions(); serverOptions.setMaxHeaderSize(httpConfiguration.limits.maxHeaderSize.asBigInteger().intValueExact()); setIdleTimeout(httpConfiguration, serverOptions); @@ -309,13 +311,7 @@ private static HttpServerOptions createSslOptions(HttpConfiguration httpConfigur if (keyStoreFileType.isPresent()) { type = keyStoreFileType.get().toLowerCase(); } else { - final String pathName = keyStorePath.toString(); - if (pathName.endsWith(".p12") || pathName.endsWith(".pkcs12") || pathName.endsWith(".pfx")) { - type = "pkcs12"; - } else { - // assume jks - type = "jks"; - } + type = findKeystoreFileType(keyStorePath); } byte[] data = getFileContent(keyStorePath); @@ -343,6 +339,22 @@ private static HttpServerOptions createSslOptions(HttpConfiguration httpConfigur return null; } + if (trustStoreFile.isPresent()) { + if (!trustStorePassword.isPresent()) { + throw new IllegalArgumentException("No trust store password provided"); + } + final String type; + final Optional trustStoreFileType = sslConfig.certificate.trustStoreFileType; + final Path trustStoreFilePath = trustStoreFile.get(); + if (trustStoreFileType.isPresent()) { + type = trustStoreFileType.get().toLowerCase(); + } else { + type = findKeystoreFileType(trustStoreFilePath); + } + createTrustStoreOptions(trustStoreFilePath, trustStorePassword.get(), type, + serverOptions); + } + for (String cipher : sslConfig.cipherSuites) { if (!cipher.isEmpty()) { serverOptions.addEnabledCipherSuite(cipher); @@ -357,6 +369,7 @@ private static HttpServerOptions createSslOptions(HttpConfiguration httpConfigur serverOptions.setSsl(true); serverOptions.setHost(httpConfiguration.host); serverOptions.setPort(httpConfiguration.determineSslPort(launchMode)); + serverOptions.setClientAuth(sslConfig.clientAuth); return serverOptions; } @@ -385,6 +398,40 @@ private static void createPemKeyCertOptions(Path certFile, Path keyFile, serverOptions.setPemKeyCertOptions(pemKeyCertOptions); } + private static void createTrustStoreOptions(Path trustStoreFile, String trustStorePassword, + String trustStoreFileType, HttpServerOptions serverOptions) throws IOException { + byte[] data = getFileContent(trustStoreFile); + switch (trustStoreFileType) { + case "pkcs12": { + PfxOptions options = new PfxOptions() + .setPassword(trustStorePassword) + .setValue(Buffer.buffer(data)); + serverOptions.setPfxTrustOptions(options); + break; + } + case "jks": { + JksOptions options = new JksOptions() + .setPassword(trustStorePassword) + .setValue(Buffer.buffer(data)); + serverOptions.setTrustStoreOptions(options); + break; + } + default: + throw new IllegalArgumentException( + "Unknown truststore type: " + trustStoreFileType + " valid types are jks or pkcs12"); + } + } + + private static String findKeystoreFileType(Path storePath) { + final String pathName = storePath.toString(); + if (pathName.endsWith(".p12") || pathName.endsWith(".pkcs12") || pathName.endsWith(".pfx")) { + return "pkcs12"; + } else { + // assume jks + return "jks"; + } + } + private static byte[] doRead(InputStream is) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); byte[] buf = new byte[1024]; diff --git a/integration-tests/vertx-http/pom.xml b/integration-tests/vertx-http/pom.xml index c6b276a8aa757..eef1a2e2f7c98 100644 --- a/integration-tests/vertx-http/pom.xml +++ b/integration-tests/vertx-http/pom.xml @@ -97,7 +97,14 @@ true true + + + -H:IncludeResources=.*\.jks + -H:EnableURLProtocols=http,https + ${graalvmHome} + true + true diff --git a/integration-tests/vertx-http/src/main/resources/application.properties b/integration-tests/vertx-http/src/main/resources/application.properties index 426541279ba0c..a93e6337fd2e6 100644 --- a/integration-tests/vertx-http/src/main/resources/application.properties +++ b/integration-tests/vertx-http/src/main/resources/application.properties @@ -1 +1,6 @@ -vertx.event-loops.size=2 \ No newline at end of file +vertx.event-loops.size=2 +quarkus.http.ssl.certificate.key-store-file=server-keystore.jks +quarkus.http.ssl.certificate.key-store-password=password +quarkus.http.ssl.certificate.trust-store-file=server-truststore.jks +quarkus.http.ssl.certificate.trust-store-password=password +quarkus.http.ssl.client-auth=REQUIRED diff --git a/integration-tests/vertx-http/src/main/resources/server-keystore.jks b/integration-tests/vertx-http/src/main/resources/server-keystore.jks new file mode 100644 index 0000000000000..d8991ddbd627d Binary files /dev/null and b/integration-tests/vertx-http/src/main/resources/server-keystore.jks differ diff --git a/integration-tests/vertx-http/src/main/resources/server-truststore.jks b/integration-tests/vertx-http/src/main/resources/server-truststore.jks new file mode 100644 index 0000000000000..8ec8e126507b6 Binary files /dev/null and b/integration-tests/vertx-http/src/main/resources/server-truststore.jks differ diff --git a/integration-tests/vertx-http/src/test/java/io/quarkus/it/vertx/VertxProducerResourceIT.java b/integration-tests/vertx-http/src/test/java/io/quarkus/it/vertx/VertxProducerResourceIT.java index f5c9009658985..b5266645a691e 100644 --- a/integration-tests/vertx-http/src/test/java/io/quarkus/it/vertx/VertxProducerResourceIT.java +++ b/integration-tests/vertx-http/src/test/java/io/quarkus/it/vertx/VertxProducerResourceIT.java @@ -1,8 +1,44 @@ package io.quarkus.it.vertx; +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.containsString; + +import java.security.Provider; +import java.security.Security; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + import io.quarkus.test.junit.NativeImageTest; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; @NativeImageTest public class VertxProducerResourceIT extends VertxProducerResourceTest { -} \ No newline at end of file + private static Provider sunECProvider; + + @BeforeAll + public static void setupSecProvider() { + //Remove SunEC provider for the test as it's not being provided for tests. + sunECProvider = Security.getProvider("SunEC"); + Security.removeProvider("SunEC"); + } + + @AfterAll + public static void restoreSecProvider() { + Security.addProvider(sunECProvider); + } + + @Test + public void testRouteRegistrationMTLS() { + RequestSpecification spec = new RequestSpecBuilder() + .setBaseUri(String.format("%s://%s", url.getProtocol(), url.getHost())) + .setPort(8443) + .setKeyStore("client-keystore.jks", "password") + .setTrustStore("client-truststore.jks", "password") + .build(); + given().spec(spec).get("/my-path").then().body(containsString("OK")); + } +} diff --git a/integration-tests/vertx-http/src/test/java/io/quarkus/it/vertx/VertxProducerResourceTest.java b/integration-tests/vertx-http/src/test/java/io/quarkus/it/vertx/VertxProducerResourceTest.java index b4016a9e545eb..857a1fb011a26 100644 --- a/integration-tests/vertx-http/src/test/java/io/quarkus/it/vertx/VertxProducerResourceTest.java +++ b/integration-tests/vertx-http/src/test/java/io/quarkus/it/vertx/VertxProducerResourceTest.java @@ -1,17 +1,26 @@ package io.quarkus.it.vertx; import static io.restassured.RestAssured.get; +import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; +import java.net.URL; + import org.junit.jupiter.api.Test; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.test.junit.DisabledOnNativeImage; import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; @QuarkusTest public class VertxProducerResourceTest { + @TestHTTPResource(ssl = true) + URL url; + @Test public void testInjection() { get("/").then().body(containsString("vert.x has been injected")); @@ -19,7 +28,7 @@ public void testInjection() { @Test public void testInjectedRouter() { - RestAssured.given().contentType("text/plain").body("Hello world!") + given().contentType("text/plain").body("Hello world!") .post("/").then().body(is("Hello world!")); } @@ -28,4 +37,16 @@ public void testRouteRegistration() { get("/my-path").then().body(containsString("OK")); } + @DisabledOnNativeImage + @Test + public void testRouteRegistrationMTLS() { + RequestSpecification spec = new RequestSpecBuilder() + .setBaseUri(String.format("%s://%s", url.getProtocol(), url.getHost())) + .setPort(url.getPort()) + .setKeyStore("client-keystore.jks", "password") + .setTrustStore("client-truststore.jks", "password") + .build(); + given().spec(spec).get("/my-path").then().body(containsString("OK")); + } + } diff --git a/integration-tests/vertx-http/src/test/resources/client-keystore.jks b/integration-tests/vertx-http/src/test/resources/client-keystore.jks new file mode 100644 index 0000000000000..cf6d6ba454864 Binary files /dev/null and b/integration-tests/vertx-http/src/test/resources/client-keystore.jks differ diff --git a/integration-tests/vertx-http/src/test/resources/client-truststore.jks b/integration-tests/vertx-http/src/test/resources/client-truststore.jks new file mode 100644 index 0000000000000..112fb9857fbd7 Binary files /dev/null and b/integration-tests/vertx-http/src/test/resources/client-truststore.jks differ diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPResource.java b/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPResource.java index 1ec6663fa389f..db9ed0d427955 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPResource.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPResource.java @@ -22,4 +22,10 @@ * @return The path part of the URL */ String value() default ""; + + /** + * + * @return If the URL should use the HTTPS protocol and SSL port + */ + boolean ssl() default false; } diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPResourceManager.java b/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPResourceManager.java index 6d80e24f349d9..104000e22dcd6 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPResourceManager.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPResourceManager.java @@ -20,6 +20,14 @@ public static String getUri() { return "http://" + host + ":" + port + contextPath; } + public static String getSslUri() { + Config config = ConfigProvider.getConfig(); + String host = config.getOptionalValue("quarkus.http.host", String.class).orElse("localhost"); + String port = config.getOptionalValue("quarkus.http.test-ssl-port", String.class).orElse("8444"); + String contextPath = config.getOptionalValue("quarkus.servlet.context-path", String.class).orElse(""); + return "https://" + host + ":" + port + contextPath; + } + public static void inject(Object testCase) { Map, TestHTTPResourceProvider> providers = getProviders(); Class c = testCase.getClass(); @@ -34,10 +42,18 @@ public static void inject(Object testCase) { } String path = resource.value(); String val; - if (path.startsWith("/")) { - val = getUri() + path; + if (resource.ssl()) { + if (path.startsWith("/")) { + val = getSslUri() + path; + } else { + val = getSslUri() + "/" + path; + } } else { - val = getUri() + "/" + path; + if (path.startsWith("/")) { + val = getUri() + path; + } else { + val = getUri() + "/" + path; + } } f.setAccessible(true); try {