#!/usr/bin/env python3 """Independent verifier of the live-composition corpus (extensions/live-compose.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. Each room is verified by the live-rooms corpus's own second implementation, live_rooms_verify.py (live-rooms §9), whose lifecycle replay this verifier steps through itself, record by record, to take the room at each instant (§4.1). Everything else is here, from the text: - the descriptor (§3): read, its canvas, its field rules, its signature and who may sign; - the participant set (§4): sources, attributes, order, and the set's digest; - the layout function (§5): the grid and its candidates, the featured tile, picture in picture, the fill modes; and the mixer (§6), over the corpus's PCM; - the receipt (§8): read, its inputs, and its verification. For every case it requires the outcome, reason and place, and the order, tiles, voices, samples, digests, participants and sources the corpus states; of the corpus, that every reason of §10 refuses a case. Then it mutates copies of the corpus, one defect each, and requires every copy to fail. python3 live_compose_verify.py Exit status 0 only if every case verifies and every mutated copy fails. Python 3.8 or later; standard library only. """ import copy import hashlib import json import os 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 import live_rooms_verify as LRV # noqa: E402 import _ed25519 as ED # noqa: E402 Refuse = LEV.Refuse DESCRIPTOR_DOMAIN = b"wai:live-compose\x01" RECEIPT_DOMAIN = b"wai:live-compose-receipt\x01" SET_DOMAIN = b"wai:live-compose-set\x01" ZERO = bytes(32) AUDIO, VIDEO, SCREEN = 1, 2, 3 MEDIA_NAME = {1: "audio", 2: "video", 3: "screen"} COMPOSE_POWER = 4 JOIN = 0x10 GRID, PIP = 1, 2 AUTO, VIDEO_ASPECT, SQUARE, PORTRAIT = 0, 1, 2, 3 DEFAULT, FILL, COVER, CONTAIN = 0, 1, 2, 3 STATIC, DYNAMIC = 1, 2 SINGLE, SIMULCAST, SVC, STAGED = 0, 1, 2, 3 # §10, in its order. REASONS = [ "canvas_out_of_range", "compose_invalid", "compose_not_permitted", "not_yet_effective", "lifecycle_incomplete", "attr_malformed", "mix_input_malformed", "receipt_invalid", "descriptor_mismatch", "compositor_not_permitted", "output_not_published", "state_mismatch", "inputs_mismatch", ] PARAM_KEYS = ["width", "height", "fps", "layout", "gap", "aspect", "fill", "featured_attr", "order_attr", "omit_stopped", "pip_attr", "corner", "pip_width", "pip_height", "offset", "behaviour", "channels", "sample_rate", "gain_attr", "pan_attr", "max_tiles", "max_voices"] def sha(*parts): h = hashlib.sha256() for p in parts: h.update(p) return h.digest() class Reader: """Fields in order; running out is `truncated`.""" 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(" 3 or p["fill"] > 3: return False for k in ("featured_attr", "order_attr", "pip_attr", "gain_attr", "pan_attr"): if p[k] != "" and not is_key(p[k].encode()): return False if p["layout"] == GRID: if p["pip_attr"] != "" or p["corner"] or p["pip_width"] or p["pip_height"] or p["offset"] or p["behaviour"]: return False else: o = p["offset"] if not (1 <= p["corner"] <= 4 and p["behaviour"] in (STATIC, DYNAMIC) and o < w and o < h and p["pip_width"] + o <= w and p["pip_height"] + o <= h): return False if p["channels"] not in (1, 2) or not 8000 <= p["sample_rate"] <= 192000: return False if not (1 <= p["max_tiles"] <= 64 and 1 <= p["max_voices"] <= 64): return False k = least_root(p["max_tiles"]) need = k + (k - 1) * g return g < h and w >= need and h - g - (h - g) * 3 // 4 >= need def check_params(p): if not canvas_ok(p): raise Refuse("canvas_out_of_range") if not fields_ok(p): raise Refuse("compose_invalid") def read_descriptor(b): """§3.3: the descriptor, or Refuse.""" read_head(b, b"WCD1") r = Reader(b, 5) c = {"room": r.take(32), "signer": r.take(32), "effective": r.u64(), "compositor": r.take(32)} p = {"width": r.u16(), "height": r.u16(), "fps": r.u8()} c["background"], c["layer"] = r.take(3), r.u8() p["layout"], p["gap"], p["aspect"], p["fill"] = r.u8(), r.u16(), r.u8(), r.u8() raw = {"featured_attr": r.short(), "order_attr": r.short()} omit = r.u8() raw["pip_attr"] = r.short() p["corner"], p["pip_width"], p["pip_height"], p["offset"], p["behaviour"] = r.u8(), r.u16(), r.u16(), r.u16(), r.u8() p["channels"], p["sample_rate"] = r.u8(), r.u32() raw["gain_attr"], raw["pan_attr"] = r.short(), r.short() p["max_tiles"], p["max_voices"] = r.u8(), r.u8() body = b[:r.p] c["sig"] = r.take(64) if r.p != len(b): raise Refuse("malformed") keys_ok = all(v == b"" or is_key(v) for v in raw.values()) for k, v in raw.items(): p[k] = v.decode("latin-1") p["omit_stopped"] = omit == 1 if not canvas_ok(p): raise Refuse("canvas_out_of_range") if not keys_ok or omit > 1 or not fields_ok(p): raise Refuse("compose_invalid") bad = ED.verify(c["signer"], DESCRIPTOR_DOMAIN + body, c["sig"]) if bad: raise Refuse(bad) c["params"] = p c["digest"] = sha(body) return c def fields_of(c): out = {"room": c["room"].hex(), "signer": c["signer"].hex(), "effective": c["effective"], "compositor": c["compositor"].hex(), "background": c["background"].hex(), "layer": c["layer"]} for k in PARAM_KEYS: out[k] = c["params"][k] return out # ---------------------------------------------------------------- §4.3 attributes def decimal(v): """A decimal integer of §4.3, or None.""" neg = v.startswith("-") digits = v[1:] if neg else v if not 1 <= len(digits) <= 10 or any(ch not in "0123456789" for ch in digits): return None if digits[0] == "0" and (len(digits) > 1 or neg): return None n = int(digits) return -n if neg else n def read_attributes(p, attrs): """§4.3 for one participant: (featured, order, pip, gain, pan), or Refuse.""" def look(key): if key == "": return None return next((v for k, v in attrs if k == key), None) def boolean(key): v = look(key) if v is None or v == "false": return False if v == "true": return True raise Refuse("attr_malformed") def ranged(key, lo, hi, absent): v = look(key) if v is None: return absent n = decimal(v) if n is None or not lo <= n <= hi: raise Refuse("attr_malformed") return n featured = boolean(p["featured_attr"]) order = ranged(p["order_attr"], -2 ** 31, 2 ** 31 - 1, None) pip = boolean(p["pip_attr"]) gain = ranged(p["gain_attr"], 0, 16384, 4096) pan = ranged(p["pan_attr"], -4096, 4096, 0) return featured, order, pip, gain, pan # ---------------------------------------------------------------- §5 the layout def fit(bw, bh, a, b): """§5.1.""" if bw * b <= bh * a: return bw, max(1, bw * b // a) return max(1, bh * a // b), bh def centred(outer, w, h): x, y, ow, oh = outer return (x + (ow - w) // 2, y + (oh - h) // 2, w, h) def reference(aspect): return {SQUARE: (1, 1), PORTRAIT: (3, 4)}.get(aspect, (16, 9)) def grid(region, n, gap, aspect): """§5.3: n tile rectangles.""" X, Y, W, H = region a, b = reference(aspect) chosen = None for c in range(1, n + 1): r = -(-n // c) if (c - 1) * gap >= W or (r - 1) * gap >= H: continue cw, ch = (W - (c - 1) * gap) // c, (H - (r - 1) * gap) // r if cw < 1 or ch < 1: continue tw, th = fit(cw, ch, a, b) if chosen is None or tw * th >= chosen[0]: chosen = (tw * th, c, r, cw, ch) _, c, r, cw, ch = chosen y0 = Y + (H - (r * ch + (r - 1) * gap)) // 2 out = [] for k in range(n): i, j = k // c, k % c m = n - (r - 1) * c if i == r - 1 else c x0 = X + (W - (m * cw + (m - 1) * gap)) // 2 cell = (x0 + j * (cw + gap), y0 + i * (ch + gap), cw, ch) out.append(cell if aspect == AUTO else centred(cell, *fit(cw, ch, a, b))) return out def show(tile, size, mode): """§5.6: (destination, source) of a source of `size` in `tile`.""" tx, ty, tw, th = tile sw, sh = size wider = sw * th >= sh * tw whole = (0, 0, sw, sh) if mode == FILL: return tile, whole if mode == CONTAIN: if wider: dh = max(1, tw * sh // sw) return (tx, ty + (th - dh) // 2, tw, dh), whole dw = max(1, th * sw // sh) return (tx + (tw - dw) // 2, ty, dw, th), whole if wider: cw = max(1, sh * tw // th) return tile, ((sw - cw) // 2, 0, cw, sh) ch = max(1, sw * th // tw) return tile, (0, (sh - ch) // 2, sw, ch) def layout(p, entrants): """§5 and §6's voices over entrants (principal, join, video, audio, attrs): a dict.""" check_params(p) members = [] for e in entrants: featured, order, pip, gain, pan = read_attributes(p, e["attrs"]) members.append(dict(e, featured=featured, order=order, pip=pip, gain=gain, pan=pan)) # §4.4 members.sort(key=lambda m: (m["order"] is None, m["order"] or 0, m["join"], m["principal"])) voices = [{"principal": m["principal"].hex(), "gain": m["gain"], "pan": m["pan"]} for m in members if m["audio"]][:p["max_voices"]] # §5.2 shown = [m for m in members if not (p["omit_stopped"] and m["video"] is None)] pip_m = next((m for m in shown if m["pip"]), None) if p["layout"] == PIP else None rest = [m for m in shown if m is not pip_m] f = next((i for i, m in enumerate(rest) if m["featured"]), None) if f is not None: rest.insert(0, rest.pop(f)) rest = rest[:p["max_tiles"] - (1 if pip_m is not None else 0)] featured = f is not None and bool(rest) window = pip_m if pip_m is not None and not rest and p["behaviour"] == DYNAMIC: rest, featured, window = [pip_m], pip_m["featured"], None tiles = grid_tiles(p, rest, featured) if window is not None: tiles.append(pip_tile(p, window)) return {"order": [m["principal"].hex() for m in members], "tiles": tiles, "voices": voices} def tile_of(p, m, rect, place, z): mode = p["fill"] if p["fill"] != DEFAULT else (CONTAIN if place == "featured" else COVER) out = {"principal": m["principal"].hex(), "place": place, "z": z, "tile": list(rect), "dst": None, "src": None} if m["video"] is not None: dst, src = show(rect, m["video"], mode) out["dst"], out["src"] = list(dst), list(src) return out def grid_tiles(p, ms, featured): """§5.3, §5.4.""" w, h, g = p["width"], p["height"], p["gap"] if not ms: return [] if not featured: return [tile_of(p, m, r, "grid", 0) for m, r in zip(ms, grid((0, 0, w, h), len(ms), g, p["aspect"]))] if len(ms) == 1: return [tile_of(p, ms[0], (0, 0, w, h), "featured", 0)] mh = (h - g) * 3 // 4 out = [tile_of(p, ms[0], (0, 0, w, mh), "featured", 0)] band = (0, mh + g, w, h - mh - g) out += [tile_of(p, m, r, "grid", 0) for m, r in zip(ms[1:], grid(band, len(ms) - 1, g, p["aspect"]))] return out def pip_tile(p, m): """§5.5.""" w, h, o = p["width"], p["height"], p["offset"] if p["pip_width"] > 0 and p["pip_height"] > 0: pw, ph = p["pip_width"], p["pip_height"] else: a, b = m["video"] if m["video"] is not None else (w, h) if p["pip_width"] > 0: bw = p["pip_width"] elif p["pip_height"] > 0: bw = w - o else: bw = min(w // 4, w - o) bh = p["pip_height"] if p["pip_height"] > 0 else h - o pw, ph = fit(bw, bh, a, b) x = {1: o, 2: w - o - pw, 3: o, 4: w - o - pw}[p["corner"]] y = {1: o, 2: o, 3: h - o - ph, 4: h - o - ph}[p["corner"]] return tile_of(p, m, (x, y, pw, ph), "pip", 1) # ---------------------------------------------------------------- §6 the mix def mix(channels, frames, inputs): """§6: the mixed samples, or Refuse.""" if channels not in (1, 2): raise Refuse("mix_input_malformed") for v in inputs: if v["channels"] not in (1, 2) or not 0 <= v["gain"] <= 16384 or not -4096 <= v["pan"] <= 4096 \ or len(v["pcm"]) != frames * v["channels"]: raise Refuse("mix_input_malformed") sums = [0] * (frames * channels) for v in inputs: x, g, ch = v["pcm"], v["gain"], v["channels"] if channels == 1: for f in range(frames): sums[f] += (x[f] * 4096 if ch == 1 else (x[2 * f] + x[2 * f + 1]) * 2048) * g else: al, ar = 4096 - max(v["pan"], 0), 4096 + min(v["pan"], 0) for f in range(frames): left, right = (x[f], x[f]) if ch == 1 else (x[2 * f], x[2 * f + 1]) sums[2 * f] += left * g * al sums[2 * f + 1] += right * g * ar return [min(32767, max(-32768, (s + (1 << 23)) >> 24)) for s in sums] def pcm_of(h): b = bytes.fromhex(h) if len(b) % 2: raise ValueError("odd PCM") return list(struct.unpack("<%dh" % (len(b) // 2), b)) def pcm_hex(samples): return struct.pack("<%dh" % len(samples), *samples).hex() # ---------------------------------------------------------------- §4 the room at an instant def render_field(f): """MOQT §8.8.""" return "".join(chr(c) if (chr(c).isascii() and chr(c).isalnum()) or c == 0x5F else ".%02x" % c for c in f.encode()) def track_digest(room_id, owner, media, name): """live-events §3.1's anchor track: the rendered namespace, a zero byte, the name.""" ns = "-".join(render_field(f) for f in (room_id.hex(), owner, MEDIA_NAME[media])) return sha(ns.encode(), b"\x00", name.encode()) class RoomView: """A room that verified (§4.1), its replay stepped record by record.""" def __init__(self, d, held, steps, final): self.d, self.held, self.steps, self.final = d, held, steps, final def grant(self, digest): return next(g for g in self.held if g["digest"] == digest) def at(self, t): """The room at t: the state before the first record of the merged order past t.""" return next((s for rt, s in self.steps if rt > t), self.final) def snapshot(room, joins): return {"present": dict(room.present), "own": dict(room.own), "slots": copy.deepcopy(room.slots), "joins": dict(joins)} def view_room(room): """live-rooms §9 over the room, stepped: a RoomView, or (reason, place).""" descriptor = bytes.fromhex(room["descriptor"]) grants = [bytes.fromhex(g) for g in room["grants"]] head = room["key_log_head"] chains = [[bytes.fromhex(r) for r in c] for c in room["chains"]] try: d = LRV.read_descriptor(descriptor) except Refuse as e: return e.reason, ["room", "descriptor"] try: revoked = LRV.check_key_log(d, head) except Refuse as e: return e.reason, ["room", "key_log"] held, bad = LRV.check_grants(d, grants) if bad: return bad[1], ["room", "grant", bad[0]] state = LRV.Room(d, held, revoked) auth = LRV.authority_policy() ver = LEV.Verifier(d["room"], auth, {}) seen, joins, steps, at_index = {}, {}, [], [] numbered = 0 for c, i in LRV.merged_order(d, chains): b = chains[c][i] at_index.append((c, i)) try: ev = LEV.read_record(b) duplicate = ev["digest"] in seen except Refuse: ev, duplicate = None, False if ev is not None: steps.append((ev["t"], snapshot(state, joins))) try: LRV.step(d, state, ver, seen, auth, b) except Refuse as e: return e.reason, ["room", "record", c, i] if not duplicate and LRV.is_room_key(d, ev["principal"]) and ev["kind"] == JOIN: joins[ver.accepted[-1][2]["subject"]] = numbered numbered += 1 if ver.gaps: if any(LRV.is_room_key(d, p) for p, _, _ in ver.gaps): return "lifecycle_incomplete", ["room"] else: mismatch = LEV.stated_tally_mismatch(ver.accepted) if mismatch is not None: c, i = at_index[mismatch] return "tally_mismatch", ["room", "record", c, i] return RoomView(d, held, steps, snapshot(state, joins)) def source(view, c, profile, layers, slot, media): """§4.2: a source and its composed layer.""" prof = view.d["profiles"][profile] layer = min(layers - 1, c["layer"]) w, h = prof["layers"][layer][0], prof["layers"][layer][1] return {"slot": slot, "media": media, "profile": profile, "layers": layers, "layer": layer, "layering": prof["layering"], "width": w, "height": h} def tracks(view, s, principal): """§8.2: the tracks a source is read from.""" owner = principal.hex() if s["slot"] == "" else "_" + s["slot"] if s["layering"] == SIMULCAST: names = [s["layer"]] elif s["layering"] == STAGED: names = list(range(s["layer"] + 1)) else: names = [0] return [track_digest(view.d["room"], owner, s["media"], str(n)) for n in names] def participants(view, c, t): """§4.2 at t, in ascending order of key.""" st = view.at(t) out = [] slots = list(zip(view.d["slots"], st["slots"])) def own(p, m): h = st["own"].get((p, m)) return None if h is None else source(view, c, h[0], h[1], "", m) def live_in(p, kinds): for s, ss in slots: if ss["live"] and ss["live"][0] == p and s["media"] in kinds: return source(view, c, ss["live"][1], ss["live"][2], s["name"].decode(), s["media"]) return None for p in sorted(st["present"]): if p == c["compositor"]: continue if not (any(q == p for q, _ in st["own"]) or any(ss["live"] and ss["live"][0] == p for _, ss in slots)): continue video = own(p, VIDEO) or own(p, SCREEN) or live_in(p, (VIDEO, SCREEN)) audio = own(p, AUDIO) or live_in(p, (AUDIO,)) g = view.grant(st["present"][p]) out.append({"principal": p, "grant": g["digest"], "join": st["joins"][p], "attrs": LRV.read_attrs(g["attrs"]), "video": video, "audio": audio}) return out def set_digest(view, t, ps): """§4.5.""" def src(s): if s is None: return b"\x00" slot = s["slot"].encode() return bytes([s["media"], len(slot)]) + slot + bytes([s["profile"], s["layers"], s["layer"]]) \ + struct.pack(" 1)["video"]["tracks"].reverse()), mutate("grid_session: an instant moved", lambda d: case(d, "compositions", "grid_session")["instants"].__setitem__(4, 4500000)), mutate("pip_session: a voice's pan", lambda d: frame_at(d, "pip_session", 6000000)["voices"][0].update(pan=1)), mutate("studio: a lifecycle record not delivered", relay_drop), mutate("studio: a byte of a grant", lambda d: d["rooms"]["studio"]["grants"].__setitem__(1, flip(d["rooms"]["studio"]["grants"][1], 200))), mutate("receipt video: a byte of it", lambda d: case(d, "receipts", "video").update(receipt=flip(case(d, "receipts", "video")["receipt"], 150))), mutate("receipt video: its stated digest", lambda d: case(d, "receipts", "video").update(digest="00" * 32)), mutate("receipt inputs_missing: stated accepted", lambda d: case(d, "receipts", "inputs_missing").update(outcome="accept")), mutate("receipt state_stale: stated inputs_mismatch", lambda d: case(d, "receipts", "state_stale").update(reason="inputs_mismatch")), ] def main(corpus_dir): fails = [] st = ED._self_test() print(("PASS" if not st else "FAIL") + " Ed25519 strict rule self-test " + ",".join(st)) if st: fails.append("ed25519 self-test") with open(os.path.join(corpus_dir, "vectors.json"), encoding="utf-8") as f: doc = json.load(f) rooms = Rooms() fails += check(doc, rooms) seen = {v.get("reason") for s, _ in SECTIONS for v in doc[s]} missing = [r for r in REASONS if r not in seen] if missing: fails.append("no case is refused %s" % ", ".join(missing)) print("%s every reason of §10 refuses a case" % ("PASS" if not missing else "FAIL")) for label, mutated in mutations(doc): caught = check(mutated, rooms, quiet=True) print(("PASS" if caught else "FAIL") + " mutated copy fails: %s%s" % (label, " (%s)" % caught[0][:120] if caught else "")) if not caught: fails.append("a mutated copy verifies: %s" % label) for f in fails: print("FAIL " + f) total = sum(len(doc[s]) for s, _ in SECTIONS) print(("PASS" if not fails else "FAIL") + " live-composition corpus: %d cases" % total) return 1 if fails else 0 if __name__ == "__main__": if len(sys.argv) != 2: print("usage: live_compose_verify.py ", file=sys.stderr) sys.exit(2) sys.exit(main(sys.argv[1]))