summaryrefslogtreecommitdiffstats
path: root/CLAUDE.md
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-14 01:28:12 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-14 01:28:12 +0200
commitd7120761fe8cf406f374ef769db6e1f9bf1fe287 (patch)
treecc73da554575c51d7ee3837085a4318e08c610aa /CLAUDE.md
parent8f6e2f724fd24a077de11d4a3b3ae069d369324d (diff)
downloadmeshbay-d7120761fe8cf406f374ef769db6e1f9bf1fe287.tar.gz
docs: record the invite redesign — H3 and M3 closed
draft-v5 §2: against an active hub, reading content moves from "❌ H3" to "❌ T3 (browser) · ✅ native". The defensible sentence becomes "the hub cannot read your content unless it ships you malicious client code" — T3 is now the only path, it is an artifact rather than a silent directory lie, and it does not exist for a native client. New §5.5 describes admission and key delivery, with the four properties that carry it and the one exception (open-join groups, where the hub can walk in the front door — a property of open joining, and the setting is read from node.toml). Corrected while writing it: §5.1 said the C5b fix stopped a group admin who does not run the node from inviting, and that the redesign reverses this. It does not, because delegation was deferred. What changed is the timing — the operator issues a code and is then out of the loop. devel-phases-next: 12.1 is done and NOT as written. The plan was key transparency plus safety numbers; what shipped removes the directory read instead. Safety numbers make substitution detectable by a human who checks, at first contact, when there is nothing to check against. 12.2 (served-SPA integrity) is now the highest-value item in that phase. Phase 14 marked for what landed. second-review: H3 and M3 annotated closed at the finding, with what actually closed them. The §7 verdict table is left intact — it is the record of an audit on a date, and falsifying it would be worse than leaving it — with a note pointing at draft-v5 §2 for current state. CLAUDE.md matters most here, being loaded every session: NS4 read "admin_pk_ed25519 auto-pinned from keystore ✅ DONE", which is M3 described as a feature. Rewritten, with the two fixes that must never be attempted (auto-pin, hub lookup). QE/deploy/README.md: set-admin-pk retired from the walkthrough; the regression checklist now exercises pairing, joining by code, recognition without a code, and revocation. USERGUIDE.md is beyond the invite work but was actively wrong: it told users to POST GEK bundles to a hub endpoint deleted in Phase 12, and to re-wrap for every remaining member on revocation. Both replaced with what the code does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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