diff options
Diffstat (limited to 'docs/MESHBAY_DESIGN.md')
| -rw-r--r-- | docs/MESHBAY_DESIGN.md | 34 |
1 files changed, 27 insertions, 7 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md index e9b63b5..2966e5e 100644 --- a/docs/MESHBAY_DESIGN.md +++ b/docs/MESHBAY_DESIGN.md @@ -286,7 +286,11 @@ The binding is a **one-time code the new device generates and displays**, hashed together with the new keys: `code_hash = sha256(code ‖ new_pk_ed25519 ‖ new_pk_x25519)`. The approving device asks the node for this account's pending requests **with their stored hashes** and recomputes the hash for each until one -matches. +matches. **An approval answers a pending request**: it names the request's hash, +and the node admits the keys only if that request was filed by those same keys. A +countersignature alone — however it was obtained — admits nothing. Retiring a +device is signed under a prefix of its own, never the admission transcript: a +signature given to retire a key would otherwise admit it. > **The code never reaches the node.** That is what makes a substituted key > impossible rather than merely detectable: a node offering fabricated keys would @@ -546,7 +550,7 @@ declare a group open would be handed its key. An unknown group reads as `invite` **Changing a known passphrase** re-seals every reachable node's identity bundle under the new `M` **before** touching the hub — if the fan-out fails, the account is unchanged. The pepper does not change, and the client, which does not keep it, -asks for it with the open session (`GET /v1/users/me/bundle-pepper`). Only then is +asks for it with the old passphrase (`POST /v1/users/me/bundle-pepper`). Only then is `POST /v1/users/password` called with the old and new `auth_key`. In a browser, nodes that were unreachable are named to the user, with the operator fallback (`member unpin` plus a fresh code) as the way to fix each one. The desktop @@ -616,10 +620,14 @@ The **pepper** is 32 random bytes per account, created by the hub on first use, sealed at rest with the key that seals e-mail addresses and bound to the account (`auth.seal_pepper`). The hub hands it out only where the caller has just proved the passphrase or a device key: in the response of `POST /v1/users/login` and -`POST /v1/users/auth`, and from `GET /v1/users/me/bundle-pepper` for a session -that did (a stored session from before, a passphrase change). **Never** on a token -refresh — that proves possession of a refresh token and nothing else — never to a -node token, never inside a token, never in a log. Erasing the account clears it; +`POST /v1/users/auth`, and from `POST /v1/users/me/bundle-pepper` with the +passphrase (`auth_key`) for a session that lacks the key derived from it (a +restored session, a passphrase change). **Never to a token alone** — a refreshed +one proves possession of a refresh token and nothing else, and one lifted from a +page would put the pepper beside the bundles an operator keeps — never to a node +token, never inside a token, never in a log. For the same reason **registering a +device's hub key takes the passphrase too**: a registered key signs in without +it, and every such sign-in carries the pepper. Erasing the account clears it; a passphrase reset keeps it. What that buys is the reason for all of it: **the tag of a bundle on a node's disk @@ -2387,7 +2395,19 @@ What running it establishes, and what each fact costs: (`transport.js`, `_identityFromKeys` in a browser, `_nativeIdentityHandle` here), so nothing above it knows where the keys are — and no code in the page reads a private key outside that object (`test_identity_seam.py`). Whether a - node keeps a sealed copy is the account's browser access, decided here. + node keeps a sealed copy is the account's browser access, decided here, and the + keyring refuses to seal one while it is off, whatever the page asks. +- **An identity signs a kind, never bytes.** The page names what it is signing — + a join, a device's hello, a device request, an approval, a retirement, a chat + line, an admin operation — and gives the fields; the main process builds the + transcript itself (`transcripts.js`, byte for byte `meshbay_common` and the + page's `transcriptFor`), with the identity's own public keys wherever one is + named, refuses a field naming another node or another account or a moment far + from now, and signs no other kind. A page able to submit bytes would obtain a + signature over anything, reusable outside the operation it claimed to be for. + What a page can still obtain is a signature of a named kind within the session + it runs in — the same reach as the person using the window, which is the scope + accepted for the renderer. - **OS-backed secret storage is real on a desktop and honest without one.** With a keyring it is keyring-backed; headless, the same code reports unavailable and **refuses to store rather than downgrading silently**. |