Repository Public
1# Threat model (STRIDE-style summary)23Scope: one owner, balances of tens of USD. Owner of every control: Simon-Pierre Boucher. Severity is for the stated scope.45| # | Threat | Sev. | Abuse scenario | Prevention | Detection | Containment | Test | Residual |6|---|---|---|---|---|---|---|---|---|7| 1 | Stolen session cookie | High | attacker replays cookie, tries to send | cookie is `__Host-`, HttpOnly, Secure, SameSite=Strict, 30 min idle / 12 h max; a session cannot sign — passkey assertion required per transfer | audit log `transfer.*`, sessions list | revoke all sessions | api.test "a session alone cannot move funds" | attacker can read balances/history |8| 2 | XSS / malicious extension | High | script alters review screen or swaps address | CSP `script-src 'self'`, no inline script, no third-party scripts, React escaping | — | signer limits, halt | CSP header test | misleading display → loss ≤ daily limit |9| 3 | Dependency compromise | High | malicious package in API or web bundle | exact pinned versions + lockfile, small dependency set, bundles built once | — | signer limits; signer has its own bundle | — | signer bundle itself could be poisoned (noble/scure/viem/simplewebauthn) |10| 4 | Compromised API node | High | attacker has shell on node A | no keys on node A; signer verifies passkey + policy itself; HMAC only authenticates the channel | signer journal, audit chain | halt via signer CLI | signer.test, api.test "tampered stored intent" | can halt the service, show false data, wait for a real approval and swap intent before the prompt (bounded by limits) |11| 5 | Compromised signer node | Critical | root on node B | dedicated machine, no other apps, LAN-only port restricted to the API node IP, HMAC | signer journal hash chain | move funds to a fresh wallet | — | **total loss of wallet** (software keys) |12| 6 | Replayed approval | High | resend an old assertion/proposal | challenge = intent hash; proposal id single-use and persisted before signing; expiry ≤ 5 min; HMAC nonce cache | `sign.denied` in journal | — | signer.test replay | none known |13| 7 | Address poisoning / fraudulent QR | Med | look-alike address in history | first/last characters highlighted, new-destination warning + mandatory checkbox, look-alike detection, address book | — | limits | api.test warnings | owner inattention |14| 8 | Attacker-controlled or faulty RPC | Med | wrong nonce/balance/UTXO, fake deposit | two independent endpoints per chain must agree for anything that feeds a signature; chain id / genesis hash and USDC `symbol`/`decimals` verified on-chain; fail closed | network health screen | — | live smoke test | both providers colluding; display-only reads use one endpoint |15| 9 | Chain reorg | Low | confirmed tx disappears | states `confirming` → `finalized` only at chain finality (EVM `finalized` tag, BTC 6 conf, Solana `finalized`); `reorged` state + rebroadcast | activity screen | — | — | deep reorgs beyond finality |16| 10 | Token impersonation | High | fake "USDC" | assets resolved by pinned contract/mint address only, per network; signer refuses anything else | — | — | core.test, signer.test | none known |17| 11 | BTC UTXO/change redirection | High | change sent to attacker | signer rebuilds tx: all inputs from own address, change only to own address, inputs = outputs + fee, fee ceiling; BIP-143 commits to input values | — | — | core.test Bitcoin builder | none known |18| 12 | Solana instruction substitution | High | extra instruction drains account | signer builds the legacy message itself from a fixed instruction set; no lookup tables, no versioned tx, no opaque payload | — | — | core.test cross-check with @solana/kit decoder | none known |19| 13 | DNS takeover / proxy compromise | High | phishing origin | passkeys are origin-bound (rpId `wallet.spboucher.ai`); signer checks origin + rpId in every assertion; HSTS | — | — | signer.test wrong origin | TLS terminates on the OVH gateway: it sees traffic in clear |20| 14 | Physical theft of signer Mac | High | disk read | keystore encrypted, but wrap key on same disk, no FileVault | — | sweep funds with recovery phrase | — | **keys recoverable by thief** |21| 15 | Seed loss / disk failure | High | signer SSD dies | 24-word paper backup via `export-recovery` | — | restore on new node | keystore round-trip test | **until the phrase is written down, a disk failure loses the funds** |22| 16 | Lost phone / passkey | Med | cannot approve | iCloud-synced passkey; optional second passkey; `reset-passkeys` on signer CLI | — | — | signer.test enrollment | whoever has shell on the signer can re-enroll |23| 17 | Coercion | — | forced transfer | limits cap the loss | — | — | — | not mitigated |24| 18 | Ransomware / backup corruption | Med | DB destroyed | DB holds no keys; nightly dump; wallet state is on-chain | audit chain verification | redeploy, re-run setup | audit immutability test | history/labels since last dump |25| 19 | Insider/operator or AI agent misuse | Med | automation signs | no code path signs without a passkey assertion; `export-recovery` refuses non-interactive terminals | signer journal | halt | signer.test | an agent with shell on the signer node is equivalent to #5 |2627Not done (required by the original specification before larger balances): independent penetration test, third-party review of28the custody boundary, signing-library provenance review, SBOM/SAST in CI, Bitcoin Core node (public Esplora indexers are used).29