From 9628c0b3fd646bc19f542d78cea08caf0f92a681 Mon Sep 17 00:00:00 2001 From: Arthur Navarro Date: Mon, 16 Mar 2026 17:57:35 +0100 Subject: [PATCH] Motivation The rise of quantum computers threatens traditional asymmetric key exchange protocol due to their ability to break private keys. Post-quantum cryptography must use problems that quantum computers can't solve as quickly. Module-lattice-based problems have been found to resist quantum computers. ML-KEM, for module-lattice-based key encapsulation mechanism, is an instance of a key exchange protocol resistant to quantum computers. Due to its relative short existence, it has been recommended to use it alongside traditional Diffie-Hellman with elliptic curve. Thus, the new hybrid key exchange protocol x25519mlkem768 uses both Diffie-Hellman with elliptic curve 25519 and ML-KEM. Its has been integrated in OpenSsl starting with version 3.5. Changes: This features relies on the netty-tcnative-openssl-dynamic bound to version 3.6 of openssl at runtime (provided by smallrye-openssl), and netty-tcnative-classes at build-time. Changes have been brought to Vert.x, we simply provide an API to setup the SSLEngine used in the underlying Vert.x server and/or client. In `application.properties`, user can set `quarkus.tls.my-config.hybrid=true` to leverage the new hybrid key exchange provided by OpenSSL > 3.5. --- bom/application/pom.xml | 5 + extensions/tls-registry/runtime/pom.xml | 1 + .../tls/runtime/VertxCertificateHolder.java | 1 + .../tls/runtime/config/TlsBucketConfig.java | 6 + .../runtime/VertxCertificateHolderTest.java | 10 ++ .../http/tls/HybridKeyExchangeMtlsTest.java | 109 +++++++++++++++++ .../vertx/http/tls/HybridKeyExchangeTest.java | 111 ++++++++++++++++++ .../options/HttpServerOptionsUtils.java | 5 + .../client/impl/ClientBuilderImpl.java | 5 + 9 files changed, 253 insertions(+) create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/tls/HybridKeyExchangeMtlsTest.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/tls/HybridKeyExchangeTest.java diff --git a/bom/application/pom.xml b/bom/application/pom.xml index d1850652bc858..3e859430ce126 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -292,6 +292,11 @@ pom import + + io.netty + netty-tcnative-classes + 2.0.76.Final + com.aayushatharva.brotli4j brotli4j diff --git a/extensions/tls-registry/runtime/pom.xml b/extensions/tls-registry/runtime/pom.xml index 18d5f7c9fa25b..70ae781803244 100644 --- a/extensions/tls-registry/runtime/pom.xml +++ b/extensions/tls-registry/runtime/pom.xml @@ -32,6 +32,7 @@ + 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); + } } }