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
38 changes: 35 additions & 3 deletions vertx-core/src/main/asciidoc/http.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,46 @@ To handle `h2` requests, TLS must be enabled:
----

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.
Vert.x supports post-quantum cryptography (PQC) key exchange via 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:
PQC is configured with two properties:

- {@link io.vertx.core.net.SSLOptions#setKeyExchangeGroups} — a list of key exchange group names (e.g. `["X25519MLKEM768", "X25519"]`)
- {@link io.vertx.core.net.SSLOptions#setPqcEnforcementPolicy} — the enforcement policy: `RELAXED` (default), `CLIENT_NEGOTIATED`, or `STRICT`

The three enforcement modes behave as follows:

- **RELAXED**: uses the specified key exchange groups as-is; if none are specified, the SSL engine negotiates normally. No PQC enforcement.
- **CLIENT_NEGOTIATED**: the server requires PQC support (X25519MLKEM768 is prepended to the groups if not present), but tolerates clients that do not support PQC.
- **STRICT**: both server and client must support PQC. The key exchange groups are replaced with only X25519MLKEM768. Non-PQC clients will fail the TLS handshake.

For both `STRICT` and `CLIENT_NEGOTIATED`, the application fails with either "PQC enforcement policy requires X25519MLKEM768 but the configured SSL engine does not support it" or "PQC enforcement policy requires X25519MLKEM768 but neither JDK nor OpenSSL support it" if pqc is not supported by the SSL engine.

For `STRICT` and `CLIENT_NEGOTIATED` modes, you need an SslEngine supporting X25519MLKEM768. `JdkSslEngine` supports it starting with JDK 27 (preview feature), and OpenSsl supports it starting with OpenSsl 3.5. If no `SslEngine` is specified by the user, the first engine supporting pqc will be selected, starting with `JdkSslEngine`

Using OpenSsl >= 3.5 requires 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.
**PQC enforcement policy summary:**
[cols="1,1,1,2", options="header"]
|===
| pqc-enforcement-policy | server-pqc-supports | client-pqc-supports | Result
.3+| strict
| false | required / true / false | **app fails** with error "PQC enforcement policy STRICT requires X25519MLKEM768 but the configured SSL engine does not support it"
| true | true / required | app starts and ssl handshake succeeds
| true | false | app starts but **ssl handshake fails**

.2+| client_negotiated
| false | required / true / false | **app fails** with error "PQC enforcement policy STRICT requires X25519MLKEM768 but the configured SSL engine does not support it"
| true | required / true / false | app starts and ssl handshake succeeds

.3+| relaxed
| true | required / true / false | app starts and ssl handshake succeeds
| false | true / false | app starts and ssl handshake succeeds
| false | required | app starts but **ssl handshake fails**
|===

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,19 @@ 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());
case "keyExchangeGroups":
if (member.getValue() instanceof JsonArray) {
java.util.ArrayList<java.lang.String> list = new java.util.ArrayList<>();
((Iterable<Object>)member.getValue()).forEach( item -> {
if (item instanceof String)
list.add((String)item);
});
obj.setKeyExchangeGroups(list);
}
break;
case "pqcEnforcementPolicy":
if (member.getValue() instanceof String) {
obj.setPqcEnforcementPolicy(io.vertx.core.net.PqcEnforcementPolicy.valueOf((String)member.getValue()));
}
break;
case "enabledSecureTransportProtocols":
Expand Down Expand Up @@ -101,7 +111,14 @@ 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.getKeyExchangeGroups() != null) {
JsonArray array = new JsonArray();
obj.getKeyExchangeGroups().forEach(item -> array.add(item));
json.put("keyExchangeGroups", array);
}
if (obj.getPqcEnforcementPolicy() != null) {
json.put("pqcEnforcementPolicy", obj.getPqcEnforcementPolicy().name());
}
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 @@ -41,22 +41,22 @@ public class SslChannelProvider {

private final Executor workerPool;
private final boolean sni;
private final boolean useHybridKeyExchangeProtocol;
private final List<String> keyExchangeGroups;
private final SslContextProvider sslContextProvider;

public SslChannelProvider(VertxInternal vertx,
SslContextProvider sslContextProvider,
boolean sni) {
this(vertx, sslContextProvider, sni, false);
this(vertx, sslContextProvider, sni, null);
}

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

Expand All @@ -77,8 +77,8 @@ public SslHandler createClientSslHandler(HostAndPort peer,
} else {
sslHandler = sslContext.newHandler(ByteBufAllocator.DEFAULT, delegatedTaskExec);
}
if (useHybridKeyExchangeProtocol) {
applyHybridCurves(sslHandler);
if (keyExchangeGroups != null && !keyExchangeGroups.isEmpty()) {
applyKeyExchangeGroups(sslHandler, keyExchangeGroups);
}
sslHandler.setHandshakeTimeout(sslHandshakeTimeout, sslHandshakeTimeoutUnit);
return sslHandler;
Expand All @@ -101,8 +101,8 @@ private SslHandler createServerSslHandler(List<String> applicationProtocols, lon
} else {
sslHandler = sslContext.newHandler(ByteBufAllocator.DEFAULT, delegatedTaskExec);
}
if (useHybridKeyExchangeProtocol) {
applyHybridCurves(sslHandler);
if (keyExchangeGroups != null && !keyExchangeGroups.isEmpty()) {
applyKeyExchangeGroups(sslHandler, keyExchangeGroups);
}
sslHandler.setHandshakeTimeout(sslHandshakeTimeout, sslHandshakeTimeoutUnit);
return sslHandler;
Expand All @@ -111,19 +111,20 @@ private SslHandler createServerSslHandler(List<String> applicationProtocols, lon
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,
useHybridKeyExchangeProtocol, remoteAddress);
keyExchangeGroups, remoteAddress);
}

static void applyHybridCurves(SslHandler sslHandler) {
static void applyKeyExchangeGroups(SslHandler sslHandler, List<String> groups) {
String curvesList = String.join(":", groups);
try {
long sslPtr = ((ReferenceCountedOpenSslEngine) sslHandler.engine()).sslPointer();
boolean success = SSL.setCurvesList(sslPtr, "X25519MLKEM768");
boolean success = SSL.setCurvesList(sslPtr, curvesList);
if (!success) {
log.error("Failed to set hybrid PQC groups on SSL instance, closing engine to prevent non-PQC fallback");
log.error("Failed to set key exchange groups [" + curvesList + "] on SSL instance, closing engine");
sslHandler.engine().closeOutbound();
}
} catch (Exception e) {
log.error("Unable to apply hybrid PQC curves: " + e.getMessage() + ", closing engine to prevent non-PQC fallback");
log.error("Unable to apply key exchange groups: " + e.getMessage() + ", closing engine");
sslHandler.engine().closeOutbound();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import io.netty.util.AsyncMapping;
import io.vertx.core.net.HostAndPort;

import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;

Expand All @@ -28,15 +29,15 @@
class VertxSniHandler extends SniHandler {

private final Executor delegatedTaskExec;
private final boolean useHybridKeyExchangeProtocol;
private final List<String> keyExchangeGroups;
private final HostAndPort remoteAddress;

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

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

Expand All @@ -48,8 +49,8 @@ protected SslHandler newSslHandler(SslContext context, ByteBufAllocator allocato
} else {
sslHandler = context.newHandler(allocator, delegatedTaskExec);
}
if (useHybridKeyExchangeProtocol) {
SslChannelProvider.applyHybridCurves(sslHandler);
if (keyExchangeGroups != null && !keyExchangeGroups.isEmpty()) {
SslChannelProvider.applyKeyExchangeGroups(sslHandler, keyExchangeGroups);
}
sslHandler.setHandshakeTimeout(handshakeTimeoutMillis, TimeUnit.MILLISECONDS);
return sslHandler;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import io.vertx.core.http.ClientAuth;
import io.vertx.core.impl.utils.LruCache;
import io.vertx.core.internal.ContextInternal;
import io.vertx.core.internal.logging.Logger;
import io.vertx.core.internal.logging.LoggerFactory;
import io.vertx.core.net.*;
import io.vertx.core.spi.tls.SslContextFactory;

Expand All @@ -37,6 +39,8 @@
*/
public abstract class SslContextManager<P extends SslContextProvider> {

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

public static io.netty.handler.ssl.ClientAuth mapClientAuth(ClientAuth auth) {
return CLIENT_AUTH_MAPPING.get(auth);
}
Expand Down Expand Up @@ -73,6 +77,38 @@ public SslContextManager(SSLEngineOptions sslEngineOptions, int cacheMaxSize) {
* Resolve the ssl engine options to use for properly running the configured options.
*/
public static SSLEngineOptions resolveEngineOptions(SSLEngineOptions engineOptions, boolean useAlpn) {
return resolveEngineOptions(engineOptions, useAlpn, PqcEnforcementPolicy.RELAXED);
}

/**
* Resolve the ssl engine options to use for properly running the configured options,
* taking PQC enforcement policy into account.
*/
public static SSLEngineOptions resolveEngineOptions(SSLEngineOptions engineOptions, boolean useAlpn, PqcEnforcementPolicy pqcPolicy) {
if (pqcPolicy == null) {
pqcPolicy = PqcEnforcementPolicy.RELAXED;
}
if (pqcPolicy == PqcEnforcementPolicy.STRICT || pqcPolicy == PqcEnforcementPolicy.CLIENT_NEGOTIATED) {
if (engineOptions != null) {
boolean pqcSupported;
if (engineOptions instanceof JdkSSLEngineOptions) {
pqcSupported = JdkSSLEngineOptions.isPqcAvailable();
} else {
pqcSupported = OpenSSLEngineOptions.isPqcAvailable();
}
if (!pqcSupported) {
throw new VertxException("PQC enforcement policy " + pqcPolicy + " requires X25519MLKEM768 but the configured SSL engine does not support it");
}
} else {
if (JdkSSLEngineOptions.isPqcAvailable()) {
engineOptions = new JdkSSLEngineOptions();
} else if (OpenSSLEngineOptions.isPqcAvailable()) {
engineOptions = new OpenSSLEngineOptions();
} else {
throw new VertxException("PQC enforcement policy " + pqcPolicy + " requires X25519MLKEM768 but neither JDK nor OpenSSL support it");
}
}
}
if (engineOptions == null) {
if (useAlpn) {
if (JdkSSLEngineOptions.isAlpnAvailable()) {
Expand Down Expand Up @@ -110,6 +146,48 @@ public static SSLEngineOptions resolveEngineOptions(SSLEngineOptions engineOptio
return engineOptions;
}

private static final String X25519MLKEM768 = "X25519MLKEM768";

// If the user didn't specify any group we return a default list, with X25519MLKEM768 prepended.
// This list is the default when weak named curves are removed (see https://www.java.com/en/configure_crypto.html)
private static final List<String> DEFAULT_KEY_EXCHANGE_GROUPS = List.of(X25519MLKEM768, "X25519", "secp256r1", "x448",
"secp384r1", "secp521r1");

/**
* Resolve the effective key exchange groups based on the PQC enforcement policy.
* Called once at startup to avoid per-connection computation and logging.
*/
public static List<String> resolveKeyExchangeGroups(List<String> groups, PqcEnforcementPolicy pqcPolicy) {
if (pqcPolicy == null) {
pqcPolicy = PqcEnforcementPolicy.RELAXED;
}
switch (pqcPolicy) {
case STRICT:
if (groups != null && !groups.isEmpty()) {
if (!(groups.size() == 1 && groups.contains(X25519MLKEM768))) {
log.warn("PQC enforcement policy is STRICT: overriding key exchange groups " + groups + " with [" + X25519MLKEM768 + "]");
}
}
return List.of(X25519MLKEM768);
case CLIENT_NEGOTIATED:
if (groups == null || groups.isEmpty()) {
log.warn("No key exchange groups list was specified, a default list containing X25519MLKEM768 is selected");
return DEFAULT_KEY_EXCHANGE_GROUPS;
}
if (!groups.contains(X25519MLKEM768)) {
log.warn("PQC enforcement policy is CLIENT_NEGOTIATED: prepending " + X25519MLKEM768 + " to key exchange groups " + groups);
List<String> result = new ArrayList<>(groups.size() + 1);
result.add(X25519MLKEM768);
result.addAll(groups);
return result;
}
return groups;
case RELAXED:
default:
return groups;
}
}

public synchronized int sniEntrySize() {
int size = 0;
for (Future<P> fut : sslContextProviderMap.values()) {
Expand Down
14 changes: 12 additions & 2 deletions vertx-core/src/main/java/io/vertx/core/net/ClientSSLOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,18 @@ public ClientSSLOptions setUseAlpn(boolean useAlpn) {
}

@Override
public ClientSSLOptions setUseHybridKeyExchangeProtocol(boolean useHybridKeyExchangeProtocol) {
return (ClientSSLOptions) super.setUseHybridKeyExchangeProtocol(useHybridKeyExchangeProtocol);
public ClientSSLOptions setKeyExchangeGroups(List<String> keyExchangeGroups) {
return (ClientSSLOptions) super.setKeyExchangeGroups(keyExchangeGroups);
}

@Override
public ClientSSLOptions addKeyExchangeGroup(String group) {
return (ClientSSLOptions) super.addKeyExchangeGroup(group);
}

@Override
public ClientSSLOptions setPqcEnforcementPolicy(PqcEnforcementPolicy pqcEnforcementPolicy) {
return (ClientSSLOptions) super.setPqcEnforcementPolicy(pqcEnforcementPolicy);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
public class JdkSSLEngineOptions extends SSLEngineOptions {

private static Boolean jdkAlpnAvailable;
private static Boolean jdkPqcAvailable;

/**
* @return if alpn support is available via the JDK SSL engine
Expand Down Expand Up @@ -57,6 +58,24 @@ public static synchronized boolean isAlpnAvailable() {
return jdkAlpnAvailable;
}

/**
* @return if PQC key exchange (X25519MLKEM768) is available via the JDK SSL engine
*/
public static synchronized boolean isPqcAvailable() {
if (jdkPqcAvailable == null) {
boolean available = false;
try {
Class<?> kemClass = Class.forName("javax.crypto.KEM");
java.lang.reflect.Method getInstance = kemClass.getDeclaredMethod("getInstance", String.class);
getInstance.invoke(null, "ML-KEM-768");
available = true;
} catch (Exception ignore) {
}
jdkPqcAvailable = available;
}
return jdkPqcAvailable;
}

public JdkSSLEngineOptions() {
}

Expand Down
Loading
Loading