applicationLayerProtocols;
@@ -90,6 +96,7 @@ public SSLOptions(SSLOptions other) {
this.crlPaths = new ArrayList<>(other.getCrlPaths());
this.crlValues = new ArrayList<>(other.getCrlValues());
this.useAlpn = other.useAlpn;
+ this.useHybridKeyExchangeProtocol = other.useHybridKeyExchangeProtocol;
this.enabledSecureTransportProtocols = other.getEnabledSecureTransportProtocols() == null ? new LinkedHashSet<>() : new LinkedHashSet<>(other.getEnabledSecureTransportProtocols());
this.applicationLayerProtocols = other.getApplicationLayerProtocols() != null ? new ArrayList<>(other.getApplicationLayerProtocols()) : null;
}
@@ -112,6 +119,7 @@ protected void init() {
crlPaths = new ArrayList<>();
crlValues = new ArrayList<>();
useAlpn = DEFAULT_USE_ALPN;
+ useHybridKeyExchangeProtocol = DEFAULT_USE_HYBRID;
enabledSecureTransportProtocols = new LinkedHashSet<>(DEFAULT_ENABLED_SECURE_TRANSPORT_PROTOCOLS);
applicationLayerProtocols = null;
}
@@ -253,6 +261,36 @@ public SSLOptions setUseAlpn(boolean useAlpn) {
return this;
}
+ /**
+ * @return whether the hybrid key exchange protocol X25519MLKEM768 is enabled
+ */
+ public boolean isUseHybridKeyExchangeProtocol() {
+ return useHybridKeyExchangeProtocol;
+ }
+
+ /**
+ * Enable or disable the hybrid post-quantum key exchange protocol X25519MLKEM768.
+ *
+ * When enabled, TLS connections will use X25519MLKEM768 for key exchange, providing
+ * protection against quantum computer attacks.
+ *
+ * This feature requires OpenSSL and will not work with the JDK SSL engine. You must:
+ *
+ * - Use {@link OpenSSLEngineOptions} as the SSL engine
+ * - Have {@code io.netty:netty-tcnative-classes} on the classpath
+ * - Have an OpenSSL provider (e.g. {@code io.smallrye:smallrye-openssl}) on the classpath
+ *
+ * If OpenSSL is not available, the TLS handshake will fail rather than silently falling back
+ * to a non-quantum-safe key exchange.
+ *
+ * @param useHybridKeyExchangeProtocol {@code true} to enable hybrid key exchange
+ * @return a reference to this, so the API can be used fluently
+ */
+ public SSLOptions setUseHybridKeyExchangeProtocol(boolean useHybridKeyExchangeProtocol) {
+ this.useHybridKeyExchangeProtocol = useHybridKeyExchangeProtocol;
+ return this;
+ }
+
/**
* Returns the enabled SSL/TLS protocols
* @return the enabled protocols
@@ -365,6 +403,7 @@ public boolean equals(Object obj) {
Objects.equals(crlPaths, that.crlPaths) &&
Objects.equals(crlValues, that.crlValues) &&
useAlpn == that.useAlpn &&
+ useHybridKeyExchangeProtocol == that.useHybridKeyExchangeProtocol &&
Objects.equals(enabledSecureTransportProtocols, that.enabledSecureTransportProtocols);
}
return false;
@@ -372,7 +411,7 @@ public boolean equals(Object obj) {
@Override
public int hashCode() {
- return Objects.hash(sslHandshakeTimeoutUnit.toNanos(sslHandshakeTimeout), keyCertOptions, trustOptions, enabledCipherSuites, crlPaths, crlValues, useAlpn, enabledSecureTransportProtocols);
+ return Objects.hash(sslHandshakeTimeoutUnit.toNanos(sslHandshakeTimeout), keyCertOptions, trustOptions, enabledCipherSuites, crlPaths, crlValues, useAlpn, useHybridKeyExchangeProtocol, enabledSecureTransportProtocols);
}
/**
diff --git a/vertx-core/src/main/java/io/vertx/core/net/ServerSSLOptions.java b/vertx-core/src/main/java/io/vertx/core/net/ServerSSLOptions.java
index 6fa8339e231..53e7d3204b0 100644
--- a/vertx-core/src/main/java/io/vertx/core/net/ServerSSLOptions.java
+++ b/vertx-core/src/main/java/io/vertx/core/net/ServerSSLOptions.java
@@ -128,6 +128,11 @@ public ServerSSLOptions setUseAlpn(boolean useAlpn) {
return (ServerSSLOptions) super.setUseAlpn(useAlpn);
}
+ @Override
+ public ServerSSLOptions setUseHybridKeyExchangeProtocol(boolean useHybridKeyExchangeProtocol) {
+ return (ServerSSLOptions) super.setUseHybridKeyExchangeProtocol(useHybridKeyExchangeProtocol);
+ }
+
@Override
public ServerSSLOptions setSslHandshakeTimeout(long sslHandshakeTimeout) {
return (ServerSSLOptions) super.setSslHandshakeTimeout(sslHandshakeTimeout);
diff --git a/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/ChannelProvider.java b/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/ChannelProvider.java
index 485e2ead0fa..1d1f0aa6f9e 100644
--- a/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/ChannelProvider.java
+++ b/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/ChannelProvider.java
@@ -114,7 +114,7 @@ private void initSSL(HostAndPort peerAddress, String serverName, boolean ssl,
} else {
applicationProtocols = null;
}
- SslChannelProvider sslChannelProvider = new SslChannelProvider(context.owner(), sslContextProvider, false);
+ SslChannelProvider sslChannelProvider = new SslChannelProvider(context.owner(), sslContextProvider, false, sslOptions.isUseHybridKeyExchangeProtocol());
SslHandler sslHandler = sslChannelProvider.createClientSslHandler(peerAddress, serverName, applicationProtocols, sslOptions.getSslHandshakeTimeout(), sslOptions.getSslHandshakeTimeoutUnit());
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("ssl", sslHandler);
diff --git a/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/NetServerImpl.java b/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/NetServerImpl.java
index d210433609e..30f80b0c2f1 100644
--- a/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/NetServerImpl.java
+++ b/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/NetServerImpl.java
@@ -236,7 +236,7 @@ private void configurePipeline(Channel ch, SslContextProvider sslContextProvider
} else {
applicationProtocols = null;
}
- SslChannelProvider sslChannelProvider = new SslChannelProvider(vertx, sslContextProvider, sslOptions.isSni());
+ SslChannelProvider sslChannelProvider = new SslChannelProvider(vertx, sslContextProvider, sslOptions.isSni(), sslOptions.isUseHybridKeyExchangeProtocol());
ch.pipeline().addLast("ssl", sslChannelProvider.createServerHandler(applicationProtocols, sslOptions.getSslHandshakeTimeout(),
sslOptions.getSslHandshakeTimeoutUnit(), HttpUtils.socketAddressToHostAndPort(ch.remoteAddress())));
ChannelPromise p = ch.newPromise();
diff --git a/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/NetSocketImpl.java b/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/NetSocketImpl.java
index 226cfb7f586..ec273b8dfd3 100644
--- a/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/NetSocketImpl.java
+++ b/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/NetSocketImpl.java
@@ -133,12 +133,12 @@ private Future sslUpgrade(String serverName, SSLOptions sslOptions, ByteBu
ClientSSLOptions clientSSLOptions = (ClientSSLOptions) sslOptions;
ClientSslContextManager clientSslContextManager = (ClientSslContextManager)sslContextManager;
f = clientSslContextManager.resolveSslContextProvider(clientSSLOptions, context)
- .map(p -> new SslChannelProvider(context.owner(), p, false));
+ .map(p -> new SslChannelProvider(context.owner(), p, false, clientSSLOptions.isUseHybridKeyExchangeProtocol()));
} else {
ServerSSLOptions serverSSLOptions = (ServerSSLOptions) sslOptions;
ServerSslContextManager serverSslContextManager = (ServerSslContextManager)sslContextManager;
f = serverSslContextManager.resolveSslContextProvider(serverSSLOptions, context)
- .map(p -> new SslChannelProvider(context.owner(), p, serverSSLOptions.isSni()));
+ .map(p -> new SslChannelProvider(context.owner(), p, serverSSLOptions.isSni(), serverSSLOptions.isUseHybridKeyExchangeProtocol()));
}
return f.compose(provider -> {
PromiseInternal p = context.promise();
diff --git a/vertx-core/src/main/java/module-info.java b/vertx-core/src/main/java/module-info.java
index ee82b9c0edb..6ed7e4e2ca4 100644
--- a/vertx-core/src/main/java/module-info.java
+++ b/vertx-core/src/main/java/module-info.java
@@ -28,6 +28,7 @@
requires static io.netty.transport.unix.common;
requires static io.netty.codec.haproxy;
requires static io.netty.codec.classes.quic;
+ requires static io.netty.tcnative.classes.openssl;
// Annotation processing
diff --git a/vertx-core/src/main/java21/module-info.java b/vertx-core/src/main/java21/module-info.java
index 92c0170f4af..7311d57fe72 100644
--- a/vertx-core/src/main/java21/module-info.java
+++ b/vertx-core/src/main/java21/module-info.java
@@ -30,6 +30,7 @@
requires static io.netty.transport.unix.common;
requires static io.netty.codec.haproxy;
requires static io.netty.codec.classes.quic;
+ requires static io.netty.tcnative.classes.openssl;
// Annotation processing
diff --git a/vertx-core/src/test/java/io/vertx/it/HybridKeyExchangeTest.java b/vertx-core/src/test/java/io/vertx/it/HybridKeyExchangeTest.java
new file mode 100644
index 00000000000..07cae832b8f
--- /dev/null
+++ b/vertx-core/src/test/java/io/vertx/it/HybridKeyExchangeTest.java
@@ -0,0 +1,529 @@
+/*
+ * Copyright (c) 2011-2019 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ * which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ */
+
+package io.vertx.it;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.*;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.ssl.*;
+import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
+import io.netty.internal.tcnative.SSL;
+import io.vertx.core.http.*;
+import io.vertx.core.net.ClientSSLOptions;
+import io.vertx.core.net.OpenSSLEngineOptions;
+import io.vertx.core.net.ServerSSLOptions;
+import io.vertx.test.tls.Cert;
+import io.vertx.test.tls.Trust;
+import io.vertx.test.http.HttpTestBase;
+import org.junit.Test;
+
+import javax.net.ssl.SSLHandshakeException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests hybrid key exchange (X25519MLKEM768) with OpenSSL.
+ */
+public class HybridKeyExchangeTest extends HttpTestBase {
+
+ @Test
+ public void testHybridKeyExchangeHandshake() throws Exception {
+ ServerSSLOptions serverSslOptions = new ServerSSLOptions()
+ .setUseHybridKeyExchangeProtocol(true)
+ .setKeyCertOptions(Cert.SERVER_PEM.get());
+
+ server = vertx.httpServerBuilder()
+ .with(new HttpServerConfig(new HttpServerOptions()
+ .setPort(DEFAULT_HTTPS_PORT)
+ .setHost(DEFAULT_HTTPS_HOST)))
+ .with(new OpenSSLEngineOptions())
+ .with(serverSslOptions)
+ .build();
+ server.requestHandler(req -> req.response().end("hybrid-ok"));
+ startServer(server);
+
+ ClientSSLOptions hybridClientSsl = new ClientSSLOptions()
+ .setUseHybridKeyExchangeProtocol(true)
+ .setTrustAll(true);
+ client = vertx.httpClientBuilder()
+ .with(new HttpClientOptions().setSsl(true))
+ .with(new OpenSSLEngineOptions())
+ .with(hybridClientSsl)
+ .build();
+
+ ClientSSLOptions nonHybridClientSsl = new ClientSSLOptions()
+ .setUseHybridKeyExchangeProtocol(false)
+ .setTrustAll(true);
+ HttpClientAgent client2 = vertx.httpClientBuilder()
+ .with(new HttpClientOptions().setSsl(true))
+ .with(new OpenSSLEngineOptions())
+ .with(nonHybridClientSsl)
+ .build();
+
+ var bodyBuffer = client.request(HttpMethod.GET, DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/")
+ .expecting(req -> req.connection().sslSession().getProtocol().equals("TLSv1.3"))
+ .compose(HttpClientRequest::send)
+ .expecting(HttpResponseExpectation.SC_OK)
+ .compose(HttpClientResponse::body)
+ .await();
+ assertEquals("hybrid-ok", bodyBuffer.toString());
+
+
+ try {
+ client2.request(HttpMethod.GET, DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/")
+ .compose(HttpClientRequest::send)
+ .await();
+ fail("Expected SSLHandshakeException");
+ } catch (Exception expected) {
+ assertEquals(SSLHandshakeException.class, expected.getClass());
+ }
+ testComplete();
+ }
+
+ @Test
+ public void testHybridKeyExchangeHandshakeMTLS() throws Exception {
+ ServerSSLOptions serverSslOptions = new ServerSSLOptions()
+ .setUseHybridKeyExchangeProtocol(true)
+ .setClientAuth(io.vertx.core.http.ClientAuth.REQUIRED)
+ .setKeyCertOptions(Cert.SERVER_PEM_ROOT_CA.get())
+ .setTrustOptions(Trust.SERVER_PEM_ROOT_CA.get());
+
+ server = vertx.httpServerBuilder()
+ .with(new HttpServerConfig(new HttpServerOptions()
+ .setPort(DEFAULT_HTTPS_PORT)
+ .setHost(DEFAULT_HTTPS_HOST)))
+ .with(new OpenSSLEngineOptions())
+ .with(serverSslOptions)
+ .build();
+ server.requestHandler(req -> {
+ assertTrue(req.isSSL());
+ req.response().end("mtls-hybrid-ok");
+ });
+ startServer(server);
+
+ ClientSSLOptions hybridClientSsl = new ClientSSLOptions()
+ .setUseHybridKeyExchangeProtocol(true)
+ .setKeyCertOptions(Cert.CLIENT_PEM_ROOT_CA.get())
+ .setTrustAll(true);
+ client = vertx.httpClientBuilder()
+ .with(new HttpClientOptions().setSsl(true))
+ .with(new OpenSSLEngineOptions())
+ .with(hybridClientSsl)
+ .build();
+
+ ClientSSLOptions nonHybridClientSsl = new ClientSSLOptions()
+ .setUseHybridKeyExchangeProtocol(false)
+ .setKeyCertOptions(Cert.CLIENT_PEM_ROOT_CA.get())
+ .setTrustAll(true);
+ HttpClientAgent client2 = vertx.httpClientBuilder()
+ .with(new HttpClientOptions().setSsl(true))
+ .with(new OpenSSLEngineOptions())
+ .with(nonHybridClientSsl)
+ .build();
+
+
+ var buffer = client.request(HttpMethod.GET, DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/")
+ .expecting(req -> req.connection().sslSession().getProtocol().equals("TLSv1.3"))
+ .compose(HttpClientRequest::send)
+ .expecting(HttpResponseExpectation.SC_OK)
+ .compose(HttpClientResponse::body)
+ .await();
+
+ assertEquals("mtls-hybrid-ok", buffer.toString());
+
+ try {
+ client2.request(HttpMethod.GET, DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/")
+ .compose(HttpClientRequest::send)
+ .await();
+ fail("Expected a SSLHandshakeException");
+ } catch (Exception e) {
+ assertTrue(e instanceof javax.net.ssl.SSLHandshakeException);
+ }
+ testComplete();
+ }
+
+ @Test
+ public void testHybridFailsServerSideWhenPqcNotAvailable() throws Exception {
+ ServerSSLOptions serverSslOptions = new ServerSSLOptions()
+ .setUseHybridKeyExchangeProtocol(true)
+ .setKeyCertOptions(Cert.SERVER_PEM.get());
+
+ server = vertx.httpServerBuilder()
+ .with(new HttpServerConfig(new HttpServerOptions()
+ .setPort(DEFAULT_HTTPS_PORT)
+ .setHost(DEFAULT_HTTPS_HOST)))
+ .with(serverSslOptions)
+ .build();
+ server.requestHandler(req -> req.response().end("should-not-reach"));
+ startServer(server);
+
+ ClientSSLOptions clientSsl = new ClientSSLOptions()
+ .setTrustAll(true);
+ client = vertx.httpClientBuilder()
+ .with(new HttpClientOptions().setSsl(true))
+ .with(clientSsl)
+ .build();
+
+ try {
+ client.request(HttpMethod.GET, DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/")
+ .compose(HttpClientRequest::send)
+ .await();
+ fail("Expected a thing");
+ } catch ( Exception e) {
+ assertTrue(e instanceof javax.net.ssl.SSLHandshakeException);
+ }
+ testComplete();
+ }
+
+ @Test
+ public void testHybridFailsClientSideWhenPqcNotAvailable() throws Exception {
+ ServerSSLOptions serverSslOptions = new ServerSSLOptions()
+ .setKeyCertOptions(Cert.SERVER_PEM.get());
+
+ server = vertx.httpServerBuilder()
+ .with(new HttpServerConfig(new HttpServerOptions()
+ .setPort(DEFAULT_HTTPS_PORT)
+ .setHost(DEFAULT_HTTPS_HOST)))
+ .with(serverSslOptions)
+ .build();
+ server.requestHandler(req -> req.response().end("should-not-reach"));
+ startServer(server);
+
+ ClientSSLOptions clientSsl = new ClientSSLOptions()
+ .setUseHybridKeyExchangeProtocol(true)
+ .setTrustAll(true);
+ client = vertx.httpClientBuilder()
+ .with(new HttpClientOptions().setSsl(true))
+ .with(clientSsl)
+ .build();
+
+ try {
+ client.request(HttpMethod.GET, DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/")
+ .compose(HttpClientRequest::send)
+ .await();
+ fail("Expected an exceptin");
+ } catch (Exception e) {
+ assertTrue(e instanceof javax.net.ssl.SSLHandshakeException);
+ }
+ testComplete();
+ }
+
+ @Test
+ public void testHybridFailsWhenPqcNotAvailable() throws Exception {
+ ServerSSLOptions serverSslOptions = new ServerSSLOptions()
+ .setUseHybridKeyExchangeProtocol(true)
+ .setKeyCertOptions(Cert.SERVER_PEM.get());
+
+ server = vertx.httpServerBuilder()
+ .with(new HttpServerConfig(new HttpServerOptions()
+ .setPort(DEFAULT_HTTPS_PORT)
+ .setHost(DEFAULT_HTTPS_HOST)))
+ .with(serverSslOptions)
+ .build();
+ server.requestHandler(req -> req.response().end("should-not-reach"));
+ startServer(server);
+
+ ClientSSLOptions clientSsl = new ClientSSLOptions()
+ .setUseHybridKeyExchangeProtocol(true)
+ .setTrustAll(true);
+ client = vertx.httpClientBuilder()
+ .with(new HttpClientOptions().setSsl(true))
+ .with(clientSsl)
+ .build();
+
+ try {
+ client.request(HttpMethod.GET, DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/")
+ .compose(HttpClientRequest::send)
+ .await();
+ fail("qsdfqsfd");
+ } catch (Exception e) {
+ assertTrue(e instanceof javax.net.ssl.SSLHandshakeException);
+ }
+ testComplete();
+ }
+
+ @Test
+ public void testHybridKeyExchangeWithSNI() throws Exception {
+ ServerSSLOptions serverSslOptions = new ServerSSLOptions()
+ .setUseHybridKeyExchangeProtocol(true)
+ .setSni(true)
+ .setKeyCertOptions(Cert.SERVER_PEM.get());
+
+ server = vertx.httpServerBuilder()
+ .with(new HttpServerConfig(new HttpServerOptions()
+ .setPort(DEFAULT_HTTPS_PORT)
+ .setHost(DEFAULT_HTTPS_HOST)))
+ .with(new OpenSSLEngineOptions())
+ .with(serverSslOptions)
+ .build();
+ server.requestHandler(req -> req.response().end("sni-hybrid-ok"));
+ startServer(server);
+
+ ClientSSLOptions hybridClientSsl = new ClientSSLOptions()
+ .setUseHybridKeyExchangeProtocol(true)
+ .setTrustAll(true);
+ client = vertx.httpClientBuilder()
+ .with(new HttpClientOptions().setSsl(true))
+ .with(new OpenSSLEngineOptions())
+ .with(hybridClientSsl)
+ .build();
+
+ var body = client.request(HttpMethod.GET, DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/")
+ .compose(HttpClientRequest::send)
+ .expecting(HttpResponseExpectation.SC_OK)
+ .compose(HttpClientResponse::body)
+ .await();
+ assertEquals("sni-hybrid-ok", body.toString());
+ }
+
+ @Test
+ public void testHybridFailsWithSNIWhenPqcNotAvailable() throws Exception {
+ ServerSSLOptions serverSslOptions = new ServerSSLOptions()
+ .setUseHybridKeyExchangeProtocol(true)
+ .setSni(true)
+ .setKeyCertOptions(Cert.SERVER_PEM.get());
+
+ server = vertx.httpServerBuilder()
+ .with(new HttpServerConfig(new HttpServerOptions()
+ .setPort(DEFAULT_HTTPS_PORT)
+ .setHost(DEFAULT_HTTPS_HOST)))
+ .with(serverSslOptions)
+ .build();
+ server.requestHandler(req -> req.response().end("should-not-reach"));
+ startServer(server);
+
+ ClientSSLOptions clientSsl = new ClientSSLOptions()
+ .setTrustAll(true);
+ client = vertx.httpClientBuilder()
+ .with(new HttpClientOptions().setSsl(true))
+ .with(clientSsl)
+ .build();
+
+ try {
+ client.request(HttpMethod.GET, DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/")
+ .compose(HttpClientRequest::send)
+ .await();
+ fail("Was expecting a SSLHandshakException");
+ } catch(Exception e) {
+ assertTrue(e instanceof javax.net.ssl.SSLHandshakeException);
+ }
+ testComplete();
+ await();
+ }
+
+ @Test
+ public void testHybridWithRawNettySocket() throws Exception {
+ ServerSSLOptions serverSslOptions = new ServerSSLOptions()
+ .setUseHybridKeyExchangeProtocol(true)
+ .setKeyCertOptions(Cert.SERVER_PEM.get());
+
+ server = vertx.httpServerBuilder()
+ .with(new HttpServerConfig(new HttpServerOptions()
+ .setPort(DEFAULT_HTTPS_PORT)
+ .setHost(DEFAULT_HTTPS_HOST)))
+ .with(new OpenSSLEngineOptions())
+ .with(serverSslOptions)
+ .build();
+ server.requestHandler(req -> req.response().end("hybrid-ok"));
+ startServer(server);
+
+ SslContext sslContext = SslContextBuilder.forClient()
+ .sslProvider(SslProvider.OPENSSL)
+ .trustManager(InsecureTrustManagerFactory.INSTANCE)
+ .build();
+
+ CompletableFuture negotiatedGroup = new CompletableFuture<>();
+
+ EventLoopGroup group = new NioEventLoopGroup();
+ try {
+ Bootstrap bootstrap = new Bootstrap()
+ .group(group)
+ .channel(NioSocketChannel.class)
+ .handler(new ChannelInitializer() {
+ @Override
+ protected void initChannel(SocketChannel ch) {
+ SslHandler sslHandler = sslContext.newHandler(ch.alloc(),
+ DEFAULT_HTTPS_HOST, DEFAULT_HTTPS_PORT);
+
+ ReferenceCountedOpenSslEngine engine =
+ (ReferenceCountedOpenSslEngine) sslHandler.engine();
+ SSL.setCurvesList(engine.sslPointer(), "X25519MLKEM768");
+
+ ch.pipeline().addLast("server-hello-interceptor",
+ new ServerHelloGroupExtractor(negotiatedGroup));
+ ch.pipeline().addLast("ssl", sslHandler);
+
+ sslHandler.handshakeFuture().addListener(future -> {
+ if (!future.isSuccess()) {
+ negotiatedGroup.completeExceptionally(future.cause());
+ }
+ });
+ }
+ });
+
+ Channel ch = bootstrap.connect(DEFAULT_HTTPS_HOST, DEFAULT_HTTPS_PORT)
+ .sync().channel();
+
+ int groupId = negotiatedGroup.get(10, TimeUnit.SECONDS);
+ // 0x11ec = 4588 = X25519MLKEM768 see https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml
+ assertEquals(0x11ec, groupId);
+
+ ch.close().sync();
+ } finally {
+ group.shutdownGracefully();
+ }
+ }
+
+ @Test
+ public void testHybridMTLSWithRawNettySocket() throws Exception {
+ ServerSSLOptions serverSslOptions = new ServerSSLOptions()
+ .setUseHybridKeyExchangeProtocol(true)
+ .setClientAuth(io.vertx.core.http.ClientAuth.REQUIRED)
+ .setKeyCertOptions(Cert.SERVER_PEM_ROOT_CA.get())
+ .setTrustOptions(Trust.SERVER_PEM_ROOT_CA.get());
+
+ server = vertx.httpServerBuilder()
+ .with(new HttpServerConfig(new HttpServerOptions()
+ .setPort(DEFAULT_HTTPS_PORT)
+ .setHost(DEFAULT_HTTPS_HOST)))
+ .with(new OpenSSLEngineOptions())
+ .with(serverSslOptions)
+ .build();
+ server.requestHandler(req -> {
+ assertTrue(req.isSSL());
+ req.response().end("mtls-hybrid-ok");
+ });
+ startServer(server);
+
+ SslContext sslContext = SslContextBuilder.forClient()
+ .sslProvider(SslProvider.OPENSSL)
+ .trustManager(InsecureTrustManagerFactory.INSTANCE)
+ .keyManager(
+ getClass().getClassLoader().getResourceAsStream("tls/client-cert-root-ca.pem"),
+ getClass().getClassLoader().getResourceAsStream("tls/client-key.pem"))
+ .build();
+
+ CompletableFuture negotiatedGroup = new CompletableFuture<>();
+
+ EventLoopGroup group = new NioEventLoopGroup();
+ try {
+ Bootstrap bootstrap = new Bootstrap()
+ .group(group)
+ .channel(NioSocketChannel.class)
+ .handler(new ChannelInitializer() {
+ @Override
+ protected void initChannel(SocketChannel ch) {
+ SslHandler sslHandler = sslContext.newHandler(ch.alloc(),
+ DEFAULT_HTTPS_HOST, DEFAULT_HTTPS_PORT);
+
+ ReferenceCountedOpenSslEngine engine =
+ (ReferenceCountedOpenSslEngine) sslHandler.engine();
+ SSL.setCurvesList(engine.sslPointer(), "X25519MLKEM768");
+
+ ch.pipeline().addLast("server-hello-interceptor",
+ new ServerHelloGroupExtractor(negotiatedGroup));
+ ch.pipeline().addLast("ssl", sslHandler);
+
+ sslHandler.handshakeFuture().addListener(future -> {
+ if (!future.isSuccess()) {
+ negotiatedGroup.completeExceptionally(future.cause());
+ }
+ });
+ }
+ });
+
+ Channel ch = bootstrap.connect(DEFAULT_HTTPS_HOST, DEFAULT_HTTPS_PORT)
+ .sync().channel();
+
+ int groupId = negotiatedGroup.get(10, TimeUnit.SECONDS);
+ // 0x11ec = 4588 = X25519MLKEM768 see https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml
+ assertEquals(0x11ec, groupId);
+
+ ch.close().sync();
+ } finally {
+ group.shutdownGracefully();
+ }
+ }
+
+
+ static class ServerHelloGroupExtractor extends ChannelInboundHandlerAdapter {
+
+ private static final int HANDSHAKE_CONTENT_TYPE = 0x16;
+ private static final int SERVER_HELLO = 0x02;
+ private static final int KEY_SHARE_EXTENSION = 0x0033;
+
+ private final CompletableFuture result;
+
+ ServerHelloGroupExtractor(CompletableFuture result) {
+ this.result = result;
+ }
+
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+ if (msg instanceof ByteBuf && !result.isDone()) {
+ ByteBuf buf = (ByteBuf) msg;
+ int readerIndex = buf.readerIndex();
+ try {
+ parseServerHello(buf);
+ } catch (Exception e) {
+ // Not a ServerHello or not parseable yet, ignore
+ } finally {
+ buf.readerIndex(readerIndex);
+ }
+ }
+ super.channelRead(ctx, msg);
+ }
+
+ private void parseServerHello(ByteBuf buf) {
+ if (buf.readableBytes() < 5) return;
+
+ int contentType = buf.readUnsignedByte();
+ if (contentType != HANDSHAKE_CONTENT_TYPE) return;
+
+ buf.skipBytes(2); // protocol version
+ int recordLength = buf.readUnsignedShort();
+ if (buf.readableBytes() < recordLength) return;
+
+ int handshakeType = buf.readUnsignedByte();
+ if (handshakeType != SERVER_HELLO) return;
+
+ buf.skipBytes(3); // handshake length
+ buf.skipBytes(2); // server version (0x0303)
+ buf.skipBytes(32); // random
+
+ int sessionIdLen = buf.readUnsignedByte();
+ buf.skipBytes(sessionIdLen); // session id
+
+ buf.skipBytes(2); // cipher suite
+ buf.skipBytes(1); // compression method
+
+ if (buf.readableBytes() < 2) return;
+ int extensionsLength = buf.readUnsignedShort();
+
+ int extensionsEnd = buf.readerIndex() + extensionsLength;
+ while (buf.readerIndex() < extensionsEnd && buf.readableBytes() >= 4) {
+ int extType = buf.readUnsignedShort();
+ int extLen = buf.readUnsignedShort();
+
+ if (extType == KEY_SHARE_EXTENSION && extLen >= 2) {
+ int groupId = buf.readUnsignedShort();
+ result.complete(groupId);
+ return;
+ }
+ buf.skipBytes(extLen);
+ }
+ }
+ }
+}
diff --git a/vertx-core/src/test/java/module-info.java b/vertx-core/src/test/java/module-info.java
index decd225fd7a..d0cc143da68 100644
--- a/vertx-core/src/test/java/module-info.java
+++ b/vertx-core/src/test/java/module-info.java
@@ -37,6 +37,7 @@
requires io.netty.handler.proxy;
requires io.netty.codec.http3;
requires io.netty.codec.dns;
+ requires static io.netty.tcnative.classes.openssl;
requires jdk.management;
provides VerticleFactory with ClasspathVerticleFactory, io.vertx.tests.vertx.AccessEventBusFromInitVerticleFactory;