Skip to content

Crypto

spec v0.5.0
  • Confidentiality: trap status and command traffic is not readable to a passive observer.
  • Integrity: a frame cannot be modified in flight without detection.
  • Authenticity: a frame’s claimed source is genuine to the level of “possesses the group key.”
  • Replay protection: a captured frame cannot be replayed later.
  • Bounded blast radius: compromise of a single node does not silently compromise the rest of the network forever.
  • AES-128-CCM with a 7-byte nonce, 4-byte MIC, and the frame header as AAD.
  • Hardware-accelerated on both nRF52840 (CryptoCell) and ESP32-S3 (AES peripheral).
  • A single primitive across all roles. No custom crypto.
  • 128-bit symmetric key.
  • Shared by every node in a deployment (or “group” in cross-deployment scenarios).
  • Used to encrypt and authenticate all normal traffic.
  • Provisioned via the deployment wizard over BLE (or USB during initial flash).
  • Rotated periodically (see Key rollover).
  • 128-bit symmetric key.
  • Held only by HUB and TECH nodes.
  • Used to sign (HMAC-style, or by re-encrypting under K_admin) privileged COMMAND frames that endpoints / routers will accept.
  • An endpoint only accepts COMMANDs whose K_admin signature is valid.
  • A compromised endpoint cannot mint COMMANDs because it does not have K_admin.

Each deployment has its own {K_group, K_admin} pair. Cross-country deployments are simply different groups with different keys. The hub holds the keys for whichever groups it manages.

The LoRa sync word is also derived per-deployment (e.g. from a hash of K_group) so that traffic from another deployment on the same band is filtered in hardware before reaching the MAC layer, saving power.

For every frame:

ciphertext, MIC = AES-CCM-Encrypt(
key = K_group,
nonce = src(4) || seq(2) || dir(1),
aad = header_clear,
plaintext = type_specific_payload
)
  • dir = 0 for uplink (toward hub), dir = 1 for downlink.
  • Nonce uniqueness is guaranteed by the monotonic seq per source.
  • The receiver rejects any frame where seq ≤ last_seen_seq[src] (replay).

A COMMAND frame from the hub or a TECH node carries an inner K_admin-signed blob:

plaintext_payload = COMMAND_OUTER {
cmd_type,
cmd_payload,
admin_mic = AES-CMAC(K_admin, src||dst||cmd_type||cmd_payload||seq)
}

The whole thing is then encrypted under K_group as a normal frame.

An endpoint receiving a COMMAND verifies:

  1. The outer K_group MIC (normal frame check).
  2. The inner admin_mic using its copy of K_admin.

Only if both pass is the command applied.

  • wake_ble(duration) — turn on BLE for N minutes
  • set_primary_router, set_secondary_router
  • set_check_in_interval
  • factory_reset_confirm (a plain reset doesn’t need K_admin, but a remote one does)
  • key_rollover — see below
  • STATUS_ACK with no payload
  • WHO_ARE_YOU
  • Routine ping / heartbeat

Possessing K_group is not sufficient to command an endpoint. Endpoints additionally enforce:

  • Downlink source must be primary or secondary router for any non-COMMAND traffic.
  • COMMAND frames must additionally pass K_admin verification.
  • An endpoint will silently drop traffic from a router not in its {primary, secondary} set.

Routers enforce:

  • Uplinks from non-child endpoints are dropped. Each router maintains a list of child endpoint IDs, set at provisioning.
  • The exception is HELP frames, which any router will forward (so an orphan endpoint can be adopted).

Key rollover bounds the lifetime of a compromised K_group and provides operational hygiene.

Key rollover is always hub-initiated and can occur at any time. There is no firmware-enforced minimum or maximum interval; the mechanism is deliberately decoupled from any specific schedule so that organisational policy (annual rotation, post-incident rotation, ad-hoc rotation on staff turnover) drives cadence without firmware changes.

