summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/config.py
Commit message (Collapse)AuthorAgeFilesLines
* feat(node): operator surface — member list, invite, revoke, unpin over SSHChristophe Besson2026-08-141-0/+14
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A node admits people from its own roster, and until now a headless operator had no way to put anyone on it: pairing worked from the CLI, everything else needed a browser on a machine that does not have one. Absorbs milestones 14.3/14.4. member list who is admitted, role, status, when and how pinned member invite <username> one-time code; the node wraps the key when they connect, so nobody has to be online then member revoke <username> stop serving them the key member unpin <username> forget the pin so they can pair again after a reset All of it goes through the daemon's loopback API with the per-run session token (11.5.3) — _daemon_api() in daemon.py, which also replaced three hand-rolled urllib blocks. `status` deliberately still reads the keystore, config and roster directly, so it works while the daemon is stopped. Two things the commands say out loud, because getting them wrong is silent: - revoke ends by telling the operator to rotate the key. The ex-member stops receiving it on their next connection, but they hold the current one, and "revoked" reads like it took the key back. - revoke/unpin refuse a username the roster does not know instead of acting on nobody. A typo must not look like success. Code lifetimes now differ by what the act is: 7 days for an invitation, which crosses a human conversation and gets answered whenever someone reads their messages, and 24 h for operator pairing, which is typed during the SSH session that printed it. Both configurable ([node] invite_ttl_hours, pair_ttl_hours). A day was long enough for the second and not for the first — a code that dies over a weekend means finding a browser to issue another one. The roster is also in the local admin UI, escaped: usernames come from the hub and land on the page that can re-key groups and read the audit log, so H2's rule covers them exactly as it covers filenames. Verified by driving the real CLI against a stub daemon over a socket, which is how the "known: <nothing>" bug in the not-found path turned up. Tests: 89 node here (roster, endpoints, CLI routing, TTL config). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(node)!: the node wraps the group key — closes H3 and M3Christophe Besson2026-08-141-2/+14
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The invite flow fetched the invitee's pk_x25519 from the hub and wrapped the GEK for whatever came back (app.js:1466, and gek-init did the same server-side). The hub is the key directory, so a hub answering with its own key was handed the group key by an honest member following the protocol exactly. No forgery, no injection, nothing for the client to notice. That was H3. The fix is not safety numbers. Nobody reads the directory any more: - the node holds the GEK and wraps it itself, on every connection, for the X25519 key the joiner signed with their Ed25519 identity in one transcript (meshbay:join:v1), so the identity key vouches for the encryption key; - identities are bound to accounts by a one-time code the hub never sees — 40 bits, single use, one account, bounded per connection AND node-wide; - the node's own roster decides who may receive the key. Hub membership lets someone reach a node; it no longer gets them anything. A hub that invents an account and mints it a token is answered not_authorized_for_group. Safety numbers would have made substitution detectable by a human who checks, at the moment there is nothing to check against — first contact. Removing the lookup makes it impossible, and costs the user one code to pass along. M3 falls out of the same work. The daemon auto-pinned its own keystore key as admin_pk_ed25519 while the browser signs with the user identity key, so every privileged operation failed closed with a signature error that looked like a bug somewhere else; the demo only worked because a deploy script overwrote the value. Authority now comes from the roster, established locally by `operator pair`. Asking the hub for the operator's key — the obvious-looking fix — would have let the hub install itself as node administrator. BREAKING: gek_bundle_store is deleted, not gated. No member hands the node key material at all, so C5b becomes structural rather than an authorization to check. Existing stored bundles are still served, so current deployments keep working. Also: - join_policy (invite|open) is read from node.toml, never from the hub — a hub able to declare a group open would be handed its key. Unknown group ⇒ invite. - admin signatures are verified against the roster on every check, so unpinning takes effect without a restart. admin_pk_ed25519 stays readable as legacy. - two C5b tests were rewritten, deliberately: they asserted that gek_bundle_store demanded an operator signature, and the message is gone. They now assert the stronger property. The file says not to fix these tests, so this is the record of why they changed. - a slice-1 bug found while writing slice 2: connect() never passed skEdB64, so pairing would have failed at runtime with no test able to catch it. Tests: 152 node+common here, including an end-to-end DataChannel run where a member who has never held the group key redeems a code in the pre-proof window and receives the key wrapped for a key only they can open. Design: docs/invite-pairing-v1.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(node)!: remove unauthenticated HTTP file API and TCP transportChristophe Besson2026-08-131-19/+10
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Phase 11.5.A — findings C1 and C6 (see second-review.md). C1: the per-group HTTP file API bound 0.0.0.0 for every configured group, private ones included, and served two endpoints with no authentication at all: GET /index (full Mesh Group Index) and GET /file/{id} (raw plaintext file via FileResponse). Anyone able to reach the port — LAN, forwarded port, permissive IPv6 — read every private file. This bypassed the entire GEK-proof and node sovereignty layer. Deleted rather than patched: it duplicated MNP without any of its controls. C6: the TCP+TLS chunk server accepted a bare JWT with no GEK proof, leaving a second non-compliant handshake path. Deleted; QUIC remains and will be brought to parity with WebRTC by the unified handshake in 11.5.4. Transport decision recorded in transport/__init__.py: WebRTC/ICE is primary for browser and native clients (the only NAT traversal validated here — 2 ISPs, IPv4 STUN + IPv6, 4G CGNAT); QUIC is kept for LAN, port-forwarded and hub-less group:// access. punch_nat() is a direct-connection helper, not a traversal stack. Also removed server_ssl_context()/client_ssl_context() from tls_cert.py (no remaining callers) and a dead import of the former in quic_server.py. generate_self_signed_cert() stays: QUIC uses it, and the certificate hash is the intended channel-binding anchor for 11.5.6, since QUIC has no DTLS fingerprint to bind the GEK proof to. BREAKING CHANGE: node.toml keys `port` and `http_port` are gone. Regenerate config with `meshbay-node init`. Env var MESHBAY_PORT -> MESHBAY_QUIC_PORT. Tests: 198 passed (209 - 7 test_http_server - 4 test_transport). No other test changed status. Net -1300 lines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat: Phase 12 — P2P crypto material, password split, node Ed25519 authChristophe Besson2026-08-131-3/+9
| | | | | | | | | | | | | | | | Baseline commit capturing in-progress Phase 12 work that was already present in the working tree (uncommitted) before the Phase 11.5 security remediation begins. Committed as-is, without review or modification, so that remediation changes arrive as a separable diff. Contents: BundleStore (P2P GEK + keypair bundles), password split (auth_key / bundle_key), node Ed25519 auth (POST /v1/nodes/auth, node-scoped JWT), GEK-HMAC handshake proof with DTLS channel binding, Ed25519 admin challenge-response, node local admin UI rewrite, browser key persistence. Not authored in this session — captured to establish a baseline. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(node): Phase 11 — production-ready daemon with WebRTC, WS, chat, HTTPChristophe Besson2026-08-111-0/+4
| | | | | | | | | | | | | | | | | | The node daemon was previously a skeleton that only started QUIC/TCP servers and the local web UI. All browser-facing functionality (WebRTC, hub WebSocket, chat store, HTTP file API) lived in QE demo scripts. This rewrites daemon.py to be fully self-contained: - WebRTC transport for browser clients (aiortc DataChannel) - Hub WebSocket task (signaling, revocations, WebRTC offers) - ChatStore per group (SQLite in ~/.local/share/meshbay/) - HTTP file API per group (create_http_app on configured port) - Graceful shutdown (all transports, stores, tasks) - hub_client: _ws tracking + send_ws() for chat notifications - config: data_dir field for persistent state - systemd: security hardening (ProtectSystem, StateDirectory) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: Phase 7 — Node v2 (multi-group, Sender Keys, 0-RTT, chat, denylist)Christophe Besson2026-08-101-1/+3
| | | | | | | | | | | | | | | | | | | | | | | | | | | Implements all 8 milestones (7.0-7.7): - 7.0: JWT carries `groups` claim; node verifies group membership at MNP handshake (QUIC + TCP+TLS). Resolves security review C2. - 7.1: QUIC 0-RTT session resumption via stored session tickets (17-21ms reconnect vs 47ms cold). - 7.2: Hub→node WebSocket signaling for NAT punch coordination (`client_incoming`/`punch_ready`) + jti denylist push. Denylist class blocks revoked users/jtis at handshake. - 7.3: Multi-group daemon — one QUIC port serves N groups with per-group GEK, shared_root, and index routing. - 7.4: HLS streaming via QUIC (STREAM_SEGMENT message type, ffmpeg segment extraction). - 7.5: Sender Keys protocol for group chat (Signal Groups approach). Each member has own sending chain key, HKDF chain ratchet, AES-256-GCM encryption, Ed25519 signing. Resolves security review C1. - 7.6: Chat store (SQLite via aiosqlite), CHAT_MESSAGE MNP wire type with peer broadcast, web UI with WebSocket push. - 7.7: Argon2id calibration CLI. First security review included (first-review.md). 109 tests, demo-v3 validated against meshbay.org production hub. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(node): multi-group config support — 6.2Christophe Besson2026-08-091-24/+65
| | | | | | | | | Config now supports [[groups]] array (N groups) alongside back-compat [group] single section. Each group has independent port/quic_port/http_port and visibility. GroupConfig gains quic_port, http_port, visibility fields. NodeConfig gains quic_port and http_port defaults. 70/70 tests. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* feat(node): add config, daemon, and local web UIChristophe Besson2026-08-091-0/+123
config.py: TOML + env var overrides, sane defaults. daemon.py: full startup sequence (keystore→hub→GEK→indexer→ server→UI), SIGINT/SIGTERM shutdown, calibrate-argon2 command. ui/app.py: FastAPI on localhost:18000, status+files JSON API, HTML status page (auto-refresh 10s). All bound to 127.0.0.1. Full suite: 29/29 tests. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>