unisat

ADR-008: CCSDS-agnostic command-dispatcher wire format

Status: Accepted — 2026-04-17 Phase: 1b + 2 Commits: 99774cf (original) → 942abce (counter added)

Context

The command dispatcher receives frames from the AX.25 streaming decoder and has to decide: authentic? fresh? act on it?

Two designs competed:

  1. Consume a parsed CCSDS_Packet_t — the decoder calls CCSDS_Parse(bytes, len, &pkt) first, and the dispatcher operates on the parsed struct (APID, secondary header timestamp, data[]).
  2. Consume raw bytes, treat the payload as opaque — the dispatcher only knows “here are N bytes, verify + extract counter + forward to handler”, zero CCSDS awareness.

Decision

Option 2. Dispatcher wire format is:

[ 4-byte counter (BE) ][ opaque body ][ 32-byte HMAC tag ]
  \_________ authenticated ________/

The dispatcher neither parses nor validates CCSDS fields. The registered CommandHandler_t receives the “body” bytes and is free to interpret them as CCSDS (or anything else) at its own pace.

Rationale

Consequences

Positive:

Negative:

Alternatives considered

Implementation

void CCSDS_Dispatcher_Submit(const uint8_t *data, uint16_t len) {
    /* 1. length >= 4 counter + 1 body + 32 tag = 37 */
    /* 2. recompute HMAC over data[0..len-32], compare to data[len-32..] */
    /* 3. extract counter from data[0..4] BE */
    /* 4. replay-window check */
    /* 5. forward data[4..len-32] to handler */
}

Source: firmware/stm32/Core/Src/command_dispatcher.c + tests firmware/tests/test_command_dispatcher.c (11/11) + integration firmware/tests/test_boot_security.c (4/4) + Python mirror ground-station/utils/hmac_auth.py with test_hmac_auth.py (22/22).