#!/usr/bin/env python3 """Independent verifier of the live-rooms corpus (extensions/live-rooms.md). Written from the extension's text. It shares no code with wai-rs, which generates the corpus, and never runs it. It checks Ed25519 signatures with ../claim-conformance/_ed25519.py, WAI's strict rule (SPEC §7 *Signatures*) written from RFC 8032 with the standard library, and computes BLAKE3 (for a key log's commitments) with ../claim-conformance/_blake3.py. Every event is verified by the live-events corpus's own second implementation, live_events_verify.py (live-events §6), which is likewise written from its text alone. Everything else is here: - the room descriptor (§3): its reader, its field rules, its digest and signature, and the registered publish profiles (§8), each written from §8.2's table and compared with the record the corpus states; - the room grant (§4): its reader and the eight checks of §4.3 in their order, its attributes (§4.4), and its window; - revocation (§5): the key-log head held for the descriptor's origin (jwp-receipts §7.2 rules 1 to 3: its members, its canonical form and its root's signature), the authority's entry, and the revoked grants and viewers; - track naming (§6) and the lifecycle detail (§7.2), with slots, takeover and failover; - room verification (§9): the merged order, each record's steps, the room state, the outcome, and the `moqt` scopes of each grant (§10.2), with the catalog and receipts tracks of §6.1; - proof of possession (§10.5): the proof record, its base64url header form, and the seven checks of §10.5.4 in their order; - seeking back (§10.6): a request that starts in a group below the track's live edge is served only under a grant whose powers have bit 4, seek. For every vector it requires the outcome, reason and place, and for one that does not refuse the room id, the gaps or the grant digests, merged order, state, tally and scopes the corpus states, with each grant's seek power; for every proof and every request of `seek` its outcome and reason; and of the corpus, that every reason of §11 refuses a vector, a proof or a request. python3 live_rooms_verify.py Exit status 0 only if every vector verifies. Python 3.8 or later; standard library only. """ import hashlib import json import os import re import struct import sys HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) sys.path.insert(0, os.path.join(HERE, "..", "claim-conformance")) import live_events_verify as LEV # noqa: E402 from _blake3 import blake3 # noqa: E402 import _ed25519 as ED # noqa: E402 Refuse = LEV.Refuse DESCRIPTOR_MAGIC = b"WRD1" GRANT_MAGIC = b"WRG1" DESCRIPTOR_DOMAIN = b"wai:live-room\x01" GRANT_DOMAIN = b"wai:live-room-grant\x01" VIEWER_DOMAIN = b"wai:live-viewer\x01" AUTHORITY = "live/room" MAX_GRANT_S = 14 * 86400 NO_CEILING = 2 ** 64 - 1 MEDIA = {1: "audio", 2: "video", 3: "screen"} MODERATE, DIRECT, COMPOSE, RECORD, SEEK = 1, 2, 4, 8, 16 POWERS = MODERATE | DIRECT | COMPOSE | RECORD | SEEK # §3.4, §4.3 step 5: none above bit 4 CONTROL, DECISION = 0x09, 0x0B JOIN, LEAVE, PUBLISH, UNPUBLISH, EXCHANGE, TAKEOVER, FAILOVER = range(0x10, 0x17) # §11, in its order. REASONS = [ "descriptor_invalid", "key_log_missing", "key_log_not_held", "authority_not_listed", "authority_revoked", "grant_invalid", "grant_too_long", "caps_exceed_role", "grant_exceeds_room", "exchange_unlinked", "exchange_mismatch", "lifecycle_malformed", "grant_unknown", "grant_wrong_principal", "grant_revoked", "grant_reused", "already_present", "not_present", "participant_limit", "outside_grant_window", "power_not_granted", "ai_unmarked", "publish_not_permitted", "profile_not_granted", "already_published", "not_published", "slot_unknown", "slot_full", "publisher_limit", "exchange_not_current", "exchange_off_tick", "takeover_not_higher", "failover_not_standby", "failover_early", "pop_missing", "pop_malformed", "pop_wrong_grant", "pop_wrong_key", "pop_bad_signature", "pop_replayed", "pop_wrong_challenge", "pop_wrong_context", "seek_not_granted", ] class Reader: """Reads fields in order; running out is `truncated` (§3.4).""" def __init__(self, b, p=0): self.b, self.p = b, p def take(self, n): if self.p + n > len(self.b): raise Refuse("truncated") s = self.b[self.p:self.p + n] self.p += n return s def u8(self): return self.take(1)[0] def u16(self): return struct.unpack("> (k % 8) & 1 == 1 def within(a, b): """Every bit of bitmap or byte `a` is set in `b`.""" if isinstance(a, int): return a & ~b == 0 return all(x & ~y == 0 for x, y in zip(a, b)) def lifecycle_permitted(policy): return any(LEV.permits(policy, k) for k in range(0x10, 0x20)) def powers_cover(kinds, powers): """§3.3: decision needs moderate; control needs direct or record.""" return (not has(kinds, DECISION) or powers & MODERATE) and (not has(kinds, CONTROL) or powers & (DIRECT | RECORD)) # ---------------------------------------------------------------- §8 profiles def profile_bytes(name, media, codecs, layering, mode, layers, sample_rate, channels, processing): """§8.1's record.""" o = bytes([len(name)]) + name.encode() + bytes([media, len(codecs)]) for c in codecs: o += bytes([len(c)]) + c.encode() o += bytes([layering, len(mode)]) + mode.encode() + bytes([len(layers)]) for w, h, fps, lo, hi in layers: o += struct.pack(" 3: return False if (p["layering"] == 2) != (len(p["mode"]) > 0) or (p["mode"] and not MODE.fullmatch(p["mode"])): return False n = len(p["layers"]) if not 1 <= n <= 8 or (p["layering"] == 0 and n != 1): return False audio = p["media"] == 1 for w, h, fps, lo, hi in p["layers"]: if hi == 0 or lo > hi: return False if audio and (w, h, fps) != (0, 0, 0): return False if not audio and not (w and h and fps): return False if audio: return p["sample_rate"] > 0 and 1 <= p["channels"] <= 8 and p["processing"] & ~0x0F == 0 return p["sample_rate"] == 0 and p["channels"] == 0 and p["processing"] == 0 # ---------------------------------------------------------------- §3 the descriptor def read_descriptor(b): """§3.4, then §3.2: the descriptor, or Refuse.""" read_head(b, DESCRIPTOR_MAGIC) r = Reader(b, 5) d = {"authority": r.take(32), "clock": r.take(32), "t0": r.u64(), "tick": r.u64(), "origin": r.string(), "max_present": r.u32(), "max_publishers": r.u16(), "reval": r.u32(), "ceiling": r.take(65)} d["roles"] = [] for _ in range(r.u8()): d["roles"].append({"name": r.name(), "publish": r.u8(), "subscribe": r.u8(), "max_layer": r.u8(), "powers": r.u8(), "events": r.take(32)}) d["slots"] = [] for _ in range(r.u8()): d["slots"].append({"name": r.name(), "media": r.u8(), "redundancy": r.u8(), "failover_ms": r.u32()}) d["profiles"] = [read_profile(r) for _ in range(r.u8())] d["review"] = (r.take(32), r.u32(), r.u8()) d["mode"], d["retention"], d["commit_interval_ms"], d["jitter_ms"] = r.u8(), r.u16(), r.u32(), r.u32() d["regions"] = [r.take(2) for _ in range(r.u8())] body = b[:r.p] d["sig"] = r.take(64) if r.p != len(b): raise Refuse("malformed") if not descriptor_ok(d): raise Refuse("descriptor_invalid") bad = ED.verify(d["authority"], DESCRIPTOR_DOMAIN + body, d["sig"]) if bad: raise Refuse(bad) d["room"] = hashlib.sha256(body).digest() try: d["ceiling_policy"] = LEV.read_policy(d["ceiling"]) except Refuse: raise Refuse("descriptor_invalid") return d def descriptor_ok(d): """§3.4's field rules.""" if d["tick"] == 0 or not (d["origin"] == b"" or is_token(d["origin"], 255)): return False if d["max_present"] == 0 or d["max_publishers"] == 0: return False try: ceiling = LEV.read_policy(d["ceiling"]) except Refuse: return False if lifecycle_permitted(ceiling) or not 1 <= len(d["roles"]) <= 16: return False names = [x["name"] for x in d["roles"]] if len(set(names)) != len(names): return False for x in d["roles"]: if not is_name(x["name"]) or x["subscribe"] > 1 or x["publish"] & ~0b111 or x["powers"] & ~POWERS: return False if x["subscribe"] == 0 and x["max_layer"] != 0: return False if not within(x["events"], ceiling["kinds"]) or not powers_cover(x["events"], x["powers"]): return False if len(d["slots"]) > 16 or len({s["name"] for s in d["slots"]}) != len(d["slots"]): return False for s in d["slots"]: if not is_slot_name(s["name"]) or s["media"] not in MEDIA: return False if (s["redundancy"], s["failover_ms"] > 0) not in ((1, False), (2, True)): return False if len(d["profiles"]) > 16 or len({p["name"] for p in d["profiles"]}) != len(d["profiles"]): return False for p in d["profiles"]: if not profile_ok(p): return False reg = REGISTERED.get(p["name"].decode()) if reg is not None and reg != p["bytes"]: return False kinds, deadline, fallback = d["review"] if any(kinds): if not within(kinds, ceiling["kinds"]) or deadline == 0 or fallback not in (1, 2): return False elif deadline != 0 or fallback != 0: return False if d["mode"] > 2 or len(d["regions"]) > 16: return False if d["mode"] == 0: if d["commit_interval_ms"] != 0 or d["jitter_ms"] != 0: return False elif d["commit_interval_ms"] == 0 or d["jitter_ms"] > d["commit_interval_ms"]: return False if not all(re.fullmatch(rb"[A-Z]{2}", x) for x in d["regions"]): return False return all(a < b for a, b in zip(d["regions"], d["regions"][1:])) def is_room_key(d, k): return k == d["authority"] or k == d["clock"] # ---------------------------------------------------------------- §4 grants def read_attrs(b): """§4.4: the entries, or None when they break a rule.""" if len(b) > 1024: return None r = Reader(b) out, last = [], None try: while r.p < len(b): k, v = r.name(), r.string() if not is_slot_name(k) or (last is not None and last >= k): return None out.append((k.decode(), v.decode("utf-8"))) last = k except (Refuse, UnicodeDecodeError): return None return out def read_grant(b): """§4.3 step 1: the grant's fields as read.""" read_head(b, GRANT_MAGIC) r = Reader(b, 5) g = {"room": r.take(32), "principal": r.take(32), "prev": r.take(32), "effective": r.u64(), "nbf": r.u64(), "exp": r.u64(), "role": r.u8(), "publish": r.u8(), "subscribe": r.u8(), "max_layer": r.u8(), "powers": r.u8(), "precedence": r.u8(), "once": r.take(16), "viewer": r.take(16), "policy_bytes": r.take(65), "joules": r.u64(), "funds": r.u64(), "currency": r.take(3)} g["profiles"] = list(r.take(r.u8())) g["origins"] = [r.name() for _ in range(r.u8())] g["attrs"] = r.string() g["body"] = b[:r.p] g["sig"] = r.take(64) if r.p != len(b): raise Refuse("malformed") g["digest"] = hashlib.sha256(g["body"]).digest() return g def grant_fields_ok(g, d, held): """§4.3 step 5.""" if g["exp"] <= g["nbf"]: return False if g["prev"] == bytes(32): if g["effective"] != 0: return False elif g["effective"] % d["tick"] != 0: return False if g["role"] >= len(d["roles"]) or g["subscribe"] > 1: return False if g["publish"] & ~0b111 or g["powers"] & ~POWERS or (g["subscribe"] == 0 and g["max_layer"] != 0): return False try: policy = LEV.read_policy(g["policy_bytes"]) except Refuse: return False if lifecycle_permitted(policy) or not powers_cover(policy["kinds"], g["powers"]): return False funded = g["funds"] != NO_CEILING if funded != (g["currency"] != bytes(3)) or (funded and not re.fullmatch(rb"[A-Z]{3}", g["currency"])): return False media = set() for i in g["profiles"]: if i >= len(d["profiles"]): return False m = d["profiles"][i]["media"] if m in media or not publishes(g, m): return False media.add(m) if len(g["origins"]) > 8 or not all(is_token(o, 255) for o in g["origins"]): return False attrs = read_attrs(g["attrs"]) if attrs is None: return False if any(k == "ai" and not re.fullmatch(r"[0-9a-f]{64}", v) for k, v in attrs): return False return all(h["digest"] != g["digest"] for h in held) def ai_model(g): """§4.4: the model an AI participant's grant names, as bytes, or None.""" for k, v in read_attrs(g["attrs"]) or []: if k == "ai": return bytes.fromhex(v) return None def publishes(g, m): return m in MEDIA and g["publish"] >> (m - 1) & 1 == 1 def check_grants(d, grants): """§4.3 over every grant, in order: the grants, or (index, reason).""" held = [] for i, b in enumerate(grants): try: g = read_grant(b) except Refuse as e: return None, (i, e.reason) bad = ED.verify(d["authority"], GRANT_DOMAIN + g["body"], g["sig"]) if bad: return None, (i, bad) if g["prev"] != bytes(32): old = next((h for h in held if h["digest"] == g["prev"]), None) if old is None: return None, (i, "exchange_unlinked") if old["room"] != g["room"] or old["principal"] != g["principal"]: return None, (i, "exchange_mismatch") if g["room"] != d["room"]: return None, (i, "wrong_room") if not grant_fields_ok(g, d, held): return None, (i, "grant_invalid") g["policy"] = LEV.read_policy(g["policy_bytes"]) if g["exp"] - g["nbf"] > MAX_GRANT_S: return None, (i, "grant_too_long") role = d["roles"][g["role"]] if (not within(g["publish"], role["publish"]) or g["subscribe"] > role["subscribe"] or g["max_layer"] > role["max_layer"] or not within(g["powers"], role["powers"]) or not within(g["policy"]["kinds"], role["events"])): return None, (i, "caps_exceed_role") p, c = g["policy"], d["ceiling_policy"] if (p["max_payload"] > c["max_payload"] or p["max_text_cp"] > c["max_text_cp"] or p["max_events"] > c["max_events"] or p["window_us"] < c["window_us"] or p["max_reactions"] > c["max_reactions"] or p["reaction_window_us"] < c["reaction_window_us"]): return None, (i, "grant_exceeds_room") held.append(g) return held, None def good_at(g, d, t): """§4.3: nbf ≤ ⌊(t0 + t) / 10^6⌋ < exp.""" return g["nbf"] <= (d["t0"] + t) // 1000000 < g["exp"] def viewer_tag(g): return None if g["viewer"] == bytes(16) else hashlib.sha256(VIEWER_DOMAIN + g["viewer"]).digest() # ---------------------------------------------------------------- §5 the key log REVISION_MEMBERS = {"kind", "origin", "seq", "prev", "issued_at", "root", "next_root", "keys", "revoked_modules", "grant_max_s", "status_max_age_s", "sig"} ENTRY_MEMBERS = {"id", "key", "may_sign", "status", "until", "compromised_since"} HEX32 = re.compile(r"[0-9a-f]{64}") LOG_TIME = re.compile(r"[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]Z") KEY_ID = re.compile(r"[A-Za-z0-9._-]{1,64}") AUTH_SEG = re.compile(r"[A-Za-z0-9._-]+") def no_duplicates(pairs): keys = [k for k, _ in pairs] if len(set(keys)) != len(keys): raise ValueError("a member named twice") return dict(pairs) def is_int(v): return isinstance(v, int) and not isinstance(v, bool) and 0 <= v <= 2 ** 53 - 1 def commitment(k): return blake3(b"wai:key-set-next\x01" + k) def jcs(v): """RFC 8785 over what a key-log revision holds: strings of ASCII, integers, null, arrays.""" return json.dumps(v, sort_keys=True, separators=(",", ":"), ensure_ascii=False) def entry_ok(e): if not isinstance(e, dict) or set(e) != ENTRY_MEMBERS: return False if not isinstance(e["id"], str) or not KEY_ID.fullmatch(e["id"]): return False if not isinstance(e["key"], str) or not HEX32.fullmatch(e["key"]) or not ED.strict_key(bytes.fromhex(e["key"])): return False ms = e["may_sign"] if not isinstance(ms, list) or not ms or len(set(map(str, ms))) != len(ms): return False for a in ms: if not isinstance(a, str) or not 1 <= len(a) <= 128 or not all(AUTH_SEG.fullmatch(s) for s in a.split("/")): return False for t in ("until", "compromised_since"): if e[t] is not None and not (isinstance(e[t], str) and LOG_TIME.fullmatch(e[t])): return False shape = {"active": e["until"] is None and e["compromised_since"] is None, "retired": e["until"] is not None and e["compromised_since"] is None, "revoked": e["compromised_since"] is not None} return shape.get(e["status"], False) def revision_ok(v): """jwp-receipts §7.2 rule 1, as far as this verifier reads it.""" if not isinstance(v, dict) or set(v) != REVISION_MEMBERS or v["kind"] != "wai-key-set": return False if not isinstance(v["origin"], str) or not is_int(v["seq"]): return False if (v["seq"] == 0) != (v["prev"] is None) or (v["prev"] is not None and not (isinstance(v["prev"], str) and HEX32.fullmatch(v["prev"]))): return False if not isinstance(v["issued_at"], str) or not LOG_TIME.fullmatch(v["issued_at"]): return False for k in ("root", "next_root"): if not isinstance(v[k], str) or not HEX32.fullmatch(v[k]): return False root, nxt = bytes.fromhex(v["root"]), bytes.fromhex(v["next_root"]) if nxt == commitment(root): return False if not isinstance(v["keys"], list) or not all(entry_ok(e) for e in v["keys"]): return False ids, keys = [e["id"] for e in v["keys"]], [e["key"] for e in v["keys"]] if len(set(ids)) != len(ids) or len(set(keys)) != len(keys): return False if any(bytes.fromhex(k) == root or commitment(bytes.fromhex(k)) == nxt for k in keys): return False rm = v["revoked_modules"] if not isinstance(rm, list) or not all(isinstance(x, str) and HEX32.fullmatch(x) for x in rm) or len(set(rm)) != len(rm): return False if not (is_int(v["grant_max_s"]) and 1 <= v["grant_max_s"] <= 86400): return False if not (is_int(v["status_max_age_s"]) and 1 <= v["status_max_age_s"] <= 3600): return False return isinstance(v["sig"], str) and re.fullmatch(r"[0-9a-f]{128}", v["sig"]) is not None def check_key_log(d, head): """§5: the revoked digests, or Refuse.""" if d["origin"] == b"": return set() if head is None: raise Refuse("key_log_missing") try: v = json.loads(head, object_pairs_hook=no_duplicates) except ValueError: raise Refuse("key_log_not_held") if not revision_ok(v) or v["origin"] != d["origin"].decode(): raise Refuse("key_log_not_held") unsigned = {k: x for k, x in v.items() if k != "sig"} if ED.verify(bytes.fromhex(v["root"]), b"wai:key-set\x01" + jcs(unsigned).encode(), bytes.fromhex(v["sig"])): raise Refuse("key_log_not_held") entry = next((e for e in v["keys"] if e["key"] == d["authority"].hex() and AUTHORITY in e["may_sign"]), None) if entry is None: raise Refuse("authority_not_listed") if entry["status"] == "revoked": raise Refuse("authority_revoked") return {bytes.fromhex(x) for x in v["revoked_modules"]} def revoked(g, revoked_set): return g["digest"] in revoked_set or viewer_tag(g) in revoked_set # ---------------------------------------------------------------- §7 lifecycle detail def read_detail(kind, b): """§7.2: the detail as a dict, or Refuse("lifecycle_malformed").""" r = Reader(b) try: def slot(required): s = r.name() if (not s and required) or (s and not is_slot_name(s)): raise Refuse("lifecycle_malformed") return s.decode() def media(): m = r.u8() if m not in MEDIA: raise Refuse("lifecycle_malformed") return m def layers(): n = r.u8() if n == 0: raise Refuse("lifecycle_malformed") return n if kind == JOIN: x = {"grant": r.take(32)} elif kind == LEAVE: x = {"reason": r.u8()} if x["reason"] not in (1, 2, 3, 4): raise Refuse("lifecycle_malformed") elif kind == PUBLISH: x = {"slot": slot(False), "media": media(), "profile": r.u8()} x["layers"] = layers() elif kind == UNPUBLISH: x = {"slot": slot(False), "media": media()} elif kind == EXCHANGE: x = {"old": r.take(32), "new": r.take(32)} elif kind == TAKEOVER: x = {"slot": slot(True), "media": media(), "profile": r.u8()} x["layers"] = layers() x["previous"] = r.take(32) elif kind == FAILOVER: x = {"slot": slot(True), "media": media(), "failed": r.take(32), "last_seen": r.u64()} else: raise Refuse("lifecycle_malformed") except Refuse: raise Refuse("lifecycle_malformed") if r.p != len(b): raise Refuse("lifecycle_malformed") return x class Room: """The room state of §9.3 and the checks of §7.2 that move it.""" def __init__(self, d, grants, revoked_set): self.d, self.grants, self.revoked = d, grants, revoked_set self.present = {} # principal -> grant digest self.own = {} # (principal, media) -> (profile, layers) self.slots = [{"live": None, "standby": None} for _ in d["slots"]] # each (principal, profile, layers) self.timeline = [] self.used_once = set() def grant(self, digest): return next((g for g in self.grants if g["digest"] == digest), None) def current(self, p): return self.grant(self.present[p]) if p in self.present else None def slot_index(self, name, m): return next((i for i, s in enumerate(self.d["slots"]) if s["name"].decode() == name and s["media"] == m), None) def publishers(self): out = {p for p, _ in self.own} for s in self.slots: out |= {h[0] for h in (s["live"], s["standby"]) if h} return out def may_publish(self, g, m, profile, layers): if not publishes(g, m): raise Refuse("publish_not_permitted") ps = self.d["profiles"] if not (profile < len(ps) and profile in g["profiles"] and ps[profile]["media"] == m and layers <= len(ps[profile]["layers"])): raise Refuse("profile_not_granted") def limit(self): if len(self.publishers()) > self.d["max_publishers"]: raise Refuse("publisher_limit") def drop(self, p): for s in self.slots: if s["standby"] and s["standby"][0] == p: s["standby"] = None if s["live"] and s["live"][0] == p: s["live"], s["standby"] = s["standby"], None def publishing(self, p): out = [(m, v[0]) for (q, m), v in sorted(self.own.items()) if q == p] for i, s in enumerate(self.slots): for h in (s["live"], s["standby"]): if h and h[0] == p: out.append((self.d["slots"][i]["media"], h[1])) return out def need_present(self, p): g = self.current(p) if g is None: raise Refuse("not_present") return g def slot_of(self, name, m): i = self.slot_index(name, m) if i is None: raise Refuse("slot_unknown") return i def apply(self, kind, subject, x, t): if kind == JOIN: g = self.grant(x["grant"]) if g is None: raise Refuse("grant_unknown") if g["principal"] != subject: raise Refuse("grant_wrong_principal") if subject in self.present: raise Refuse("already_present") if revoked(g, self.revoked): raise Refuse("grant_revoked") if g["once"] != bytes(16) and g["once"] in self.used_once: raise Refuse("grant_reused") if not good_at(g, self.d, t): raise Refuse("outside_grant_window") if len(self.present) + 1 > self.d["max_present"]: raise Refuse("participant_limit") if g["once"] != bytes(16): self.used_once.add(g["once"]) self.present[subject] = g["digest"] self.timeline.append((subject, g["digest"], t)) elif kind == LEAVE: self.need_present(subject) del self.present[subject] self.own = {k: v for k, v in self.own.items() if k[0] != subject} self.drop(subject) elif kind == PUBLISH: g = self.need_present(subject) i = None if x["slot"] == "" else self.slot_of(x["slot"], x["media"]) self.may_publish(g, x["media"], x["profile"], x["layers"]) held = (subject, x["profile"], x["layers"]) if i is None: if (subject, x["media"]) in self.own: raise Refuse("already_published") self.own[(subject, x["media"])] = (x["profile"], x["layers"]) else: s = self.slots[i] if any(h and h[0] == subject for h in (s["live"], s["standby"])): raise Refuse("already_published") if s["live"] and (self.d["slots"][i]["redundancy"] < 2 or s["standby"]): raise Refuse("slot_full") if s["live"] is None: s["live"] = held else: s["standby"] = held self.limit() elif kind == UNPUBLISH: self.need_present(subject) if x["slot"] == "": if (subject, x["media"]) not in self.own: raise Refuse("not_published") del self.own[(subject, x["media"])] else: s = self.slots[self.slot_of(x["slot"], x["media"])] if not any(h and h[0] == subject for h in (s["live"], s["standby"])): raise Refuse("not_published") if s["standby"] and s["standby"][0] == subject: s["standby"] = None else: s["live"], s["standby"] = s["standby"], None elif kind == EXCHANGE: cur = self.need_present(subject) if cur["digest"] != x["old"]: raise Refuse("exchange_not_current") g = self.grant(x["new"]) if g is None: raise Refuse("grant_unknown") if g["prev"] != x["old"]: raise Refuse("exchange_unlinked") if t != g["effective"]: raise Refuse("exchange_off_tick") if revoked(g, self.revoked): raise Refuse("grant_revoked") if not good_at(g, self.d, t): raise Refuse("outside_grant_window") for m, profile in self.publishing(subject): if not publishes(g, m): raise Refuse("publish_not_permitted") if profile not in g["profiles"]: raise Refuse("profile_not_granted") self.present[subject] = g["digest"] self.timeline.append((subject, g["digest"], t)) elif kind == TAKEOVER: g = self.need_present(subject) i = self.slot_of(x["slot"], x["media"]) s = self.slots[i] if not s["live"] or s["live"][0] != x["previous"]: raise Refuse("not_published") self.may_publish(g, x["media"], x["profile"], x["layers"]) theirs = self.current(x["previous"]) if subject == x["previous"] or g["precedence"] <= (theirs["precedence"] if theirs else 0): raise Refuse("takeover_not_higher") if s["standby"] and s["standby"][0] == subject: s["standby"] = None s["live"] = (subject, x["profile"], x["layers"]) self.limit() elif kind == FAILOVER: self.need_present(subject) i = self.slot_of(x["slot"], x["media"]) s = self.slots[i] if not s["live"] or s["live"][0] != x["failed"]: raise Refuse("not_published") if not s["standby"] or s["standby"][0] != subject: raise Refuse("failover_not_standby") if x["last_seen"] > t or t - x["last_seen"] < self.d["slots"][i]["failover_ms"] * 1000: raise Refuse("failover_early") s["live"], s["standby"] = s["standby"], None # ---------------------------------------------------------------- §6 names, §10.2 scopes def render(fields, name): """MOQT §8.8: a-z, A-Z, 0-9 and _ as themselves; every other byte as . and two hex digits.""" def one(f): return "".join(chr(c) if (chr(c).isascii() and chr(c).isalnum()) or c == 0x5F else ".%02x" % c for c in f.encode()) return "-".join(one(f) for f in fields) + "--" + one(name) def scopes(d, g): """§10.2, in its order.""" room, me = d["room"].hex(), g["principal"].hex() out = [[[0]]] for m in (1, 2, 3): if publishes(g, m): out.append([[2, 6], [room, me, MEDIA[m], None]]) for s in d["slots"]: if s["media"] == m: out.append([[6], [room, "_" + s["name"].decode(), MEDIA[m], None]]) out.append([[6], [room, "_" + s["name"].decode(), None], "catalog"]) if any(publishes(g, m) for m in (1, 2, 3)): out.append([[2, 6], [room, me, None], "catalog"]) if any(g["policy"]["kinds"]): out.append([[2, 6], [room, me, "events", None], "0"]) if g["subscribe"]: out.append([[3], [room]]) out.append([[4, 7, 8], [room], "catalog"]) for layer in range(g["max_layer"] + 1): out.append([[4, 7, 8], [room], str(layer)]) out.append([[4, 7, 8], [room], str(layer) + "_receipts"]) return out # ---------------------------------------------------------------- §10.5 proof of possession PROOF_MAGIC = b"WRP1" PROOF_DOMAIN = b"wai:live-room-proof\x01" B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" def from_base64url(t): """RFC 4648 §5 without padding, strictly: the alphabet, a length an encoding has, and zero bits past the last byte; None otherwise.""" if any(c not in B64 for c in t) or len(t) % 4 == 1: return None out = bytearray() for i in range(0, len(t), 4): c = t[i:i + 4] n = 0 for j, ch in enumerate(c): n |= B64.index(ch) << (18 - 6 * j) k = len(c) - 1 out += n.to_bytes(3, "big")[:k] if k < 3 and n & ((1 << (8 * (3 - k))) - 1): return None return bytes(out) def read_proof(b): """§10.5.4 step 2.""" r = Reader(b) try: if r.take(4) != PROOF_MAGIC or r.u8() != 1: raise Refuse("pop_malformed") p = {"grant": r.take(32), "key": r.take(32), "challenge": r.take(32)} ctx = r.take(r.u16()) p["body"] = b[:r.p] p["sig"] = r.take(64) except Refuse: raise Refuse("pop_malformed") if r.p != len(b) or any(c < 0x20 or c > 0x7E for c in ctx): raise Refuse("pop_malformed") p["context"] = ctx.decode("ascii") return p def check_proof(grant, proof, challenge, consumed, context): """§10.5.4, in its order, for a grant record already checked by §4.3.""" g = read_grant(grant) if proof is None: raise Refuse("pop_missing") p = read_proof(proof) if p["grant"] != g["digest"]: raise Refuse("pop_wrong_grant") if p["key"] != g["principal"]: raise Refuse("pop_wrong_key") if ED.verify(p["key"], PROOF_DOMAIN + p["body"], p["sig"]) is not None: raise Refuse("pop_bad_signature") if p["challenge"] != challenge: raise Refuse("pop_replayed" if p["challenge"] in consumed else "pop_wrong_challenge") if p["context"] != context: raise Refuse("pop_wrong_context") def proof_outcome(v): """A proof vector's outcome: the grant and proof from the MOQT options or the HTTP headers (§10.5.2, §10.5.3), then §10.5.4.""" if v["binding"] == "moqt": grant = bytes.fromhex(v["token"]) if len(v["proofs"]) > 1: raise ValueError("one proof per vector") proof = bytes.fromhex(v["proofs"][0]) if v["proofs"] else None else: auth = v["headers"]["authorization"] if not auth.startswith("Bearer "): raise ValueError("a Bearer token") grant = from_base64url(auth[len("Bearer "):]) raw = v["headers"].get("wai-room-proof") proof = None if raw is None else (from_base64url(raw) or b"") consumed = {bytes.fromhex(c) for c in v["consumed"]} try: check_proof(grant, proof, bytes.fromhex(v["challenge"]), consumed, v["verb"] + " " + v["target"]) except Refuse as r: return {"outcome": "refuse", "reason": r.reason} return {"outcome": "accept"} # ---------------------------------------------------------------- §10.6 seeking back def seeks_back(live_edge, start): """§10.6: a request seeks back when its start Location (g, o) has g below the live edge L, the group of the track's Largest Object; one from (L, o) does not.""" return start[0] < live_edge def seek_outcome(doc, v): """A request of `seek`: the grant it names, by vector and index, from that room's grants as §4.3 checks them, then §10.6's rule.""" room = next(x for x in doc["vectors"] if x["name"] == v["vector"]) d = read_descriptor(bytes.fromhex(room["descriptor"])) held, bad = check_grants(d, [bytes.fromhex(g) for g in room["grants"]]) if bad: raise ValueError("the room's grants do not check: %s" % (bad,)) g = held[v["grant"]] if v["request"] not in ("fetch", "fill"): raise ValueError("a request is a fetch or a fill") start = v["start"] if not (isinstance(start, list) and len(start) == 2 and all(is_int(x) for x in start) and is_int(v["live_edge"])): raise ValueError("a live edge and a start Location of integers") if seeks_back(v["live_edge"], start) and not g["powers"] & SEEK: return {"outcome": "refuse", "reason": "seek_not_granted"} return {"outcome": "serve"} # ---------------------------------------------------------------- §9 room verification def authority_policy(): """live-events §5: the default bounds, every kind permitted.""" b = b"WEP1\x01" + bitset(LEV.KINDS) + struct.pack("", file=sys.stderr) sys.exit(2) sys.exit(main(sys.argv[1]))