diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-13 04:10:14 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-13 04:10:14 +0200 |
| commit | ed9fb22ed703db38f9b07c00d17076f90aa4cbc8 (patch) | |
| tree | 448af8bdc7734798607bb33735c2a8439c362c13 /packages/meshbay-node/src/meshbay_node/transport/client.py | |
| parent | ee6573c57f721db8550e34e1c1c79c5922c62a4b (diff) | |
| download | meshbay-ed9fb22ed703db38f9b07c00d17076f90aa4cbc8.tar.gz | |
fix(node)!: remove unauthenticated HTTP file API and TCP transport
Phase 11.5.A — findings C1 and C6 (see second-review.md).
C1: the per-group HTTP file API bound 0.0.0.0 for every configured group,
private ones included, and served two endpoints with no authentication at all:
GET /index (full Mesh Group Index) and GET /file/{id} (raw plaintext file via
FileResponse). Anyone able to reach the port — LAN, forwarded port, permissive
IPv6 — read every private file. This bypassed the entire GEK-proof and node
sovereignty layer. Deleted rather than patched: it duplicated MNP without any
of its controls.
C6: the TCP+TLS chunk server accepted a bare JWT with no GEK proof, leaving a
second non-compliant handshake path. Deleted; QUIC remains and will be brought
to parity with WebRTC by the unified handshake in 11.5.4.
Transport decision recorded in transport/__init__.py: WebRTC/ICE is primary for
browser and native clients (the only NAT traversal validated here — 2 ISPs,
IPv4 STUN + IPv6, 4G CGNAT); QUIC is kept for LAN, port-forwarded and hub-less
group:// access. punch_nat() is a direct-connection helper, not a traversal
stack.
Also removed server_ssl_context()/client_ssl_context() from tls_cert.py (no
remaining callers) and a dead import of the former in quic_server.py.
generate_self_signed_cert() stays: QUIC uses it, and the certificate hash is
the intended channel-binding anchor for 11.5.6, since QUIC has no DTLS
fingerprint to bind the GEK proof to.
BREAKING CHANGE: node.toml keys `port` and `http_port` are gone. Regenerate
config with `meshbay-node init`. Env var MESHBAY_PORT -> MESHBAY_QUIC_PORT.
Tests: 198 passed (209 - 7 test_http_server - 4 test_transport). No other test
changed status. Net -1300 lines.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/transport/client.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/transport/client.py | 148 |
1 files changed, 0 insertions, 148 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/client.py b/packages/meshbay-node/src/meshbay_node/transport/client.py deleted file mode 100644 index 63d50af..0000000 --- a/packages/meshbay-node/src/meshbay_node/transport/client.py +++ /dev/null @@ -1,148 +0,0 @@ -""" -MeshBay — TCP+TLS chunk client (MNP v1). - -Used by the web client (or other nodes) to fetch files from a Mesh Node. -Verifies Ed25519 chunk signatures using the node's public key from the hub. -""" - -import asyncio -import base64 -import logging -import struct -from pathlib import Path - -import blake3 -import msgpack -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey - -from meshbay_common import MNP_VERSION -from meshbay_common.crypto import ( - chunk_key as derive_chunk_key, - decrypt_chunk, - verify_chunk_signature, -) -from meshbay_common.protocol import MNP -from meshbay_node.transport.tls_cert import client_ssl_context - -log = logging.getLogger(__name__) - -MAX_MSG = 64 * 1024 * 1024 - - -async def _send(writer, obj): - data = msgpack.packb(obj, use_bin_type=True) - writer.write(struct.pack(">I", len(data)) + data) - await writer.drain() - -async def _recv(reader): - header = await reader.readexactly(4) - length = struct.unpack(">I", header)[0] - if length > MAX_MSG: - raise ValueError(f"Message too large: {length}") - return msgpack.unpackb(await reader.readexactly(length), raw=False) - - -class ChunkClient: - """ - Async client for fetching encrypted chunks from a ChunkServer. - - Usage: - async with ChunkClient(host, port, jwt_token, gek, pk_node_b64) as client: - data = await client.fetch_chunk(file_id, chunk_index=0) - """ - - def __init__( - self, - host: str, - port: int, - jwt_token: str, - gek: bytes, - pk_node_b64: str, # node's Ed25519 PK from hub — used for sig verification - group_id: str = "", - ): - self._host = host - self._port = port - self._jwt_token = jwt_token - self._gek = gek - self._group_id = group_id - self._pk_node = Ed25519PublicKey.from_public_bytes( - base64.b64decode(pk_node_b64)) - self._reader: asyncio.StreamReader | None = None - self._writer: asyncio.StreamWriter | None = None - - async def __aenter__(self): - await self.connect() - return self - - async def __aexit__(self, *_): - await self.close() - - async def connect(self) -> None: - ssl_ctx = client_ssl_context() - self._reader, self._writer = await asyncio.open_connection( - self._host, self._port, ssl=ssl_ctx) - - handshake_msg = { - "type": MNP.HANDSHAKE, - "v": MNP_VERSION, - "token": self._jwt_token, - } - if self._group_id: - handshake_msg["group_id"] = self._group_id - await _send(self._writer, handshake_msg) - ack = await _recv(self._reader) - if ack.get("type") != MNP.HANDSHAKE_ACK: - raise ConnectionError(f"Handshake rejected: {ack}") - log.debug("Connected to node %s:%d", self._host, self._port) - - async def close(self) -> None: - if self._writer: - self._writer.close() - await self._writer.wait_closed() - - async def fetch_index(self) -> bytes: - """Request the Mesh Group Index. Returns raw wire bytes (encrypted).""" - await _send(self._writer, {"type": MNP.INDEX_SYNC, "v": MNP_VERSION}) - msg = await _recv(self._reader) - return base64.b64decode(msg["index_b64"]) - - async def fetch_chunk(self, file_id: str, chunk_index: int) -> bytes: - """ - Fetch, verify, and decrypt one chunk. - Returns plaintext bytes. - """ - await _send(self._writer, { - "type": MNP.FILE_REQUEST, - "v": MNP_VERSION, - "file_id": file_id, - "chunk_index": chunk_index, - }) - msg = await _recv(self._reader) - - if msg.get("type") == "error": - raise LookupError(msg.get("detail", "Unknown error")) - - ct = base64.b64decode(msg["ct_b64"]) - nonce = base64.b64decode(msg["nonce_b64"]) - ct_hash = base64.b64decode(msg["ct_hash_b64"]) - pt_hash = base64.b64decode(msg["pt_hash_b64"]) - sig = base64.b64decode(msg["sig_b64"]) - file_hash = base64.b64decode(msg["file_hash_b64"]) - ci = msg["chunk_index"] - - # 1. Verify Ed25519 signature - verify_chunk_signature(self._pk_node, ci, nonce, ct_hash, sig) - - # 2. Verify ciphertext hash - if blake3.blake3(ct).digest() != ct_hash: - raise ValueError("Ciphertext hash mismatch") - - # 3. Decrypt - ckey = derive_chunk_key(self._gek, file_hash, ci) - plaintext = decrypt_chunk(ckey, nonce, ct) - - # 4. Verify plaintext hash - if blake3.blake3(plaintext).digest() != pt_hash: - raise ValueError("Plaintext hash mismatch after decryption") - - return plaintext |