aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/MESHBAY_DESIGN.md34
-rw-r--r--docs/MESHBAY_NODE_PROTOCOL.md14
2 files changed, 40 insertions, 8 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**.
diff --git a/docs/MESHBAY_NODE_PROTOCOL.md b/docs/MESHBAY_NODE_PROTOCOL.md
index 9efef08..9e51dc9 100644
--- a/docs/MESHBAY_NODE_PROTOCOL.md
+++ b/docs/MESHBAY_NODE_PROTOCOL.md
@@ -1001,6 +1001,9 @@ D_req = "meshbay:device_req:v1" || LP(node_pk) || LP(user_id) || LP(pk_ed) ||
D_add = "meshbay:device_add:v1" || LP(node_pk) || LP(user_id) || LP(pk_ed) ||
LP(pk_x) || LP(nonce_s) || LP(ts) signed by a PINNED device
+D_rev = "meshbay:device_revoke:v1" || LP(node_pk) || LP(user_id) || LP(pk_ed) ||
+ LP(nonce_s) || LP(ts) signed by a PINNED device
+
code_hash = sha256( code "\x1f" pk_ed25519_b64 "\x1f" pk_x25519_b64 )
```
@@ -1009,6 +1012,11 @@ code_hash = sha256( code "\x1f" pk_ed25519_b64 "\x1f" pk_x25519_b64 )
* `D_add` deliberately **omits the code**: the code is a bearer secret used to find the
request, never signed, never echoed. What is signed is the key pair being admitted,
so a signature collected for one device cannot admit another.
+* `device_add` **must name a pending request** (`code_hash`) filed by the same
+ `pk_ed25519` and `pk_x25519`; without one, or with one filed by other keys, it is
+ refused and the request is not spent. A countersignature alone admits nothing.
+* `D_rev` has a prefix of its own. Were a retirement signed over `D_add`, a signature
+ given to retire a key would admit that key wherever it is not pinned yet.
* Because both keys go into `code_hash`, a node cannot answer the approver with a
substituted key: the approver recomputes the hash from what it typed and what it was
given. Nothing here rests on a human comparing digits.
@@ -1022,7 +1030,7 @@ code_hash = sha256( code "\x1f" pk_ed25519_b64 "\x1f" pk_x25519_b64 )
N -> C device_list_result {pending, devices: [{pk_ed25519, label, pinned_at,
pinned_via, added_by_pk, is_this_one}]}
- C -> N device_revoke {pk_ed25519, ts, sig over D_add for the victim's keys}
+ C -> N device_revoke {pk_ed25519, ts, sig over D_rev for the victim's key}
N -> C device_add_ack {revoked: pk_ed25519}
```
@@ -2361,6 +2369,10 @@ device add "meshbay:device_add:v1" LP(node_pk) LP(user_id) LP(pk_ed25519) LP
LP(nonce_s) LP(ts)
-> Ed25519 by an ALREADY-PINNED device of the same account
+device rev "meshbay:device_revoke:v1" LP(node_pk) LP(user_id) LP(pk_ed25519)
+ LP(nonce_s) LP(ts)
+ -> Ed25519 by an ALREADY-PINNED device of the same account
+
device hello "meshbay:device_hello:v1" LP(node_pk) LP(group_id) LP(user_id)
LP(pk_ed25519) LP(nonce_s) LP(ts)
-> Ed25519 by the device claiming this connection