Conversation
| Mixed into the handshake hash, so it must match byte-for-byte: | ||
|
|
||
| ```text | ||
| Noise_XXhfs_25519+ML-KEM-768_ChaChaPoly_SHA256 |
There was a problem hiding this comment.
The - must be dropped according to https://noiseprotocol.org/noise.html#cryptographic-algorithm-name-sections
There was a problem hiding this comment.
Right, 8.2 allows alphanumerics and / only. Suite is now Noise_XXhfs_25519+MLKEM768_ChaChaPoly_SHA256, and both implementations follow:
- mcginty/snow#210: 858dc27 renames the parsed name and
Kem::name(). - libp2p/rust-libp2p#6481: 1ae21ce renames
PARAMS_XX_HFS.
Handshake tests pass on both. Thanks for catching it.
| ``` | ||
|
|
||
| Advertised ahead of `/noise`. Two capable peers pick it; otherwise negotiation | ||
| falls back to `/noise` with no extra round trip. |
There was a problem hiding this comment.
It seems to me that there's still a roundtrip if the receiver does not support mlkem768:
- Emitter sends
/noise-mlkem768-hfs/0.1.0 - Receiver replies
na - Emitter sends
/noise - Receiver replies back with
/noise - Handshake starts
There was a problem hiding this comment.
Idea for lines 27-28:
An initiator supporting both protocols 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.
There was a problem hiding this comment.
Correct, the na costs a round trip. Took your wording verbatim, so the claim is gone.
| ## To pin for interop | ||
|
|
There was a problem hiding this comment.
I'd rename this to Open issues or similar.
| -> s, se | ||
| ``` | ||
|
|
||
| - `e1`: initiator sends the ML-KEM-768 encap key (1184 B). |
There was a problem hiding this comment.
| - `e1`: initiator sends the ML-KEM-768 encap key (1184 B). | |
| - `e1`: initiator sends the ML-KEM-768 encapsulation key (1184 B). |
| ## Why | ||
|
|
||
| [`/noise`][noise] is X25519 only, so recorded sessions are open to | ||
| harvest-now-decrypt-later. This adds a separate handshake that mixes an |
There was a problem hiding this comment.
| harvest-now-decrypt-later. This adds a separate handshake that mixes an | |
| 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 |
There was a problem hiding this comment.
| Auth is unchanged and stays classical: in libp2p the identity key signs the | |
| Authentication is unchanged and stays classical: in libp2p the identity key signs the |
|
|
||
| ## Handshake | ||
|
|
||
| `XX` with the `hfs` modifier. Token placement and KDF order are exactly Noise |
There was a problem hiding this comment.
| `XX` with the `hfs` modifier. Token placement and KDF order are exactly Noise | |
| `XX` with the `hfs` modifier. Token placement and KDF order are exactly as specified by Noise |
| - 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. |
There was a problem hiding this comment.
| release, the impl pins `snow` via `[patch]`; KATs land with it. | |
| release, the implementation pins `snow` via `[patch]`; KATs land with it. |
| ```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) | ||
| ``` |
There was a problem hiding this comment.
Is this diagram needed or would the pattern described in line 43 be enough?
There was a problem hiding this comment.
Agreed, the pattern block says it. Diagram removed.
|
I'm closing #716 in favour of this one, so there's a single spec for the suite. Some of what #716 carried maps onto your open issues, and it's yours to take or leave:
Say which of these you want and I'll open a PR against your branch, or just lift the text. One factual note, not an argument: the four implementations above currently ship |
Our spec PR libp2p/specs#716 was closed on 2026-09-18 in favour of royzah's libp2p/specs#727, which is now the single Working Draft for Noise_XXhfs_25519+MLKEM768_ChaChaPoly_SHA256. The README's work table cites #727 and records #716 as closed, with the closed text still readable on the fork branch it was opened from. Also states /noise-mlkem768-hfs/0.2.0 as what the implementations ship rather than as a spec-endorsed identifier: #727 writes 0.1.0 and lists the identifier string as its first open issue.
libp2p/specs#716, this author's own draft, was closed on 2026-09-18 in favour of royzah's libp2p/specs#727, now the single Working Draft for Noise_XXhfs_25519+MLKEM768_ChaChaPoly_SHA256. NOISE_HFS_SPEC.md cites #727 for the raw ML-KEM-768 pattern and records #716 as closed. Both docs now state /noise-mlkem768-hfs/0.2.0 as what this implementation ships rather than as a spec-endorsed identifier: #727 writes 0.1.0 and lists the identifier string as its first open issue.
libp2p/specs#727 (Stage 1A Working Draft, by royzah) is the single spec for Noise_XXhfs_25519+MLKEM768_ChaChaPoly_SHA256. The separate draft libp2p/specs#716 by this author was closed on 2026-09-18 in its favour; NOISE_HFS_SPEC.md records that and links the closed text. Both docs now state /noise-mlkem768-hfs/0.2.0 as what this implementation ships rather than as a spec-endorsed identifier: iftech#727 writes 0.1.0 and lists the identifier string as its first open issue.
libp2p/specs#727 (Stage 1A Working Draft, by royzah) is the spec for Noise_XXhfs_25519+MLKEM768_ChaChaPoly_SHA256. The kem.py docstring cited the tracking issue libp2p/specs#723 in a way that read as if the spec endorsed /noise-mlkem768-hfs/0.2.0; it does not. libp2p#727 writes 0.1.0 and lists the identifier string as its first open issue, so both docstrings now state 0.2.0 as what this implementation ships. No behaviour change.
Comments only, no code change. libp2p/specs#727 (Stage 1A Working Draft, by royzah) is the spec for Noise_XXhfs_25519+MLKEM768_ChaChaPoly_SHA256. The HFS/STALE_HFS constants now carry a note that /noise-mlkem768-hfs/0.2.0 is what this branch ships rather than a spec-endorsed id: libp2p#727 writes 0.1.0 and lists the identifier string as its first open issue.
…p#727 libp2p#716 was closed on 2026-09-18 in favour of libp2p#727 by royzah, so one document rather than two carries the proposal for Noise_XXhfs_25519+MLKEM768_ChaChaPoly_SHA256. This text is kept for reference; the material it carries was offered to libp2p#727.
|
One thing I would like to propose for the draft, with a demonstration behind it rather than an argument. The downgrade is real and silent, and I can now show it rather than assert it. Two js-libp2p nodes, both configured with the hybrid encrypter first and Nothing detects it afterwards, which is the part worth knowing. I checked four channels: Suggested addition to the draft, in security considerations: an implementation SHOULD provide a mode in which Two things I am deliberately not proposing here. Binding the negotiation transcript into the Noise prologue does not work, because a successful downgrade runs classical Happy to write the security-considerations text if you want it. |
|
You're right, I checked. I'll put a runtime knob on Security considerations text: yes please, write it. The SHOULD reads right, and https://github.com/paschal533/pq-noise-artifacts/tree/main/experiments/downgrade-demo is why. Agree on dropping the prologue binding too, a downgrade never reaches that code. On the id, I'm on 0.1.0 and you're on 0.2.0. I'll follow whatever the spec lands on. |
multistream-select picks the connection encrypter in plaintext and is not authenticated, so an on-path attacker can strip this protocol and leave both peers on /noise. Both then complete a valid, mutually authenticated classical session and neither can tell, which the linked harness shows against two real libp2p nodes rather than arguing. Adds a SHOULD that an implementation provide a mode in which /noise is neither offered nor accepted, so an operator who needs the post-quantum property can choose connection failure over a silent downgrade. That is the only mitigation a deployment has today, and implementations differ on whether it is reachable at all, so it is worth stating normatively. Says plainly that this cannot be fixed inside this handshake: a successful downgrade runs /noise and never reaches the code specified here, so binding the negotiation into this suite's prologue or handshake hash does nothing. An earlier suggestion of mine to do that is withdrawn. Notes that the general fix, an identity signature covering the handshake hash and the offered protocol set, is a change to /noise itself and out of scope for this document.
The two variants were selectable on both encrypters, which made one of the four combinations unusable: the identity variant on /noise. It widens what identity_sig covers, and /noise is an identifier every libp2p implementation already answers to, so such a peer negotiates /noise successfully and then fails signature verification against all of them, in any mode. Offering it there is offering a network partition behind a flag. The extension variant stays on /noise, since it is the only one that can ship incrementally: an older peer ignores the unknown fields. The identity variant is refused at construction there, which turns that partition into a configuration error the operator sees at once rather than a fleet of handshake failures. On the hybrid suite the identity variant is available and moves the protocol identifier, /noise-mlkem768-hfs/0.3.0 rather than 0.2.0. The identifier is the mechanism, not bookkeeping: a peer verifying a different message must not answer to the identifier used by peers that do not, or multistream-select pairs them and the failure surfaces as a signature error instead of as no common protocol. 0.3.0 is derived from the 0.2.0 this implementation ships and follows whatever libp2p/specs#727 settles on for the base. Tests follow the split rather than looping both variants over one encrypter: extension handshakes on /noise, construction refusal for identity there, and the identity variant's honest handshake, downgrade refusal and false-positive control on the hybrid encrypter, including that it advertises the separate identifier only when the binding is on.
Mirrors the split made on the TypeScript side. The two variants were selectable on both transports, which made one of the four combinations unusable: the identity variant on /noise. It widens what identity_sig covers, and /noise is an identifier every libp2p implementation already answers to, so such a peer negotiates /noise successfully and then fails signature verification against all of them, in any mode. Offering it there is offering a network partition behind a flag, so Transport now refuses it at construction and says where it is available instead. TransportPQ gains IDENTITY_BOUND_PROTOCOL_ID, /noise-mlkem768-hfs/0.3.0, and a protocol_id attribute derived from the configuration by the new protocol_id_for(). The identifier is the mechanism rather than bookkeeping: a peer verifying a different message must not answer to the identifier used by peers that do not, or multistream-select pairs them and the difference surfaces as a signature failure instead of as no protocol in common. Because a caller builds TranscriptBindingConfig here rather than the transport deriving it, the two can disagree. The downgrade check compares the negotiated protocol against actual_protocol, so a config naming an identifier this transport does not advertise would compare against the wrong thing and either miss a downgrade or invent one. TransportPQ now rejects that at construction. 0.3.0 derives from the 0.2.0 this implementation ships and follows whatever libp2p/specs#727 settles on for the base identifier.
|
@royzah the security considerations text is up as a PR against your branch rather than pasted here, so it is a diff you can take or rewrite: royzah#1. One section before Open issues plus one link definition. It carries the SHOULD, says plainly that this cannot be fixed inside the handshake, and notes the general fix belongs to |
|
Merged, thanks. Took it whole: I only changed the rust-libp2p sentence, since the knob landed in between. It now says every implementation has the mode, with |
Concrete draft for #723, so we have something byte-level to react to instead of an open question.
Additive, separately negotiated handshake that mixes ML-KEM-768 into the
XXpattern via Noise HFS. Advertised ahead of/noise, falls back with no flag day. Auth stays classical (identity key signs the static key), only secrecy is hybridized.Pins the protocol id and suite, defers token/KDF order to Noise HFS. Still open (called out in the doc):
Reference impl: libp2p/rust-libp2p#6481 (off-by-default
mlkem-hfs). Would love go/js eyes early. cc @jxs @thomaseizinger @mxinden @MarcoPolo @achingbrain