diff --git a/pom.xml b/pom.xml index 4cbcb0f849a..a684b4eb6e6 100644 --- a/pom.xml +++ b/pom.xml @@ -206,8 +206,14 @@ io.netty - netty-tcnative-boringssl-static - test + netty-tcnative-classes + 2.0.76.Final + + + io.smallrye + smallrye-openssl + linux-x86_64 + 0.1.4 com.aayushatharva.brotli4j diff --git a/src/main/asciidoc/http.adoc b/src/main/asciidoc/http.adoc index 16b2d1bd91b..9a64219a2f9 100644 --- a/src/main/asciidoc/http.adoc +++ b/src/main/asciidoc/http.adoc @@ -41,6 +41,10 @@ To handle `h2` requests, TLS must be enabled along with {@link io.vertx.core.htt {@link examples.HTTP2Examples#example0} ---- +The rise of quantum computers will make key exchange protocols such as x25519 obsolete as they will be able to "crack" secret keys quickly. +Vert.x proposes a quantum-safe key exchange protocol, x25519MLKEM768 (official recommendation of NIST) to ensure sessions over TLS are safe against quantum computers. +Hybrid key exchange must be enabled along with {@link io.vertx.core.http.HttpServerOptions#setUseHybrid(boolean)} and only works using OpenSsl ({$@link io.vertx.core.http.HttpServerOptions#setSslEngineOptions(SSLEngineOptions)}) + ALPN is a TLS extension that negotiates the protocol before the client and the server start to exchange data. Clients that don't support ALPN will still be able to do a _classic_ SSL handshake. diff --git a/src/main/generated/io/vertx/core/eventbus/EventBusOptionsConverter.java b/src/main/generated/io/vertx/core/eventbus/EventBusOptionsConverter.java index c750e1938bd..79e6fa8dfe5 100644 --- a/src/main/generated/io/vertx/core/eventbus/EventBusOptionsConverter.java +++ b/src/main/generated/io/vertx/core/eventbus/EventBusOptionsConverter.java @@ -279,6 +279,11 @@ static void fromJson(Iterable> json, EventBu obj.setUseAlpn((Boolean)member.getValue()); } break; + case "useHybrid": + if (member.getValue() instanceof Boolean) { + obj.setUseHybrid((Boolean)member.getValue()); + } + break; case "writeIdleTimeout": if (member.getValue() instanceof Number) { obj.setWriteIdleTimeout(((Number)member.getValue()).intValue()); @@ -388,6 +393,7 @@ static void toJson(EventBusOptions obj, java.util.Map json) { json.put("trustStoreOptions", obj.getTrustStoreOptions().toJson()); } json.put("useAlpn", obj.isUseAlpn()); + json.put("useHybrid", obj.isUseHybrid()); json.put("writeIdleTimeout", obj.getWriteIdleTimeout()); } } diff --git a/src/main/generated/io/vertx/core/net/SSLOptionsConverter.java b/src/main/generated/io/vertx/core/net/SSLOptionsConverter.java index ed5e57e40c9..76de6b53b5e 100644 --- a/src/main/generated/io/vertx/core/net/SSLOptionsConverter.java +++ b/src/main/generated/io/vertx/core/net/SSLOptionsConverter.java @@ -69,6 +69,11 @@ static void fromJson(Iterable> json, SSLOpti obj.setUseAlpn((Boolean)member.getValue()); } break; + case "useHybrid": + if (member.getValue() instanceof Boolean) { + obj.setUseHybrid((Boolean)member.getValue()); + } + break; } } } @@ -103,5 +108,6 @@ static void toJson(SSLOptions obj, java.util.Map json) { json.put("sslHandshakeTimeoutUnit", obj.getSslHandshakeTimeoutUnit().name()); } json.put("useAlpn", obj.isUseAlpn()); + json.put("useHybrid", obj.isUseHybrid()); } } diff --git a/src/main/generated/io/vertx/core/net/TCPSSLOptionsConverter.java b/src/main/generated/io/vertx/core/net/TCPSSLOptionsConverter.java index 82392e43426..77b551b0262 100644 --- a/src/main/generated/io/vertx/core/net/TCPSSLOptionsConverter.java +++ b/src/main/generated/io/vertx/core/net/TCPSSLOptionsConverter.java @@ -179,6 +179,11 @@ static void fromJson(Iterable> json, TCPSSLO obj.setUseAlpn((Boolean)member.getValue()); } break; + case "useHybrid": + if (member.getValue() instanceof Boolean) { + obj.setUseHybrid((Boolean)member.getValue()); + } + break; case "writeIdleTimeout": if (member.getValue() instanceof Number) { obj.setWriteIdleTimeout(((Number)member.getValue()).intValue()); @@ -258,6 +263,7 @@ static void toJson(TCPSSLOptions obj, java.util.Map json) { json.put("trustStoreOptions", obj.getTrustStoreOptions().toJson()); } json.put("useAlpn", obj.isUseAlpn()); + json.put("useHybrid", obj.isUseHybrid()); json.put("writeIdleTimeout", obj.getWriteIdleTimeout()); } } diff --git a/src/main/java/io/vertx/core/eventbus/EventBusOptions.java b/src/main/java/io/vertx/core/eventbus/EventBusOptions.java index 955260e319d..85b2c6d7604 100644 --- a/src/main/java/io/vertx/core/eventbus/EventBusOptions.java +++ b/src/main/java/io/vertx/core/eventbus/EventBusOptions.java @@ -475,6 +475,11 @@ public EventBusOptions setUseAlpn(boolean useAlpn) { return (EventBusOptions) super.setUseAlpn(useAlpn); } + @Override + public EventBusOptions setUseHybrid(boolean useHybrid) { + return (EventBusOptions) super.setUseHybrid(useHybrid); + } + @Override public EventBusOptions setSslEngineOptions(SSLEngineOptions sslEngineOptions) { return (EventBusOptions) super.setSslEngineOptions(sslEngineOptions); diff --git a/src/main/java/io/vertx/core/http/HttpClientOptions.java b/src/main/java/io/vertx/core/http/HttpClientOptions.java index d5516ee040e..9cf2f3ed4dd 100755 --- a/src/main/java/io/vertx/core/http/HttpClientOptions.java +++ b/src/main/java/io/vertx/core/http/HttpClientOptions.java @@ -1154,6 +1154,11 @@ public HttpClientOptions setUseAlpn(boolean useAlpn) { return (HttpClientOptions) super.setUseAlpn(useAlpn); } + @Override + public HttpClientOptions setUseHybrid(boolean useHybrid) { + return (HttpClientOptions) super.setUseHybrid(useHybrid); + } + @Override public HttpClientOptions setSslEngineOptions(SSLEngineOptions sslEngineOptions) { return (HttpClientOptions) super.setSslEngineOptions(sslEngineOptions); diff --git a/src/main/java/io/vertx/core/http/HttpServerOptions.java b/src/main/java/io/vertx/core/http/HttpServerOptions.java index c202fe30325..da4ee0c87df 100755 --- a/src/main/java/io/vertx/core/http/HttpServerOptions.java +++ b/src/main/java/io/vertx/core/http/HttpServerOptions.java @@ -429,6 +429,18 @@ public HttpServerOptions setUseAlpn(boolean useAlpn) { return this; } + /* + Use X25519MLKEM768 instead of X25519 for key exchange. + X25519MLKEM768 is a hybrid protocol exchange ensuring that encryption will resist quantum computers + */ + @Override + public HttpServerOptions setUseHybrid(boolean useHybrid) { + super.setUseHybrid(useHybrid); + return this; + } + + + @Override public HttpServerOptions setKeyCertOptions(KeyCertOptions options) { super.setKeyCertOptions(options); diff --git a/src/main/java/io/vertx/core/http/WebSocketClientOptions.java b/src/main/java/io/vertx/core/http/WebSocketClientOptions.java index 2fa6757755d..abb0f2c89a9 100644 --- a/src/main/java/io/vertx/core/http/WebSocketClientOptions.java +++ b/src/main/java/io/vertx/core/http/WebSocketClientOptions.java @@ -552,6 +552,11 @@ public WebSocketClientOptions setUseAlpn(boolean useAlpn) { return (WebSocketClientOptions)super.setUseAlpn(useAlpn); } + @Override + public WebSocketClientOptions setUseHybrid(boolean useHybrid) { + return (WebSocketClientOptions)super.setUseHybrid(useHybrid); + } + @Override public WebSocketClientOptions setSslEngineOptions(SSLEngineOptions sslEngineOptions) { return (WebSocketClientOptions)super.setSslEngineOptions(sslEngineOptions); diff --git a/src/main/java/io/vertx/core/http/impl/HttpServerWorker.java b/src/main/java/io/vertx/core/http/impl/HttpServerWorker.java index 8098f28cb39..d907a1c50e8 100644 --- a/src/main/java/io/vertx/core/http/impl/HttpServerWorker.java +++ b/src/main/java/io/vertx/core/http/impl/HttpServerWorker.java @@ -31,11 +31,14 @@ import io.vertx.core.http.impl.cgbystrom.FlashPolicyHandler; import io.vertx.core.impl.ContextInternal; import io.vertx.core.impl.VertxInternal; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; import io.vertx.core.net.impl.*; import io.vertx.core.spi.metrics.HttpServerMetrics; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Supplier; @@ -61,6 +64,7 @@ public class HttpServerWorker implements BiConsumer private final CompressionOptions[] compressionOptions; private final Function encodingDetector; private final GlobalTrafficShapingHandler trafficShapingHandler; + private static final Logger log = LoggerFactory.getLogger(SslChannelProvider.class); public HttpServerWorker(ContextInternal context, Supplier streamContextSupplier, @@ -115,18 +119,28 @@ public void accept(Channel ch, SslChannelProvider sslChannelProvider) { if (idle != null) { ch.pipeline().remove(idle); } - configurePipeline(future.getNow(), sslChannelProvider); + try { + configurePipeline(future.getNow(), sslChannelProvider); + } catch (Exception e) { + log.error(e.getMessage()+ ", now closing the channel"); + ch.close(); + } } else { //No need to close the channel.HAProxyMessageDecoder already did handleException(future.cause()); } }); } else { - configurePipeline(ch, sslChannelProvider); + try { + configurePipeline(ch, sslChannelProvider); + } catch (Exception e) { + log.error(e.getMessage()+ ", now closing the channel"); + ch.close(); + } } } - private void configurePipeline(Channel ch, SslChannelProvider sslChannelProvider) { + private void configurePipeline(Channel ch, SslChannelProvider sslChannelProvider) throws Exception { ChannelPipeline pipeline = ch.pipeline(); if (options.isSsl()) { pipeline.addLast("ssl", sslChannelProvider.createServerHandler(HttpUtils.socketAddressToHostAndPort(ch.remoteAddress()))); diff --git a/src/main/java/io/vertx/core/net/ClientOptionsBase.java b/src/main/java/io/vertx/core/net/ClientOptionsBase.java index 2fb8b876042..703e34c6bd6 100755 --- a/src/main/java/io/vertx/core/net/ClientOptionsBase.java +++ b/src/main/java/io/vertx/core/net/ClientOptionsBase.java @@ -340,6 +340,11 @@ public ClientOptionsBase setUseAlpn(boolean useAlpn) { return (ClientOptionsBase) super.setUseAlpn(useAlpn); } + @Override + public ClientOptionsBase setUseHybrid(boolean useHyrbrid) { + return (ClientOptionsBase) super.setUseHybrid(useHyrbrid); + } + @Override public ClientOptionsBase setSslEngineOptions(SSLEngineOptions sslEngineOptions) { return (ClientOptionsBase) super.setSslEngineOptions(sslEngineOptions); diff --git a/src/main/java/io/vertx/core/net/NetClientOptions.java b/src/main/java/io/vertx/core/net/NetClientOptions.java index 79f4bef00da..b4b3a63cb88 100755 --- a/src/main/java/io/vertx/core/net/NetClientOptions.java +++ b/src/main/java/io/vertx/core/net/NetClientOptions.java @@ -263,6 +263,11 @@ public NetClientOptions setUseAlpn(boolean useAlpn) { return (NetClientOptions) super.setUseAlpn(useAlpn); } + @Override + public NetClientOptions setUseHybrid(boolean useHybrid) { + return (NetClientOptions) super.setUseHybrid(useHybrid); + } + @Override public NetClientOptions setSslEngineOptions(SSLEngineOptions sslEngineOptions) { return (NetClientOptions) super.setSslEngineOptions(sslEngineOptions); diff --git a/src/main/java/io/vertx/core/net/NetServerOptions.java b/src/main/java/io/vertx/core/net/NetServerOptions.java index 0c9352d4c6a..c8df4c07457 100755 --- a/src/main/java/io/vertx/core/net/NetServerOptions.java +++ b/src/main/java/io/vertx/core/net/NetServerOptions.java @@ -221,8 +221,8 @@ public NetServerOptions setSsl(boolean ssl) { } @Override - public NetServerOptions setUseAlpn(boolean useAlpn) { - super.setUseAlpn(useAlpn); + public NetServerOptions setUseHybrid(boolean useHybrid) { + super.setUseHybrid(useHybrid); return this; } diff --git a/src/main/java/io/vertx/core/net/SSLOptions.java b/src/main/java/io/vertx/core/net/SSLOptions.java index ed3f0bc0f69..f01f6e6da85 100644 --- a/src/main/java/io/vertx/core/net/SSLOptions.java +++ b/src/main/java/io/vertx/core/net/SSLOptions.java @@ -37,7 +37,11 @@ public class SSLOptions { /** * Default use alpn = false */ - public static final boolean DEFAULT_USE_ALPN = false; + public static final boolean DEFAULT_USE_ALPN = false; /** + + * Default use hybrid = false + */ + public static final boolean DEFAULT_USE_HYBRID = false; /** * The default value of SSL handshake timeout = 10 @@ -66,6 +70,7 @@ public class SSLOptions { private ArrayList crlPaths; private ArrayList crlValues; private boolean useAlpn; + private boolean useHybrid; private Set enabledSecureTransportProtocols; /** @@ -99,6 +104,7 @@ public SSLOptions(SSLOptions other) { this.crlPaths = new ArrayList<>(other.getCrlPaths()); this.crlValues = new ArrayList<>(other.getCrlValues()); this.useAlpn = other.useAlpn; + this.useHybrid = other.useHybrid; this.enabledSecureTransportProtocols = other.getEnabledSecureTransportProtocols() == null ? new LinkedHashSet<>() : new LinkedHashSet<>(other.getEnabledSecureTransportProtocols()); } @@ -110,6 +116,7 @@ private void init() { crlPaths = new ArrayList<>(); crlValues = new ArrayList<>(); useAlpn = DEFAULT_USE_ALPN; + useHybrid = DEFAULT_USE_HYBRID; enabledSecureTransportProtocols = new LinkedHashSet<>(DEFAULT_ENABLED_SECURE_TRANSPORT_PROTOCOLS); } @@ -236,6 +243,13 @@ public boolean isUseAlpn() { return useAlpn; } + /** + * @return whether to use or not Hybrid key exchange protocol x25519MLKEM768 + */ + public boolean isUseHybrid() { + return useHybrid; + } + /** * Set the ALPN usage. * @@ -246,6 +260,11 @@ public SSLOptions setUseAlpn(boolean useAlpn) { return this; } + public SSLOptions setUseHybrid(boolean useHybrid) { + this.useHybrid = useHybrid; + return this; + } + /** * Returns the enabled SSL/TLS protocols * @return the enabled protocols diff --git a/src/main/java/io/vertx/core/net/TCPSSLOptions.java b/src/main/java/io/vertx/core/net/TCPSSLOptions.java index a697da66476..bb8516b0393 100755 --- a/src/main/java/io/vertx/core/net/TCPSSLOptions.java +++ b/src/main/java/io/vertx/core/net/TCPSSLOptions.java @@ -712,6 +712,13 @@ public boolean isUseAlpn() { return sslOptions.isUseAlpn(); } + /** + * @return whether to use or not Hybrid key exchange protocol x25519MLKEM768 + */ + public boolean isUseHybrid() { + return sslOptions.isUseHybrid(); + } + /** * Set the ALPN usage. * @@ -722,6 +729,11 @@ public TCPSSLOptions setUseAlpn(boolean useAlpn) { return this; } + public TCPSSLOptions setUseHybrid(boolean useHybrid) { + sslOptions.setUseHybrid(useHybrid); + return this; + } + /** * @return the SSL engine implementation to use */ diff --git a/src/main/java/io/vertx/core/net/impl/ChannelProvider.java b/src/main/java/io/vertx/core/net/impl/ChannelProvider.java index 10006a636e3..1f49443ff1e 100644 --- a/src/main/java/io/vertx/core/net/impl/ChannelProvider.java +++ b/src/main/java/io/vertx/core/net/impl/ChannelProvider.java @@ -31,6 +31,8 @@ import io.vertx.core.Handler; import io.vertx.core.impl.ContextInternal; import io.vertx.core.impl.VertxInternal; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; import io.vertx.core.net.ProxyOptions; import io.vertx.core.net.ProxyType; import io.vertx.core.net.SocketAddress; @@ -49,6 +51,7 @@ */ public final class ChannelProvider { + private static final Logger log = LoggerFactory.getLogger(ChannelProvider.class); private final Bootstrap bootstrap; private final SslChannelProvider sslContextProvider; private final ContextInternal context; @@ -115,9 +118,17 @@ private void connect(Handler handler, SocketAddress remoteAddress, Sock private void initSSL(Handler handler, SocketAddress peerAddress, String serverName, boolean ssl, boolean useAlpn, Channel ch, Promise channelHandler) { if (ssl) { - SslHandler sslHandler = sslContextProvider.createClientSslHandler(peerAddress, serverName, useAlpn); + SslHandler sslHandler = null; + try { + sslHandler = sslContextProvider.createClientSslHandler(peerAddress, serverName, useAlpn); + } catch (Exception e) { + log.error(e.getMessage()); + ch.close(); + return; + } ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast("ssl", sslHandler); + SslHandler finalSslHandler = sslHandler; pipeline.addLast(new ChannelInboundHandlerAdapter() { @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { @@ -127,7 +138,7 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { if (completion.isSuccess()) { // Remove from the pipeline after handshake result ctx.pipeline().remove(this); - applicationProtocol = sslHandler.applicationProtocol(); + applicationProtocol = finalSslHandler.applicationProtocol(); if (handler != null) { context.dispatch(ch, handler); } diff --git a/src/main/java/io/vertx/core/net/impl/NetServerImpl.java b/src/main/java/io/vertx/core/net/impl/NetServerImpl.java index 9f8c3ca4949..a47925e94df 100644 --- a/src/main/java/io/vertx/core/net/impl/NetServerImpl.java +++ b/src/main/java/io/vertx/core/net/impl/NetServerImpl.java @@ -39,6 +39,7 @@ import io.vertx.core.spi.metrics.VertxMetrics; import io.vertx.core.streams.ReadStream; +import java.util.concurrent.ExecutionException; import java.util.function.BiConsumer; /** @@ -209,18 +210,28 @@ public void accept(Channel ch, SslChannelProvider sslChannelProvider) { if (idle != null) { ch.pipeline().remove(idle); } - configurePipeline(future.getNow(), sslChannelProvider); + try { + configurePipeline(future.getNow(), sslChannelProvider); + } catch (Exception e) { + log.error(e.getMessage()+ ", now closing the channel"); + ch.close(); + } } else { //No need to close the channel.HAProxyMessageDecoder already did handleException(future.cause()); } }); } else { - configurePipeline(ch, sslChannelProvider); + try { + configurePipeline(ch, sslChannelProvider); + } catch (Exception e) { + log.error(e.getMessage()+ ", now closing the channel"); + ch.close(); + } } } - private void configurePipeline(Channel ch, SslChannelProvider sslChannelProvider) { + private void configurePipeline(Channel ch, SslChannelProvider sslChannelProvider) throws Exception{ if (options.isSsl()) { ch.pipeline().addLast("ssl", sslChannelProvider.createServerHandler(HttpUtils.socketAddressToHostAndPort(ch.remoteAddress()))); ChannelPromise p = ch.newPromise(); diff --git a/src/main/java/io/vertx/core/net/impl/NetSocketImpl.java b/src/main/java/io/vertx/core/net/impl/NetSocketImpl.java index 883b653591f..4b8a2c001df 100644 --- a/src/main/java/io/vertx/core/net/impl/NetSocketImpl.java +++ b/src/main/java/io/vertx/core/net/impl/NetSocketImpl.java @@ -338,7 +338,12 @@ public Future upgradeToSsl(String serverName) { if (remoteAddress != null) { sslHandler = sslChannelProvider.createClientSslHandler(remoteAddress, serverName, false); } else { - sslHandler = sslChannelProvider.createServerHandler(HttpUtils.socketAddressToHostAndPort(chctx.channel().remoteAddress())); + try { + sslHandler = sslChannelProvider.createServerHandler(HttpUtils.socketAddressToHostAndPort(chctx.channel().remoteAddress())); + } catch (Exception e) { + promise.fail(e); + return; + } } chctx.pipeline().addFirst("ssl", sslHandler); } else { diff --git a/src/main/java/io/vertx/core/net/impl/SSLHelper.java b/src/main/java/io/vertx/core/net/impl/SSLHelper.java index 17747e45702..50ac4700abe 100755 --- a/src/main/java/io/vertx/core/net/impl/SSLHelper.java +++ b/src/main/java/io/vertx/core/net/impl/SSLHelper.java @@ -128,6 +128,7 @@ public static SSLEngineOptions resolveEngineOptions(SSLEngineOptions engineOptio private final ClientAuth clientAuth; private final boolean client; private final boolean useAlpn; + private final boolean useHybrid; private final String endpointIdentificationAlgorithm; private final SSLEngineOptions sslEngineOptions; private final List applicationProtocols; @@ -139,6 +140,7 @@ public static SSLEngineOptions resolveEngineOptions(SSLEngineOptions engineOptio private Future cachedProvider; public SSLHelper(TCPSSLOptions options, List applicationProtocols) { + this.useHybrid = options.isUseHybrid(); this.sslEngineOptions = options.getSslEngineOptions(); this.ssl = options.isSsl(); this.useAlpn = options.isUseAlpn(); @@ -264,7 +266,8 @@ public Future buildChannelProvider(SSLOptions sslOptions, Co trustAll, useAlpn, ctx.owner().getInternalWorkerPool().executor(), - c.useWorkerPool + c.useWorkerPool, + useHybrid )); } diff --git a/src/main/java/io/vertx/core/net/impl/SslChannelProvider.java b/src/main/java/io/vertx/core/net/impl/SslChannelProvider.java index 1981494a745..68c585297f2 100644 --- a/src/main/java/io/vertx/core/net/impl/SslChannelProvider.java +++ b/src/main/java/io/vertx/core/net/impl/SslChannelProvider.java @@ -12,9 +12,11 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelHandler; +import io.netty.handler.ssl.ReferenceCountedOpenSslEngine; import io.netty.handler.ssl.SniHandler; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslHandler; +import io.netty.internal.tcnative.SSL; import io.netty.util.AsyncMapping; import io.netty.util.concurrent.ImmediateExecutor; import io.vertx.core.VertxException; @@ -23,6 +25,7 @@ import io.vertx.core.net.SocketAddress; import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLEngine; import javax.net.ssl.TrustManager; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -44,6 +47,7 @@ public class SslChannelProvider { private final boolean useWorkerPool; private final boolean sni; private final boolean useAlpn; + private final boolean useHybrid; private final boolean trustAll; private final SslContextProvider sslContextProvider; private final SslContext[] sslContexts = new SslContext[2]; @@ -58,7 +62,7 @@ public SslChannelProvider(SslContextProvider sslContextProvider, boolean trustAll, boolean useAlpn, Executor workerPool, - boolean useWorkerPool) { + boolean useWorkerPool, boolean useHybrid) { this.workerPool = workerPool; this.useWorkerPool = useWorkerPool; this.useAlpn = useAlpn; @@ -67,6 +71,7 @@ public SslChannelProvider(SslContextProvider sslContextProvider, this.sslHandshakeTimeout = sslHandshakeTimeout; this.sslHandshakeTimeoutUnit = sslHandshakeTimeoutUnit; this.sslContextProvider = sslContextProvider; + this.useHybrid = useHybrid; } public int sniEntrySize() { @@ -143,7 +148,7 @@ public SslContext sslServerContext(boolean useAlpn) { }; } - public SslHandler createClientSslHandler(SocketAddress remoteAddress, String serverName, boolean useAlpn) { + public SslHandler createClientSslHandler(SocketAddress remoteAddress, String serverName, boolean useAlpn) throws Exception { SslContext sslContext = sslClientContext(serverName, useAlpn); SslHandler sslHandler; Executor delegatedTaskExec = useWorkerPool ? workerPool : ImmediateExecutor.INSTANCE; @@ -152,19 +157,35 @@ public SslHandler createClientSslHandler(SocketAddress remoteAddress, String ser } else { sslHandler = sslContext.newHandler(ByteBufAllocator.DEFAULT, remoteAddress.host(), remoteAddress.port(), delegatedTaskExec); } + if (useHybrid) { + SSLEngine engine = sslHandler.engine(); + try { + long sslPtr = ((ReferenceCountedOpenSslEngine) engine).sslPointer(); + boolean success = SSL.setCurvesList(sslPtr, "X25519MLKEM768"); + if (!success) { + throw new Exception("Failed to set hybrid PQC groups on SSL instance"); + } + } catch (Exception e) { + throw new Exception("Unable to create sslHandler: "+e.getMessage()); + } + } sslHandler.setHandshakeTimeout(sslHandshakeTimeout, sslHandshakeTimeoutUnit); return sslHandler; } - public ChannelHandler createServerHandler(HostAndPort remoteAddress) { + public ChannelHandler createServerHandler(HostAndPort remoteAddress) throws Exception { if (sni) { - return createSniHandler(remoteAddress); + SniHandler sniHandler = createSniHandler(useHybrid, remoteAddress); + if(sniHandler == null){ + throw new Exception("Unable to create a SNI handler"); + } + return sniHandler; } else { - return createServerSslHandler(useAlpn, remoteAddress); + return createServerSslHandler(useAlpn, useHybrid, remoteAddress); } } - private SslHandler createServerSslHandler(boolean useAlpn, HostAndPort remoteAddress) { + private SslHandler createServerSslHandler(boolean useAlpn, boolean useHybrid, HostAndPort remoteAddress) throws Exception { SslContext sslContext = sslServerContext(useAlpn); Executor delegatedTaskExec = useWorkerPool ? workerPool : ImmediateExecutor.INSTANCE; SslHandler sslHandler; @@ -173,13 +194,26 @@ private SslHandler createServerSslHandler(boolean useAlpn, HostAndPort remoteAdd } else { sslHandler = sslContext.newHandler(ByteBufAllocator.DEFAULT, delegatedTaskExec); } + if (useHybrid) { + SSLEngine engine = sslHandler.engine(); + try { + long sslPtr = ((ReferenceCountedOpenSslEngine) engine).sslPointer(); + boolean success = SSL.setCurvesList(sslPtr, "X25519MLKEM768"); + if (!success) { + throw new Exception("Failed to set hybrid PQC groups on SSL instance"); + } + } catch (Exception e) { + throw new Exception("Unable to create sslHandler: "+e.getMessage()); + } + } sslHandler.setHandshakeTimeout(sslHandshakeTimeout, sslHandshakeTimeoutUnit); return sslHandler; } - private SniHandler createSniHandler(HostAndPort remoteAddress) { + private SniHandler createSniHandler(boolean useHybrid, HostAndPort remoteAddress) { Executor delegatedTaskExec = useWorkerPool ? workerPool : ImmediateExecutor.INSTANCE; - return new VertxSniHandler(serverNameMapping(), sslHandshakeTimeoutUnit.toMillis(sslHandshakeTimeout), delegatedTaskExec, remoteAddress); + return new VertxSniHandler(serverNameMapping(), sslHandshakeTimeoutUnit.toMillis(sslHandshakeTimeout), delegatedTaskExec, + useHybrid, remoteAddress); } private static int idx(boolean useAlpn) { diff --git a/src/main/java/io/vertx/core/net/impl/VertxSniHandler.java b/src/main/java/io/vertx/core/net/impl/VertxSniHandler.java index 07142c6d1c4..562ddeed2ab 100644 --- a/src/main/java/io/vertx/core/net/impl/VertxSniHandler.java +++ b/src/main/java/io/vertx/core/net/impl/VertxSniHandler.java @@ -11,12 +11,17 @@ package io.vertx.core.net.impl; import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.ssl.ReferenceCountedOpenSslEngine; import io.netty.handler.ssl.SniHandler; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslHandler; +import io.netty.internal.tcnative.SSL; import io.netty.util.AsyncMapping; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; import io.vertx.core.net.HostAndPort; +import javax.net.ssl.SSLEngine; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; @@ -29,12 +34,16 @@ class VertxSniHandler extends SniHandler { private final Executor delegatedTaskExec; private final HostAndPort remoteAddress; + private final boolean useHybrid; + + private static final Logger log = LoggerFactory.getLogger(SslChannelProvider.class); public VertxSniHandler(AsyncMapping mapping, long handshakeTimeoutMillis, Executor delegatedTaskExec, - HostAndPort remoteAddress) { + boolean useHybrid, HostAndPort remoteAddress) { super(mapping, handshakeTimeoutMillis); this.delegatedTaskExec = delegatedTaskExec; + this.useHybrid = useHybrid; this.remoteAddress = remoteAddress; } @@ -46,6 +55,22 @@ protected SslHandler newSslHandler(SslContext context, ByteBufAllocator allocato } else { sslHandler = context.newHandler(allocator, delegatedTaskExec); } + if(useHybrid){ + SSLEngine engine = sslHandler.engine(); + try { + long sslPtr = ((ReferenceCountedOpenSslEngine) engine).sslPointer(); + boolean success = SSL.setCurvesList(sslPtr, "X25519MLKEM768"); + if (!success) { + throw new Exception("Failed to set hybrid PQC groups on SSL instance"); + } + } catch (Exception e) { + /* + todo : would like to throw instead of returning null to be consistent with + io.vertx.core.net.impl.SslChannelProvider.createServerSslHandler(...) but can't as we extend a netty class here. + */ + return null; + } + } sslHandler.setHandshakeTimeout(handshakeTimeoutMillis, TimeUnit.MILLISECONDS); return sslHandler; } diff --git a/src/test/java/io/vertx/it/HybridKeyExchangeTest.java b/src/test/java/io/vertx/it/HybridKeyExchangeTest.java new file mode 100644 index 00000000000..27aea3366e3 --- /dev/null +++ b/src/test/java/io/vertx/it/HybridKeyExchangeTest.java @@ -0,0 +1,355 @@ +/* + * 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.Future; +import io.vertx.core.http.*; +import io.vertx.core.http.ClientAuth; +import io.vertx.core.net.OpenSSLEngineOptions; +import io.vertx.test.tls.Cert; +import io.vertx.test.tls.Trust; +import org.junit.Test; + +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 { + server = vertx.createHttpServer(new HttpServerOptions() + .setPort(DEFAULT_HTTPS_PORT) + .setHost(DEFAULT_HTTPS_HOST) + .setSsl(true) + .setSslEngineOptions(new OpenSSLEngineOptions()) + .setUseHybrid(true) + .setKeyCertOptions(Cert.SERVER_PEM.get())); + server.requestHandler(req -> req.response().end("hybrid-ok")); + startServer(server); + + client = vertx.createHttpClient(new HttpClientOptions() + .setSsl(true) + .setSslEngineOptions(new OpenSSLEngineOptions()) + .setUseHybrid(true) + .setTrustAll(true)); + HttpClient client2 = vertx.createHttpClient(new HttpClientOptions() + .setSsl(true) + .setSslEngineOptions(new OpenSSLEngineOptions()) + .setUseHybrid(false) + .setTrustAll(true)); + + CompletableFuture cf1 = new CompletableFuture<>(); + CompletableFuture cf2 = new CompletableFuture<>(); + + Future reqSuccess = client.request(HttpMethod.GET, DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/").onComplete(onSuccess(req -> { + req.send().onComplete(onSuccess(resp -> { + assertEquals(200, resp.statusCode()); + assertEquals("TLSv1.3", req.connection().sslSession().getProtocol()); + resp.body().onComplete(onSuccess(body -> { + assertEquals("hybrid-ok", body.toString()); + cf1.complete(true); + })); + })); + })); + + Future reqFail = client2.request(HttpMethod.GET, DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/").onComplete( + onFailure(req -> { + assertTrue(req instanceof javax.net.ssl.SSLHandshakeException); + cf2.complete(true); + }) + ); + + CompletableFuture.allOf(cf1, cf2).thenAccept((v) -> testComplete()).get(); + } + + @Test + public void testHybridKeyExchangeHandshakeMTLS() throws Exception { + server = vertx.createHttpServer(new HttpServerOptions() + .setPort(DEFAULT_HTTPS_PORT) + .setHost(DEFAULT_HTTPS_HOST) + .setSsl(true) + .setSslEngineOptions(new OpenSSLEngineOptions()) + .setUseHybrid(true) + .setClientAuth(ClientAuth.REQUIRED) + .setKeyCertOptions(Cert.SERVER_PEM_ROOT_CA.get()) + .setTrustOptions(Trust.SERVER_PEM_ROOT_CA.get())); + server.requestHandler(req -> { + assertTrue(req.isSSL()); + req.response().end("mtls-hybrid-ok"); + }); + startServer(server); + + client = vertx.createHttpClient(new HttpClientOptions() + .setSsl(true) + .setSslEngineOptions(new OpenSSLEngineOptions()) + .setUseHybrid(true) + .setKeyCertOptions(Cert.CLIENT_PEM_ROOT_CA.get()) + .setTrustAll(true)); + HttpClient client2 = vertx.createHttpClient(new HttpClientOptions() + .setSsl(true) + .setSslEngineOptions(new OpenSSLEngineOptions()) + .setUseHybrid(false) + .setKeyCertOptions(Cert.CLIENT_PEM_ROOT_CA.get()) + .setTrustAll(true)); + + CompletableFuture cf1 = new CompletableFuture<>(); + CompletableFuture cf2 = new CompletableFuture<>(); + + Future reqSuccess = client.request(HttpMethod.GET, DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/").onComplete(onSuccess(req -> { + req.send().onComplete(onSuccess(resp -> { + assertEquals(200, resp.statusCode()); + assertEquals("TLSv1.3", req.connection().sslSession().getProtocol()); + resp.body().onComplete(onSuccess(body -> { + assertEquals("mtls-hybrid-ok", body.toString()); + cf1.complete(true); + })); + })); + })); + + Future reqFail = client2.request(HttpMethod.GET, DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/").onComplete( + onFailure(req -> { + assertTrue(req instanceof javax.net.ssl.SSLHandshakeException); + cf2.complete(true); + }) + ); + + CompletableFuture.allOf(cf1, cf2).thenAccept((v) -> testComplete()).get(); + } + + /** + * A test using a netty client with a listener to get the supported groups exchanged. + * @throws Exception + */ + @Test + public void testHybridWithRawNettySocket() throws Exception { + + // Start Vert.x server with hybrid + server.close(); + server = vertx.createHttpServer(new HttpServerOptions() + .setPort(DEFAULT_HTTPS_PORT) + .setHost(DEFAULT_HTTPS_HOST) + .setSsl(true) + .setSslEngineOptions(new OpenSSLEngineOptions()) + .setUseHybrid(true) + .setKeyCertOptions(Cert.SERVER_PEM.get())); + server.requestHandler(req -> req.response().end("hybrid-ok")); + startServer(server); + + // Raw Netty client + SslContext sslContext = SslContextBuilder.forClient() + .sslProvider(SslProvider.OPENSSL) + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .build(); + + // Will hold the negotiated group from key_share extension + 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); + + // Set hybrid curves on the OpenSSL engine + ReferenceCountedOpenSslEngine engine = + (ReferenceCountedOpenSslEngine) sslHandler.engine(); + SSL.setCurvesList(engine.sslPointer(), "X25519MLKEM768"); + + // Interceptor BEFORE SslHandler sees raw TLS records + 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 { + + // Start Vert.x server with hybrid + mTLS + server.close(); + server = vertx.createHttpServer(new HttpServerOptions() + .setPort(DEFAULT_HTTPS_PORT) + .setHost(DEFAULT_HTTPS_HOST) + .setSsl(true) + .setSslEngineOptions(new OpenSSLEngineOptions()) + .setUseHybrid(true) + .setClientAuth(ClientAuth.REQUIRED) + .setKeyCertOptions(Cert.SERVER_PEM_ROOT_CA.get()) + .setTrustOptions(Trust.SERVER_PEM_ROOT_CA.get())); + server.requestHandler(req -> { + assertTrue(req.isSSL()); + req.response().end("mtls-hybrid-ok"); + }); + startServer(server); + + // Raw Netty client with client cert + 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(); + + // Will hold the negotiated group from key_share extension + 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); + + // Set hybrid curves on the OpenSSL engine + ReferenceCountedOpenSslEngine engine = + (ReferenceCountedOpenSslEngine) sslHandler.engine(); + SSL.setCurvesList(engine.sslPointer(), "X25519MLKEM768"); + + // Interceptor BEFORE SslHandler sees raw TLS records + 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); + } + } + // Always forward to SslHandler + 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(); + + // Walk extensions + 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); + } + } + } +}