io.vertx
vertx-web
diff --git a/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/VertxCertificateHolder.java b/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/VertxCertificateHolder.java
index 9c87048487472..c4beb6d30da8c 100644
--- a/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/VertxCertificateHolder.java
+++ b/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/VertxCertificateHolder.java
@@ -115,6 +115,7 @@ public synchronized SSLOptions getSSLOptions() {
options.setKeyCertOptions(getKeyStoreOptions());
options.setTrustOptions(getTrustStoreOptions());
options.setUseAlpn(config().alpn());
+ options.setUseHybrid(config().hybrid());
options.setSslHandshakeTimeoutUnit(TimeUnit.SECONDS);
options.setSslHandshakeTimeout(config().handshakeTimeout().toSeconds());
options.setEnabledSecureTransportProtocols(config().protocols());
diff --git a/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/config/TlsBucketConfig.java b/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/config/TlsBucketConfig.java
index a5ba7d7be273d..cb40f97698d5a 100644
--- a/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/config/TlsBucketConfig.java
+++ b/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/config/TlsBucketConfig.java
@@ -73,6 +73,12 @@ public interface TlsBucketConfig {
@WithDefault("true")
boolean alpn();
+ /**
+ * Enables the hybrid key exchange protocol.
+ */
+ @WithDefault("false")
+ boolean hybrid();
+
/**
* Sets the list of revoked certificates (paths to files).
*
diff --git a/extensions/tls-registry/runtime/src/test/java/io/quarkus/tls/runtime/VertxCertificateHolderTest.java b/extensions/tls-registry/runtime/src/test/java/io/quarkus/tls/runtime/VertxCertificateHolderTest.java
index ca841a671e1d8..c85d888efc353 100644
--- a/extensions/tls-registry/runtime/src/test/java/io/quarkus/tls/runtime/VertxCertificateHolderTest.java
+++ b/extensions/tls-registry/runtime/src/test/java/io/quarkus/tls/runtime/VertxCertificateHolderTest.java
@@ -64,6 +64,11 @@ public boolean alpn() {
return false;
}
+ @Override
+ public boolean hybrid() {
+ return true;
+ }
+
@Override
public Duration handshakeTimeout() {
return Duration.ofSeconds(10);
@@ -83,6 +88,11 @@ void testDefault() {
assertFalse(holder.warnIfOldProtocols(Set.of(TlsBucketConfig.DEFAULT_TLS_PROTOCOLS.toLowerCase()), "test"));
}
+ @Test
+ void testHybrid() {
+ assertTrue(holder.getSSLOptions().isUseHybrid());
+ }
+
@Test
void testWarnIfOldProtocols_withSSL() {
assertTrue(holder.warnIfOldProtocols(Set.of("SSLv3"), "test"));
diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/tls/HybridKeyExchangeMtlsTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/tls/HybridKeyExchangeMtlsTest.java
new file mode 100644
index 0000000000000..8cfa26be45fda
--- /dev/null
+++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/tls/HybridKeyExchangeMtlsTest.java
@@ -0,0 +1,109 @@
+package io.quarkus.vertx.http.tls;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.net.URL;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.event.Observes;
+import jakarta.inject.Inject;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIf;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.ssl.SslProvider;
+import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
+import io.quarkus.test.QuarkusExtensionTest;
+import io.quarkus.test.common.http.TestHTTPResource;
+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.buffer.Buffer;
+import io.vertx.core.net.JksOptions;
+import io.vertx.core.net.OpenSSLEngineOptions;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.client.HttpResponse;
+import io.vertx.ext.web.client.WebClient;
+import io.vertx.ext.web.client.WebClientOptions;
+
+@EnabledIf("isOpenSslAvailable")
+@Certificates(baseDir = "target/certs", certificates = @Certificate(name = "mtls-hybrid-test", password = "secret", formats = {
+ Format.JKS, Format.PKCS12, Format.PEM }, client = true))
+public class HybridKeyExchangeMtlsTest {
+
+ @TestHTTPResource(value = "/hybrid-mtls", tls = true)
+ URL url;
+
+ @RegisterExtension
+ static final QuarkusExtensionTest config = new QuarkusExtensionTest()
+ .withApplicationRoot((jar) -> jar
+ .addClasses(MyBean.class)
+ .addAsResource(new File("target/certs/mtls-hybrid-test-keystore.jks"), "server-keystore.jks")
+ .addAsResource(new File("target/certs/mtls-hybrid-test-server-truststore.jks"),
+ "server-truststore.jks"))
+ .overrideConfigKey("quarkus.tls.key-store.jks.path", "server-keystore.jks")
+ .overrideConfigKey("quarkus.tls.key-store.jks.password", "secret")
+ .overrideConfigKey("quarkus.tls.trust-store.jks.path", "server-truststore.jks")
+ .overrideConfigKey("quarkus.tls.trust-store.jks.password", "secret")
+ .overrideConfigKey("quarkus.tls.hybrid", "true")
+ .overrideConfigKey("quarkus.http.ssl.client-auth", "REQUIRED")
+ .overrideConfigKey("quarkus.http.insecure-requests", "disabled");
+
+ @Inject
+ Vertx vertx;
+
+ @Test
+ void testHybridKeyExchangeWithMtls() {
+ WebClientOptions options = new WebClientOptions();
+ options.setSsl(true);
+ options.setSslEngineOptions(new OpenSSLEngineOptions());
+ options.setUseHybrid(true);
+ options.setTrustAll(true);
+ options.setKeyCertOptions(new JksOptions()
+ .setPath("target/certs/mtls-hybrid-test-client-keystore.jks")
+ .setPassword("secret"));
+
+ WebClient client = WebClient.create(vertx, options);
+ HttpResponse response = client.getAbs(url.toExternalForm())
+ .send().toCompletionStage().toCompletableFuture().join();
+ assertThat(response.statusCode()).isEqualTo(200);
+ assertThat(response.bodyAsString()).isEqualTo("mtls-hybrid-ok");
+ }
+
+ static boolean isOpenSslAvailable() {
+ try {
+ SslContext ctx = SslContextBuilder.forClient()
+ .sslProvider(SslProvider.OPENSSL)
+ .trustManager(InsecureTrustManagerFactory.INSTANCE)
+ .build();
+ return true;
+ } catch (Throwable e) {
+ return false;
+ }
+ }
+
+ @ApplicationScoped
+ static class MyBean {
+
+ public void register(@Observes Router router) {
+ router.get("/hybrid-mtls").handler(rc -> {
+ assertThat(rc.request().connection().isSsl()).isTrue();
+ assertThat(rc.request().isSSL()).isTrue();
+ assertThat(rc.request().connection().sslSession()).isNotNull();
+ assertThat(rc.request().connection().sslSession().getProtocol()).isEqualTo("TLSv1.3");
+ try {
+ assertThat(rc.request().connection().sslSession().getPeerCertificates()).isNotEmpty();
+ } catch (javax.net.ssl.SSLPeerUnverifiedException e) {
+ throw new RuntimeException(e);
+ }
+ rc.response().end("mtls-hybrid-ok");
+ });
+ }
+
+ }
+}
diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/tls/HybridKeyExchangeTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/tls/HybridKeyExchangeTest.java
new file mode 100644
index 0000000000000..c73a5aeb36eb8
--- /dev/null
+++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/tls/HybridKeyExchangeTest.java
@@ -0,0 +1,111 @@
+package io.quarkus.vertx.http.tls;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.File;
+import java.net.URL;
+
+import javax.net.ssl.SSLException;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.event.Observes;
+import jakarta.inject.Inject;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIf;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.ssl.SslProvider;
+import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
+import io.quarkus.test.QuarkusExtensionTest;
+import io.quarkus.test.common.http.TestHTTPResource;
+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.buffer.Buffer;
+import io.vertx.core.net.OpenSSLEngineOptions;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.client.HttpResponse;
+import io.vertx.ext.web.client.WebClient;
+import io.vertx.ext.web.client.WebClientOptions;
+
+@EnabledIf("isOpenSslAvailable")
+@Certificates(baseDir = "target/certs", certificates = @Certificate(name = "ssl-hybrid-test", password = "secret", formats = {
+ Format.JKS, Format.PKCS12, Format.PEM }))
+public class HybridKeyExchangeTest {
+
+ @TestHTTPResource(value = "/hybrid", tls = true)
+ URL url;
+
+ @RegisterExtension
+ static final QuarkusExtensionTest config = new QuarkusExtensionTest()
+ .withApplicationRoot((jar) -> jar
+ .addClasses(MyBean.class)
+ .addAsResource(new File("target/certs/ssl-hybrid-test.key"), "server-key.pem")
+ .addAsResource(new File("target/certs/ssl-hybrid-test.crt"), "server-cert.pem"))
+ .overrideConfigKey("quarkus.tls.key-store.pem.0.cert", "server-cert.pem")
+ .overrideConfigKey("quarkus.tls.key-store.pem.0.key", "server-key.pem")
+ .overrideConfigKey("quarkus.tls.hybrid", "true")
+ .overrideConfigKey("quarkus.http.insecure-requests", "disabled");
+
+ @Inject
+ Vertx vertx;
+
+ @Test
+ void testHybridKeyExchangeHandshake() {
+ WebClientOptions options = new WebClientOptions();
+ options.setSsl(true);
+ options.setSslEngineOptions(new OpenSSLEngineOptions());
+ options.setUseHybrid(true);
+ options.setTrustAll(true);
+
+ WebClient client = WebClient.create(vertx, options);
+ HttpResponse response = client.getAbs(url.toExternalForm())
+ .send().toCompletionStage().toCompletableFuture().join();
+ assertThat(response.statusCode()).isEqualTo(200);
+ assertThat(response.bodyAsString()).isEqualTo("hybrid-ok");
+ }
+
+ @Test
+ void testNonHybridClientRejected() {
+ WebClientOptions options = new WebClientOptions();
+ options.setSsl(true);
+ options.setTrustAll(true);
+
+ WebClient client = WebClient.create(vertx, options);
+ assertThatThrownBy(() -> client.getAbs(url.toExternalForm())
+ .send().toCompletionStage().toCompletableFuture().join())
+ .hasRootCauseInstanceOf(SSLException.class);
+ }
+
+ static boolean isOpenSslAvailable() {
+ try {
+ SslContext ctx = SslContextBuilder.forClient()
+ .sslProvider(SslProvider.OPENSSL)
+ .trustManager(InsecureTrustManagerFactory.INSTANCE)
+ .build();
+ return true;
+ } catch (Throwable e) {
+ return false;
+ }
+ }
+
+ @ApplicationScoped
+ static class MyBean {
+
+ public void register(@Observes Router router) {
+ router.get("/hybrid").handler(rc -> {
+ assertThat(rc.request().connection().isSsl()).isTrue();
+ assertThat(rc.request().isSSL()).isTrue();
+ assertThat(rc.request().connection().sslSession()).isNotNull();
+ assertThat(rc.request().connection().sslSession().getProtocol()).isEqualTo("TLSv1.3");
+ rc.response().end("hybrid-ok");
+ });
+ }
+
+ }
+}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java
index cbd260fa91c75..b01510748b061 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java
@@ -42,6 +42,7 @@
import io.vertx.core.http.HttpVersion;
import io.vertx.core.net.JdkSSLEngineOptions;
import io.vertx.core.net.KeyCertOptions;
+import io.vertx.core.net.OpenSSLEngineOptions;
import io.vertx.core.net.TCPSSLOptions;
import io.vertx.core.net.TrafficShapingOptions;
import io.vertx.core.net.TrustOptions;
@@ -253,6 +254,10 @@ public static void applyTlsConfigurationToHttpServerOptions(TlsConfiguration buc
if (!other.isUseAlpn()) {
serverOptions.setUseAlpn(false);
}
+ if (other.isUseHybrid()) {
+ serverOptions.setSslEngineOptions(new OpenSSLEngineOptions());
+ serverOptions.setUseHybrid(true);
+ }
serverOptions.setEnabledSecureTransportProtocols(other.getEnabledSecureTransportProtocols());
}
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 a7e02e235e844..7cf3e7530091b 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
@@ -47,6 +47,7 @@
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpVersion;
import io.vertx.core.net.JksOptions;
+import io.vertx.core.net.OpenSSLEngineOptions;
import io.vertx.core.net.ProxyOptions;
import io.vertx.core.net.ProxyType;
import io.vertx.core.net.SSLOptions;
@@ -395,6 +396,10 @@ private void populateSecurityOptionsFromTlsConfig(HttpClientOptions options) {
}
options.setEnabledSecureTransportProtocols(sslOptions.getEnabledSecureTransportProtocols());
options.setUseAlpn(sslOptions.isUseAlpn());
+ if (sslOptions.isUseHybrid()) {
+ options.setSslEngineOptions(new OpenSSLEngineOptions());
+ options.setUseHybrid(true);
+ }
}
}