|
| 1 | +# Threat Model |
| 2 | + |
| 3 | +This document enumerates Anchr's cryptographic and protocol-state invariants. |
| 4 | +Every safety claim in `README.md` must map to one of these invariants. Every |
| 5 | +invariant must have at least one test. CI enforces both directions via |
| 6 | +`deno task lint:invariants`. |
| 7 | + |
| 8 | +## How this document works |
| 9 | + |
| 10 | +Each invariant has the shape: |
| 11 | + |
| 12 | +- **Claim** — one-line property the protocol guarantees. |
| 13 | +- **Attack** — the adversary behavior this invariant defends against. |
| 14 | +- **Expected** — the observable outcome when the attack is attempted. |
| 15 | +- **Tests** — file paths + test names (or `fn` names for Rust). |
| 16 | +- **Status** — `enforced` (tests live-bear), `tests-pending-PR-N` (declared, |
| 17 | + tests land in a follow-up PR), or `cross-referenced` (covered by existing |
| 18 | + attack-class tests, marked via `// INV-NN` comment). |
| 19 | + |
| 20 | +An invariant without tests breaks CI. A test whose name references an |
| 21 | +invariant not declared here also breaks CI. |
| 22 | + |
| 23 | +When an invariant's Claim/Attack/Expected body changes, the matching entry |
| 24 | +in `docs/threat-model.lock.json` must be updated with a fresh hash plus a |
| 25 | +`justification` string describing the change. This is a drift guardrail: |
| 26 | +you can't silently weaken an invariant without a PR reviewer seeing the |
| 27 | +hash bump. |
| 28 | + |
| 29 | +## Invariants |
| 30 | + |
| 31 | +### INV-01: Worker can't forge TLSN proofs |
| 32 | + |
| 33 | +**Status:** `tests-pending-PR-2` |
| 34 | + |
| 35 | +**Claim:** The Oracle's TLSN verifier rejects any presentation whose |
| 36 | +transcript, notary signature, or MPC-TLS MAC chain is invalid. A Worker |
| 37 | +cannot produce a presentation for an HTTPS response they did not actually |
| 38 | +observe. |
| 39 | + |
| 40 | +**Attack:** Generate a valid TLSN presentation, mutate a byte in the |
| 41 | +transcript commitment / notary signature / target-host field, submit to |
| 42 | +the Oracle's verifier. |
| 43 | + |
| 44 | +**Expected:** Verifier returns a typed error (`VerifierError::Transcript`, |
| 45 | +`::Signature`, or `::Server` per mutation class). Oracle does NOT release |
| 46 | +the preimage. Oracle does NOT emit a FROST signature share. |
| 47 | + |
| 48 | +**Tests:** Declared here. Implementation lands in PR-2 after the |
| 49 | +`tlsn-verifier` crate is refactored to expose a `lib.rs` + typed error |
| 50 | +enum. Target path: `crates/tlsn-verifier/tests/invariants.rs::inv_01_*`. |
| 51 | + |
| 52 | +**Why pending:** The `tlsn-verifier` crate is currently `[[bin]]`-only |
| 53 | +with `anyhow::Error`. Writing `assert_matches!(err, VerifierError::Transcript)` |
| 54 | +requires extracting a library + typed error enum, which is scoped to PR-2. |
| 55 | + |
| 56 | +### INV-02: Oracle can't release preimage without valid proof |
| 57 | + |
| 58 | +**Status:** `enforced` |
| 59 | + |
| 60 | +**Claim:** The Oracle's HTTP wrapper never returns the Cashu HTLC preimage |
| 61 | +in response to a `POST /queries/:id/result` unless verification passes. |
| 62 | +Protocol-layer outcome: regardless of which cryptographic check fails |
| 63 | +(missing presentation, malformed JSON, wrong signature, expired |
| 64 | +presentation, empty worker_pubkey), the response body does not contain |
| 65 | +`preimage`. |
| 66 | + |
| 67 | +**Attack:** Submit adversarial payloads to `POST /queries/:id/result`: |
| 68 | +missing presentation, malformed JSON, invalid worker_pubkey, oracle not |
| 69 | +yet registered. |
| 70 | + |
| 71 | +**Expected:** HTTP response body has no `preimage` field. HTTP status |
| 72 | +rejects (4xx) or returns `ok: false`. Oracle's preimage store is not |
| 73 | +decremented. |
| 74 | + |
| 75 | +**Tests:** |
| 76 | +- `e2e/pentest/oracle-attacks.test.ts` — `ORACLE-ATTACK: Preimage |
| 77 | + protection` suite (both tests). |
| 78 | + |
| 79 | +### INV-03: Requester can't unlock escrow before timeout |
| 80 | + |
| 81 | +**Status:** `cross-referenced` |
| 82 | + |
| 83 | +**Claim:** Cashu HTLC proofs locked with `locktime > now` cannot be |
| 84 | +redeemed via the Requester's refund key. Only the Worker's key + valid |
| 85 | +preimage can redeem before locktime. The Mint enforces this, not the |
| 86 | +application layer. |
| 87 | + |
| 88 | +**Attack:** Requester attempts to swap HTLC proofs back to themselves |
| 89 | +before `locktime` has elapsed, presenting only their refund key. |
| 90 | + |
| 91 | +**Expected:** Cashu Mint rejects the swap (returns `null` from |
| 92 | +`attemptRedeem`). Funds remain locked until locktime expires. |
| 93 | + |
| 94 | +**Tests:** Cross-referenced from existing attack-class tests, annotated |
| 95 | +with `// INV-03` comments: |
| 96 | +- `e2e/regtest-htlc-trustless.test.ts` — `ATTACK: Requester refund key |
| 97 | + before locktime → Mint REJECTS` |
| 98 | +- `e2e/regtest-htlc-attacks.test.ts` — `ATTACK: Requester redeems own |
| 99 | + HTLC proofs before locktime — fails` |
| 100 | + |
| 101 | +Related (not INV-03 but same surface, kept for context): |
| 102 | +`LEGIT: Requester refund key after locktime → Mint ACCEPTS` demonstrates |
| 103 | +the refund path works once locktime elapses. |
| 104 | + |
| 105 | +## Future invariants (declared, not yet specified) |
| 106 | + |
| 107 | +- **INV-04:** FROST t-of-n threshold safety — no subset of size < t can |
| 108 | + produce a valid aggregate signature. Likely cross-referenced to |
| 109 | + `e2e/frost-threshold.test.ts::ATTACK: 1-of-3 (below threshold) → |
| 110 | + aggregation fails` once declared. |
| 111 | +- **INV-05:** C2PA manifest signature + GPS binding. Scoped after |
| 112 | + `crates/` gets a C2PA verifier. |
0 commit comments