From 1526163eecf19696202fa2aeb8a4487f669bead1 Mon Sep 17 00:00:00 2001 From: Royyan Zahir Date: Mon, 6 Jul 2026 21:29:31 +0400 Subject: [PATCH 1/2] noise: draft hybrid PQ handshake spec (X25519 + ML-KEM-768) --- noise/pq-mlkem768-hfs.md | 89 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 noise/pq-mlkem768-hfs.md diff --git a/noise/pq-mlkem768-hfs.md b/noise/pq-mlkem768-hfs.md new file mode 100644 index 000000000..f66ad4384 --- /dev/null +++ b/noise/pq-mlkem768-hfs.md @@ -0,0 +1,89 @@ +# Hybrid post-quantum Noise (X25519 + ML-KEM-768) + +| Lifecycle Stage | Maturity | Status | Latest Revision | +| --------------- | ------------- | ------ | --------------- | +| 1A | Working Draft | Active | r0, 2026-07-06 | + +Authors: [@royzah]. Interest Group: [@jxs], [@thomaseizinger], [@mxinden], +[@MarcoPolo], [@achingbrain]. + +## Why + +[`/noise`][noise] is X25519 only, so recorded sessions are open to +harvest-now-decrypt-later. This adds a separate handshake that mixes an +ML-KEM-768 KEM into the same `XX` pattern via Noise [HFS][hfs]. It stays secure +if either primitive holds. + +Auth is unchanged and stays classical: in libp2p the identity key signs the +static key, so identity cannot be broken after the fact. Only secrecy is at risk, +so only the ephemeral key exchange is hybridized. + +## Protocol ID + +```text +/noise-mlkem768-hfs/0.1.0 +``` + +Advertised ahead of `/noise`. Two capable peers pick it; otherwise negotiation +falls back to `/noise` with no extra round trip. + +## Suite + +Mixed into the handshake hash, so it must match byte-for-byte: + +```text +Noise_XXhfs_25519+ML-KEM-768_ChaChaPoly_SHA256 +``` + +## Handshake + +`XX` with the `hfs` modifier. Token placement and KDF order are exactly Noise +HFS; this spec adds nothing to them. + +```text +-> e, e1 +<- e, ee, ekem1, s, es +-> s, se +``` + +- `e1`: initiator sends the ML-KEM-768 encap key (1184 B). +- `ekem1`: responder encapsulates, returns the ciphertext (1088 B), mixes the + shared secret. + +The libp2p identity payload (signature over the static key) is carried and +verified as in [`/noise`][noise], unchanged. + +```mermaid +sequenceDiagram + participant I as Initiator + participant R as Responder + I->>R: e, e1 + R->>I: e, ee, ekem1, s, es (+ identity) + I->>R: s, se (+ identity) +``` + +## To pin for interop + +1. The protocol ID string. +2. Test vectors: fixed keys and KEM randomness with per-step handshake hash and + transport keys, so go/js/rust match byte-for-byte. +3. ML-KEM-768 only, or the 512/768/1024 family with 768 as the baseline? + +## Reference + +- rust-libp2p: [#6481][pr], off-by-default `mlkem-hfs` feature; classical path + untouched. Revives [#2168][old]. +- KEM: ML-KEM-768 in `snow` ([mcginty/snow#210][snow]). Until it ships in a + release, the impl pins `snow` via `[patch]`; KATs land with it. + +[noise]: https://github.com/libp2p/specs/blob/master/noise/README.md +[hfs]: https://github.com/noiseprotocol/noise_wiki/wiki/Hybrid-Forward-Secrecy +[pr]: https://github.com/libp2p/rust-libp2p/pull/6481 +[old]: https://github.com/libp2p/rust-libp2p/pull/2168 +[snow]: https://github.com/mcginty/snow/pull/210 +[@royzah]: https://github.com/royzah +[@jxs]: https://github.com/jxs +[@thomaseizinger]: https://github.com/thomaseizinger +[@mxinden]: https://github.com/mxinden +[@MarcoPolo]: https://github.com/MarcoPolo +[@achingbrain]: https://github.com/achingbrain From 9ee89e3033309804ccf0813b462a531641d3c1fc Mon Sep 17 00:00:00 2001 From: Royyan Zahir Date: Mon, 17 Aug 2026 08:39:20 +0400 Subject: [PATCH 2/2] noise: address review on the PQ handshake draft --- noise/pq-mlkem768-hfs.md | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/noise/pq-mlkem768-hfs.md b/noise/pq-mlkem768-hfs.md index f66ad4384..9a86c37b1 100644 --- a/noise/pq-mlkem768-hfs.md +++ b/noise/pq-mlkem768-hfs.md @@ -10,13 +10,13 @@ Authors: [@royzah]. Interest Group: [@jxs], [@thomaseizinger], [@mxinden], ## Why [`/noise`][noise] is X25519 only, so recorded sessions are open to -harvest-now-decrypt-later. This adds a separate handshake that mixes an -ML-KEM-768 KEM into the same `XX` pattern via Noise [HFS][hfs]. It stays secure -if either primitive holds. +harvest-now-decrypt-later. This specification adds a separate handshake that +mixes an ML-KEM-768 KEM into the same `XX` pattern via Noise [HFS][hfs]. It +stays secure if either primitive holds. -Auth is unchanged and stays classical: in libp2p the identity key signs the -static key, so identity cannot be broken after the fact. Only secrecy is at risk, -so only the ephemeral key exchange is hybridized. +Authentication is unchanged and stays classical: in libp2p the identity key +signs the static key, so identity cannot be broken after the fact. Only secrecy +is at risk, so only the ephemeral key exchange is hybridized. ## Protocol ID @@ -24,21 +24,22 @@ so only the ephemeral key exchange is hybridized. /noise-mlkem768-hfs/0.1.0 ``` -Advertised ahead of `/noise`. Two capable peers pick it; otherwise negotiation -falls back to `/noise` with no extra round trip. +An initiator supporting both SHOULD propose `/noise-mlkem768-hfs/0.1.0` before +`/noise`. A responder supporting this protocol MUST accept that protocol ID. If +the responder returns `na`, the initiator MAY subsequently propose `/noise`. ## Suite Mixed into the handshake hash, so it must match byte-for-byte: ```text -Noise_XXhfs_25519+ML-KEM-768_ChaChaPoly_SHA256 +Noise_XXhfs_25519+MLKEM768_ChaChaPoly_SHA256 ``` ## Handshake -`XX` with the `hfs` modifier. Token placement and KDF order are exactly Noise -HFS; this spec adds nothing to them. +`XX` with the `hfs` modifier. Token placement and KDF order are exactly as +specified by Noise HFS; this spec adds nothing to them. ```text -> e, e1 @@ -46,23 +47,14 @@ HFS; this spec adds nothing to them. -> s, se ``` -- `e1`: initiator sends the ML-KEM-768 encap key (1184 B). +- `e1`: initiator sends the ML-KEM-768 encapsulation key (1184 B). - `ekem1`: responder encapsulates, returns the ciphertext (1088 B), mixes the shared secret. The libp2p identity payload (signature over the static key) is carried and verified as in [`/noise`][noise], unchanged. -```mermaid -sequenceDiagram - participant I as Initiator - participant R as Responder - I->>R: e, e1 - R->>I: e, ee, ekem1, s, es (+ identity) - I->>R: s, se (+ identity) -``` - -## To pin for interop +## Open issues 1. The protocol ID string. 2. Test vectors: fixed keys and KEM randomness with per-step handshake hash and @@ -74,7 +66,7 @@ sequenceDiagram - rust-libp2p: [#6481][pr], off-by-default `mlkem-hfs` feature; classical path untouched. Revives [#2168][old]. - KEM: ML-KEM-768 in `snow` ([mcginty/snow#210][snow]). Until it ships in a - release, the impl pins `snow` via `[patch]`; KATs land with it. + release, the implementation pins `snow` via `[patch]`; KATs land with it. [noise]: https://github.com/libp2p/specs/blob/master/noise/README.md [hfs]: https://github.com/noiseprotocol/noise_wiki/wiki/Hybrid-Forward-Secrecy