Threat model
SPEC §0 states the whole thing in one sentence: every link is hostile — logged, spoofed, jammed, MITM'd. Links are trusted with nothing; authenticity and secrecy live only in the envelope. This document is that sentence expanded into something checkable: six chapters of adversary, what they can try, what stops them, and — where nothing fully does — a stated residual risk rather than silence.
The rule every entry follows: name the attacker a mechanism defeats, and
give every attacker an explicit residual-risk line. A mitigation with no
named attacker is a slogan. An attacker with no residual-risk line is a claim
of perfect security, which this project's Honesty contract
(MISSION.md) does not permit.
Every claim below links to a SPEC.md section or a
the security findings (now in git log) ID. Where the honest answer is
"partially" or "not at all," that is what it says — this is a document about
where the edges are, not a marketing page.
Why this exists now. An external threat-model catalogue, written against
SPORE's public site rather than its spec, raised questions this project had
already answered in code — and one it had not
(S-032, found by checking the catalogue's
"receipt spoofing" item against src/node/ingest.rs). That is the argument
for this document existing: the answers were real but undiscoverable, and in
one case checking the question against the code found a live bug.
1. Observers — passive, local, global
Asset: message content, sender/recipient identity, timing, location, social graph.
| Level | Capability | What SPORE does |
|---|---|---|
| Passive link observer | Receive and record traffic on one medium | Sealed or ratcheted 1:1 and PSK groups (§7): unreadable without the key. Open groups and microblogs: readable, by design. Unsigned DATA: readable and forgeable |
| Local traffic analyst | Correlate timing/size/frequency on one medium | Partially addressed. Without mix, sizes and times are in the clear on the medium; mix padding and Poisson batching (§9) are the partial answer, and mix is off by default |
| Global passive adversary | Observe a large fraction of the network at once | Addressed only while mix decoys flow (§9) |
Mitigation. Content confidentiality is the baseline: seal/ratchet (§7)
for 1:1, XChaCha20-Poly1305 pre-shared-key sealing with healing rotation (§7.1)
for private groups. Open-group and microblog traffic is plaintext on
purpose — MISSION.md states it outright: "open group posts are public on
purpose," which is a design choice, not an oversight, because a stranger's
device has to be able to carry it forward without holding a key.
For metadata, §9's mix mode is the actual answer: onion-wrapped envelopes through 2–3 mixes, Poisson-delayed batching (1–30 s, batch ≥ 3), and payload padding to three size classes (256/1024/4096 B) so depth doesn't show.
Residual risk — stated in the spec itself, not new here. §9's own honest
limit: "beats local observers and any subset of mixes; a global passive
observer is only beaten while decoy traffic flows." Mix mode is an explicit,
non-default opt-in (MISSION.md's locked decision table: "Tor / global
anonymity by default" is a stated non-goal) — a node that never turns it on
gets none of this, and that is visible to the user as a mode, not a silent
gap.
Explicitly out of scope. Physical-layer transmission is not made invisible by anything above it. A radio observer with a directional antenna can locate a transmitting device regardless of what the payload says — the same limit that applies to Meshtastic or any other RF system riding the same physical layer. SPORE does not claim otherwise on any radio-bound interface.
2. Participants — malicious carriers and relays
Asset: availability of the message, integrity of what's delivered, this node's own resources.
The model, stated once, is the "untrusted postman": a carrier is asked to
move an object it cannot read and should not be trusted merely because it is
carrying it. MISSION.md's "Confidential ≠ anonymous" and SPEC §0's "every
link is hostile" both point at the same design: nothing here assumes a relay
is honest.
| Attack | Mitigation | Reference |
|---|---|---|
| Forge a delivery receipt to fake "delivered" | Receipt must carry a verified signature from the actual destination | S-032 |
| Reflect/amplify via WANT | Per-interface token bucket on gossip service | S-012 |
| Claim a deeper WANT than policy allows, so one frame makes the whole mesh adopt an interest | Incoming depth is clamped to DEFAULT_WANT_DEPTH; asking for less is honoured, asking for more degrades to local policy |
M11-L — measured: a forged depth reached 23 of 24 nodes against an honest 8 |
| Bypass congestion control with a low-effort flood | Stamp (proof-of-work) threshold gate | S-003 |
| Forge a path-table entry to redirect unicast | Relays verify a signature before binding a path to it | S-002 |
| Flood ten different growable tables | Bounded, with deliberate eviction order (expired → lowest stamp → largest → oldest) | S-013 |
| Exhaust an MCU node's heap with desktop-sized ceilings | Every table ceiling derivable from a runtime's actual memory budget | audit #189 rec. 2, Limits::for_budget |
| Tamper with spilled/cached bytes | An entry read back is re-verified against its content-derived ID; a mismatch reads as "not held" | SPEC runtime contract + §6 (store) |
Residual risk — this is not solvable by cryptography, and the spec says so. A relay can still drop, delay, or reorder anything that passes through it — signing and sealing protect what the message says, not whether a hostile forwarder chooses to forward it. This is the same lesson that applies to Reticulum or any store-and-forward design: you can encrypt a packet, but you cannot cryptographically force a hostile relay to forward it. What bounds the damage is redundancy — multiple carriers, flood-fallback when a unicast path dies (§5.5–5.6), custody re-push on the next fresher-path sighting (§6) — not a guarantee that any single relay behaves.
A relay that behaves correctly most of the time and selectively drops one target's traffic (a greyhole, in the terminology an external audit used) is harder to catch than one that always fails, and nothing here specifically detects that pattern. It is bounded by the same redundancy mechanisms, not eliminated by them.
3. Identities — compromise, Sybil, and what "revoked" can mean
Asset: the ability to speak as an address; group membership; the correctness of "who sent this."
Address = first 8 B of SHA-256(pubkey) (§1). This sidesteps the classic TOFU
first-contact problem other systems have: there is no separate "channel key"
someone can hand out that grants speaking rights for an identity that isn't
theirs. What an invite.rs can forge is the hint
attached to an address — a claimed name, claimed bridges — never the address
itself; a forged invite gets you a wrong contact, never a forged identity.
That file's own doc comment says this and means it as a warning, not a
guarantee: "confirm the bridges before joining them."
Sybil resistance — a locked design guardrail, not a shipped feature. Stamp (§10) is per-envelope proof-of-work. It raises the cost of any one envelope; it says nothing about identity count, because it isn't bound to one. An attacker who mints ten thousand addresses pays the same per-envelope stamp cost as one honest sender making ten thousand honest posts — the mechanism cannot tell them apart, because it was never asked to.
Guardrail: no feature that weighs replication, priority, or trust by how many distinct peers vouch for or forward something — "send more copies toward frequently-encountered devices," reputation scores, popularity-weighted routing — ships without a documented Sybil analysis first. Nothing in the codebase today needs this guardrail; stamp and per-source quotas are both identity-agnostic by design. It exists so a future feature doesn't have to re-derive the problem from scratch, and so "many nodes agree" is never treated as "many independent people agree" without first stating what makes them independent.
Key compromise — a real, unaddressed gap. Topic (group) keys have a full
answer: rotate for forward secrecy, rekey_seal for eviction,
contribute/absorb for healing after a copied key (§7.1,
Spec). A
node's identity signing key has none of this. If the seed that derives
it is stolen, the thief can sign as that address indefinitely — there is no
revocation certificate, no protocol-level "this address's key is no longer
trusted" statement, and no CRL-equivalent, because none exists in the wire.
The only real answer today is social: stop trusting the old address, out of
band, the way you would if someone told you their phone was stolen.
Building a real answer (a signed statement peers could carry and honor) is a new wire feature under the frozen-format process, not an oversight fixable in a doc pass — recorded here so it is a stated gap, not a silent one.
Prekeys are a separate, already-solved problem. A stolen prekey secret is bounded by the 7-day offline window and daily rotation (§7.2) — narrower blast radius than an identity key by design, and already documented in Spec §7.2 and Part III's "two things this does not solve".
Multi-device identity — tracked, not decided. One keypair per device
today (§1, §11: "ratchet state is per-device — give each device its own
key"). A person-level identity spanning several authorized devices is an
architecturally significant, likely wire-affecting change, tracked as an
explicitly undecided question in ROADMAP.md's non-goals
table rather than promised or silently absent.
4. Resources — storage, CPU, battery, bandwidth, spam
This is the best-covered chapter, because it has been the subject of a dedicated audit (#189) on top of the ordinary findings register.
| Attack | Mitigation | Reference |
|---|---|---|
| Storage exhaustion (unbounded growth of any table) | Every growable table capped, with eviction order | S-013, S-006, S-016, S-017 |
| Storage exhaustion (fragment reassembly, count bounded but not bytes) | Byte budget across all incomplete fountain sets, not just a count cap | audit #189 F-3 |
| Storage exhaustion (unsigned traffic exempt from source quota) | Quota applies regardless of signature presence | audit #189 F-2 |
| CPU exhaustion (audio demod rescans its whole buffer every call) | Scan cursor makes cost proportional to new samples, not buffer size | S-031 |
| CPU exhaustion (integer overflow reachable from the wire) | Checked arithmetic on the affected path | S-019 |
| Battery exhaustion (aggressive beaconing) | Trickle timer (5→80 min, doubling on silence) instead of a fixed short interval | S-023 |
| Bandwidth exhaustion (reflection/amplification) | Per-interface WANT service budget | S-012 |
| Bandwidth exhaustion (relayed traffic crowding a link) | Token bucket at 10% of interface capacity | SPEC §5.4a |
| Every table sized for a desktop, shipped on a 226 KB-heap MCU | Limits::for_budget scales every ceiling from one number, with floors so a table never trims to zero |
audit #189 rec. 2 |
| Third party's traffic filling a node's quota | Per-source, per-topic quotas | SPEC §10, S-004 |
| Spam (unlimited low-cost envelope minting) | Stamp: proof-of-work priority, unforgeable on every medium; unsigned mail rides last | SPEC §10 |
Residual risk — stamp raises cost, it does not cap it. Proof-of-work priority means a well-resourced attacker can still mint high-stamp junk; it simply costs them real compute and battery to do it, same as it would cost an honest sender to earn priority. This is the "postage" model doc-review commentary independently arrived at, and it is the right shape of partial answer — cost, not prohibition — but it is not a cap, and nothing here claims it is one.
Explicitly out of scope. Jamming and other physical-layer denial of service. Listen-before-talk and duty-cycle limits (SPEC page 2, per-medium tables in Bridges) are politeness, not defense — nothing at this layer addresses someone filling the air with noise, and nothing here claims to.
5. Network / transport — routing, partitions, wormholes, gateways
Finding, from auditing this catalogue's "wormhole/eclipse" item against the code: path learning (§4) is local and non-transitive.
Paths::learn records up to 3 (iface, neighbor, age) candidates per
address, newest first, from the first copy of any signed envelope a node
personally receives. The wire's ANNOUNCE payload has a field for advertising
other nodes' path freshness (np:1][(addr:8, age_min:2) ea], SPEC §4) —
but the reference implementation always sends np = 0
(build_announce_at_hops: "we advertise no distant paths in this reference
build"), and absorb_announce does not parse that field on receipt at all.
So a node's belief about how to reach an address is built only from
signed traffic it has personally, directly seen — never adopted secondhand
from a peer's claim about a third party.
That bounds a wormhole/eclipse attacker's reach precisely: it can dominate
the "tried first" slot in its own direct neighbors' path table for
addresses whose traffic it relays fast enough to keep winning — by
continually being the fastest deliverer, it keeps re-inserting itself at the
front (learn always inserts at index 0). It cannot poison path beliefs
at any node it has not directly delivered signed traffic to; there is no
mechanism for that belief to spread.
The scope is narrower still: path-table lookups only gate directed
unicast with FLOOD unset (§5 rule 5). Topic, public, and FLOOD traffic
always damps-floods on every interface regardless of the path table, so an
eclipse cannot suppress or redirect anything but the one-hop unicast slice —
and even that slice flood-falls-back under custody if the winning path dies
(§5.5–5.6), so the attacker has to keep winning, not just win once.
Residual risk, within that narrow scope. A relay that wins "front of list" for a target address sees more of that address's directed unicast traffic pass through it than it otherwise would — a metadata/traffic-volume concern (chapter 1), and it can drop that slice selectively (chapter 2's already-stated relay-availability limit). Both are real, both are bounded to one hop and to unicast, and neither compromises content — everything routed through a hostile relay is still exactly as confidential and authentic as §7's mechanisms make it regardless of which relay carries it.
Gateways and bridged underlays. SPEC page 2 treats Meshtastic, Reticulum, Tor, WireGuard, and plain IP identically: "underlays with their own routing = ONE interface." A compromised or monitored underlay is then just a compromised or monitored link, already inside SPEC §0's baseline assumption — not a special case requiring its own model.
Partitions. Not an attacker (see chapter 6's "accidental" category in spirit) — a natural operating condition this whole architecture is built around, not a failure mode to defend against.
6. Implementation & evolution — parsing, crypto, supply chain, upgrades
Parser robustness. Six fuzz targets in fuzz/fuzz_targets/
(armor_and_framing, envelope_decode, fragment_reassembly, node_on_rx,
radio_codecs, seal_open) exercise every wire-facing decoder against
malformed, truncated, and adversarial input. robustness.rs runs targeted
arbitrary-bytes tests against the live receive path — the mechanism that
found S-001 (a zero-count fragment causing a
division-by-zero panic) before it shipped. No unwrap/expect/indexing
panic is reachable from untrusted input on ingest/fountain/session/
file/armor (audited, per #189's "what is already solid").
Cryptographic primitives. Ed25519 (ed25519-dalek), X25519
(x25519-dalek, crypto_box), ChaCha20-Poly1305 / XChaCha20-Poly1305
(chacha20poly1305), BLAKE2b/SHA-256 (blake2/sha2) — standard,
widely-audited crates, not hand-rolled primitives. deny.toml and the
cargo-deny/cargo-audit CI jobs (supply-chain.yml) gate every dependency
change against known vulnerabilities and license terms.
Supply chain. A compromised build pipeline or release artifact is not
the only way to get a working node: REBUILD.md, the
dependency-free reference T0 decoders (reference/), and the frozen test
vectors (reference/vectors.json) mean anyone can independently reimplement
and cross-check against the wire, not trust any single maintainer's binary.
Public-domain licensing (LICENSE) removes the legal kill switch a
corporate license could hold. Five release-integrity findings on record
(S-021,
S-025/S-026/
S-029/S-030) are
evidence this specific class — the release pipeline itself as an attack
surface — has been checked, not assumed clean.
Crypto agility under a frozen wire — a deliberate trade, stated plainly.
Envelope::decode checks buf[0] == VER for exact equality; an
envelope carrying any other version byte is rejected outright, not carried
along or tolerated. There is no version-negotiation path. A hypothetical v2
wire format is a hard fork by construction: an old node silently drops every
v2 envelope it sees, rather than routing it unread the way an IP router
forwards a packet whose contents it can't parse.
This is consistent with — not a gap in — the project's stated priorities.
MISSION.md's "small-spec + continuity" pillar and the frozen-wire hard
rule (ROADMAP.md: wire format is frozen, changed only via the
allow-frozen-change process, a deliberate major-version act) both choose
holdability over in-place upgradability. The cost is real: mixed v1/v2
meshes require every node to move, not a rolling negotiation. Agility
within v1 exists at finer grain — the one reserved flag bit (§2 flag b7,
"MUST be 0" today) and the payload-level app-tag convention the file layer
already demonstrates ([0x01]/[0x07]/[0x08]/[0x09] tags distinguishing
manifest/chunk/nested-manifest/sealed within one envelope type) both allow
new conventions without moving VER. A new AEAD suite or a revocation
certificate (chapter 3) is more likely to arrive that way than as a wire
version bump.
Accidental / environmental — not an attacker, stated for completeness. Radio interference, device failure, battery death, packet corruption, network partitions, no nodes nearby. This is the ordinary operating condition store-and-forward delivery is built to survive, not a threat requiring a countermeasure beyond what already exists (redundant carriers, expiry rather than permanence, custody re-push on next contact).
What a seized honest relay's storage reveals
Every mitigation above assumes a hostile participant. This section asks a different question, and it is the sharpest one this document has to answer honestly: a fully compliant, non-malicious node's own storage — if physically captured — is not empty of information about other people, even though it holds no plaintext it wasn't itself the endpoint for.
| Table | Retention | What it reveals |
|---|---|---|
store (envelope bytes) |
Until expiry — app default 7 d, wire-clamped ≤ 30 d (§2, §11) | Payload — ciphertext for sealed/ratcheted traffic, plaintext for public/open-group posts (by design) |
seen (dedup ids) |
≥ 30 d received (§11, SEEN_MIN_SECS/MAX_EXPIRY_HORIZON_SECS) |
Opaque content-hash IDs this node has relayed — not content, not sender/recipient by themselves |
paths (addr → iface/neighbor/age) |
Fresh < 3 h, purged at 7 d (§4) | The sharpest one. Which addresses this node has directly exchanged signed traffic with, and over which link — a real social-graph fragment |
peer_prekeys/peer_busy/peer_names |
Bounded by peer count (MAX_PEERS), not time — no explicit expiry |
Claimed display names and current prekeys of every peer met, for as long as the peer table doesn't evict them |
sessions (ratchet state) |
In-memory only, not persisted across a restart | Which addresses have an active ratchet session (an ongoing 1:1) — and, if seized live, current chain state that could open messages not yet advanced past, though never messages already past a ratchet turn (§7's forward-secrecy property) |
pending/acked (own sent-message ids) |
MAX_ACKED count-bounded |
This node's own send/receipt activity, by opaque id |
The honest conclusion: content stays protected exactly as far as §7's
mechanisms protect it, regardless of who seizes the device. Metadata does
not — paths in particular is a real, if partial, social graph, built
from ordinary operation with no malice required. This is the same
"honest-but-curious" concern chapter 2 raises about relays in general,
concretized with real retention numbers rather than left as a gesture. There
is no mitigation to cite here beyond what already exists (bounded retention,
not unlimited) — this is a residual risk of running a node at all, stated
plainly rather than left for a user to discover by reading source.
Summary: what's real, what's partial, what's not attempted
| Property | Status |
|---|---|
| Content confidentiality (sealed/ratcheted traffic) | Real — §7 |
| Content confidentiality (open groups, microblog) | Not attempted, on purpose — public by design |
| Sender authenticity | Real — Ed25519 signatures, verified before any trust decision (§2, S-002) |
| Replay resistance | Real — content-addressed IDs + dedup (§5), envelope expiry |
| Resource-exhaustion resistance | Real, and runtime-scalable — chapter 4, Limits::for_budget |
| Malicious-relay availability attack | Bounded by redundancy, not eliminated — universal to store-and-forward, chapter 2 |
| Sender/recipient metadata under passive local observation | Not attempted by default; real under opt-in mix mode |
| Metadata under a global passive adversary | Partial — only while mix decoys flow |
| Sybil resistance | Not needed today (nothing counts identities); a guardrail exists for when it is |
| Identity-key revocation | Not solved — social/out-of-band only |
| Wormhole/eclipse | Bounded — local, non-transitive, unicast-only |
| Honest-relay metadata retention | Real and bounded, but non-zero |
| Parser/implementation robustness | Real — fuzzed, tested, audited |
| Crypto agility | Deliberately narrow — payload/flag-level only, not wire-version |