Defaults documented elsewhere (e.g. annual) are policy suggestions, not technical constraints.

  • Calendar policy (e.g. annual rotation).
  • Known or suspected compromise of K_group (a node lost in the field for an extended period, theft of a hub, etc.).
  • Staff turnover where an operator with K_admin access has left.
  • Major firmware release boundary.
  1. Hub generates K_group_new.
  2. Hub sends a rotate_key COMMAND (cmd_type 0x06, signed with K_admin) to all routers, carrying K_group_new and an activation epoch T_activate.
  3. Routers begin accepting frames under either K_group or K_group_new, while continuing to TX under K_group until T_activate.
  4. For each endpoint, the hub sets config_pending on the next STATUS_ACK (and rekey_pending advisory bit). The endpoint extends its RX window and receives the rotate_key COMMAND.
  5. The endpoint stores K_group_new alongside K_group. It tries K_group_new first and falls back to K_group on decryption failure. It records last_key_rotation_at on successful application.
  6. After T_activate, routers TX under K_group_new only. Old K_group remains accepted indefinitely (no hard expiry) so orphaned-by-rollover endpoints are not lost — they are simply re-keyed at next maintenance visit.

The hub tracks per-endpoint last_key_rotation_at (returned in every ANNOUNCE) and exposes a “key age” view in the web UI so operators can detect nodes that missed a rollover window.

  • The provisioning wizard always programs the current K_group at the time of provisioning.
  • During the rollover overlap window, the wizard programs K_group_new and the node receives no surprises.

If an endpoint is somehow desynced (held wrong key, old hardware reflashed later), a TECH node can re-key it in person via BLE provisioning.

K_admin rotation is more disruptive than K_group rotation: every endpoint and router needs the new K_admin to validate future privileged commands.

v1 — Option A: physical re-provisioning only (accepted)

Section titled “v1 — Option A: physical re-provisioning only (accepted)”

v1 handles K_admin rotation by physical re-provisioning at maintenance time. Trade-off accepted: K_admin compromise is rare (requires hub theft), and the K_admin / K_field split (see below) limits the blast radius of the more likely scenario (tech-node loss).

Accepted risk: a lost hub exposes K_admin, requiring a field visit to every node in the affected network to re-provision. For a 200-trap deployment, this is roughly a week of field work. Tracked in the risk register as S-08.

TECH nodes no longer carry K_admin. Instead, two keys:

  • K_admin (hub-only): rotate K_group, factory_reset_remote, router-list manipulation, rotate K_admin itself, KEY_ROLLOVER.
  • K_field (hub + TECH nodes): wake_ble, request_announce, set_check_in_interval, orphan adoption, servicing log.

Endpoints verify the appropriate key based on the COMMAND’s cmd_type. The COMMAND frame format does not change — only which key signs which command.

Lost TECH node → rotate K_field (hub pushes to all nodes on next config-pull cycle; no field visit). Lost hub → rotate K_admin (Option A: field visit to every node).

The bootstrap problem: if any command signed with K_admin can rotate K_admin, then an attacker who already holds K_admin can immediately rotate it to a key only they know — permanently locking the legitimate operator out. A naive OTA rotation makes a bad day worse.

The upgrade path from A → B → C is additive, not a rewrite. The COMMAND verification pipeline is designed as a pluggable step: “receive COMMAND → verify auth → apply if valid → ACK.” Swapping the verification step is a localised firmware change; the rest of the pipeline (frame parsing, command dispatch, config persistence, ACK) is untouched.

Option B — “Break-glass” pre-shared K_admin_next (planned for v1.1+)

Section titled “Option B — “Break-glass” pre-shared K_admin_next (planned for v1.1+)”

At provisioning, every node receives both K_admin and a sealed, unused K_admin_next. OTA rotation must be signed by K_admin_next, which an attacker holding only the stolen K_admin does not possess (unless they also extracted a deployed node’s flash).

