""" 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)