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
Helloearns 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.