aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-cli/tests/test_roster.py
blob: cdaf90b22f81429642992fd404a873a272a98498 (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
"""
Tier 2 for chat (docs/MESHBAY_DESIGN.md §3.3): whose device signed a message,
checked against the group's roster and the keys this machine accepted before —
through `main()`, against the in-memory node, with real countersignatures.
"""

import json

import pytest
from fake_hub import PASSPHRASE, USERNAME, FakeHub
from fake_node import FakeNode
from meshbay_cli import __main__ as cli
from meshbay_cli.client.keys import Identity
from meshbay_cli.client.roster import RosterCheck, judge, verify_roster
from meshbay_cli.commands import chat, context


def _new() -> Identity:
    from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
    from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
    return Identity(Ed25519PrivateKey.generate(), X25519PrivateKey.generate())


@pytest.fixture
def node(monkeypatch, capsys):
    hub, fake = FakeHub(), FakeNode()
    monkeypatch.setattr(context, "Hub", lambda url: hub)

    async def open_group(account, group):
        return fake.session()
    monkeypatch.setattr(chat, "open_group", open_group)
    assert cli.main(["--hub", hub.url, "-u", USERNAME, "--password", PASSPHRASE,
                     "login"]) == 0
    capsys.readouterr()
    return fake


def _last(capsys, n=1) -> list[dict]:
    assert cli.main(["--json", "chat", f"-{n}", "Family"]) == 0
    return json.loads(capsys.readouterr().out)


def test_each_verdict_on_a_message_signing_device(node, capsys):
    phone = node.say("from bob's phone")
    assert _last(capsys)[0]["trust"] == "first"            # first sight: pinned
    assert _last(capsys)[0]["trust"] == "pinned"

    # A second device, countersigned by the first: no notice.
    laptop = _new()
    node.list_device("bob-test", laptop, countersigned_by=phone)
    node.say("from bob's laptop", device=laptop)
    assert _last(capsys)[0]["trust"] == "linked"

    # Named as countersigned and carrying no proof, as a lying node would list it.
    planted = _new()
    node.list_device("bob-test", planted, countersigned_by=phone, proof=False)
    node.say("hello, it's bob", device=planted)
    assert _last(capsys)[0]["trust"] == "changed"
    # Not in the roster at all.
    node.say("hello again", device=_new())
    assert _last(capsys)[0]["trust"] == "changed"

    # Said in plain text under the message, and nowhere else.
    assert cli.main(["chat", "-5", "Family"]) == 0
    out = capsys.readouterr().out.splitlines()
    assert sum("key you have not seen before" in line for line in out) == 2
    assert out[-1].lstrip().startswith("! This account")
    assert out[-3].lstrip().startswith("! This account")

    # Another account, first sight.
    node.say("hi from carol", sender="carol-test")
    assert _last(capsys)[0]["trust"] == "first"


async def test_a_device_listed_after_the_roster_was_read_is_not_a_substitution(node):
    """The roster is read once per connection; a member's new device added since
    is read again before "changed" is said."""
    from meshbay_cli.client.account import Account
    from meshbay_cli.client.store import Store
    store = Store()
    account = Account.open(context.Hub("x"), None, lambda: PASSPHRASE, store)
    session = node.session()
    phone = node.say("one")
    check = RosterCheck(account, session)
    assert await check.status("u-bob-test", phone.pk_ed) == "first"
    tablet = _new()
    node.list_device("bob-test", tablet, countersigned_by=phone)   # after the read
    assert await check.status("u-bob-test", tablet.pk_ed) == "linked"


def test_a_root_is_a_device_naming_no_countersigner():
    a, b = _new(), _new()
    rows = [{"user_id": "u", "pk_ed25519": a.pk_ed, "pk_x25519": a.pk_x,
             "added_by_pk": ""},
            {"user_id": "u", "pk_ed25519": b.pk_ed, "pk_x25519": b.pk_x,
             "added_by_pk": a.pk_ed, "add_sig": "", "add_nonce": "", "add_ts": 0}]
    got = verify_roster({"devices": rows, "node_pk": "pk"}, "pk")["u"]
    assert got.verified == [a.pk_ed] and got.unevidenced == [b.pk_ed]
    assert judge([a.pk_ed], got, b.pk_ed) == ("changed", None)
    assert judge(None, got, b.pk_ed)[0] == "first"