""" Who signed a chat message, checked rather than taken on the node's word: Tier 2 of docs/MESHBAY_DESIGN.md §3.3, as `static/transport-roster.js` does it. A message's signature proves *a device* wrote it. Whether that device belongs to the account the node names is answered here, from two things: the group's roster — each device with the already-pinned key that countersigned it and the signature, relayed by the node, which asserts nothing — and the keys this machine accepted for each account on each node before (kept in the store). The property, exactly: once this machine has seen an account, a node that later substitutes a key for it is detected. Nothing is gained at first sight, where there is nothing to compare against. pinned seen before, the same key — nothing to say linked new, countersigned by a key already pinned for the account first first sight of the account: trust on first use changed a key this account has not shown before and nothing evidences unknown the roster could not be read """ import base64 from dataclasses import dataclass, field from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey from meshbay_common import MNP_VERSION from meshbay_common.device import device_add_transcript from meshbay_common.groupbox import PURPOSE_ROSTER, unseal from .account import Account from .session import GroupSession @dataclass class AccountDevices: username: str all: list[str] verified: list[str] chain: dict[str, str] = field(default_factory=dict) # device → its countersigner unevidenced: list[str] = field(default_factory=list) def _signed(pk_b64: str, sig_b64, transcript: bytes) -> bool: try: sig = base64.b64decode(sig_b64) if isinstance(sig_b64, str) else bytes(sig_b64) Ed25519PublicKey.from_public_bytes(base64.b64decode(pk_b64)).verify(sig, transcript) return True except Exception: return False def verify_roster(payload: dict, node_pk: str) -> dict[str, AccountDevices]: """ Each account's devices, walked outwards from its roots. A root names **no** countersigner: an operator code admitted it. One that names a countersigner and carries no proof is not a root but unevidenced — a node writing the roster could otherwise launder any key in by mentioning it, which is also the honest reading of a device pinned before the evidence was kept. """ per: dict[str, list[dict]] = {} for d in payload.get("devices") or []: per.setdefault(d["user_id"], []).append(d) node = payload.get("node_pk") or node_pk out = {} for user_id, rows in per.items(): verified = [d["pk_ed25519"] for d in rows if not d.get("added_by_pk")] pending = [d for d in rows if d.get("added_by_pk")] chain: dict[str, str] = {} progress = True while progress and pending: progress = False for d in list(pending): if d["added_by_pk"] not in verified: continue try: nonce = base64.b64decode(d.get("add_nonce") or "") transcript = device_add_transcript( node_pk_b64=node, user_id=user_id, pk_ed25519_b64=d["pk_ed25519"], pk_x25519_b64=d["pk_x25519"], nonce_node=nonce, ts=int(d.get("add_ts") or 0)) except Exception: continue if _signed(d["added_by_pk"], d.get("add_sig") or "", transcript): verified.append(d["pk_ed25519"]) chain[d["pk_ed25519"]] = d["added_by_pk"] pending.remove(d) progress = True out[user_id] = AccountDevices(rows[0].get("username") or "", [d["pk_ed25519"] for d in rows], verified, chain, [d["pk_ed25519"] for d in pending]) return out def judge(known: list[str] | None, entry: AccountDevices | None, device: str) -> tuple[str, list[str] | None]: """ The status of `device` for an account, and the keys to keep if it stands. Decides and writes nothing, so a verdict on a stale roster can be taken again on a fresh one without half a set pinned first. """ if known and device in known: return "pinned", None if not known: # First sight pins everything the node says, not the verified subset: an # alarm on a legitimate second device whose evidence predates it being # kept is an alarm nobody reads. pin = entry.all if entry else None return ("first" if entry and device in entry.all else "changed"), pin if entry and device in entry.verified and entry.chain.get(device) in known: return "linked", sorted(set(known) | {device}) return "changed", None class RosterCheck: """One connection's roster, read once, and read again once per account and device before "changed" is said — a member who joined since it was read is otherwise taken for a substitution.""" def __init__(self, account: Account, session: GroupSession): self.account = account self.session = session self._roster: dict[str, AccountDevices] | None = None self._rechecked: set[tuple[str, str]] = set() async def roster(self, *, fresh: bool = False) -> dict[str, AccountDevices]: if self._roster is None or fresh: s = self.session resp = await s.ch.request({"type": "group_roster_req", "v": MNP_VERSION, "group_id": s.group_id}, "group_roster_resp") payload = unseal(s.gek, PURPOSE_ROSTER, "group_roster_resp", s.group_id, resp) self._roster = verify_roster(payload, s.node_pk) return self._roster async def status(self, user_id: str, device: str) -> str: try: roster = await self.roster() except Exception: return "unknown" known = self.account.account_pins(self.session.node_pk, user_id) verdict, pin = judge(known, roster.get(user_id), device) if verdict == "changed" and (user_id, device) not in self._rechecked: self._rechecked.add((user_id, device)) try: roster = await self.roster(fresh=True) verdict, pin = judge(known, roster.get(user_id), device) except Exception: pass # the verdict on the copy we had stands if pin is not None: await self.account.set_account_pins(self.session.node_pk, user_id, pin) return verdict