/* * Beacon payload wire format - shared by main.c's TX encode (send_beacon) * and beacon_rx.c's RX decode, so both sides agree on layout from one * place. A fixed timing prefix followed by zero or more 4 B packed * key/value entries - the extensible mechanism for everything added * after this milestone (table ids today, whatever flags/versions come * later) without a format change each time. A key this firmware doesn't * recognize is skipped, not an error - that's what makes it safe to add * new keys incrementally across a mixed-firmware deployment. * * Fixed prefix: * interval_ms 3 B ms until the next beacon (today: just the fixed * BEACON_CADENCE_MS; no schedule drift modeling * yet). * timestamp_ms 5 B ms since BEACON_EPOCH_UNIX_MS - a ~34-year span, * long enough that rollover is outside a device's * life. 0 (not "unsynced" per se - just the epoch * itself) until central's own wall clock has * synced (time_ctx.h). * * KV entries, 4 B each, BE: top 12 bits key, bottom 20 bits value * (BEACON_KV_KEY_BITS/BEACON_KV_VALUE_MASK). Key 0x000 is reserved - never * assigned, so a stray all-zero word (padding, a truncated read) can't be * mistaken for a real entry. Keys currently defined: * 0x001 current pubkey table (see pubkey_table.h) this node trusts. * Omitted entirely (not sent with a sentinel value) until table * load/provisioning exists - an absent key means "no claim", * which is the point of this being a KV scheme rather than * fixed fields. * 0x002 an older table_id still safe to validate against (no revoked * entries) - advisory, so a node running behind 0x001 still * trusts peers it already has rather than treating itself as * untrusted. May repeat (multiple 0x002 entries = multiple * still-safe fallbacks). * * After the KV region: BEACON_SIG_LEN bytes, central's signature (via * ident_sign() in main.c) over everything before it (the fixed prefix + * KV region, i.e. beacon_frame_encode()'s own output) - not over the * frame header or the group MAC trailer those ride outside this payload * entirely. beacon_frame_decode() never sees these bytes; the caller * strips them first (see beacon_rx.c). */ #ifndef BEACON_FRAME_H_ #define BEACON_FRAME_H_ #include #include #include "frame/frame.h" #include "crypto/group_mac.h" #include "crypto/ident.h" /* Scheduling cadence - also the basis of the per-source uncertainty * beacon_rx.c offers time_ctx.h for beacon-derived time (half the * cadence; see project notes on why that's a reasonable starting bound * - beacon intervals are necessarily orders of magnitude longer than * whole-network propagation time). Moved here from main.c since it's * as much a protocol constant (every Beacon's own interval_ms field) * as a scheduling choice. */ #define BEACON_CADENCE_MS 30000 /* 2026-01-01T00:00:00Z, the epoch timestamp_ms counts from. */ #define BEACON_EPOCH_UNIX_MS 1767225600000ULL #define BEACON_PAYLOAD_FIXED_LEN 8 #define BEACON_KV_LEN 4 #define BEACON_KV_KEY_BITS 12 #define BEACON_KV_VALUE_BITS 20 #define BEACON_KV_VALUE_MASK ((1UL << BEACON_KV_VALUE_BITS) - 1) #define BEACON_KEY_RESERVED 0x000 #define BEACON_KEY_TABLE_CURRENT 0x001 #define BEACON_KEY_TABLE_ALT 0x002 /* * Signature trailer length - appended on the wire after the KV region, * same as pubkey_table.h's own trailing-signature layout. This file * only knows the length (so BEACON_MAX_KV_PAIRS/BEACON_PAYLOAD_MAX_LEN * below size correctly); it never touches key material or calls * ident_sign()/ident_verify() itself - that's main.c (TX, central only) * and beacon_rx.c (RX)'s job. beacon_frame_encode()/beacon_frame_decode() * only ever see the prefix+KV region, never the signature bytes - the * caller is responsible for appending (TX) or stripping (RX) exactly * BEACON_SIG_LEN bytes around those calls. */ #define BEACON_SIG_LEN IDENT_SIG_LEN /* Largest number of KV pairs a Beacon can carry within FRAME_MAX_LEN, * alongside the signature trailer - derived, not hand-picked, so it * tracks FRAME_MAX_LEN/BEACON_SIG_LEN if either ever changes. */ #define BEACON_MAX_KV_PAIRS \ ((FRAME_MAX_LEN - FRAME_HDR_LEN - GROUP_MAC_LEN - BEACON_PAYLOAD_FIXED_LEN - BEACON_SIG_LEN) \ / BEACON_KV_LEN) /* Largest full wire payload (prefix + KV + signature trailer) - what a * caller's buffer needs to hold end to end. beacon_frame_encode() itself * only fills/returns the prefix+KV prefix of this. */ #define BEACON_PAYLOAD_MAX_LEN \ (BEACON_PAYLOAD_FIXED_LEN + BEACON_MAX_KV_PAIRS * BEACON_KV_LEN + BEACON_SIG_LEN) struct beacon_frame { uint32_t interval_ms; uint64_t timestamp_ms; uint16_t kv_keys[BEACON_MAX_KV_PAIRS]; uint32_t kv_values[BEACON_MAX_KV_PAIRS]; size_t kv_count; }; /* * Encodes into buf (caller-sized >= BEACON_PAYLOAD_MAX_LEN). kv_count must * not exceed BEACON_MAX_KV_PAIRS - callers building from a fixed-size * source array (e.g. main.c's alt_table_ids) already can't exceed it, so * this asserts rather than returning an error. * Returns the encoded payload length. */ size_t beacon_frame_encode(uint8_t *buf, uint32_t interval_ms, uint64_t timestamp_ms, const uint16_t *kv_keys, const uint32_t *kv_values, size_t kv_count); /* * Decodes payload (the frame's payload region, as frame_decode() returns * it) into out. Returns 0 on success, -EINVAL if payload_len is too short * for the fixed prefix, if the trailing KV region isn't a whole number of * 4 B entries, or if it holds more entries than out->kv_keys/kv_values can * hold. Every KV entry is stored regardless of key - filtering by key * (e.g. "only care about table ids") is the caller's job, not this * function's. */ int beacon_frame_decode(const uint8_t *payload, size_t payload_len, struct beacon_frame *out); #endif /* BEACON_FRAME_H_ */