transport: ML-KEM-768 hybrid prelude and onionwire:v2 invites #183
Loading…
Reference in a new issue
No description provided.
Delete branch "wt/t_8bb448b5"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Every OnionWire session can now carry an ML-KEM-768 (FIPS 203) secret alongside X25519.
New install key: ML-KEM-768 keypair. The 64-byte decapsulation seed lives in the SQLCipher store beside
identity_sk; the 1184-byte encapsulation key travels in a v2 invite askem=.onionwire:v2invites:kem=<base64url-unpadded 1184-byte pk>(~1.9 KB, cap stays 4096). Signature domain isonionwire-invite-v2 || 0x00 || k || 0x00 || o || 0x00 || spk || 0x00 || kem. Decode still acceptsonionwire:v1(nokem) and yields a classical friend; encode emits v2 only.Wire: frame
version = 2is the PQ prelude, bodyuint16 ct_len(BE) + exactly 1088 bytes of ML-KEM ciphertext. The initiator sends one prelude, then Noise msg 1 withversion = 1.Prologue when a prelude is present is exactly:
onionwire-v2 || 0x00 || ct (1088) || 0x00 || ss_kem (32)(
onionwire-v2is 12 ASCII bytes; the classical prologue staysonionwire-v1, also 12 bytes — the old PROTOCOL.md said 13, corrected here.)No silent downgrade, decided by the roster row: friend has
kem_pkand the opening is version 1 -> hard failprelude required; friend has nokem_pkand a prelude arrives -> hard failunexpected prelude. Legacy friends are labelledclassicalin the roster.F8rotates the KEM key together with the prekey: hard cut, stored ratchet state dropped, every friend re-pastes the invite.Tests that prove the anti-downgrade / anti-strip properties
invite_v2_kem_strip_fails_signature_and_v1_downgrade_is_rejected(tests/qr.rs) — removingkem=from v2 ismissing kem; relabelling v2 as v1 failsbad signature; substituting the kem bytes failsbad signature.invite_v1_still_decodes_to_a_classical_friend_row(tests/qr.rs).a_prelude_less_session_against_a_kem_friend_is_rejected(src/node.rs) — realhandle_incoming, no application frame accepted.a_classical_opening_to_a_kem_friend_is_refusedanda_prelude_to_a_classical_friend_row_is_refused(tests/kem_transport.rs).tampered_prelude_fails_the_handshake(tests/kem_transport.rs) — one ct byte flipped; responder never completes.hybrid_prologue_has_the_documented_layoutpins the byte layout and length.pq_prelude_session_round_trips_a_real_rat_frame(src/node.rs) — a genuineratframe encoded by the peer's ratchet is ingested end to end after a hybrid handshake.the_outbox_hands_the_kem_key_from_the_friend_row_to_the_dialer(tests/outbox.rs).kem_encaps_decaps_round_trip_through_encode_decode(tests/kem.rs).Verification
cargo test --locked— 37 test targets, exit 0.cargo clippy --locked --all-targets -- -D warnings— clean.cargo test --locked53 unit tests + integration, exit 0;cargo clippy --locked --all-targets -- -D warningsclean;crates/onionwire-sdk/Cargo.lockregenerated and committed for the new dependency.cargo ndk -t arm64-v8a -P 26 check --lockedincrates/onionwire-sdk— exit 0 withml-kemin the graph.Docs in this PR
docs/PROTOCOL.md(keys, framing v2, prologue layouts, no-downgrade rule, invite v2 + legacy accept + cap),docs/THREAT_MODEL.md(what PQ transport buys and does not: epoch 0 is still prekey-derived and classical; ed25519 auth is classical; legacy friends get nothing),README.md(invite size/format, F8, classical label, no-PQ caveat).Honest residual gap
The chat ratchet bootstrap epoch 0 is still derived only from the two long-lived X25519 prekeys, so a future quantum adversary holding a recorded
ratchain and both prekeys can reconstruct the first epoch; later ratchet steps are X25519 too. This PR protects the one-shot Noise transport only. PQ ratchet steps are a follow-up. A restore from a backup also keeps the current install KEM key rather than the backed-up one, so peers re-paste after a restore — same hard-cut semantics as F8, and backups do not carry the KEM seed.