Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions vertx-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,13 @@
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-tcnative-boringssl-static</artifactId>
<scope>test</scope>
<artifactId>netty-tcnative-classes</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye</groupId>
<artifactId>smallrye-openssl</artifactId>
<classifier>linux-x86_64</classifier>
<version>0.1.4</version>
</dependency>
<dependency>
<groupId>com.aayushatharva.brotli4j</groupId>
Expand Down
10 changes: 10 additions & 0 deletions vertx-core/src/main/asciidoc/http.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ To handle `h2` requests, TLS must be enabled:
{@link examples.HttpExamples#configurationOfAnH2Server}
----

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 with {@link io.vertx.core.net.SSLOptions#setUseHybridKeyExchangeProtocol(boolean)} and **requires OpenSSL** — it does not work with the JDK SSL engine. You must configure {@link io.vertx.core.net.OpenSSLEngineOptions} and add the following dependencies:

- `io.netty:netty-tcnative-classes` (version managed by the Netty BOM)
- An OpenSSL provider such as `io.smallrye:smallrye-openssl`

If OpenSSL is not available at runtime, the TLS handshake will fail rather than silently falling back to a non-quantum-safe key exchange.

With plain text (TLS is disabled), the server handles `h2c` requests that wants to upgrade connections presenting an
HTTP/1.1 upgrade request to HTTP/2. It also accepts direct `h2c` (with prior knowledge) connections beginning with
the `PRI * HTTP/2.0\r\nSM\r\n` preface.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, SSLOpti
obj.setUseAlpn((Boolean)member.getValue());
}
break;
case "useHybridKeyExchangeProtocol":
if (member.getValue() instanceof Boolean) {
obj.setUseHybridKeyExchangeProtocol((Boolean)member.getValue());
}
break;
case "enabledSecureTransportProtocols":
if (member.getValue() instanceof JsonArray) {
java.util.LinkedHashSet<java.lang.String> list = new java.util.LinkedHashSet<>();
Expand Down Expand Up @@ -96,6 +101,7 @@ static void toJson(SSLOptions obj, java.util.Map<String, Object> json) {
json.put("crlValues", array);
}
json.put("useAlpn", obj.isUseAlpn());
json.put("useHybridKeyExchangeProtocol", obj.isUseHybridKeyExchangeProtocol());
if (obj.getEnabledSecureTransportProtocols() != null) {
JsonArray array = new JsonArray();
obj.getEnabledSecureTransportProtocols().forEach(item -> array.add(item));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@

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.concurrent.ImmediateExecutor;
import io.vertx.core.internal.VertxInternal;
import io.vertx.core.internal.logging.Logger;
import io.vertx.core.internal.logging.LoggerFactory;
import io.vertx.core.internal.tls.ClientSslContextProvider;
import io.vertx.core.internal.tls.ServerSslContextProvider;
import io.vertx.core.internal.tls.SslContextProvider;
Expand All @@ -34,15 +38,20 @@
*/
public class SslChannelProvider {

private static final Logger log = LoggerFactory.getLogger(SslChannelProvider.class);

private final Executor workerPool;
private final boolean sni;
private final boolean useHybridKeyExchangeProtocol;
private final SslContextProvider sslContextProvider;

public SslChannelProvider(VertxInternal vertx,
SslContextProvider sslContextProvider,
boolean sni) {
boolean sni,
boolean useHybridKeyExchangeProtocol) {
this.workerPool = vertx.internalWorkerPool().executor();
this.sni = sni;
this.useHybridKeyExchangeProtocol = useHybridKeyExchangeProtocol;
this.sslContextProvider = sslContextProvider;
}

Expand All @@ -63,6 +72,9 @@ public SslHandler createClientSslHandler(HostAndPort peer,
} else {
sslHandler = sslContext.newHandler(ByteBufAllocator.DEFAULT, delegatedTaskExec);
}
if (useHybridKeyExchangeProtocol) {
applyHybridCurves(sslHandler);
}
sslHandler.setHandshakeTimeout(sslHandshakeTimeout, sslHandshakeTimeoutUnit);
return sslHandler;
}
Expand All @@ -84,13 +96,31 @@ private SslHandler createServerSslHandler(List<String> applicationProtocols, lon
} else {
sslHandler = sslContext.newHandler(ByteBufAllocator.DEFAULT, delegatedTaskExec);
}
if (useHybridKeyExchangeProtocol) {
applyHybridCurves(sslHandler);
}
sslHandler.setHandshakeTimeout(sslHandshakeTimeout, sslHandshakeTimeoutUnit);
return sslHandler;
}

private SniHandler createSniHandler(List<String> applicationProtocols, long sslHandshakeTimeout, TimeUnit sslHandshakeTimeoutUnit, HostAndPort remoteAddress) {
Executor delegatedTaskExec = sslContextProvider.useWorkerPool() ? workerPool : ImmediateExecutor.INSTANCE;
return new VertxSniHandler(((ServerSslContextProvider)sslContextProvider).serverNameAsyncMapping(delegatedTaskExec, applicationProtocols), sslHandshakeTimeoutUnit.toMillis(sslHandshakeTimeout), delegatedTaskExec, remoteAddress);
return new VertxSniHandler(((ServerSslContextProvider)sslContextProvider).serverNameAsyncMapping(delegatedTaskExec, applicationProtocols), sslHandshakeTimeoutUnit.toMillis(sslHandshakeTimeout), delegatedTaskExec,
useHybridKeyExchangeProtocol, remoteAddress);
}

static void applyHybridCurves(SslHandler sslHandler) {
try {
long sslPtr = ((ReferenceCountedOpenSslEngine) sslHandler.engine()).sslPointer();
boolean success = SSL.setCurvesList(sslPtr, "X25519MLKEM768");
if (!success) {
log.error("Failed to set hybrid PQC groups on SSL instance, closing engine to prevent non-PQC fallback");
sslHandler.engine().closeOutbound();
}
} catch (Exception e) {
log.error("Unable to apply hybrid PQC curves: " + e.getMessage() + ", closing engine to prevent non-PQC fallback");
sslHandler.engine().closeOutbound();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@
class VertxSniHandler extends SniHandler {

private final Executor delegatedTaskExec;
private final boolean useHybridKeyExchangeProtocol;
private final HostAndPort remoteAddress;

public VertxSniHandler(AsyncMapping<? super String, ? extends SslContext> mapping, long handshakeTimeoutMillis, Executor delegatedTaskExec,
HostAndPort remoteAddress) {
boolean useHybridKeyExchangeProtocol, HostAndPort remoteAddress) {
super(mapping, handshakeTimeoutMillis);

this.delegatedTaskExec = delegatedTaskExec;
this.useHybridKeyExchangeProtocol = useHybridKeyExchangeProtocol;
this.remoteAddress = remoteAddress;
}

Expand All @@ -46,6 +48,9 @@ protected SslHandler newSslHandler(SslContext context, ByteBufAllocator allocato
} else {
sslHandler = context.newHandler(allocator, delegatedTaskExec);
}
if (useHybridKeyExchangeProtocol) {
SslChannelProvider.applyHybridCurves(sslHandler);
}
sslHandler.setHandshakeTimeout(handshakeTimeoutMillis, TimeUnit.MILLISECONDS);
return sslHandler;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ public ClientSSLOptions setUseAlpn(boolean useAlpn) {
return (ClientSSLOptions) super.setUseAlpn(useAlpn);
}

@Override
public ClientSSLOptions setUseHybridKeyExchangeProtocol(boolean useHybridKeyExchangeProtocol) {
return (ClientSSLOptions) super.setUseHybridKeyExchangeProtocol(useHybridKeyExchangeProtocol);
}

@Override
public ClientSSLOptions setSslHandshakeTimeout(long sslHandshakeTimeout) {
return (ClientSSLOptions) super.setSslHandshakeTimeout(sslHandshakeTimeout);
Expand Down
41 changes: 40 additions & 1 deletion vertx-core/src/main/java/io/vertx/core/net/SSLOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ public class SSLOptions {
*/
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
*/
Expand Down Expand Up @@ -66,6 +71,7 @@ public class SSLOptions {
List<String> crlPaths;
List<Buffer> crlValues;
private boolean useAlpn;
private boolean useHybridKeyExchangeProtocol;
private Set<String> enabledSecureTransportProtocols;
private List<String> applicationLayerProtocols;

Expand All @@ -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;
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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.
* <p>
* When enabled, TLS connections will use X25519MLKEM768 for key exchange, providing
* protection against quantum computer attacks.
* <p>
* This feature requires OpenSSL and will not work with the JDK SSL engine. You must:
* <ul>
* <li>Use {@link OpenSSLEngineOptions} as the SSL engine</li>
* <li>Have {@code io.netty:netty-tcnative-classes} on the classpath</li>
* <li>Have an OpenSSL provider (e.g. {@code io.smallrye:smallrye-openssl}) on the classpath</li>
* </ul>
* 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
Expand Down Expand Up @@ -365,14 +403,15 @@ 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;
}

@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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,12 @@ private Future<Void> 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<Void> p = context.promise();
Expand Down
1 change: 1 addition & 0 deletions vertx-core/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions vertx-core/src/main/java21/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading
Loading