mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-05-28 18:11:31 +02:00
401f114e4f
Second commit on this branch (first added the per-sender cap + accept_replica primitive). This commit wires the actual cross-node propagation: Outbound (sender side) ---------------------- * New ``DMRelay._replicate_envelope_to_peers_async()`` — fire-and-forget thread that POSTs the envelope to every authenticated relay peer via the same per-peer HMAC pattern gate-message replication uses (#256 ``X-Peer-Url`` + ``X-Peer-HMAC`` headers, ``resolve_peer_key_for_url``). * ``deposit()`` now calls the replication helper after a successful local accept. Per-peer errors are swallowed — slow Tor peers must not block the sender's UX, and the recipient polling from a healthy peer works fine even if some peers are down. * Metrics: dm_replication_push_ok / _rejected / _error. Inbound (receiving side) ------------------------ * New endpoint ``POST /api/mesh/dm/replicate-envelope`` in routers/mesh_peer_sync.py. * Same HMAC auth gate (``_verify_peer_push_hmac``) as the existing infonet/gate peer-push endpoints. Unauthenticated requests get 403. * Body cap of 64 KB (DM envelope is bounded by MESH_DM_MAX_MSG_BYTES). * Calls DMRelay.accept_replica which enforces the per-sender cap as a network rule — hostile sender's relay can hold extras locally but honest peers reject them on inbound replication. End-to-end flow now works ------------------------- 1. Alice's node accepts a deposit to Bob's mailbox (local cap check). 2. Alice's node spawns a background thread that POSTs the envelope to MESH_RELAY_PEERS with per-peer HMAC. 3. Each peer's /api/mesh/dm/replicate-envelope verifies the HMAC and calls accept_replica, which re-enforces the per-sender cap. 4. Bob (offline at the time of send) eventually logs into ANY node in MESH_RELAY_PEERS, his existing pollDmMailboxes pulls from the local mailbox there, finds Alice's envelope, decrypts. Tests ----- backend/tests/test_dm_replicate_envelope_endpoint.py — 4 tests: TestReplicateEndpointAuth: - rejects requests without peer HMAC (403) - rejects requests with WRONG peer HMAC (403) — confirms the HMAC is actually verified, not just present - rejects oversize bodies (>64 KB) with 400/413 TestReplicateEndpointRegistered: - static check that POST /api/mesh/dm/replicate-envelope is registered on app.routes — catches future refactor that drops the router include All 38 backend tests touching the new code paths still pass: test_dm_relay_per_sender_cap.py (14) test_dm_replicate_envelope_endpoint.py (4) test_no_new_duplicate_routes.py (1) — new route is unique test_per_peer_secret_resolver.py (19) — HMAC primitive unaffected What's still ahead (PR-3+) -------------------------- * ack propagation: when recipient pulls a message on node X, peers Y/Z should prune their copies to free the sender's quota network-wide. Without this, the sender's quota frees only on the node the recipient actually polled — other peers still see N pending until TTL expiry. Workable but suboptimal. PR-3 will add a /api/mesh/dm/ack endpoint with the same HMAC pattern. * recipient pull-from-peers: today the recipient's poll only hits their own node's relay. If they log into a peer they didn't deposit with, they need a way to fetch envelopes from other peers in MESH_RELAY_PEERS. Today this works as long as the recipient's current node is one of the peers Alice's node pushed to — which is true in a fully-meshed deployment but not guaranteed for partial meshes. PR-4 if telemetry shows this matters.
151 lines
5.5 KiB
Python
151 lines
5.5 KiB
Python
"""POST /api/mesh/dm/replicate-envelope — receiving side of cross-node DM
|
|
mailbox replication.
|
|
|
|
This is the endpoint that peer relays call when they want to hand off an
|
|
encrypted DM envelope to us (so the recipient can log into our node and
|
|
find their messages). It re-enforces the per-(sender, recipient) anti-spam
|
|
cap so hostile sender relays can't widen the cap by skipping the local
|
|
check on their own deposit path.
|
|
|
|
The endpoint:
|
|
|
|
* authenticates the caller via the existing per-peer HMAC pattern
|
|
(same one /api/mesh/infonet/peer-push and /api/mesh/gate/peer-push
|
|
use, introduced in #256 — ``X-Peer-Url`` + ``X-Peer-HMAC`` headers
|
|
keyed off ``resolve_peer_key_for_url``)
|
|
* rejects bodies > 64 KB (DM envelope size is bounded by
|
|
``MESH_DM_MAX_MSG_BYTES`` — 64KB ceiling has generous headroom)
|
|
* rejects requests without a valid peer HMAC with 403
|
|
* passes the envelope to ``DMRelay.accept_replica`` which enforces
|
|
the cap
|
|
|
|
This file pins the endpoint contract. The cap enforcement itself is
|
|
tested in ``test_dm_relay_per_sender_cap.py`` against the relay's
|
|
``accept_replica`` method directly.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
|
|
import pytest
|
|
from httpx import ASGITransport, AsyncClient
|
|
|
|
|
|
@pytest.fixture
|
|
def remote_client():
|
|
"""ASGI client with peer IP 1.2.3.4 — never on the local-operator
|
|
allowlist. Used to prove the endpoint isn't accidentally reachable
|
|
by random remote callers without peer HMAC."""
|
|
from main import app
|
|
|
|
class _RemoteClient:
|
|
def __init__(self):
|
|
self._loop = asyncio.new_event_loop()
|
|
self._transport = ASGITransport(app=app, client=("1.2.3.4", 12345))
|
|
self._base = "http://1.2.3.4:8000"
|
|
|
|
def post(self, url, **kw):
|
|
async def go():
|
|
async with AsyncClient(transport=self._transport, base_url=self._base) as ac:
|
|
return await ac.post(url, **kw)
|
|
return self._loop.run_until_complete(go())
|
|
|
|
def close(self):
|
|
self._loop.close()
|
|
|
|
c = _RemoteClient()
|
|
yield c
|
|
c.close()
|
|
|
|
|
|
class TestReplicateEndpointAuth:
|
|
def test_rejects_request_without_peer_hmac(self, remote_client):
|
|
"""A peer push that does NOT carry X-Peer-Url + X-Peer-HMAC
|
|
must be rejected with 403 before the envelope is ever passed
|
|
to the relay. Same gate the existing infonet/gate peer-push
|
|
endpoints enforce."""
|
|
payload = {
|
|
"envelope": {
|
|
"msg_id": "dm_unauth_1",
|
|
"mailbox_key": "mb",
|
|
"sender_block_ref": "sender",
|
|
"ciphertext": "x",
|
|
},
|
|
}
|
|
r = remote_client.post(
|
|
"/api/mesh/dm/replicate-envelope",
|
|
json=payload,
|
|
)
|
|
assert r.status_code == 403
|
|
assert "peer HMAC" in r.text or "peer hmac" in r.text.lower()
|
|
|
|
def test_rejects_wrong_peer_hmac(self, remote_client, monkeypatch):
|
|
"""A request with a peer HMAC header keyed off the WRONG secret
|
|
is rejected. Confirms the HMAC is actually verified — a tampered
|
|
body or a key-substitution attack doesn't sneak through."""
|
|
# Plant a known peer secret. The request will sign with a
|
|
# DIFFERENT key, so verification must fail.
|
|
from services.config import get_settings
|
|
monkeypatch.setenv("MESH_PEER_PUSH_SECRET", "real-secret-32-chars-min-padding-padding")
|
|
get_settings.cache_clear()
|
|
|
|
body = json.dumps({
|
|
"envelope": {
|
|
"msg_id": "dm_wronghmac",
|
|
"mailbox_key": "mb",
|
|
"sender_block_ref": "sender",
|
|
"ciphertext": "x",
|
|
},
|
|
}).encode("utf-8")
|
|
wrong_hmac = hmac.new(b"wrong-key", body, hashlib.sha256).hexdigest()
|
|
r = remote_client.post(
|
|
"/api/mesh/dm/replicate-envelope",
|
|
content=body,
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"X-Peer-Url": "http://example-peer.onion:8000",
|
|
"X-Peer-HMAC": wrong_hmac,
|
|
},
|
|
)
|
|
assert r.status_code == 403
|
|
|
|
def test_rejects_oversize_body(self, remote_client):
|
|
"""64 KB ceiling — anything bigger doesn't even get parsed.
|
|
Defends against memory amplification via giant ciphertexts."""
|
|
# 100 KB body is well over the 64 KB cap.
|
|
big = b"{" + b"x" * 100_000 + b"}"
|
|
r = remote_client.post(
|
|
"/api/mesh/dm/replicate-envelope",
|
|
content=big,
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"Content-Length": str(len(big)),
|
|
},
|
|
)
|
|
assert r.status_code in (400, 413), (
|
|
f"oversize body should be rejected with 400/413, got {r.status_code}"
|
|
)
|
|
|
|
|
|
class TestReplicateEndpointRegistered:
|
|
def test_route_present_in_app(self):
|
|
"""Static check that the route is actually wired into the app.
|
|
Catches a future refactor that drops the router include or
|
|
deletes the endpoint by accident."""
|
|
from main import app
|
|
|
|
paths_methods = set()
|
|
for route in app.routes:
|
|
path = getattr(route, "path", None)
|
|
methods = getattr(route, "methods", set()) or set()
|
|
for m in methods:
|
|
paths_methods.add((m, path))
|
|
|
|
assert ("POST", "/api/mesh/dm/replicate-envelope") in paths_methods, (
|
|
"POST /api/mesh/dm/replicate-envelope is not registered on the app"
|
|
)
|