summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-common/src/meshbay_common/join.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-common/src/meshbay_common/join.py')
-rw-r--r--packages/meshbay-common/src/meshbay_common/join.py63
1 files changed, 63 insertions, 0 deletions
diff --git a/packages/meshbay-common/src/meshbay_common/join.py b/packages/meshbay-common/src/meshbay_common/join.py
new file mode 100644
index 0000000..6ee543f
--- /dev/null
+++ b/packages/meshbay-common/src/meshbay_common/join.py
@@ -0,0 +1,63 @@
+"""
+Join and pairing transcript (MNP).
+
+A client proves, in one signature, that the X25519 key it wants the group key
+wrapped for belongs to the Ed25519 identity the node pins. Both keys travel inside
+the transcript, so the identity key vouches for the encryption key it is paired
+with — that is what makes "wrap the GEK for the key the peer presented" safe.
+
+Why this exists at all (H3): the invite flow used to fetch the invitee's public key
+from the hub and wrap the group key for whatever came back. The hub is the key
+directory, so a hub answering with its own key was handed the GEK by an honest
+inviter following the protocol exactly. The key now comes from the peer over an
+authenticated channel and is bound to an identity by a one-time pairing code the
+hub never sees. See `docs/invite-pairing-v1.md`.
+
+Fields are length-prefixed and domain-separated, per L4 — the same rule as
+`handshake.py` and `adminop.py`. `nonce_node` is the handshake nonce the node just
+issued, so a signed join cannot be lifted onto another connection.
+"""
+
+from __future__ import annotations
+
+JOIN_PREFIX = b"meshbay:join:v1"
+
+# A join older than this is refused. Same value as the admin challenge: both are
+# interactive exchanges that complete in milliseconds.
+JOIN_TTL = 120 # seconds
+
+ROLE_OPERATOR = "operator"
+ROLE_DELEGATE = "delegate" # reserved; delegation is deferred (§6.2 of the design)
+ROLE_MEMBER = "member"
+
+
+def join_transcript(
+ node_pk_b64: str,
+ group_id: str,
+ user_id: str,
+ pk_ed25519_b64: str,
+ pk_x25519_b64: str,
+ nonce_node: bytes,
+ ts: int,
+) -> bytes:
+ """
+ Bytes signed by a client asking to be pinned by, or recognised on, a node.
+
+ `group_id` is empty for operator pairing, which is node-wide rather than
+ per-group. The node builds this from its own state and the values in the
+ message; nothing signed is ever taken from the wire unverified.
+ """
+ fields = [
+ node_pk_b64.encode(),
+ group_id.encode(),
+ user_id.encode(),
+ pk_ed25519_b64.encode(),
+ pk_x25519_b64.encode(),
+ nonce_node,
+ str(ts).encode(),
+ ]
+ out = bytearray(JOIN_PREFIX)
+ for field in fields:
+ out += len(field).to_bytes(4, "big")
+ out += field
+ return bytes(out)