unisat

ADR-003: Dual-slot HMAC key store with CRC + monotonic generation

Status: Accepted — 2026-04-17 Phase: 2 (Replay protection + secure key store) Commit: 508aba7

Context

The command dispatcher (HMAC-authenticated uplink) at 99774cf (Track 1b) held the pre-shared key in a RAM variable populated at runtime. This left two unresolved questions:

  1. Where does the key come from on a cold boot? If nothing pre-populates the variable, the satellite refuses every uplink — including legitimate traffic. The satellite needs a durable key that survives power cycles.
  2. How is a key rotated in flight? If an operator detects a key compromise (ground station intrusion, captured HF trace), the recovery procedure must replace the key without physical access to the spacecraft.

Storing a single key in a dedicated flash sector (straight-line design) solves (1) but not (2) safely: a power-loss during the erase-program cycle of a rotation leaves half-erased bytes and no usable key at the next boot — the satellite bricks itself.

Decision

A/B dual-slot persistent store with monotonic generation counter, CRC-protected records, and “write-to-inactive-then-switch” rotation semantics.

+--------- slot A ----------+   +--------- slot B ----------+
| magic | gen | key | CRC32 |   | magic | gen | key | CRC32 |
+---------------------------+   +---------------------------+
     41 B                           41 B

Boot-time rule: pick the slot with the highest generation whose magic marker and CRC both validate. Rotation rule: write new record to the currently-INACTIVE slot, verify by read-back, then accept. If a power-loss happens mid-erase of the inactive slot, the active slot with the previous generation is still valid and next boot uses it — graceful degradation.

Rationale

Consequences

Positive:

Negative:

Alternatives considered

Implementation

See firmware/stm32/Core/Src/key_store.c + tests in firmware/tests/test_key_store.c (10/10) + firmware/tests/test_boot_security.c (4/4).

Platform hooks (key_store_platform_{read,write,erase}) are weak, defaulting to in-RAM for host tests; a target build overrides them with HAL_FLASHEx_Erase + HAL_FLASH_Program.