aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-common/src/meshbay_common/handshake.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-23 17:14:26 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-23 17:14:26 +0200
commit339cb427f886a0177014126bb684335837eff067 (patch)
tree5f79dc0df617be66287a06fc4f0c5dcc61ceb167 /packages/meshbay-common/src/meshbay_common/handshake.py
parentcd85808c13926c89a97987d320ac26391eae3267 (diff)
downloadmeshbay-339cb427f886a0177014126bb684335837eff067.tar.gz
feat: the node signs its handshake challenge (MNP 3.4)
node_pk in handshake_challenge is now signed over the channel binding and both nonces, so a client can check the node key before a join rather than only at the ack. Both transports; the browser and the QUIC client refuse a wrong signature and treat an absent one as an older node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-common/src/meshbay_common/handshake.py')
-rw-r--r--packages/meshbay-common/src/meshbay_common/handshake.py25
1 files changed, 24 insertions, 1 deletions
diff --git a/packages/meshbay-common/src/meshbay_common/handshake.py b/packages/meshbay-common/src/meshbay_common/handshake.py
index 4d2cac2..73e0b2e 100644
--- a/packages/meshbay-common/src/meshbay_common/handshake.py
+++ b/packages/meshbay-common/src/meshbay_common/handshake.py
@@ -12,7 +12,7 @@ The sequence:
client → node handshake {token, group_id, nonce_c, v, v_min}
node check_version() supported range, both ways
node authorize_token() JWT, scope, denylist, membership, hosting
- node → client handshake_challenge {nonce_s, v, v_min}
+ node → client handshake_challenge {nonce_s, v, v_min, node_pk, sig}
client → node handshake_response {proof}
node verify client proof HMAC(GEK, client transcript)
node → client handshake_ack {proof, sig, node_pk, nonce, ct}
@@ -65,6 +65,7 @@ import jwt
from meshbay_common import MNP_VERSION
HANDSHAKE_PREFIX = b"meshbay:mnp:handshake:v1"
+CHALLENGE_PREFIX = b"meshbay:mnp:challenge:v1"
# The oldest peer this build will talk to.
#
@@ -157,6 +158,28 @@ def handshake_transcript(
return bytes(out)
+def challenge_transcript(
+ group_id: str,
+ nonce_client: bytes,
+ nonce_node: bytes,
+ binding: bytes,
+) -> bytes:
+ """
+ Bytes the node signs in `handshake_challenge` (MNP 3.4).
+
+ The ack proves the node key, but a join is sent before the ack — so without
+ this, `node_pk` in the challenge was an announcement anyone answering
+ signaling could make. `nonce_client` makes the signature fresh and `binding`
+ pins it to this connection, so one cannot be recorded and relayed. No role
+ field: it has its own prefix, and nothing else is signed under it.
+ """
+ out = bytearray(CHALLENGE_PREFIX)
+ for field in (group_id.encode(), nonce_client, nonce_node, binding):
+ out += len(field).to_bytes(4, "big")
+ out += field
+ return bytes(out)
+
+
def make_proof(
gek: bytes,
role: str,