From 586472c248188db0a7a210f6b2ac1fd94895386f Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Thu, 6 Jun 2024 11:12:04 +0300 Subject: [PATCH] Introduce support for the TLS Registry in the REST Client Closes: #41005 --- .../main/asciidoc/tls-registry-reference.adoc | 2 +- .../restclient/config/RestClientConfig.java | 17 +++ ...ClientFallbackConfigSourceInterceptor.java | 1 + .../restclient/config/RestClientsConfig.java | 14 ++ .../rest-client/deployment/pom.xml | 9 ++ .../tls/MtlsConfigFromRegistryCdiTest.java | 74 +++++++++++ .../tls/TlsConfigFromPropertiesCdiTest.java | 65 +++++++++ .../tls/TlsConfigFromRegistryCdiTest.java | 66 ++++++++++ .../tls/TlsConfigFromRegistryManualTest.java | 73 +++++++++++ .../reactive/{ssl => tls}/TrustAllTest.java | 2 +- .../rest-client/runtime/pom.xml | 8 ++ .../reactive/QuarkusRestClientBuilder.java | 6 + .../runtime/QuarkusRestClientBuilderImpl.java | 7 + .../runtime/RestClientBuilderImpl.java | 56 ++++++++ .../runtime/RestClientCDIDelegateBuilder.java | 28 +++- .../resteasy/reactive/client/TlsConfig.java | 80 +++++++++++ .../client/impl/ClientBuilderImpl.java | 124 +++++++++++++----- 17 files changed, 593 insertions(+), 39 deletions(-) create mode 100644 extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/MtlsConfigFromRegistryCdiTest.java create mode 100644 extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TlsConfigFromPropertiesCdiTest.java create mode 100644 extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TlsConfigFromRegistryCdiTest.java create mode 100644 extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TlsConfigFromRegistryManualTest.java rename extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/{ssl => tls}/TrustAllTest.java (97%) create mode 100644 independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/TlsConfig.java diff --git a/docs/src/main/asciidoc/tls-registry-reference.adoc b/docs/src/main/asciidoc/tls-registry-reference.adoc index 4b2198da13ebc..3c918ee0b26a8 100644 --- a/docs/src/main/asciidoc/tls-registry-reference.adoc +++ b/docs/src/main/asciidoc/tls-registry-reference.adoc @@ -518,4 +518,4 @@ When the application starts, the TLS registry performs some checks to ensure the - the cipher suites and protocols are valid - the CRLs are valid -If any of these checks fail, the application will fail to start. \ No newline at end of file +If any of these checks fail, the application will fail to start. diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java index 71c1ba1be8d2d..9267e5bc9fe31 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java @@ -42,6 +42,7 @@ public class RestClientConfig { EMPTY.keyStorePassword = Optional.empty(); EMPTY.keyStoreType = Optional.empty(); EMPTY.hostnameVerifier = Optional.empty(); + EMPTY.tlsConfigurationName = Optional.empty(); EMPTY.connectionTTL = Optional.empty(); EMPTY.connectionPoolSize = Optional.empty(); EMPTY.keepAliveEnabled = Optional.empty(); @@ -201,6 +202,20 @@ public class RestClientConfig { @ConfigItem public Optional hostnameVerifier; + /** + * The name of the TLS configuration to use. + *

+ * If not set and the default TLS configuration is configured ({@code quarkus.tls.*}) then that will be used. + * If a name is configured, it uses the configuration from {@code quarkus.tls..*} + * If a name is configured, but no TLS configuration is found with that name then an error will be thrown. + *

+ * If no TLS configuration is set, then the keys-tore, trust-store, etc. properties will be used. + *

