diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-23 17:14:26 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-23 17:14:26 +0200 |
| commit | 339cb427f886a0177014126bb684335837eff067 (patch) | |
| tree | 5f79dc0df617be66287a06fc4f0c5dcc61ceb167 /packages/meshbay-common/src/meshbay_common/handshake.py | |
| parent | cd85808c13926c89a97987d320ac26391eae3267 (diff) | |
| download | meshbay-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.py | 25 |
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, |