diff options
Diffstat (limited to 'docs/MESHBAY_NODE_PROTOCOL.md')
| -rw-r--r-- | docs/MESHBAY_NODE_PROTOCOL.md | 49 |
1 files changed, 40 insertions, 9 deletions
diff --git a/docs/MESHBAY_NODE_PROTOCOL.md b/docs/MESHBAY_NODE_PROTOCOL.md index b2508c1..ed06edd 100644 --- a/docs/MESHBAY_NODE_PROTOCOL.md +++ b/docs/MESHBAY_NODE_PROTOCOL.md @@ -1,6 +1,6 @@ # MeshBay Node Protocol (MNP) -**Wire version:** `3.3` — `meshbay_common/__init__.py` (`MNP_VERSION`) +**Wire version:** `3.4` — `meshbay_common/__init__.py` (`MNP_VERSION`) **Oldest peer accepted:** `3.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`, @@ -385,7 +385,8 @@ fails if a transport skips a step. | 4. GEK exists for group | | | | 5. handshake_challenge | - | {v, v_min, nonce: b64(nonce_s), node_pk} | + | {v, v_min, nonce: b64(nonce_s), node_pk, | + | sig: b64(Ed25519(C))} (3.4, section 6.5) | |<--------------------------------------------------------------| | | | 5b. client checks the node's range the same way | @@ -508,10 +509,37 @@ This is deliberate and safe: * a first-time joiner needs it *before* the ack — `join_request` signs a transcript naming this node (§8.2), and someone who has never held the GEK cannot complete the handshake that would prove the key; -* it is **unverified at that point**. The ack proves possession and signs the - transcript; the client refuses if `ack.node_pk` differs from the announced value; +* the ack proves possession and signs the transcript; the client refuses if + `ack.node_pk` differs from the announced value; * a wrong value only makes the node's own verification fail. +**Since 3.4 it is also signed, in the challenge itself:** + +``` +C = "meshbay:mnp:challenge:v1" || LP(group_id) || LP(nonce_c) || LP(nonce_s) || LP(binding) +sig = Ed25519(sk_node, C) +``` + +`binding` is the transport's channel binding (§6.4), already known when the challenge +is sent; `nonce_c` makes the signature fresh. So it cannot be recorded and replayed, +nor relayed through a peer with different fingerprints. The prefix is its own, so it +is never interchangeable with the ack's signature over `T("node")`. + +Why it exists: a join (§8) is sent **in the pre-proof window**, before the ack — so +before 3.4 an invitation code went to whichever peer answered signaling, and in a +group with two hosts, to whichever host answered first. With the signature, a client +that knows which node key to expect can refuse to send a code anywhere else. + +What it does not do: it proves that the peer holds *the key it announces*, not that +this is the key the client wanted. A client with no expectation learns nothing more +than before, and the ack remains the proof of GEK possession. + +Client rule, from the node's answer and never from its version (§13): a `sig` that +does not verify is a refusal (`Node challenge signature invalid`); no `sig` is an +older node, whose key is proved only at the ack. A node signs whenever it has a +binding, and a node with none sends no signature rather than an unbound one — the +proof would be refused on that connection anyway. + ### 6.6 `handshake_ack` fields Three fields are in clear, and the rest travel **sealed under a group-key-derived @@ -1899,7 +1927,7 @@ it back (§3.5). | Type | Dir | Stage | Purpose | |---|---|---|---| | `handshake` | C→N | — | open the session; version range, token, group, client nonce | -| `handshake_challenge` | N→C | — | node nonce, node's version range, announced `node_pk` | +| `handshake_challenge` | N→C | — | node nonce, node's version range, announced `node_pk`, and since 3.4 `sig` over the challenge transcript | | `handshake_response` | C→N | — | client's `HMAC(GEK, T("client"))` | | `handshake_ack` | N→C | — | node proof, node signature, session parameters **sealed** (§11.1a) | | `keypair_bundle_fetch` / `_resp` | C→N / N→C | pre | the caller's encrypted identity bundle | @@ -2012,9 +2040,9 @@ message: ## 13. Versioning and compatibility -MNP versions independently of the package version. Current: **`3.3`**; oldest peer -accepted: **`3.0`** — 3.1, 3.2 and 3.3 are all additive, so the floor does not move with -them. +MNP versions independently of the package version. Current: **`3.4`**; oldest peer +accepted: **`3.0`** — 3.1, 3.2, 3.3 and 3.4 are all additive, so the floor does not move +with them. 3.4 adds the challenge signature (section 6.5). 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 @@ -2163,6 +2191,9 @@ walks through the gate meant to stop it. handshake "meshbay:mnp:handshake:v1" LP(role) LP(group_id) LP(nonce_c) LP(nonce_s) LP(binding) -> HMAC-SHA256 under the GEK; role in {"client","node"} +challenge "meshbay:mnp:challenge:v1" LP(group_id) LP(nonce_c) LP(nonce_s) LP(binding) + -> Ed25519 by the node key, in handshake_challenge (3.4) + admin op "meshbay:admin:v1" LP(op) LP(node_pk) LP(group_id) LP(subject) LP(nonce) LP(ts) -> Ed25519 by an operator key from the roster @@ -2212,7 +2243,7 @@ LP(x) = uint32be(len(x)) || x every field, no exceptions | Constant | Value | Source | |---|---|---| -| `MNP_VERSION` | `3.2` | `meshbay_common/__init__.py` | +| `MNP_VERSION` | `3.4` | `meshbay_common/__init__.py` | | `MNP_MIN_SUPPORTED` | `3.0` | `handshake.py` | | `NONCE_LEN` | 32 bytes (both handshake nonces) | `handshake.py` | | `ADMIN_CHALLENGE_TTL` | 120 s | `adminop.py` | |