After one OTA rotation, the K_admin_next slot is spent — the node reverts to physical-only until a field visit installs a fresh K_admin_next.

What changes from Option A:

  • Provisioning writes one extra key to flash (~50 lines of firmware).
  • Rotation verification checks K_admin_next instead of K_admin.
  • No wire format change. No frame envelope change.

Risks (speculative, pending implementation):

  • Extra key on every node increases flash exposure surface.
  • “Spent” state must be tracked and visible to the operator; stale K_admin_next values are a silent vulnerability.
  • If an attacker extracts both K_admin and K_admin_next from a single node’s flash, Option B provides no protection for that network. Mitigation: APPROTECT + field-audit of node tamper state.
  • One-shot only; after use, back to Option A until a field visit replenishes the slot.

Option C — PKI / hub public-key signing (planned for v2.0+)

Section titled “Option C — PKI / hub public-key signing (planned for v2.0+)”

Hub holds a long-term ECDSA private key. Every node has the hub’s public key. K_admin rotation commands are signed with the hub’s private key. Arbitrary rotations possible.

What changes from Option B:

  • Provisioning writes a hub public key to flash (instead of or alongside K_admin_next).
  • Rotation verification switches from AES-CMAC to ECDSA-verify. The nRF52840’s CryptoCell has hardware ECDSA support.
  • The COMMAND frame’s admin_mic field becomes an ECDSA signature (64 bytes vs 8 bytes). This is the only wire-format change, and it’s in a variable-length field inside the AES-CCM envelope, so the frame envelope does not change.
  • Hub-side: generate and protect a private key. Backup and recovery story needed for this new critical asset.

Risks (speculative, pending implementation):

  • Hub private key becomes the single most sensitive asset in the system. Loss = inability to rotate. Compromise = full admin takeover (worse than K_admin compromise because it cannot be physically recovered without re-provisioning every node with a new public key).
  • RNG quality on the hub matters for key generation. ESP32-S3 has a hardware TRNG but it must be used correctly.
  • ECDSA signature verification is computationally heavier than AES-CMAC. On nRF52840 with CryptoCell this is milliseconds, not a power concern — but it’s a complexity increase.
  • Larger admin_mic field (64 bytes) increases COMMAND frame size. Still within LoRa payload limits but worth accounting for.
  • Operational complexity: key ceremony for the hub private key, backup procedures, HSM considerations for large deployments.

To preserve the A → B → C upgrade path, v1 firmware must implement COMMAND verification as a clean, swappable step:

receive_command()
→ parse_command_frame()
→ verify_admin_auth(cmd, key_material) // ← this function swaps
→ apply_command(cmd)
→ send_command_ack()

verify_admin_auth() in v1 checks AES-CMAC against K_admin or K_field. In Option B it checks against K_admin_next. In Option C it checks ECDSA against the hub public key. The rest of the pipeline is unchanged. This is not extra work — it’s just good code structure.

  • nRF52840: keys stored in the internal flash, in a dedicated config page, ideally protected by the chip’s APPROTECT (debug-port lockout).
  • ESP32-S3: keys stored in encrypted NVS, using the eFuse-backed Flash Encryption + Secure Boot V2 where practical.
  • Keys are never logged or transmitted in the clear, including over USB.

In scope:

  • Passive eavesdroppers on the LoRa band.
  • An attacker stealing a deployed trap node and extracting its flash. They obtain K_group for that group, but cannot mint privileged commands without K_admin.
  • An attacker replaying captured frames.
  • An attacker on the WiFi LAN attempting to interact with the hub web UI (handled by hub-side auth, see provisioning.md).

Out of scope for v1:

  • An attacker stealing a HUB or TECH node (recovery: rotate K_admin manually).
  • Side-channel attacks on the radio MCU.
  • Physical destruction or jamming of traps (acceptable failure mode: missing check-ins trigger an alert, ranger investigates).