+ * This property is not applicable to the RESTEasy Client. + */ + @ConfigItem + public Optional tlsConfigurationName; + /** * The time in ms for which a connection remains unused in the connection pool before being evicted and closed. * A timeout of {@code 0} means there is no timeout. @@ -317,6 +332,7 @@ public static RestClientConfig load(String configKey) { instance.keyStorePassword = getConfigValue(configKey, "key-store-password", String.class); instance.keyStoreType = getConfigValue(configKey, "key-store-type", String.class); instance.hostnameVerifier = getConfigValue(configKey, "hostname-verifier", String.class); + instance.tlsConfigurationName = getConfigValue(configKey, "tls-configuration-name", String.class); instance.connectionTTL = getConfigValue(configKey, "connection-ttl", Integer.class); instance.connectionPoolSize = getConfigValue(configKey, "connection-pool-size", Integer.class); instance.keepAliveEnabled = getConfigValue(configKey, "keep-alive-enabled", Boolean.class); @@ -358,6 +374,7 @@ public static RestClientConfig load(Class interfaceClass) { instance.keyStorePassword = getConfigValue(interfaceClass, "key-store-password", String.class); instance.keyStoreType = getConfigValue(interfaceClass, "key-store-type", String.class); instance.hostnameVerifier = getConfigValue(interfaceClass, "hostname-verifier", String.class); + instance.tlsConfigurationName = getConfigValue(interfaceClass, "tls-configuration-name", String.class); instance.connectionTTL = getConfigValue(interfaceClass, "connection-ttl", Integer.class); instance.connectionPoolSize = getConfigValue(interfaceClass, "connection-pool-size", Integer.class); instance.keepAliveEnabled = getConfigValue(interfaceClass, "keep-alive-enabled", Boolean.class); diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientFallbackConfigSourceInterceptor.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientFallbackConfigSourceInterceptor.java index a30fe54566ebf..84507edb3cb36 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientFallbackConfigSourceInterceptor.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientFallbackConfigSourceInterceptor.java @@ -38,6 +38,7 @@ public class RestClientFallbackConfigSourceInterceptor extends FallbackConfigSou CLIENT_PROPERTIES.put("key-store", "keyStore"); CLIENT_PROPERTIES.put("key-store-password", "keyStorePassword"); CLIENT_PROPERTIES.put("key-store-type", "keyStoreType"); + CLIENT_PROPERTIES.put("tls-configuration-name", "tlsConfigurationName"); CLIENT_PROPERTIES.put("follow-redirects", "followRedirects"); CLIENT_PROPERTIES.put("proxy-address", "proxyAddress"); CLIENT_PROPERTIES.put("query-param-style", "queryParamStyle"); diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java index b02e27baa1aa1..ab20ac8605a6b 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java @@ -278,6 +278,20 @@ public class RestClientsConfig { @ConfigItem public Optional keyStoreType; + /** + * The name of the TLS configuration to use. + *

+ * If not set and the default TLS configuration is configured ({@code quarkus.tls.*}) then that will be used. + * If a name is configured, it uses the configuration from {@code quarkus.tls..*} + * If a name is configured, but no TLS configuration is found with that name then an error will be thrown. + *

+ * If no TLS configuration is set, then the keys-tore, trust-store, etc. properties will be used. + *

+ * This property is not applicable to the RESTEasy Client. + */ + @ConfigItem + public Optional tlsConfigurationName; + /** * If this is true then HTTP/2 will be enabled. */ diff --git a/extensions/resteasy-reactive/rest-client/deployment/pom.xml b/extensions/resteasy-reactive/rest-client/deployment/pom.xml index a6c55e6b09299..3751bd26d6a59 100644 --- a/extensions/resteasy-reactive/rest-client/deployment/pom.xml +++ b/extensions/resteasy-reactive/rest-client/deployment/pom.xml @@ -32,6 +32,10 @@ io.quarkus quarkus-rest-client-config-deployment + + io.quarkus + quarkus-tls-registry-deployment + io.quarkus @@ -98,6 +102,11 @@ stork-service-discovery-static-list test + + me.escoffier.certs + certificate-generator-junit5 + test + diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/MtlsConfigFromRegistryCdiTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/MtlsConfigFromRegistryCdiTest.java new file mode 100644 index 0000000000000..1d48304c8e113 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/MtlsConfigFromRegistryCdiTest.java @@ -0,0 +1,74 @@ +package io.quarkus.rest.client.reactive.tls; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; + +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Context; + +import org.assertj.core.api.Assertions; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.ext.web.RoutingContext; +import me.escoffier.certs.Format; +import me.escoffier.certs.junit5.Certificate; +import me.escoffier.certs.junit5.Certificates; + +@Certificates(baseDir = "target/certs", certificates = @Certificate(name = "mtls-test", password = "secret", formats = { + Format.JKS, Format.PKCS12, Format.PEM }, client = true)) +public class MtlsConfigFromRegistryCdiTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(Client.class, Resource.class) + .addAsResource(new File("target/certs/mtls-test-keystore.p12"), "server-keystore.p12") + .addAsResource(new File("target/certs/mtls-test-server-truststore.p12"), "server-truststore.p12") + .addAsResource(new File("target/certs/mtls-test-client-keystore.p12"), "client-keystore.p12") + .addAsResource(new File("target/certs/mtls-test-client-truststore.p12"), "client-truststore.p12")) + + .overrideConfigKey("quarkus.tls.server.key-store.p12.path", "server-keystore.p12") + .overrideConfigKey("quarkus.tls.server.key-store.p12.password", "secret") + .overrideConfigKey("quarkus.tls.server.trust-store.p12.path", "server-truststore.p12") + .overrideConfigKey("quarkus.tls.server.trust-store.p12.password", "secret") + .overrideConfigKey("quarkus.http.tls-configuration-name", "server") + + .overrideConfigKey("quarkus.tls.rest-client.key-store.p12.path", "client-keystore.p12") + .overrideConfigKey("quarkus.tls.rest-client.key-store.p12.password", "secret") + .overrideConfigKey("quarkus.tls.rest-client.trust-store.p12.path", "client-truststore.p12") + .overrideConfigKey("quarkus.tls.rest-client.trust-store.p12.password", "secret") + .overrideConfigKey("quarkus.rest-client.rc.url", "https://localhost:${quarkus.http.test-ssl-port:8444}") + .overrideConfigKey("quarkus.rest-client.rc.tls-configuration-name", "rest-client"); + + @RestClient + Client client; + + @Test + void shouldHello() { + assertThat(client.echo("w0rld")).isEqualTo("hello, w0rld"); + } + + @Path("/hello") + @RegisterRestClient(configKey = "rc") + public interface Client { + @POST + String echo(String name); + } + + @Path("/hello") + public static class Resource { + @POST + public String echo(String name, @Context RoutingContext rc) { + Assertions.assertThat(rc.request().connection().isSsl()).isTrue(); + Assertions.assertThat(rc.request().isSSL()).isTrue(); + Assertions.assertThat(rc.request().connection().sslSession()).isNotNull(); + return "hello, " + name; + } + } +} diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TlsConfigFromPropertiesCdiTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TlsConfigFromPropertiesCdiTest.java new file mode 100644 index 0000000000000..8526ad416012b --- /dev/null +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TlsConfigFromPropertiesCdiTest.java @@ -0,0 +1,65 @@ +package io.quarkus.rest.client.reactive.tls; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; + +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Context; + +import org.assertj.core.api.Assertions; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.ext.web.RoutingContext; +import me.escoffier.certs.Format; +import me.escoffier.certs.junit5.Certificate; +import me.escoffier.certs.junit5.Certificates; + +@Certificates(baseDir = "target/certs", certificates = @Certificate(name = "tls-test", password = "secret", formats = { + Format.JKS, Format.PKCS12, Format.PEM })) +public class TlsConfigFromPropertiesCdiTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(Client.class, Resource.class) + .addAsResource(new File("target/certs/tls-test-keystore.jks"), "keystore.jks") + .addAsResource(new File("target/certs/tls-test-truststore.jks"), "truststore.jks")) + .overrideConfigKey("quarkus.tls.key-store.jks.path", "keystore.jks") + .overrideConfigKey("quarkus.tls.key-store.jks.password", "secret") + + .overrideConfigKey("quarkus.rest-client.rc.url", "https://localhost:${quarkus.http.test-ssl-port:8444}") + .overrideConfigKey("quarkus.rest-client.rc.trust-store", "classpath:truststore.jks") + .overrideConfigKey("quarkus.rest-client.rc.trust-store-password", "secret"); + + @RestClient + Client client; + + @Test + void shouldHello() { + assertThat(client.echo("w0rld")).isEqualTo("hello, w0rld"); + } + + @Path("/hello") + @RegisterRestClient(configKey = "rc") + public interface Client { + @POST + String echo(String name); + } + + @Path("/hello") + public static class Resource { + @POST + public String echo(String name, @Context RoutingContext rc) { + Assertions.assertThat(rc.request().connection().isSsl()).isTrue(); + Assertions.assertThat(rc.request().isSSL()).isTrue(); + Assertions.assertThat(rc.request().connection().sslSession()).isNotNull(); + return "hello, " + name; + } + } +} diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TlsConfigFromRegistryCdiTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TlsConfigFromRegistryCdiTest.java new file mode 100644 index 0000000000000..09239c2e628f7 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TlsConfigFromRegistryCdiTest.java @@ -0,0 +1,66 @@ +package io.quarkus.rest.client.reactive.tls; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; + +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Context; + +import org.assertj.core.api.Assertions; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.ext.web.RoutingContext; +import me.escoffier.certs.Format; +import me.escoffier.certs.junit5.Certificate; +import me.escoffier.certs.junit5.Certificates; + +@Certificates(baseDir = "target/certs", certificates = @Certificate(name = "tls-test", password = "secret", formats = { + Format.JKS, Format.PKCS12, Format.PEM })) +public class TlsConfigFromRegistryCdiTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(Client.class, Resource.class) + .addAsResource(new File("target/certs/tls-test-keystore.jks"), "keystore.jks") + .addAsResource(new File("target/certs/tls-test-truststore.jks"), "truststore.jks")) + .overrideConfigKey("quarkus.tls.key-store.jks.path", "keystore.jks") + .overrideConfigKey("quarkus.tls.key-store.jks.password", "secret") + .overrideConfigKey("quarkus.tls.rest-client.trust-store.jks.path", "truststore.jks") + .overrideConfigKey("quarkus.tls.rest-client.trust-store.jks.password", "secret") + + .overrideConfigKey("quarkus.rest-client.rc.url", "https://localhost:${quarkus.http.test-ssl-port:8444}") + .overrideConfigKey("quarkus.rest-client.rc.tls-configuration-name", "rest-client"); + + @RestClient + Client client; + + @Test + void shouldHello() { + assertThat(client.echo("w0rld")).isEqualTo("hello, w0rld"); + } + + @Path("/hello") + @RegisterRestClient(configKey = "rc") + public interface Client { + @POST + String echo(String name); + } + + @Path("/hello") + public static class Resource { + @POST + public String echo(String name, @Context RoutingContext rc) { + Assertions.assertThat(rc.request().connection().isSsl()).isTrue(); + Assertions.assertThat(rc.request().isSSL()).isTrue(); + Assertions.assertThat(rc.request().connection().sslSession()).isNotNull(); + return "hello, " + name; + } + } +} diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TlsConfigFromRegistryManualTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TlsConfigFromRegistryManualTest.java new file mode 100644 index 0000000000000..8f246f30ef55f --- /dev/null +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TlsConfigFromRegistryManualTest.java @@ -0,0 +1,73 @@ +package io.quarkus.rest.client.reactive.tls; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.net.URL; +import java.util.Optional; + +import jakarta.inject.Inject; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Context; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.tls.TlsConfiguration; +import io.quarkus.tls.TlsConfigurationRegistry; +import io.vertx.ext.web.RoutingContext; +import me.escoffier.certs.Format; +import me.escoffier.certs.junit5.Certificate; +import me.escoffier.certs.junit5.Certificates; + +@Certificates(baseDir = "target/certs", certificates = @Certificate(name = "tls-test", password = "secret", formats = { + Format.JKS, Format.PKCS12, Format.PEM })) +public class TlsConfigFromRegistryManualTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(Client.class, Resource.class) + .addAsResource(new File("target/certs/tls-test-keystore.jks"), "keystore.jks") + .addAsResource(new File("target/certs/tls-test-truststore.jks"), "truststore.jks")) + .overrideConfigKey("quarkus.tls.key-store.jks.path", "keystore.jks") + .overrideConfigKey("quarkus.tls.key-store.jks.password", "secret") + .overrideConfigKey("quarkus.tls.rest-client.trust-store.jks.path", "truststore.jks") + .overrideConfigKey("quarkus.tls.rest-client.trust-store.jks.password", "secret"); + + @TestHTTPResource(tls = true) + URL url; + + @Inject + TlsConfigurationRegistry registry; + + @Test + void shouldHello() { + Optional maybeTlsConfiguration = TlsConfiguration.from(registry, Optional.of("rest-client")); + assertThat(maybeTlsConfiguration).isPresent(); + Client client = QuarkusRestClientBuilder.newBuilder().baseUrl(url).tlsConfiguration(maybeTlsConfiguration.get()) + .build(Client.class); + assertThat(client.echo("w0rld")).isEqualTo("hello, w0rld"); + } + + @Path("/hello") + public interface Client { + @POST + String echo(String name); + } + + @Path("/hello") + public static class Resource { + @POST + public String echo(String name, @Context RoutingContext rc) { + assertThat(rc.request().connection().isSsl()).isTrue(); + assertThat(rc.request().isSSL()).isTrue(); + assertThat(rc.request().connection().sslSession()).isNotNull(); + return "hello, " + name; + } + } +} diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/ssl/TrustAllTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TrustAllTest.java similarity index 97% rename from extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/ssl/TrustAllTest.java rename to extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TrustAllTest.java index 8014a4b7a1f23..fa2bab6630adb 100644 --- a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/ssl/TrustAllTest.java +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TrustAllTest.java @@ -1,4 +1,4 @@ -package io.quarkus.rest.client.reactive.ssl; +package io.quarkus.rest.client.reactive.tls; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; diff --git a/extensions/resteasy-reactive/rest-client/runtime/pom.xml b/extensions/resteasy-reactive/rest-client/runtime/pom.xml index affc60590db5a..596db0e5e613d 100644 --- a/extensions/resteasy-reactive/rest-client/runtime/pom.xml +++ b/extensions/resteasy-reactive/rest-client/runtime/pom.xml @@ -26,6 +26,10 @@ io.quarkus quarkus-rest-client-config + + io.quarkus + quarkus-tls-registry + io.smallrye.stork stork-api @@ -44,6 +48,10 @@ + + io.quarkus + quarkus-tls-registry + diff --git a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/QuarkusRestClientBuilder.java b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/QuarkusRestClientBuilder.java index c67adff8fa9e9..1b475b298deaa 100644 --- a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/QuarkusRestClientBuilder.java +++ b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/QuarkusRestClientBuilder.java @@ -21,6 +21,7 @@ import io.quarkus.rest.client.reactive.runtime.QuarkusRestClientBuilderImpl; import io.quarkus.rest.client.reactive.runtime.RestClientBuilderImpl; +import io.quarkus.tls.TlsConfiguration; import io.vertx.core.http.HttpClientOptions; /** @@ -114,6 +115,11 @@ static QuarkusRestClientBuilder newBuilder() { */ QuarkusRestClientBuilder readTimeout(long timeout, TimeUnit unit); + /** + * Set the transport layer security configuration configured by Quarkus + */ + QuarkusRestClientBuilder tlsConfiguration(TlsConfiguration tlsConfiguration); + /** * Specifies the SSL context to use when creating secured transport connections to server endpoints from web targets * created by the client instance that is using this SSL context. diff --git a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/QuarkusRestClientBuilderImpl.java b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/QuarkusRestClientBuilderImpl.java index 5a663f92641c4..884f878844e56 100644 --- a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/QuarkusRestClientBuilderImpl.java +++ b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/QuarkusRestClientBuilderImpl.java @@ -20,6 +20,7 @@ import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder; import io.quarkus.rest.client.reactive.runtime.context.ClientHeadersFactoryContextResolver; import io.quarkus.rest.client.reactive.runtime.context.HttpClientOptionsContextResolver; +import io.quarkus.tls.TlsConfiguration; import io.vertx.core.http.HttpClientOptions; public class QuarkusRestClientBuilderImpl implements QuarkusRestClientBuilder { @@ -54,6 +55,12 @@ public QuarkusRestClientBuilder readTimeout(long timeout, TimeUnit unit) { return this; } + @Override + public QuarkusRestClientBuilder tlsConfiguration(TlsConfiguration tlsConfiguration) { + proxy.tlsConfiguration(tlsConfiguration); + return this; + } + @Override public QuarkusRestClientBuilder sslContext(SSLContext sslContext) { proxy.sslContext(sslContext); diff --git a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java index f5c68a2a5d4c8..8d1dd587eb6f4 100644 --- a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java +++ b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; @@ -27,6 +28,7 @@ import org.eclipse.microprofile.rest.client.RestClientDefinitionException; import org.eclipse.microprofile.rest.client.ext.QueryParamStyle; import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper; +import org.jboss.resteasy.reactive.client.TlsConfig; import org.jboss.resteasy.reactive.client.api.ClientLogger; import org.jboss.resteasy.reactive.client.api.InvalidRestClientDefinitionException; import org.jboss.resteasy.reactive.client.api.LoggingScope; @@ -45,6 +47,10 @@ import io.quarkus.rest.client.reactive.runtime.ProxyAddressUtil.HostAndPort; import io.quarkus.restclient.config.RestClientLoggingConfig; import io.quarkus.restclient.config.RestClientsConfig; +import io.quarkus.tls.TlsConfiguration; +import io.vertx.core.net.KeyCertOptions; +import io.vertx.core.net.SSLOptions; +import io.vertx.core.net.TrustOptions; /** * Builder implementation for MicroProfile Rest Client @@ -101,6 +107,56 @@ public RestClientBuilderImpl readTimeout(long timeout, TimeUnit timeUnit) { return this; } + public RestClientBuilderImpl tlsConfiguration(TlsConfiguration tlsConfiguration) { + clientBuilder.tlsConfig(new TlsConfig() { + @Override + public KeyStore getKeyStore() { + return tlsConfiguration.getKeyStore(); + } + + @Override + public KeyCertOptions getKeyStoreOptions() { + return tlsConfiguration.getKeyStoreOptions(); + } + + @Override + public KeyStore getTrustStore() { + return tlsConfiguration.getTrustStore(); + } + + @Override + public TrustOptions getTrustStoreOptions() { + return tlsConfiguration.getTrustStoreOptions(); + } + + @Override + public SSLOptions getSSLOptions() { + return tlsConfiguration.getSSLOptions(); + } + + @Override + public SSLContext createSSLContext() throws Exception { + return tlsConfiguration.createSSLContext(); + } + + @Override + public Optional getHostnameVerificationAlgorithm() { + return tlsConfiguration.getHostnameVerificationAlgorithm(); + } + + @Override + public boolean usesSni() { + return tlsConfiguration.usesSni(); + } + + @Override + public boolean isTrustAll() { + return tlsConfiguration.isTrustAll(); + } + }); + return this; + } + @Override public RestClientBuilderImpl sslContext(SSLContext sslContext) { clientBuilder.sslContext(sslContext); diff --git a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java index fce45202ece0a..186ca14629a67 100644 --- a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java +++ b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java @@ -26,10 +26,13 @@ import org.jboss.resteasy.reactive.client.api.QuarkusRestClientProperties; import org.jboss.resteasy.reactive.client.impl.multipart.PausableHttpPostRequestEncoder; +import io.quarkus.arc.Arc; import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder; import io.quarkus.restclient.config.RestClientConfig; import io.quarkus.restclient.config.RestClientsConfig; import io.quarkus.runtime.configuration.MemorySize; +import io.quarkus.tls.TlsConfiguration; +import io.quarkus.tls.TlsConfigurationRegistry; public class RestClientCDIDelegateBuilder { @@ -68,7 +71,7 @@ void configureBuilder(QuarkusRestClientBuilder builder) { configureBaseUrl(builder); configureTimeouts(builder); configureProviders(builder); - configureSsl(builder); + configureTLS(builder); configureRedirects(builder); configureQueryParamStyle(builder); configureProxy(builder); @@ -216,8 +219,29 @@ private void configureShared(QuarkusRestClientBuilder builder) { } } - private void configureSsl(QuarkusRestClientBuilder builder) { + private void configureTLS(QuarkusRestClientBuilder builder) { + Optional maybeConfiguration = resolveTlsConfigurationForRegistry(); + if (maybeConfiguration.isPresent()) { + builder.tlsConfiguration(maybeConfiguration.get()); + } else { + configureTLSFromProperties(builder); + } + } + + private Optional resolveTlsConfigurationForRegistry() { + if (Arc.container() != null) { + var registry = Arc.container().select(TlsConfigurationRegistry.class).orNull(); + if (registry != null) { + Optional maybeTlsConfigurationName = oneOf(clientConfigByClassName().tlsConfigurationName, + clientConfigByConfigKey().tlsConfigurationName, + configRoot.tlsConfigurationName); + return TlsConfiguration.from(registry, maybeTlsConfigurationName); + } + } + return Optional.empty(); + } + private void configureTLSFromProperties(QuarkusRestClientBuilder builder) { Optional maybeTrustStore = oneOf(clientConfigByClassName().trustStore, clientConfigByConfigKey().trustStore, configRoot.trustStore); if (maybeTrustStore.isPresent() && !maybeTrustStore.get().isBlank() && !NONE.equals(maybeTrustStore.get())) { diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/TlsConfig.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/TlsConfig.java new file mode 100644 index 0000000000000..1d85e98c0996f --- /dev/null +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/TlsConfig.java @@ -0,0 +1,80 @@ +package org.jboss.resteasy.reactive.client; + +import java.security.KeyStore; +import java.util.Optional; + +import javax.net.ssl.SSLContext; + +import io.vertx.core.net.KeyCertOptions; +import io.vertx.core.net.SSLOptions; +import io.vertx.core.net.TrustOptions; + +public interface TlsConfig { + + /** + * Returns the key store. + * + * @return the key store if configured. + */ + KeyStore getKeyStore(); + + /** + * Returns the key store options. + * + * @return the key store options if configured. + */ + KeyCertOptions getKeyStoreOptions(); + + /** + * Returns the trust store. + * + * @return the trust store if configured. + */ + KeyStore getTrustStore(); + + /** + * Returns the trust store options. + * + * @return the trust store options if configured. + */ + TrustOptions getTrustStoreOptions(); + + /** + * Returns the (Vert.x) SSL options. + * + * @return the {@link SSLOptions}, {@code null} if not configured. + */ + SSLOptions getSSLOptions(); + + /** + * Creates and returns the SSL Context. + * + * @return the {@link SSLContext}, {@code null} if not configured. + * @throws Exception if the SSL Context cannot be created. + */ + SSLContext createSSLContext() throws Exception; + + /** + * Returns the hostname verification algorithm for this configuration. + * {@code "NONE"} means no hostname verification. + * + * @return the hostname verification algorithm. + */ + Optional getHostnameVerificationAlgorithm(); + + /** + * Returns whether the key store is configured to use SNI. + * When SNI is used, the client indicate the server name during the TLS handshake, allowing the server to select the + * right certificate. + * + * @return {@code true} if the key store is configured to use SNI, {@code false} otherwise. + */ + boolean usesSni(); + + /** + * Returns whether the trust store is configured to trust all certificates. + * + * @return {@code true} if the trust store is configured to trust all certificates, {@code false} otherwise. + */ + boolean isTrustAll(); +} diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java index 12e4381f7088d..8bfcc874f444c 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java @@ -27,6 +27,7 @@ import jakarta.ws.rs.ext.MessageBodyWriter; import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.client.TlsConfig; import org.jboss.resteasy.reactive.client.api.ClientLogger; import org.jboss.resteasy.reactive.client.api.LoggingScope; import org.jboss.resteasy.reactive.client.interceptors.ClientGZIPDecodingInterceptor; @@ -40,25 +41,17 @@ import io.vertx.core.http.HttpVersion; import io.vertx.core.net.JksOptions; import io.vertx.core.net.ProxyOptions; +import io.vertx.core.net.SSLOptions; public class ClientBuilderImpl extends ClientBuilder { private static final Logger log = Logger.getLogger(ClientBuilderImpl.class); private static final ClientContextResolver CLIENT_CONTEXT_RESOLVER = ClientContextResolver.getInstance(); - private static final char[] EMPTY_CHAR_ARARAY = new char[0]; + private static final char[] EMPTY_CHAR_ARRAY = new char[0]; public static final String PIPE = Pattern.quote("|"); private ConfigurationImpl configuration; - private HostnameVerifier hostnameVerifier; - private KeyStore keyStore; - private char[] keystorePassword; - private SSLContext sslContext; - private KeyStore trustStore; - private char[] trustStorePassword; - private boolean http2; - private boolean alpn; - private String proxyHost; private int proxyPort; private String proxyPassword; @@ -66,11 +59,23 @@ public class ClientBuilderImpl extends ClientBuilder { private String nonProxyHosts; private boolean followRedirects; + + private boolean http2; + private boolean alpn; + + // security settings + private KeyStore keyStore; + private char[] keystorePassword; + private KeyStore trustStore; + private char[] trustStorePassword; private boolean trustAll; private boolean verifyHost = true; + // overridden security settings + private TlsConfig tlsConfig; private LoggingScope loggingScope; private Integer loggingBodySize = 100; + private int maxChunkSize = 8096; private MultiQueryParamMode multiQueryParamMode; @@ -89,6 +94,11 @@ public ClientBuilder withConfig(Configuration config) { return this; } + public ClientBuilder tlsConfig(TlsConfig tlsConfig) { + this.tlsConfig = tlsConfig; + return this; + } + @Override public ClientBuilder sslContext(SSLContext sslContext) { // TODO @@ -215,29 +225,10 @@ public ClientImpl build() { options.setAlpnVersions(List.of(HttpVersion.HTTP_2, HttpVersion.HTTP_1_1)); } - options.setVerifyHost(verifyHost); - if (trustAll) { - options.setTrustAll(true); - options.setVerifyHost(false); - } - - char[] effectiveTrustStorePassword = trustStorePassword == null ? EMPTY_CHAR_ARARAY : trustStorePassword; - Buffer keyStore = asBuffer(this.keyStore, keystorePassword); - Buffer trustStore = asBuffer(this.trustStore, effectiveTrustStorePassword); - if (keyStore != null || trustStore != null) { - options = options.setSsl(true); - if (keyStore != null) { - JksOptions jks = new JksOptions(); - jks.setValue(keyStore); - jks.setPassword(new String(keystorePassword)); - options = options.setKeyStoreOptions(jks); - } - if (trustStore != null) { - JksOptions jks = new JksOptions(); - jks.setValue(trustStore); - jks.setPassword(new String(effectiveTrustStorePassword)); - options.setTrustStoreOptions(jks); - } + if (tlsConfig != null) { + populateSecurityOptionsFromTlsConfig(options); + } else { + populateSecurityOptionsFromExplicitTlsProperties(options); } if (proxyHost != null) { @@ -297,8 +288,8 @@ public ClientImpl build() { return new ClientImpl(options, configuration, CLIENT_CONTEXT_RESOLVER.resolve(Thread.currentThread().getContextClassLoader()), - hostnameVerifier, - sslContext, + null, + null, followRedirects, multiQueryParamMode, loggingScope, @@ -306,6 +297,69 @@ public ClientImpl build() { } + private void populateSecurityOptionsFromTlsConfig(HttpClientOptions options) { + options.setSsl(true); + + if (tlsConfig.getTrustStoreOptions() != null) { + options.setTrustOptions(tlsConfig.getTrustStoreOptions()); + } + + // For mTLS: + if (tlsConfig.getKeyStoreOptions() != null) { + options.setKeyCertOptions(tlsConfig.getKeyStoreOptions()); + } + + if (tlsConfig.isTrustAll()) { + options.setTrustAll(true); + } + if (tlsConfig.getHostnameVerificationAlgorithm().isPresent() + && tlsConfig.getHostnameVerificationAlgorithm().get().equals("NONE")) { + // Only disable hostname verification if the algorithm is explicitly set to NONE + options.setVerifyHost(false); + } + + SSLOptions sslOptions = tlsConfig.getSSLOptions(); + if (sslOptions != null) { + options.setSslHandshakeTimeout(sslOptions.getSslHandshakeTimeout()); + options.setSslHandshakeTimeoutUnit(sslOptions.getSslHandshakeTimeoutUnit()); + for (String suite : sslOptions.getEnabledCipherSuites()) { + options.addEnabledCipherSuite(suite); + } + for (Buffer buffer : sslOptions.getCrlValues()) { + options.addCrlValue(buffer); + } + options.setEnabledSecureTransportProtocols(sslOptions.getEnabledSecureTransportProtocols()); + options.setUseAlpn(sslOptions.isUseAlpn()); + } + } + + private void populateSecurityOptionsFromExplicitTlsProperties(HttpClientOptions options) { + options.setVerifyHost(verifyHost); + if (trustAll) { + options.setTrustAll(true); + options.setVerifyHost(false); + } + + char[] effectiveTrustStorePassword = trustStorePassword == null ? EMPTY_CHAR_ARRAY : trustStorePassword; + Buffer keyStore = asBuffer(this.keyStore, keystorePassword); + Buffer trustStore = asBuffer(this.trustStore, effectiveTrustStorePassword); + if (keyStore != null || trustStore != null) { + options.setSsl(true); + if (keyStore != null) { + JksOptions jks = new JksOptions(); + jks.setValue(keyStore); + jks.setPassword(new String(keystorePassword)); + options.setKeyStoreOptions(jks); + } + if (trustStore != null) { + JksOptions jks = new JksOptions(); + jks.setValue(trustStore); + jks.setPassword(new String(effectiveTrustStorePassword)); + options.setTrustStoreOptions(jks); + } + } + } + private void configureNonProxyHosts(HttpClientOptions options, String nonProxyHosts) { if (nonProxyHosts != null) { for (String host : nonProxyHosts.split(PIPE)) {