1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
|
"""
What a connection may leave waiting for a signature (docs/MESHBAY_DESIGN.md §13.5b).
Anyone authenticated can ask for an admin challenge — the signature is checked
later — so a member who never answers must not make the node keep every request.
Measured before the bound: 200 `root_add` (an op since removed) of 1 MiB each
from a plain member held 200 pending operations and ~400 MiB for the life of
the connection.
"""
import struct
import time
import msgpack
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from meshbay_node.transport.webrtc.admin import MAX_ADMIN_OP_BYTES, MAX_PENDING_ADMIN_OPS
from meshbay_node.transport.webrtc_server import WebRTCPeerSession
GROUP = "g" * 32
class _Channel:
readyState = "open"
def __init__(self):
self.sent = []
def send(self, data: bytes) -> None:
(n,) = struct.unpack(">I", data[:4])
self.sent.append(msgpack.unpackb(data[4:4 + n], raw=False))
class _PC:
connectionState = "connected"
iceConnectionState = "connected"
remoteDescription = None
localDescription = None
sctp = None
def _member_session():
"""An authenticated member — not the operator — on a node that has one."""
ctx = {"sk_node": Ed25519PrivateKey.from_private_bytes(b"\x01" * 32),
"groups": {GROUP: {}}, "has_admin_authority": True}
s = WebRTCPeerSession(_PC(), ctx, peer_id="peer")
s._channel = _Channel()
s._audit = lambda *a, **k: None
s._user_id, s._group_id = "member-1", GROUP
return s
def _ask(s, path: str) -> dict:
"""A signed op whose subject is whatever the caller sends."""
s._dispatch_message({"type": "chat_directory", "path": path})
return s._channel.sent[-1]
def test_a_member_cannot_pile_up_challenges():
s = _member_session()
for i in range(MAX_PENDING_ADMIN_OPS):
assert _ask(s, f"/srv/{i}")["type"] == "admin_challenge"
refused = _ask(s, "/srv/one-too-many")
assert refused["type"] == "error" and refused["code"] == "too_many_pending"
assert len(s._admin_ops) == MAX_PENDING_ADMIN_OPS
def test_an_oversized_request_is_not_kept():
s = _member_session()
refused = _ask(s, "x" * (MAX_ADMIN_OP_BYTES + 1))
assert refused["type"] == "error" and refused["code"] == "too_large"
assert s._admin_ops == {}
def test_an_expired_challenge_frees_its_place():
s = _member_session()
for i in range(MAX_PENDING_ADMIN_OPS):
_ask(s, f"/srv/{i}")
for pending in s._admin_ops.values():
pending["ts"] -= 10_000
assert _ask(s, "/srv/after-expiry")["type"] == "admin_challenge"
assert len(s._admin_ops) == 1
def test_answering_a_challenge_frees_its_place():
s = _member_session()
for i in range(MAX_PENDING_ADMIN_OPS):
_ask(s, f"/srv/{i}")
op_id = next(iter(s._admin_ops))
s._dispatch_message({"type": "admin_response", "op_id": op_id, "signature": "!!"})
assert len(s._admin_ops) == MAX_PENDING_ADMIN_OPS - 1
assert _ask(s, "/srv/next")["type"] == "admin_challenge"
assert all(time.time() - p["ts"] < 5 for p in s._admin_ops.values())
# ── What a challenge covers (docs/MESHBAY_DESIGN.md §5.4) ────────────────────
#
# The signature covers the subject and nothing else of a request, so every value
# the executor acts on has to be in it.
def test_invite_create_signs_the_name_it_records():
from meshbay_common.adminop import invite_create_subject
s = _member_session()
s._ctx["roster"] = object() # only its presence is checked before the challenge
s._dispatch_message({"type": "invite_create", "group_id": GROUP,
"user_id": "u-1", "username": "alice"})
assert s._channel.sent[-1]["subject"] == invite_create_subject("u-1", "alice")
def test_tmdb_config_signs_the_token_without_writing_it():
from meshbay_common.adminop import tmdb_config_subject
s = _member_session()
s._dispatch_message({"type": "tmdb_config", "token": "secret-token",
"language": "fr-FR"})
subject = s._channel.sent[-1]["subject"]
assert subject == tmdb_config_subject("secret-token", "fr-FR")
assert "secret-token" not in subject
|