1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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/MESHBAY_DESIGN.md` §3.4.
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)
|