aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py45
1 files changed, 36 insertions, 9 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
index 370a8b4..0154319 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
@@ -45,11 +45,13 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
from meshbay_common import MNP_VERSION
from meshbay_common.handshake import (
+ MNP_MIN_SUPPORTED,
NONCE_LEN,
ROLE_CLIENT,
ROLE_NODE,
HandshakeError,
authorize_token,
+ check_version,
handshake_transcript,
make_proof,
verify_proof,
@@ -87,6 +89,7 @@ from meshbay_common.device import (
device_code_hash,
device_request_transcript,
)
+from meshbay_common.groupbox import PURPOSE_ACK, seal
from meshbay_common.join import (
JOIN_TTL,
ROLE_MEMBER,
@@ -609,6 +612,15 @@ class WebRTCPeerSession:
group_id = msg.get("group_id", "")
log.info("WebRTC handshake request: group=%s (peer=%s)",
group_id[:8] if group_id else "none", self._peer_id)
+ # Before the token, and before anything is decided from it: a peer we
+ # cannot speak to is refused with a code it can act on, rather than
+ # served messages it will misread as missing fields (L2).
+ try:
+ check_version(msg.get("v", ""), msg.get("v_min", ""))
+ except HandshakeError as refusal:
+ self._send({"type": "error", "detail": str(refusal),
+ "code": refusal.code})
+ return
try:
peer = authorize_token(
msg.get("token", ""),
@@ -655,6 +667,9 @@ class WebRTCPeerSession:
self._send({
"type": MNP.HANDSHAKE_CHALLENGE,
"v": MNP_VERSION,
+ # Our half of the range. The client refuses us on this rather than
+ # discovering the mismatch when a field it expected is not there.
+ "v_min": MNP_MIN_SUPPORTED,
"nonce": base64.b64encode(self._gek_challenge).decode(),
# Announced here because a first-time joiner needs it *before* the
# ack: join_request signs a transcript naming this node, and someone
@@ -729,13 +744,15 @@ class WebRTCPeerSession:
gek, ROLE_NODE, self._group_id or "", self._nonce_client,
self._gek_challenge or b"", binding)
- ack = {
- "type": MNP.HANDSHAKE_ACK,
- "v": MNP_VERSION,
- "node_pk": pk_to_b64(self._ctx["sk_node"].public_key()),
- "proof": base64.b64encode(node_proof).decode(),
- "sig": base64.b64encode(
- self._ctx["sk_node"].sign(node_transcript)).decode(),
+ # Everything the client needs in order to *authenticate* us stays in clear —
+ # node_pk, proof and sig are what it checks before it would trust a
+ # decryption, so they cannot themselves be behind one. The configuration
+ # below is sealed under a GEK-derived subkey, which gives it an
+ # authentication tag from a key the hub does not hold. Until MNP 1.0 the
+ # signed transcript named no ack field at all, so is_node_admin,
+ # enabled_apps, video_root and the rest were authenticated by the DTLS
+ # channel and nothing else.
+ config = {
"is_node_admin": self._is_node_admin(),
# So the interface knows whether to offer uploading at all. Not a
# permission — the node refuses regardless — but without it the
@@ -790,10 +807,20 @@ class WebRTCPeerSession:
},
}
if node_user_id:
- ack["node_user_id"] = node_user_id
+ config["node_user_id"] = node_user_id
pk_x_b64 = self._ctx.get("pk_x25519_b64")
if pk_x_b64:
- ack["node_pk_x25519"] = pk_x_b64
+ config["node_pk_x25519"] = pk_x_b64
+
+ ack = {
+ "type": MNP.HANDSHAKE_ACK,
+ "v": MNP_VERSION,
+ "node_pk": pk_to_b64(self._ctx["sk_node"].public_key()),
+ "proof": base64.b64encode(node_proof).decode(),
+ "sig": base64.b64encode(
+ self._ctx["sk_node"].sign(node_transcript)).decode(),
+ **seal(gek, PURPOSE_ACK, MNP.HANDSHAKE_ACK, self._group_id or "", config),
+ }
self._send(ack)
self._audit("handshake")