aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/transport/quic_client.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/transport/quic_client.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/quic_client.py40
1 files changed, 35 insertions, 5 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/quic_client.py b/packages/meshbay-node/src/meshbay_node/transport/quic_client.py
index 4debd27..b22b8df 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/quic_client.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/quic_client.py
@@ -23,11 +23,14 @@ from aioquic.quic.events import QuicEvent, StreamDataReceived
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from meshbay_common import MNP_VERSION
+from meshbay_common.groupbox import PURPOSE_ACK, PURPOSE_INDEX, unseal
from meshbay_common.protocol import MNP, file_chunk_plaintext
from meshbay_common.handshake import (
+ MNP_MIN_SUPPORTED,
NONCE_LEN,
ROLE_CLIENT,
ROLE_NODE,
+ check_version,
handshake_transcript,
make_proof,
quic_binding,
@@ -141,6 +144,8 @@ class QuicChunkClient:
# TLS session the server does not re-send its certificate.
self._peer_cert_der: bytes | None = peer_cert_der
self._session_ticket = session_ticket
+ # The handshake_ack's sealed payload, once connect() has opened it.
+ self._node_config: dict = {}
async def __aenter__(self):
await self.connect()
@@ -178,6 +183,10 @@ class QuicChunkClient:
self._proto._send(self._ctrl_stream, {
"type": MNP.HANDSHAKE,
"v": MNP_VERSION,
+ # The oldest node this build can talk to. Declared in the first
+ # message so a mismatch is a refusal with a code, not a field that
+ # turns up missing three messages later (L2).
+ "v_min": MNP_MIN_SUPPORTED,
"token": self._jwt_token,
"group_id": self._group_id,
"nonce": base64.b64encode(nonce_c).decode(),
@@ -186,6 +195,8 @@ class QuicChunkClient:
reply = await self._proto._recv(self._ctrl_stream)
if reply.get("type") != MNP.HANDSHAKE_CHALLENGE:
raise ConnectionError(f"QUIC handshake rejected: {reply}")
+ # The node's half of the range, checked before we speak to it further.
+ check_version(reply.get("v", ""), reply.get("v_min", ""))
nonce_s = base64.b64decode(reply["nonce"])
@@ -231,6 +242,15 @@ class QuicChunkClient:
except Exception as exc:
raise ConnectionError(f"Node signature invalid: {exc}") from exc
+ # Verify, then decrypt — in that order, and it is not incidental. The
+ # proof and the signature above are what decide whether this peer is worth
+ # trusting at all; opening the payload first would mean acting on data from
+ # someone we have not authenticated. Empty on this transport today (D5),
+ # but it must still open: a payload that does not is a peer we cannot talk
+ # to, not a node with no configuration.
+ self._node_config = unseal(
+ self._gek, PURPOSE_ACK, MNP.HANDSHAKE_ACK, self._group_id, ack)
+
log.debug("QUIC connected to %s:%d", self._host, self._port)
@property
@@ -253,14 +273,24 @@ class QuicChunkClient:
"""
Request the Mesh Group Index.
- Returns the message itself — `{group_id, version, entries, dirs, roots}` —
- which is what the WebRTC client has always received. It used to return the
- bytes of a `GroupIndex.serialize()` envelope for the caller to deserialize:
- the same message type carrying a different encoding on this transport alone.
+ Returns `{group_id, version, entries, dirs, roots}` — the sealed payload
+ opened, with `group_id` from the envelope that carried it. It used to return
+ the bytes of a `GroupIndex.serialize()` envelope for the caller to
+ deserialize: the same message type carrying a different encoding on this
+ transport alone.
+
+ A payload that does not open raises. It is never an empty index — that is
+ indistinguishable from a group with no files, which is why a fallback here
+ would be worse than a stop (groupbox.py, §3.4).
"""
sid = self._new_stream()
self._proto._send(sid, {"type": MNP.INDEX_SYNC, "v": MNP_VERSION})
- return await self._proto._recv(sid)
+ msg = await self._proto._recv(sid)
+ if msg.get("type") == "error":
+ raise LookupError(msg.get("detail", "index_sync refused"))
+ payload = unseal(
+ self._gek, PURPOSE_INDEX, MNP.INDEX_SYNC, self._group_id, msg)
+ return {"group_id": msg.get("group_id", self._group_id), **payload}
async def fetch_chunk(self, file_id: str, chunk_index: int) -> bytes:
"""