Stage 3 / Checkpoint C deliverable. Status: Draft normative spec. Date: 2026-06-09.
Depends on: packet-format.md (AEAD_TAG, SEQ, EPOCH, nonce derivation, SIG fields).
Analysis basis: security-options.md (bandwidth budget; ~16 B/packet ≈ 0% over the MAC budget).
Normative language per RFC 2119/8174. This document specifies identities, key agreement, per-frame confidentiality+integrity, broadcast authentication, replay protection without clock sync, and reboot behaviour. It is sized to LoRa's bandwidth: no wire handshake, no transmitted nonce, an 8-byte tag (
packet-format.md§11).
packet-format.md §10).Red-team finding F8. This is the most important limitation to state plainly, because a naive reading of "self-certifying addresses + signed beacons" overclaims.
Loomwave authenticates identities — a key proves it owns its ADDR24, and a signature proves a
frame came from that key. Loomwave does not, by default, authorize authority: there is no enrollment
authority and no PKI (goal #4), so possessing a valid identity does not prove a node is entitled to any
role. In particular, any node can act as a coordinator. An adversary can:
packet-format.md §8.3) with a
plausible election score. Being the first or loudest beacon a new client hears, it is TOFU-trusted
(§2) like any other coordinator.The election corroboration of routing-spec.md §5 raises the cost of winning election among honest
participants (a claim is weighted by other nodes' measured reach, so a node cannot simply assert a high
score), but it does not stop a client from TOFU-trusting a rogue it hears first, and corroboration is
itself Sybil-gameable (an attacker with K radios manufactures K corroborations — §1.2).
Default posture (v1): Loomwave is an open mesh — like Meshtastic/MeshCore, it trusts the RF environment. In an untrusted-RF setting a rogue coordinator can capture new clients. This is accepted and documented, not silently assumed.
Optional hardening (provisioned networks, future): a deployment that needs authority MAY provision a channel endorsement key out of band (the same out-of-band step the channel PSK already requires, §4). Coordinator claims and LSAs are then only honored if endorsed by that key; clients in a provisioned channel reject unendorsed coordinators. This is default-off so the zero-config open mesh is unaffected, and it also bounds backbone LSA-poisoning (red-team F14). Specified later; flagged here so implementations reserve the endorsement field.
Identities are free (ADDR24 = TRUNC_24(H(pubkey)), generated on-device), so there is no per-identity
cost. An attacker can mint many identities to exhaust a cell's slot table (admission DoS) or to
manufacture election corroboration (§1.1). v1 does not attempt a global cost function (proof-of-work
is battery-hostile and community-corrosive — explicitly WONTFIX as a default). The partial defense the
MAC SHOULD adopt is airtime-based admission: meter slot grants against airtime heard from a source's
radio, not per identity — because Sybil identities from one attacker share one physical radio, so an
airtime budget blunts the cheapest attack without penalizing honest nodes. The residual (a true
multi-radio Sybil costs the attacker hardware) is accepted.
Red-team finding F19. Restates and sharpens the §1 "out of scope" line.
Per-frame AEAD hides payloads; the structure of a scheduled network leaks more than a flood does:
ADDR24. Cell membership, presence, and
talk patterns are legible to a passive listener even though contents are not.A flood has a crowd's anonymity; an order is a surveillance affordance. For the v1 threat model
(hobbyist/community mesh) this is accepted and documented. Privacy-sensitive deployments must treat it
as disqualifying. A mitigation exists (slotmap by ephemeral slot-id rotated per epoch rather than
ADDR24, which complicates grant/ACK addressing) and is tracked as research, not committed.
ADDR24 = TRUNC_24(H(pubkey)) (routing-spec.md §2) makes the address self-certifying.packet-format.md §8.3), signed, rate-capped.
A node MUST verify that an advertised key hashes to the claimed ADDR24 before caching it.For a unicast pair (A,B), the shared secret is static-static X25519:
K_AB = HKDF( X25519(sk_A, pk_B) , info = "loomwave/v1/pair" )
Both sides compute K_AB from cached public keys — nothing is transmitted to establish it (the
airtime win over any TLS/Noise handshake). Per-epoch and per-direction subkeys are derived (§5) so the
static secret is never used directly as an AEAD key.
Trade-off (stated honestly). Static-static ECDH gives no forward secrecy: compromise of a long-term key exposes past traffic to that peer. The alternative — an ephemeral handshake — costs multiple round-trips of airtime per session, which
security-options.mdshowed is unaffordable at LoRa rates for a messaging/telemetry system. v1 accepts the trade; epoch rotation (§5) limits the exposure window and a future version MAY add an OPTIONAL ephemeral ratchet for high-value channels (aVER-gated extension).
K_chan distributed out-of-band (the
Meshtastic-style pre-shared channel key model), used for AEAD on channel (DST=0x000000) frames.K_chan proves membership, not identity. Frames that need provable origin (beacons, adverts,
coordinator-claims) are signed, not merely sealed (§6).K_chan-independent signature-only authentication (§6) so any listener — even one
without the channel key — can validate schedule origin and follow the MAC.packet-format.md §11 — 2⁻⁶⁴ forgery, beyond any LoRa-rate online attack).K = HKDF(K_AB or K_chan, info = EPOCH ‖ DIR).nonce = TRUNC_12( H( SRC_ADDR ‖ SEQ ‖ EPOCH ‖ DIR ) )
exactly as packet-format.md §7. DIR (1 bit) separates the two directions of a pair so the same
(SRC,SEQ,EPOCH) never yields a reused (key,nonce) across directions.packet-format.md §4.1), so any header tamper fails the tag.PAYLOAD only; header stays cleartext for relaying.The security of AEAD requires (key, nonce) never repeats. SEQ is 1 byte (wraps at 256) and
EPOCH is part of the nonce and the subkey, so the invariant is maintained by this normative rule:
SEQ cycle) under a single EPOCH for
a given (DIR). Before SEQ would wrap, the sender MUST advance to a new EPOCH.EPOCH (32-bit, packet-format.md §8.1) is monotonic and advanced by: (a) the coordinator on
each schedule epoch (beacons carry the current EPOCH), and (b) a sender locally when it exhausts
SEQ space within an epoch (it bumps to the next epoch value it has seen + 1 and resyncs at the next
beacon). Because both EPOCH and SEQ feed the nonce and the subkey, advancing EPOCH yields a
fresh subkey, so even a SEQ value reused in a later epoch produces a different (key,nonce).EPOCH seen per peer and MUST reject frames from a stale
epoch older than its replay horizon (§7).This makes 256-frame
SEQsafe: the (key,nonce) space is really(EPOCH, SEQ, DIR), andEPOCHmonotonicity guarantees no repeat. The 1-byteSEQkeeps DATA overhead at 16 B while epoch rotation carries the uniqueness burden.
SIG = full 64-byte Ed25519 over the frame
body (packet-format.md §8). A receiver MUST verify SIG against the key that hashes to
SRC_ADDR and MUST discard on failure (packet-format.md §9 step 5).VER MAY adopt a short symmetric beacon tag (network-keyed MAC) where identity
proof is not required, but that proves membership, not origin.sim-result-v3/v5).Replay is prevented without any clock using (EPOCH, SEQ) and a per-peer sliding window:
SEQ in the current EPOCH and a bitmap
window of width W (default 64) of recently seen SEQ values below it.EPOCH is current (or a valid advance, §5.1) and its SEQ is ahead
of the window, or within the window and not already set. Otherwise it is a replay/duplicate and
MUST be dropped (packet-format.md §9 step 6).EPOCH advance, the window resets. This needs no time synchronization — ordering is
by the monotonic (EPOCH, SEQ) pair, exactly the goal-#4 requirement.A coordinator's TOFU identity cache MAY be volatile; a restarted coordinator silently drops sealed frames from peers it no longer knows. Clients therefore MUST re-transmit their identity ADVERT periodically while unregistered (suggested: after every 4 unanswered REGISTERs) — observed on hardware as a permanent join-failure loop without it.
A naive reboot would reuse low SEQ/EPOCH values and risk nonce reuse. Normative handling:
EPOCH (preferred — small,
infrequent write), or (b) enough state to never reissue a used (EPOCH, SEQ).EPOCH strictly greater than any it previously used before
sending sealed/ signed traffic. If persistence is unavailable (a node with no stable store), it
MUST obtain a fresh EPOCH from a coordinator BEACON before transmitting DATA, and MUST NOT
originate sealed DATA in the coordinator-free regime until it has locally generated an EPOCH
provably greater than its pre-reboot value (e.g. seeded from a monotonic boot counter in NVRAM).node-roles-spec.md): INFRA nodes have persistent storage and advance EPOCH
cleanly; CLIENT/REPEATER on constrained MCUs MUST reserve a few bytes of NVRAM for the epoch
counter — this is the per-role replay/reboot item from design-decision.md §8, resolved here.| Item | Wire cost | When |
|---|---|---|
| AEAD tag | 8 B | every sealed frame (DATA/REGISTER) |
| Nonce | 0 B | derived (§5) |
| Key-agreement handshake | 0 B | implicit static ECDH (§3) |
| Signature | 64 B (full Ed25519; see §6 erratum) | only on rate-capped BEACON/ADVERT |
| Epoch (in beacon) | 4 B | once per superframe, broadcast |
DATA security overhead = 8 B within the 16 B header+tag total → 238 B app payload. This meets the
security-options.md budget with no payload regression.
A conformant node MUST: (1) use self-certifying identities and verify advertised keys hash to
their address (§2); (2) derive pair/channel keys with no wire handshake (§3–4); (3) AEAD-seal DATA with
an 8-byte tag, header as AAD, derived nonce (§5); (4) never reuse (key,nonce) — honor the
SEQ-wrap/EPOCH-rotation rule (§5.1); (5) verify full Ed25519 signatures on beacons/adverts (§6); (6)
enforce the sliding-window replay check without clocks (§7); (7) advance EPOCH across reboot before
transmitting (§8). A node MUST discard any frame failing integrity/signature/replay before relaying
or surfacing it (packet-format.md §9).
Builds on packet-format.md; consumed by mac-spec.md (beacon signing, epoch keying) and
routing-spec.md (self-certifying addresses, signed LSAs/claims). Reviewed at Checkpoint C before
firmware.