summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_admin_challenge_bounds.py
blob: 9dcb750f137855fac96fe832a1254335fd618660 (plain) (blame)
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