unisat

ADR-004: Reserve counter = 0 as uninitialised sentinel in replay window

Status: Accepted — 2026-04-17 Phase: 2 (Anti-replay) Commit: 942abce

Context

The command dispatcher gained a 32-bit monotonic counter + 64-bit sliding-window bitmap to reject replayed uplink commands. Two design choices around counter semantics bit us during test:

  1. On cold boot, g_high_counter is 0 and the bitmap is empty. A prerecorded frame with counter = 0 would walk the “first of epoch” branch and be accepted — replayable indefinitely across reboots.
  2. If 0 is a valid counter, the ground side must carefully track its first transmission; off-by-one here produces either a reject or a replay-window misalignment.

Decision

Counter value 0 is reserved. The firmware rejects every frame with counter == 0 regardless of HMAC validity; the ground library (ground-station/utils/hmac_auth.py) raises ReplayCounterError when a caller tries to build a frame with counter <= 0.

Senders start at counter = 1 and increment monotonically.

Rationale

Consequences

Positive:

Negative:

Implementation

Firmware (firmware/stm32/Core/Src/command_dispatcher.c):

if (counter == 0U) {
    return false;        /* reserved sentinel */
}

Ground (ground-station/utils/hmac_auth.py):

if counter <= 0 or counter > 0xFFFFFFFF:
    raise ReplayCounterError(...)

Both paths are exercised by dedicated tests: