summaryrefslogtreecommitdiffstats
path: root/CLAUDE.md
diff options
context:
space:
mode:
Diffstat (limited to 'CLAUDE.md')
-rw-r--r--CLAUDE.md32
1 files changed, 30 insertions, 2 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index 549e83b..b2fb9b0 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -116,7 +116,11 @@ Scope: `hub`, `node`, `common`, or omitted for cross-cutting
- **NS1** GEK-HMAC proof in handshake — blocks hub admin from accessing any group content ✅ DONE
- **NS2** Ed25519 challenge-response for admin operations — blocks hub admin impersonation ✅ DONE
- **NS3** `gek_req` endpoint removed — node never serves GEK in plaintext ✅ DONE
-- **NS4** `admin_pk_ed25519` pinned in node.toml — auto-pinned from keystore ✅ DONE
+- **NS4** ~~`admin_pk_ed25519` auto-pinned from keystore~~ ❌ **that was finding M3.** The
+ keystore key is not the key the browser signs with, so every admin operation failed
+ closed. Authority now comes from the node's roster — `meshbay-node operator pair`
+ (2026-08-14). `admin_pk_ed25519` is still read as a legacy form; never auto-pin again,
+ and never resolve the operator's key through the hub
- **NS5** DTLS channel binding in GEK-HMAC — `HMAC(GEK, nonce || offer_fp || answer_fp)` detects WebRTC signaling MitM ✅ DONE
- **NS6** Chat `sender_id` enforced from authenticated session — prevents impersonation ✅ DONE
- **NS7** Node Ed25519 auth — node daemon authenticates to hub via `POST /v1/nodes/auth` (Ed25519 signed timestamp), no auth_key/password on node. JWT `scope: "node"` blocks group management (create/add/delete/join). Operator manages groups from browser only. ✅ DONE
@@ -124,7 +128,10 @@ Scope: `hub`, `node`, `common`, or omitted for cross-cutting
**Known remaining trust assumptions (Phase 12 — all actionable items done):**
- **T1** ✅ DONE: password split (auth_key / bundle_key, independent PBKDF2). Legacy migration on first login.
-- **T2** Hub controls public key distribution → can substitute keys during invite. Fix: out-of-band key verification (safety numbers)
+- **T2** ✅ **CLOSED 2026-08-14** (the finding is H3). Not by safety numbers: the invite
+ path stopped reading the directory. The node holds the GEK and wraps it for a key the
+ recipient proves possession of; identities are bound to accounts by one-time codes the
+ hub never sees. See `docs/invite-pairing-v1.md`
- **T3** SPA served by hub → fundamentally unsolvable in browser. Fix: native client or browser extension
**T3 attack surface reduction (2026-08-12, all phases complete):**
@@ -165,6 +172,27 @@ on the WebRTC path only, and three other paths into the node were left behind.
- **H3** Hub is the key directory → key substitution at invite yields the GEK. "Unreadable
even by the hub" is true against a *passive* hub only
+## Invite redesign (2026-08-14) — closes H3 and M3
+
+See `docs/invite-pairing-v1.md`. Read it before touching invites, admin authority or
+`gek_bundle_store`.
+
+- **The node wraps the group key**, on every connection, for the X25519 key the joiner
+ signed with their pinned Ed25519 identity. **Nothing fetches a public key from the hub
+ to wrap for** — not the SPA, not `gek-init`. That lookup *was* H3
+- **`gek_bundle_store` is deleted**, not gated. No member hands the node key material
+- **The node's roster decides who gets the key**, not hub membership: a hub that invents
+ an account and mints it a token gets `not_authorized_for_group`
+- **One-time codes** bind a key to an account without the directory. 40 bits, single use,
+ one account, node-wide lockout. 7 days for invitations, 24 h for operator pairing, both
+ in `[node]` of node.toml
+- **`join_policy`** (`invite`|`open`) is read from **node.toml, never the hub** — a hub
+ able to declare a group open would be handed its key. Unknown group ⇒ `invite`
+- Operator surface over SSH: `operator pair`, `member list|invite|revoke|unpin`. Deleting
+ a file is the last browser-only operation
+- Revocation now works for key delivery (nothing stored survives it) — but **still rotate
+ the GEK**, the ex-member holds the current one
+
**Corrections to remember:**
- `punch_nat()` is **not** a NAT traversal stack — one UDP probe, no STUN, no candidate
gathering, one ISP validated. **ICE/STUN (WebRTC) is the traversal path**, for native