Milestone 9a: classical session rekey (~120s rotation) + epoch handling (#9) #90

Merged
vxfemboy merged 13 commits from feat/rekey-9a-session-rotation into main 2026-07-22 04:51:55 +00:00
vxfemboy commented 2026-07-19 06:10:19 +00:00 (Migrated from github.com)

Milestone 9a — classical session rekey (~120s) + epoch handling

Closes the rekey half of #9 (session rekey + PQ-hybrid handshake). This is 9a (classical rekey); 9b (PQ-hybrid, McEliece + ML-KEM → PSK) is the sequel, built on this machinery.

Each peer's Noise-IK session now rotates ~every 120s for forward secrecy, with an old→new epoch overlap so in-flight packets keep decrypting across the switch. No wire-format change; yip-crypto and yip-wire are untouched.

What's here (bin/yipd)

  • EpochSet (epoch.rs) — a pure state machine holding current (outbound), next (the responder's derived-but-unconfirmed new epoch, keyed by the initiator's Noise ephemeral), and previous (receive-only grace). inbound_open tries current→next→previous and promotes next→current on the first frame that decrypts under it (the responder's confirmed-switch). Steady state is a single decrypt attempt — behaviorally identical to before.
  • PeerState::Established(Box<EpochSet>) — inbound routes through inbound_open, outbound through current.
  • Scheduling (PeerManager::tick) — the glare-winner (existing local_pub < peer.pubkey tiebreak) starts a fresh handshake at REKEY_INTERVAL_MS (120_000; override via YIP_REKEY_INTERVAL_MS), one-in-flight, loser-fallback at 2×interval, retransmit/give-up. The live session keeps flowing throughout — rekey failure is a no-op.
  • Completion — initiator switches outbound immediately on the Resp (WireGuard confirmed-switch); responder installs next and switches only when it receives a frame under it. Survives a lost msg2 with no black-hole.
  • conn_tag rotates per epoch (derived from each session's keys) — the linkability rotation, for free.

Scope & fixes worth calling out

  • Relay peers are gated out of rekey scheduling. Relay-path rekey completion is unwired, and scheduling it would churn a never-completing ~1 Hz handshake (a constant-cadence DPI fingerprint). Direct/punched peers rotate; relay-only sessions don't yet — a documented follow-up.
  • Idempotent responder rekey (final-review Critical fix). The responder recognizes a retransmitted Init by its cleartext Noise ephemeral and resends the cached Resp without minting a new session — so a retransmitted Init before the first Resp (RTT > retry, or Resp reordering) can no longer make the two peers commit to different sessions and black-hole the rotation. Two end-to-end tests reproduce and close the scenario.
  • The EpochInbound::Send path carries full EgressDatagrams (preserving dst+fate) — a fix for a relay-peer misdirection found in review.

Testing

  • Unit: the full EpochSet switch state machine (promote/retire/lost-msg2-no-blackhole/guards), rekey scheduling (winner/one-in-flight/loser-fallback/retransmit/abandon), completion (initiator promote / responder install-next / idempotent retransmit / cold-start dedup), all with real Noise-IK crypto + AEAD, not stubs.
  • netns money test (run-netns-rekey.sh, ran live under sudo on both the poll and YIP_USE_URING=1 drivers): 0% packet loss across ~10 fast rotations, and a rekey_epoch_witness tool proves 10/10 distinct rekey rounds on the wire (via distinct cleartext Noise ephemerals — the naive conn_tag-byte check is vacuous under per-packet header masking). Wired into CI.
  • 217 unit + 27 netns tests green; clippy -D warnings clean.

Follow-ups (non-blocking)

  • Relay-path rekey completion — wire relayed_handshake_init/respinstall_next/promote_from_rekey so relay-only sessions also rotate (deferred to avoid the churn above).
  • Rekey-liveness hardening — a spoofed/duplicate [HandshakeResp] can abandon an in-flight rekey (delaying rotation, not breaking the tunnel); rides with #34 (authenticated endpoint).
  • Anti-DPI — a mid-session rekey Init skips the cold-start junk-burst; benign today (obf pads uniformly), file if a future oracle probes mid-session rekey.
  • 9b — PQ-hybrid handshake over this rekey path.

Final whole-branch review (opus): the one Critical (rotation black-hole under Init-retransmit/Resp-reorder) was fixed and re-reviewed Approved; fail-closed throughout (a rekey never disturbs a live session), no wire/crypto change, forbid-unsafe / no as / no bare #[allow].

## Milestone 9a — classical session rekey (~120s) + epoch handling Closes the rekey half of **#9** (session rekey + PQ-hybrid handshake). This is **9a (classical rekey)**; **9b (PQ-hybrid, McEliece + ML-KEM → PSK)** is the sequel, built on this machinery. Each peer's Noise-IK session now rotates ~every 120s for **forward secrecy**, with an old→new epoch overlap so in-flight packets keep decrypting across the switch. No wire-format change; `yip-crypto` and `yip-wire` are untouched. ### What's here (`bin/yipd`) - **`EpochSet`** (`epoch.rs`) — a pure state machine holding `current` (outbound), `next` (the responder's derived-but-unconfirmed new epoch, keyed by the initiator's Noise ephemeral), and `previous` (receive-only grace). `inbound_open` tries current→next→previous and promotes `next`→current on the first frame that decrypts under it (the responder's confirmed-switch). Steady state is a single decrypt attempt — behaviorally identical to before. - **`PeerState::Established(Box<EpochSet>)`** — inbound routes through `inbound_open`, outbound through `current`. - **Scheduling** (`PeerManager::tick`) — the glare-winner (existing `local_pub < peer.pubkey` tiebreak) starts a fresh handshake at `REKEY_INTERVAL_MS` (120_000; override via `YIP_REKEY_INTERVAL_MS`), one-in-flight, loser-fallback at 2×interval, retransmit/give-up. The live session keeps flowing throughout — **rekey failure is a no-op**. - **Completion** — initiator switches outbound immediately on the Resp (WireGuard confirmed-switch); responder installs `next` and switches only when it receives a frame under it. Survives a lost msg2 with no black-hole. - **`conn_tag` rotates per epoch** (derived from each session's keys) — the linkability rotation, for free. ### Scope & fixes worth calling out - **Relay peers are gated out of rekey scheduling.** Relay-path rekey completion is unwired, and scheduling it would churn a never-completing ~1 Hz handshake (a constant-cadence DPI fingerprint). Direct/punched peers rotate; relay-only sessions don't yet — a documented follow-up. - **Idempotent responder rekey** (final-review Critical fix). The responder recognizes a retransmitted Init by its cleartext Noise ephemeral and resends the cached Resp *without minting a new session* — so a retransmitted Init before the first Resp (RTT > retry, or Resp reordering) can no longer make the two peers commit to different sessions and black-hole the rotation. Two end-to-end tests reproduce and close the scenario. - The `EpochInbound::Send` path carries full `EgressDatagram`s (preserving `dst`+`fate`) — a fix for a relay-peer misdirection found in review. ### Testing - Unit: the full `EpochSet` switch state machine (promote/retire/lost-msg2-no-blackhole/guards), rekey scheduling (winner/one-in-flight/loser-fallback/retransmit/abandon), completion (initiator promote / responder install-next / idempotent retransmit / cold-start dedup), all with **real Noise-IK crypto + AEAD**, not stubs. - **netns money test** (`run-netns-rekey.sh`, ran live under sudo on both the poll and `YIP_USE_URING=1` drivers): **0% packet loss across ~10 fast rotations**, and a `rekey_epoch_witness` tool proves **10/10 distinct rekey rounds on the wire** (via distinct cleartext Noise ephemerals — the naive `conn_tag`-byte check is vacuous under per-packet header masking). Wired into CI. - 217 unit + 27 netns tests green; clippy `-D warnings` clean. ### Follow-ups (non-blocking) - **Relay-path rekey completion** — wire `relayed_handshake_init`/`resp` → `install_next`/`promote_from_rekey` so relay-only sessions also rotate (deferred to avoid the churn above). - **Rekey-liveness hardening** — a spoofed/duplicate `[HandshakeResp]` can abandon an in-flight rekey (delaying rotation, not breaking the tunnel); rides with **#34** (authenticated endpoint). - **Anti-DPI** — a mid-session rekey Init skips the cold-start junk-burst; benign today (obf pads uniformly), file if a future oracle probes mid-session rekey. - **9b** — PQ-hybrid handshake over this rekey path. Final whole-branch review (opus): the one Critical (rotation black-hole under Init-retransmit/Resp-reorder) was fixed and re-reviewed **Approved**; fail-closed throughout (a rekey never disturbs a live session), no wire/crypto change, `forbid-unsafe` / no `as` / no bare `#[allow]`.
Sign in to join this conversation.
No description provided.