aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/MESHBAY_DESIGN.md24
-rw-r--r--docs/MESHBAY_NODE_PROTOCOL.md52
2 files changed, 54 insertions, 22 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md
index 6daa778..07aef05 100644
--- a/docs/MESHBAY_DESIGN.md
+++ b/docs/MESHBAY_DESIGN.md
@@ -16,7 +16,7 @@
> them — it names the invariant that holds today, not the incident that produced
> it. §13 is the register of those labels.
>
-> Wire versions at the time of writing: **MNP 4.0** (oldest peer accepted 4.0),
+> Wire versions at the time of writing: **MNP 5.0** (oldest peer accepted 4.0),
> **MHP 0.1**, packages **0.16.0**. The normative source for the wire format is
> `MESHBAY_NODE_PROTOCOL.md`; this document states the design the protocol
> serves, not its byte layout.
@@ -99,7 +99,7 @@ opens them.
│ └─────────┘
MHP 0.1 │ signalling (SDP/ICE, <1 KB), presence, revocation push MHP
│
- ┌────┴────┐ MNP 4.0 ┌──────────┐
+ ┌────┴────┐ MNP 5.0 ┌──────────┐
│ node │◄──────── WebRTC DataChannel / QUIC ──────────►│ client │
└─────────┘ index, file chunks, streams, chat, admin └──────────┘
holds the files browser SPA or desktop
@@ -1070,7 +1070,15 @@ TTL 120 s. **The client reconstructs the transcript from announced fields and
refuses to sign if the operation or subject is not what the user asked for**
(**H5**) — a challenge of opaque random bytes signed blind is an unbound signing
oracle. The transcript's subject names the *outcome*, not the operation: what the
-operator is shown before signing has to be what happens.
+operator is shown before signing has to be what happens. **Every value the node acts
+on is in the subject**: the signature covers nothing else of the request, so an
+operation whose effect is several values signs all of them as canonical JSON — a
+root's path *and* whether every member may write there, a group's name *and* the
+directory it exposes — and a secret by its SHA-256, since the subject is audited.
+
+What waits for a signature is bounded: anyone authenticated can ask for a challenge,
+so a connection holds at most eight pending, each at most 64 KiB, expired ones
+dropped (**AV31**).
Verification is against `roster.operator_pks()`, rebuilt from node state, **never**
from anything in the response.
@@ -1285,10 +1293,11 @@ checks the version its peer declared and **branches on none of it**.
> which point it silently takes the other. **A field kept "just in case" is how
> the branches come back.**
-**The floor is not necessarily the current version, and MINOR additions are why.**
-It is `MNP_MIN_SUPPORTED` in `handshake.py` and it equals the last MAJOR. Today the
-two coincide at 4.0, but 3.1, 3.2, 3.3 and 3.4 were each added above the 3.0 floor
-without moving it, and the next MINOR will be added above 4.0 the same way. So a
+**The floor is not necessarily the current version, and what is added above it is why.**
+It is `MNP_MIN_SUPPORTED` in `handshake.py`, and it is the last MAJOR that had to
+refuse at the handshake: 3.1–3.4 were added above the 3.0 floor without moving it,
+and 5.0, a MAJOR confined to four signed operations that a peer across the break
+refuses to sign, sits above the 4.0 floor. So a
peer can be reachable and still not do something the current version can, and the
client has to cope with that — **by reading the peer's own answer, never by comparing
version numbers**.
@@ -3270,6 +3279,7 @@ had already been asked.
| **AV29** | **An invitation link is bounded on both halves and its mail on the sender** (§3.4, §7.3). Twenty outstanding per group on the node (bearer codes) and on the hub (tickets); and because a link mail reaches an address the hub has no relationship with, at the request of anyone who owns a group, it is counted **per sending account per day** (`mail.invite_link_daily_cap`, 10), under the recipient and instance bounds and outside the recovery reserve (`invite_link` is not a recovery purpose) |
| **AV28** | **How many node keys one account may announce is bounded** (§7.2). Each is a row plus an IP-log row under a one-year retention, so an account in a loop writes a year of storage on the operator's disk having paid only for signatures. Proof of possession (**M8**) settles whose key it is and not how many. Counted only where a row is added: re-announcing a key already held keeps working at the ceiling, or a node that reached it could never refresh its address again |
| **AV30** | **What one member's offers cost a node is bounded per account and per node, and the bound admits the heaviest ordinary account** (§7.2). Each offer makes the node allocate a peer connection. A budget of 120 per node refilled at two a second bounds a member there without touching their other nodes, and it is counted by account because a mobile carrier shares one IPv4 address among many subscribers. Pending offers are capped at 32 per account. Both refusals carry `Retry-After` and the client retries them, because a refused offer otherwise reads as a node that is down |
+| **AV31** | **What waits for a signature is bounded** (§5.4). Any authenticated member can ask for an admin challenge, since the signature is checked afterwards, and a pending challenge kept its whole request until answered — measured, 200 requests of 1 MiB held 400 MiB for the life of one connection. At most eight pending per connection, 64 KiB each, expired ones dropped |
### 13.6 Chat design findings
diff --git a/docs/MESHBAY_NODE_PROTOCOL.md b/docs/MESHBAY_NODE_PROTOCOL.md
index e4219b5..5bf367b 100644
--- a/docs/MESHBAY_NODE_PROTOCOL.md
+++ b/docs/MESHBAY_NODE_PROTOCOL.md
@@ -1,6 +1,6 @@
# MeshBay Node Protocol (MNP)
-**Wire version:** `4.0` — `meshbay_common/__init__.py` (`MNP_VERSION`)
+**Wire version:** `5.0` — `meshbay_common/__init__.py` (`MNP_VERSION`)
**Oldest peer accepted:** `4.0` — `handshake.py` (`MNP_MIN_SUPPORTED`)
**Normative implementation:** `meshbay-common` (`protocol.py`, `handshake.py`,
`groupbox.py`, `chatbox.py`, `adminop.py`, `join.py`, `device.py`, `crypto.py`,
@@ -157,7 +157,7 @@ Codes in use:
| Upload | `upload_not_sealed`, `no_group_key`, `lease_not_granted`, `upload_incomplete`, `bad_chunk_encoding`, `bad_chunk_index`, `invalid_filename`, `no_roots`, `no_such_root`, `no_writable_root`, `root_read_only`, `root_unavailable`, `no_such_directory`, `already_exists`, `not_started`, `too_large` (§11.4) |
| Directories | `root_read_only`, `root_unavailable` (§11.5) |
| Chat | `chat_too_large`, `chat_rate_limited` (§11.7) |
-| Operator controls | `not_operator` (§10.4) |
+| Operator controls | `not_operator`, `too_many_pending`, `too_large` (§10.4) |
| Metadata | `transcode_not_applicable`, `tmdb_search_rate_limited` (§11.9) |
Everything else refuses with `detail` alone. A code is added when a client has a
@@ -1099,7 +1099,7 @@ broadcast, every connected peer in the group learns the change without reconnect
|---|---|---|---|---|
| `file_delete` | `file_id` | operator **or** any non-revoked device of the uploading account (`uploader_id`, looked up in the roster); the recorded `uploader_pk` only when the roster cannot answer | `file_delete_ack{file_id}` | no |
| `dir_delete` | path relative to the root | operator | `dir_delete_ack{dir}` | no |
-| `invite_create` | invitee `user_id` | operator only (delegation designed, deferred) | `invite_result{code, expires_at, user_id, username}` | no — the code is shown once |
+| `invite_create` | `{user_id, username}` (canonical JSON, see below) | operator only (delegation designed, deferred) | `invite_result{code, expires_at, user_id, username}` | no — the code is shown once |
| `invite_link_create` | `link:<group_id>`, the session's group | operator only | `invite_link_result{code, invite_id, expires_at, group_id}` | no — the code is shown once |
| `invite_cancel` | `invite_id` (32 hex) | operator only | `ack{detail: "invite_cancelled", invite_id}` | no |
| `member_revoke` | `user_id` | operator | `member_revoke_ack` | no |
@@ -1107,12 +1107,13 @@ broadcast, every connected peer in the group learns the change without reconnect
| `gek_rotate` | `group_id` | operator | `gek_rotate_ack{group_id, authorized_members, note}` | no |
| `apps_enabled` | the app set | operator | `apps_enabled_ack{apps}` | yes |
| `set_scan_settings` | the interval/debounce pair | operator | `set_scan_settings_ack{...}` | yes |
-| `tmdb_config` | `custom_token=yes\|no,language=...` | operator | `tmdb_config_ack{token_customized, language}` | yes (never the token) |
+| `tmdb_config` | `{token, language}` — `token` is `null` (unchanged), `""` (clear) or `sha256:<hex>` of the token, never the token | operator | `tmdb_config_ack{token_customized, language}` | yes (never the token) |
| `tmdb_enabled` | `enabled` | operator | `tmdb_enabled_ack{enabled}` | yes |
| `tmdb_override` | `file_id=..,tmdb_id=..,media_type=..` | operator | `tmdb_override_ack{file_id, tmdb_id, media_type}` | yes |
| `tmdb_rematch` | `file_id=..` | operator | `tmdb_rematch_ack{file_id}` | yes |
| `musicbrainz_enabled` | `enabled` | operator | `musicbrainz_enabled_ack{enabled}` | yes |
-| `root_add`, `root_remove` | the root | operator | `root_add_ack` / `root_remove_ack` | no |
+| `root_add` | `{path, name, kind, writable, removable}` | operator | `root_add_ack` | no |
+| `root_remove` | the root name | operator | `root_remove_ack` | no |
| `root_update` | `<root>:rw=on\|off,rem=on\|off` | operator | `root_update_ack` | yes |
| `root_eject`, `root_plug` | the root name | operator | `root_eject_ack` / `root_plug_ack` | yes |
| `app_directories` | `<app>:<dir>,<dir>,...` | operator | `app_directories_ack{app, dirs}` | yes |
@@ -1121,7 +1122,8 @@ broadcast, every connected peer in the group learns the change without reconnect
| `search_listed` | `on\|off` | operator | `search_listed_ack{listed}` | yes |
| `transfer_limits` | `d=<n>,u=<n>` | operator | `transfer_limits_ack{limits}` | yes |
| `chat_epoch` | `group_id` | operator | `chat_epoch_ack{epoch}` | yes |
-| `group_attach`, `group_detach` | `group_id` | operator | `group_attach_ack` / `group_detach_ack` | no |
+| `group_attach` | `{name, shared_dir, writable}` | operator | `group_attach_ack` | no |
+| `group_detach` | the group name | operator | `group_detach_ack` | no |
**Upload policy is not in this table**, and that is the design: whether a member may
write is a property of each root (`root_update`), not a switch over the group. A single
@@ -1159,6 +1161,20 @@ as the CLI and the loopback admin API. The distinction from the signed table abo
a signed op proves possession of an operator key for *this* operation, while these
prove it once per connection, through the device the connection identified.
+**The subject covers everything the node acts on.** The signature covers `op`, the
+node, the group, the subject, the nonce and the time — nothing else of the request —
+so a value the executor uses and the subject omits is a value the operator never
+signed. Where an operation's effect is several values, the subject is canonical JSON
+of all of them (`adminop.structured_subject`, `adminSubject` in `crypto.js`: sorted
+keys, no whitespace, UTF-8), which keeps `null`, `""` and a value distinct and cannot
+be forged by a field that contains a separator. A secret is named by its SHA-256,
+because the subject is written to the audit log.
+
+**What waits for a signature is bounded.** Any authenticated member can ask for a
+challenge — the signature is checked later — so a connection holds at most 8 pending
+operations (`too_many_pending` beyond), each at most 64 KiB of subject and payload
+(`too_large`), and an expired one is dropped when the next is issued.
+
Rules that hold across the table:
* **No MNP message can activate a group key.** The rule (I2) targets key material
@@ -2103,14 +2119,20 @@ message:
## 13. Versioning and compatibility
-MNP versions independently of the package version. Current: **`4.0`**; oldest peer
-accepted: **`4.0`**. 4.0 is a MAJOR: a member presents a short-lived node-audience token
-bound to one node (§6.3) instead of its hub session token, which is a change to what a
-peer must *present*, so a pre-4.0 client is refused at the handshake and the floor moved
-with the version. It carries everything 3.x added — the challenge signature (§6.5),
-invitation links (§8.6), audio-track and subtitle selection, per-account blobs — so
-nothing is currently above the floor, and every capability this document describes is
-one every reachable peer has.
+MNP versions independently of the package version. Current: **`5.0`**; oldest peer
+accepted: **`4.0`**.
+
+4.0 is the floor: a member presents a short-lived node-audience token bound to one node
+(§6.3) instead of its hub session token, a change to what a peer must *present*, so a
+pre-4.0 client is refused at the handshake. It carries everything 3.x added — the
+challenge signature (§6.5), invitation links (§8.6), audio-track and subtitle
+selection, per-account blobs.
+
+5.0 is a MAJOR confined to four signed operations — `root_add`, `group_attach`,
+`invite_create`, `tmdb_config` — whose subjects now name every value the node acts on
+(§10.4). A peer across the break refuses to sign the other side's subject, so those
+four fail with a refusal and everything else works; no node accepts the old subjects,
+so nothing is left unsigned on either side. That is why the floor did not move.
The two numbers are separate on purpose. `MNP_VERSION` says what this build speaks;
`MNP_MIN_SUPPORTED` says what it will talk to, and moving the second is a decision about
@@ -2319,7 +2341,7 @@ LP(x) = uint32be(len(x)) || x every field, no exceptions
| Constant | Value | Source |
|---|---|---|
-| `MNP_VERSION` | `4.0` | `meshbay_common/__init__.py` |
+| `MNP_VERSION` | `5.0` | `meshbay_common/__init__.py` |
| `MNP_MIN_SUPPORTED` | `4.0` | `handshake.py` |
| `MNP_AUD` / `HUB_API_AUD` | `meshbay:mnp` / `meshbay:hub-api` | `tokens.py` |
| MNP token lifetime | 900 s | `meshbay-hub/auth.py` (`issue_mnp_token`) |