Link security constraints

The supported design is USB provisioning plus a mutually authenticated session. The threat model covers passive/active RF attackers and a lost device. There is no flash encryption or secure boot: possession of a cat exposes that pair's master; possession of the tower exposes every pair. This physical-access risk is accepted; re-provisioning is the recovery procedure.

Each pair has a provisioned master; fresh session nonces derive a RAM-only LMK. A fresh key on radio bring-up avoids relying on CCMP packet counters surviving a radio reset. This symmetric derivation does not provide forward secrecy after a master is disclosed. The set's PMK wraps peer keys and is not a substitute for pair authentication.

The byte contract, transcript binding and constant-time verification live in session_kdf.rs and link.rs. Preserve these properties:

  • Authenticate both directions and the complete transcript, including the protocol version, identities, role and both nonces, plus every other field a handshake frame carries. Failed authentication must not establish trust.
  • A new session replaces an established one only once its confirm verifies. A replayed Hello earns an answer nobody can confirm; the live session never notices.
  • A confirmed session routes nothing until its key is on the cat's peer entry (TowerSessions::on_keyed, reported by the task that installed it), and a key that cannot be handed to that task drops the session instead of leaving one that is routed for without a key. On a freshly booted Coordinator the MAC has no peer entry at all, so a unicast from it in that window is plaintext.
  • Only the handshake travels broadcast. A broadcast is never encrypted, so input and session frames that arrive broadcast are dropped whatever MAC they claim.
  • An unusable provisioned record leaves a cat silent and the tower refusing sessions. Never recover through a default key, plaintext input or legacy-key fallback.
  • Draw entropy only while the radio provides a live TRNG source. Do not hold a TRNG handle across radio teardown or introduce an ADC-owning source that conflicts with battery monitoring.
  • Do not log secrets; preserve redacted record formatting and key zeroization.

Wire tag 4 (WireFrame::Pairing) is reserved: an unauthenticated X25519 ceremony once held it and was never shipped. Wireless pairing, if it comes, needs an authenticated exchange (bare ECDH permits MITM), deliberate local intent on both devices, fresh entropy, verified persistent writes before success, no downgrade fallback, and a new tag. A boot-time cat gesture is required; injected runtime keys must not arm pairing.

Unresolved RF risk: plaintext unicast under a cat's MAC

Session frames are authenticated only by the radio's encryption: the tower accepts a Resync or a key frame because it arrived unicast from an established, keyed cat's MAC and names that session's id, and the id is public (it rides the plaintext HelloAck). The cat's side is the same shape: once its key is installed it acts on any unicast from the tower's MAC (cat_rules::accept_source), so the spoof would drag it off-channel or poison its acks rather than inject keys. If the radio delivers a plaintext unicast frame that spoofs an encrypted peer's MAC, a Resync{next_seq + 2^30} followed by a KeyPress is keystroke injection, not merely a disturbed control frame. Whether ESP-NOW delivers such a frame is unverified: test it with a third S3 while a session is up. If it is delivered, add a truncated per-frame tag under a third KDF output. The broadcast form of the same spoof is dropped by the rule above.

Independent test vectors

python3 tools/protocol/test_session_kdf.py recomputes every known answer of session_kdf.rs with a longhand HKDF/HMAC over Python's standard library, checked against the RFC vectors. It reads the Rust test fixture and expected arrays, and states the labels, salts and layout itself, so a change on either side fails. Never copy Rust output in as its own oracle.