Track: 1 (AX.25 Link Layer) Updated: 2026-04-17 β T2 replay protection closed (Phase 2).
Vector: an attacker transmits a crafted AX.25 UI frame carrying a syntactically valid CCSDS command packet.
Mitigation β primitives available (Track 1b partial):
firmware/stm32/Drivers/Crypto/hmac_sha256.c
(+ Python mirror at ground-station/utils/hmac_auth.py). RFC 4231
test vectors asserted on both sides so a ground-computed tag equals
the satellite-computed tag bit-for-bit.hmac_sha256_verify) to defeat
timing side-channels.Mitigation β wired (Track 1b complete + Phase 2 replay):
firmware/stm32/Core/Src/command_dispatcher.c provides the strong
CCSDS_Dispatcher_Submit symbol that overrides the weak no-op in
comm.c. Every frame emitted by the AX.25 streaming decoder is now:
counter || body with the pre-shared key.hmac_sha256_verify).Unit tests (firmware/tests/test_command_dispatcher.c, 11/11 green):
accepted +1.rejected_bad_tag +1.rejected_too_short +1.Residual risk: the pre-shared key is currently held in g_key[]
RAM installed at boot. Persistent key storage in a dedicated flash
sector with CRC-protected rotation is the remaining Phase 2 item
(tracked in docs/project/GAPS_AND_ROADMAP.md as S-SEC-KEYSTORE).
Vector: capture a legitimate command off-air and retransmit it later.
Mitigation (wired, Phase 2): every authenticated frame carries a
32-bit monotonic counter (big-endian, prepended to the authenticated
span so HMAC covers counter || body). The dispatcher maintains:
g_high_counter β highest counter ever accepted (init 0).g_window β 64-bit bitmap; bit i = βcounter
g_high_counter β i already acceptedβ.g_window_valid β cleared on every rekey or explicit reset.Acceptance rules (see replay_window_check_and_update):
| Condition | Result |
|---|---|
counter == 0 |
reject (reserved sentinel) |
| First frame for this key epoch | accept, init window |
counter > g_high_counter |
accept, shift window up |
counter <= g_high_counter β 64 |
reject (outside window) |
counter already has its bit set in window |
reject (duplicate) |
counter inside window, bit unset |
accept, set its bit |
Rekeying (CommandDispatcher_SetKey) and ResetReplayWindow() both
clear the window so a ground-operator key rotation implicitly starts
a fresh counter epoch. CommandDispatcher_GetStats() exports
rejected_replay and highest_counter for downlink monitoring.
Unit-test coverage (firmware/tests/test_command_dispatcher.c):
ResetReplayWindow() β same semantics as rekey for counter state.Residual risk: an attacker who replays a frame within the same key epoch and before the legitimate operator transmits it once still gets one acceptance. This is inherent to any freshness scheme that relies on counters rather than synchronised clocks. Mitigated at the operational level by ground-side counter tracking (every ground TX increments a local counter and records the last-acked value; a gap triggers an anomaly alert). A true clock-based freshness gate requires a reliable RTC β open for Phase 3 FDIR work.
Vector: transmit bytes crafted to cause worst-case stuffing expansion, inflating decoder CPU cost per byte received.
Mitigation: REQ-AX25-012 β the decoder hard-rejects frames > 400 B
(AX25_MAX_FRAME_BYTES) at every stage (flag scanner, unstuffer,
parser). Recovery is O(1) per error (REQ-AX25-024: reset to HUNT,
offending byte not reprocessed).
The per-byte decode cost is bounded by constant work (shift-register update + 5-ones check). Throughput budget (Β§4.11) shows 0.6 % CPU at 9600 bps even at maximum frame rate.
Vector: jam the RF band with random bytes to exhaust the decoder.
Mitigation: REQ-AX25-014 β the decoder never crashes or leaks state on arbitrary garbage. This is fuzz-tested with 10 000 random iterations (C, deterministic LCG) + 500 hypothesis cases (Python). Beacon TX runs on its own cadence independent of RX activity.
Vector: a subtle mismatch between the C and Python implementations allows an attacker to craft a frame accepted by one but rejected by the other, bypassing ground-side validation.
Mitigation: 28 shared golden vectors (REQ-AX25-015); both implementations assert bit-identical output against the same fixtures. CI runs the check on every commit touching the library.