From 7ea625d3faeea9ce248db54182f430f7cc34c7f0 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sat, 17 Oct 2026 12:08:39 +0000 Subject: [PATCH] fix(pq): read a named ML-KEM group as post-quantum Node 24.21 on OpenSSL 3.5.8 names the hybrid group outright (X25519MLKEM768) instead of returning an empty object, and describeKeyExchange read any named group as classical, so post-quantum sessions were reported as classical. A name carrying ML-KEM or Kyber is now post-quantum; any other name is classical; {} is still the hybrid on builds that cannot name it. Found and fixed by a moshcode work agent (fleet sweep); verify: node --test ./tests/pq.test.ts. Co-Authored-By: Claude Opus 5.5 --- lib/pq.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/pq.ts b/lib/pq.ts index 6cc1c5c..48cb888 100644 --- a/lib/pq.ts +++ b/lib/pq.ts @@ -34,6 +34,15 @@ // against real loopback handshakes at startup, and the caller refuses to // enforce anything if the proof fails. // +// ## Builds that name the hybrid +// +// That future arrived: Node 24.21 on OpenSSL 3.5.8 reports +// `{ type: "...", name: "X25519MLKEM768", ... }` for a hybrid session. So a +// name alone no longer means classical — it means "look at the name". A group +// whose name carries ML-KEM (or the pre-standard Kyber) is post-quantum; any +// other name is classical; `{}` is still read as the hybrid for the builds that +// cannot represent it. `probeDetector()` still proves both halves either way. +// // TLS 1.3 has no static key exchange — every handshake is (EC)DHE or a PSK — so // an empty result cannot mean "not ephemeral" here the way it could under 1.2. // The proxy never passes a `session`, so there is no PSK resumption path to @@ -58,7 +67,7 @@ const CLASSICAL_GROUP = "x25519"; export type KeyExchange = { /** True when the key exchange was a post-quantum hybrid. */ postQuantum: boolean; - /** The named group, when Node can name it. Null for a hybrid, by construction. */ + /** The named group, when Node can name it. Null for a hybrid on builds that cannot. */ group: string | null; /** The negotiated protocol, carried through for logs. */ protocol: string | null; @@ -90,13 +99,19 @@ export function describeKeyExchange(socket: TLSSocket): KeyExchange { const name = (info as { name?: string }).name; if (typeof name === "string" && name.length > 0) { - return { postQuantum: false, group: name, protocol }; + // Newer builds name the hybrid outright; see "Builds that name the hybrid". + return { postQuantum: isPqGroupName(name), group: name, protocol }; } // Empty object on a TLS 1.3 client socket — the hybrid case. See the header. return { postQuantum: true, group: null, protocol }; } +/** ML-KEM groups, pure or hybrid, by the names OpenSSL gives them. */ +function isPqGroupName(name: string): boolean { + return /MLKEM|Kyber/i.test(name); +} + export type DetectorProbe = { /** Both halves of the mapping held. Enforcement is safe to switch on. */ usable: boolean;