From 5e599af77416480afdb9ff5f8888deb43fb00813 Mon Sep 17 00:00:00 2001 From: SSD DDD Date: Fri, 10 Jul 2026 11:16:04 +0700 Subject: [PATCH] =?UTF-8?q?docs(security):=20RFC=20to=20fix=20the=20mesh?= =?UTF-8?q?=20crypto=20handshake=20(N3-N5)=20=E2=80=94=20for=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Proposal only (no code). The wave audit found the mesh handshake is not authenticated: complete_initiator/responder compute only ee+ss and pass ss in both the es and se slots (real es/se DH never done, no transcript hash, AllowList never consulted), and its own MITM test asserts the attacker decrypts. Plus a constant HELLO MAC key with verify_mac never called, and static keys derived from the public NodeId. This RFC specifies the fix over the existing X25519 + ChaCha20 primitives (Noise-IK-style key agreement with real es/se + transcript hash + allow-list gating; per-link HELLO MAC from the session key with RX-path verification; out-of-band static-key provisioning), a test plan (including rewriting the inverted MITM test to assert failure), and the decisions needed from the owner. Crypto stays hand-written Rust (t27 cannot express crate-based crypto — see #61, #62). Needs sign-off on the approach before implementation. Refs #58. Co-Authored-By: Claude Fable 5 phi^2 + phi^-2 = 3 --- docs/SECURITY_crypto_handshake_RFC.md | 80 +++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 docs/SECURITY_crypto_handshake_RFC.md diff --git a/docs/SECURITY_crypto_handshake_RFC.md b/docs/SECURITY_crypto_handshake_RFC.md new file mode 100644 index 00000000..26ea2c28 --- /dev/null +++ b/docs/SECURITY_crypto_handshake_RFC.md @@ -0,0 +1,80 @@ +# RFC: fix the mesh crypto handshake (N3-N5) — for review before implementation + +Status: PROPOSAL. Not implemented. Crypto stays hand-written Rust (t27 cannot +express it — see #62); this design is for the human-written `src/crypto.rs` / +`src/bin/trios_meshd.rs` and needs sign-off on the approach before coding. +Anchor: phi^2 + phi^-2 = 3. + +## The three defects (from the wave audit, verified in code) + +- **N3 — handshake is not authenticated.** `NoiseXX::complete_initiator` + (`crypto.rs:132`) and `complete_responder` (`:151`) compute only `ee` and + `ss`, then call `combine_dh_shares(&ee, &ss, &ss)` — passing `ss` in BOTH the + `es` and `se` slots. The real `es`/`se` Diffie-Hellman operations are never + performed, and no transcript hash binds ephemeral<->static keys. The + `AllowList` (`:183`) exists but is never consulted inside `complete_*`. The + test `noise_xx_resistant_to_mitm` (`:738`) even asserts the MITM party + DECRYPTS the victim's traffic — encoding the break as expected behaviour. +- **N4 — HELLO metric auth is dead + keyed by a constant.** `HELLO_MAC_KEY` + (`trios_meshd.rs:13`) is a source-embedded constant; `verify_mac`/`is_fresh` + are never called in the daemon RX path, so a neighbour can forge the `heard` + list and game ETX. +- **N5 — static keys are public.** `seed_for(id) = Sha256("trios-mesh/demo/v1/ + node/" || id)` (`trios_meshd.rs:37`) derives each node's static secret from + its public NodeId, so any reader of the open-source daemon recomputes every + node's private key. + +## Proposed design + +### 1. Correct the handshake (Noise-IK-style over the existing primitives) +Keep X25519 + ChaCha20-Poly1305 + the existing `Session`/`ratchet` transport. +Change only key agreement + peer authentication: + +- In `complete_initiator`, compute THREE distinct shares: + `ee = e_i . e_r`, `es = e_i . s_r`, `se = s_i . e_r`; mirror in + `complete_responder` (`es = s_i . e_r`, `se = e_i . s_r` by role). Feed the + real `(ee, es, se)` into `combine_dh_shares` (its signature already expects + three distinct inputs — only the call sites are wrong). +- Mix a running transcript hash `h = H(h || each public key/DH output)` into the + KDF so the session key binds both parties' ephemeral AND static keys. +- Make `complete_*` take `&AllowList` (or the expected peer NodeId->PublicKey) + and REJECT if `peer_static` is not the allow-listed key for the claimed + identity, returning `Result` instead of an + always-succeeding `Session`. +- Rewrite `noise_xx_resistant_to_mitm` to assert the MITM party FAILS to open + the victim's frames (the current assertion is inverted). + +### 2. Wire HELLO authentication (N4) +- Derive the per-link HELLO MAC key from the established session key + (e.g. `HKDF(session_key, "hello-mac")`), not a global constant. Remove + `HELLO_MAC_KEY`. +- In the daemon RX path, before `observe()` feeds a neighbour's `heard` list + into ETX, call `verify_mac` + `is_fresh` and track last-seen seq for replay; + drop on failure. Use a constant-time comparison (`subtle::ConstantTimeEq`). + +### 3. Provision real static keys (N5) +- Load each node's static secret from an out-of-band keystore file (env-pointed, + not world-readable), never `from_seed(NodeId)`. Keep `from_seed` only behind a + `--demo` flag for local loopback. +- Populate the `AllowList` (NodeId -> PublicKey) from the same provisioning, and + gate session establishment on it (step 1). + +## Test plan (must all pass) +- MITM: an attacker substituting its own static key cannot open either party's + frames (rewritten `noise_xx_resistant_to_mitm`). +- Allow-list: a peer whose static key is not allow-listed gets `MeshError::Auth`. +- HELLO forge: a neighbour forging another node's `heard` list is rejected + (bad MAC) and does not move ETX. +- Replay: a re-sent HELLO is rejected by `is_fresh`/seq tracking. +- Regression: the existing 103 tests stay green; add the four above. + +## Formal check (optional, Lane B) +Model the revised handshake in Tamarin (AutoTam, arXiv:2606.19937) to prove +mutual authentication + forward secrecy, rather than asserting it. + +## Decision needed from the owner +1. Noise-IK vs a full Noise-XX (three-message) — IK is one-round-trip and fits + a known-peer mesh; confirm. +2. Keystore format / provisioning mechanism for static keys (file? per-node + config? an existing provisioning path?). +3. Whether to gate this behind a feature flag during rollout.