summaryrefslogtreecommitdiffstats
path: root/CLAUDE.md
diff options
context:
space:
mode:
Diffstat (limited to 'CLAUDE.md')
-rw-r--r--CLAUDE.md85
1 files changed, 79 insertions, 6 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index 846a594..47c2a10 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -112,8 +112,66 @@ Scope: `hub`, `node`, `common`, or omitted for cross-cutting
- **S4** AES-GCM keystore IV fixed: 128-bit → 96-bit (NIST SP 800-38D) ✅ DONE
- **S5** Refresh token rotation (one-time use) ✅ DONE (Phase 8.3 — family-based reuse detection)
+**Node sovereignty (2026-08-12):**
+- **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
+- **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
+- **NS8** GEK-required enforcement — node REFUSES connections when GEK is None (no `gek_required: false` bypass). GEK initialization via node local admin UI only. ✅ DONE
+
+**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)
+- **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):**
+- **Phase 1** ✅ DONE: GEK bundles moved from hub to node P2P (WebRTC DataChannel). No hub fallback.
+- **Phase 2** ✅ DONE: Keypair bundles moved from hub to node P2P. Registration stores locally, pushed to node on first connect. Hub never stores keypair bundles.
+- **Phase 3** ✅ DONE: Hub GEK cleanup — `GET /gek` endpoint removed, `GEKBundle` model removed, `gek_bundles` table dropped, `keypair_bundle` column removed, member-add URL cleaned (`/gek` suffix removed), Alembic migrations updated.
+
+**Browser crypto hardening (2026-08-13):**
+- `_bundleKey` persisted in IndexedDB (CryptoKey survives page refresh)
+- `_sessionKeys` persisted in sessionStorage (survives refresh, cleared on tab close)
+- `_pkFromSk()`: derive X25519 public key from recovered private key via JWK export (no hub fetch)
+- Removed auto-`regenerateKeys()` on login (was silently rotating hub keys, breaking GEK unwrap)
+- Raw answer SDP saved before `setRemoteDescription` (Chrome strips sha-256 from multi-hash SDP)
+- Upload chunk size: 48KB (fits aiortc SCTP limit after msgpack overhead)
+
**Architecture validated:** crypto primitives, GEK wrapping (ECIES), trust model,
-key hierarchy, on-the-fly encryption, transport abstraction.
+key hierarchy, on-the-fly encryption, transport abstraction, DTLS channel binding.
+
+## Second security review (2026-08-13) — see `second-review.md`
+
+**6 critical, 7 high findings. Phase 11.5 is BLOCKING — see `devel-phases-next.md`.**
+The current build must not host real private data.
+
+The claims above about node sovereignty and P2P crypto material were **overstated**. The
+GEK-HMAC proof, Ed25519 admin challenge and channel binding are real, but they are enforced
+on the WebRTC path only, and three other paths into the node were left behind.
+
+- **C1** Node HTTP API (`http_server.py`) serves private group **index and plaintext files
+ with no authentication**, on `0.0.0.0`, for every group — bypasses the entire sovereignty layer
+- **C2** `/v1/nodes/ws` trusts a client-supplied `node_id` → any user hijacks a node's
+ signaling identity and impersonates it to browsers
+- **C3** The node never authenticates itself to the client (`node_pk` is never verified, no proof of possession)
+- **C4** Keypair bundles are served pre-proof and pushed to every node joined; PBKDF2-only → offline password attack
+- **C5** Any member can overwrite arbitrary shared files (upload) and seize the group GEK (`gek_bundle_store` + auto-activation)
+- **C6** GEK proof exists on WebRTC only — QUIC and TCP accept a bare JWT (chat injection)
+- **H1** Multi-group nodes share one `chat_store` and one peer registry → cross-group chat leak
+- **H2** Stored XSS in the node admin UI via uploaded filename → node takeover
+- **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
+
+**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
+ clients too (via `aiortc` in Python)
+- Argon2id 256 MB was applied to the **hub only**; `crypto.py` keystore is still 64 MB
+- Sender keys must be distributed **pairwise to identity keys**, never GEK-derived
+- Chat is plaintext on the wire and at rest; the index is plaintext on the WebRTC path
## Known calibration TODOs
@@ -175,23 +233,38 @@ SFR residential Fedora 44 → meshbay.org OVH VPS:
| i18n (browser) | `static/i18n.js` | `t()` lookup, ESM, localStorage lang selection |
| Admin API (hub) | `meshbay_hub.api.admin` | Phase 10.2 — user/group mgmt, audit logs, stats |
| Admin UI (browser) | `static/app.js` | Phase 10.3–10.4 — AdminPage component, 5 tabs |
-| Auth dependencies | `meshbay_hub.api.deps` | `require_admin`, `require_moderator`, `get_current_user` |
+| Auth dependencies | `meshbay_hub.api.deps` | `require_admin`, `require_moderator`, `get_current_user`, `require_user_scope` |
+| Node auth (hub) | `meshbay_hub.api.nodes` | `POST /v1/nodes/auth` — Ed25519 challenge-response, node-scoped JWT |
| Site overlay | `site/` | Phase 10.1 — landing, about, downloads (meshbay.org-specific) |
| Notifications (hub) | `meshbay_hub.api.notifications` | Phase 10.5 — CRUD, per-user, triggered by admin/group actions |
| Version check (hub) | `meshbay_hub.api.hub` | Phase 10.10 — `GET /v1/hub/version` |
-| Group self-service (hub) | `meshbay_hub.api.groups` | Phase 10b — create, join, members, GEK bundle store |
+| Group self-service (hub) | `meshbay_hub.api.groups` | Phase 10b — create, join, members (GEK exchange is P2P) |
| File upload (node) | `meshbay_node.transport.webrtc_server` | Phase 10b.4 — FILE_UPLOAD MNP handler |
| GEK wrap AES (browser) | `static/crypto.js` | Phase 10b.2 — AES-256-GCM ECIES for WebCrypto |
+| GEK HMAC proof (browser) | `static/crypto.js` | `hmacGEK()` — HMAC-SHA256 with DTLS channel binding |
+| DTLS fp extraction (browser) | `static/transport.js` | `_extractDtlsFingerprint()` — SDP fingerprint for channel binding |
+| DTLS fp extraction (node) | `meshbay_node.transport.webrtc_server` | `_extract_dtls_fingerprint()` — SDP fingerprint for channel binding |
+| Ed25519 sign (browser) | `static/keyderive.js` | `signChallenge()` — admin challenge-response |
+| Auth key derivation (browser) | `static/keyderive.js` | `deriveAuthKey()` — password split, hub never sees raw password |
| GEK wrap AES (Python) | `meshbay_common.crypto` | Phase 10b.2 — `wrap_gek_aes()` / `unwrap_gek_aes()` |
| IndexedDB cache (browser) | `static/app.js` | Phase 10b.5 — group index caching |
| Cross-group search (browser) | `static/app.js` | Phase 10b.6 — SearchPage, client-side |
| MSE video streaming (node) | `meshbay_node.transport.webrtc_server` | Phase 10c — ffmpeg fMP4 remux + encrypted segments |
| MSE video streaming (browser) | `static/app.js` | Phase 10c — MediaSource + SourceBuffer progressive playback |
| Video codec detection | `meshbay_node.transport.webrtc_server` | Phase 10c — `_probe_video()` ffprobe + MSE codec strings |
-| Node daemon (production) | `meshbay_node.daemon` | Phase 11 — WebRTC + WS + chat + HTTP all wired |
-| Node config | `meshbay_node.config` | `node.toml` loader, `data_dir` for chat DBs |
-| Hub WS client | `meshbay_node.hub_client` | `maintain_ws()` + `send_ws()` for signaling |
+| Node daemon (production) | `meshbay_node.daemon` | Phase 11 — WebRTC + WS + chat + HTTP + audit all wired |
+| Node config | `meshbay_node.config` | `node.toml` loader, `data_dir` for chat/audit DBs |
+| Hub WS client | `meshbay_node.hub_client` | `login()` (Ed25519) + `maintain_ws()` + `send_ws()` — no auth_key on node |
| Chat store | `meshbay_node.chat.store` | SQLite per-group, `data_dir/{group_id}/chat.db` |
+| Audit store | `meshbay_node.audit` | SQLite IP/action log, `data_dir/audit.db` (legal compliance) |
+| Bundle store (node) | `meshbay_node.bundle_store` | SQLite P2P GEK + keypair bundles, `data_dir/bundles.db` — hub never stores crypto |
+| P2P bundle exchange (MNP) | `meshbay_common.protocol` | GEK + keypair bundle STORE/FETCH/RESP message types |
+| Bundle via DataChannel | `static/transport.js` | GEK + keypair bundle fetch during handshake, store after connect |
+| Key persistence (browser) | `static/app.js` | `_bundleKey` in IndexedDB, `_sessionKeys` in sessionStorage |
+| pkX from private key | `static/transport.js` | `_pkFromSk()` — JWK export to derive X25519 public key |
+| Group delete (hub) | `meshbay_hub.api.groups` | `DELETE /v1/groups/{group_id}` — admin only |
+| JWT scope enforcement | `meshbay_hub.api.deps` | `require_user_scope` — blocks node-scoped tokens from mutations |
+| Node local admin UI | `meshbay_node.ui.app` | Dashboard, peers, groups, audit log (localhost:18000) |
| Demo scripts | — | `QE/demo-v1/*.py`, `QE/demo-v2/*.py`, `QE/demo-v3/*.py` (not versioned) |
## meshbay.org server (état cible)