open-standards/wai/live-conformance/remote_desktop_verify.py
Transaction Science 5452b60cca wai.live.remote: key, text and clipboard actions sealed to the host; the commitment level and the opened level
A host states a sealing key (an X25519 public key) as its screen statement's last field. A
controller seals every key, text and clipboard action to it: a sealed action (type 8) keeps in the
clear the type it seals, its dt_us, a clipboard text's length and a salted commitment binding its
fields, its order and its author; the statement ends with one HPKE seal (RFC 9180 base mode,
DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-128-GCM) of the actions' openings. Pointer, wheel and
touch actions stay in the clear. §5.3 gains rule 8 (input_unsealed) and rule 9 (input_bad_seal),
§12 a fourth table (seal_no_key, seal_not_opened, seal_unreadable, seal_commitment), §11.4 the two
verification levels.

wai-rs: Input::seal, Input::open, commitment, decide_opened, verify_opened; HPKE moves to a crate
module behind its own feature (hpke), which telemetry_aggregate and remote_desktop both use. The
corpus keeps its 42 vectors byte for byte and appends ten sealed ones, each stating the opened
level; remote_desktop_verify.py checks both levels.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 10:30:56 -04:00

927 lines
44 KiB
Python

#!/usr/bin/env python3
"""Independent verifier of the remote desktop corpus (extensions/remote-desktop.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), written from its text alone. Seals are opened with the HPKE base mode, X25519,
HKDF-SHA256 and AES-128-GCM of the aggregation corpus's second implementation,
telemetry_aggregate_verify.py, written from RFC 9180, RFC 7748, RFC 5869, FIPS 197 and SP
800-38D, itself checked against RFC 9180 A.1.1 here. Everything else is here:
- the grant attributes `remote_host` and `remote_control` (§3.1);
- the screen track's digest, the screen statement, its rules and its chain (§3.2 to §3.4);
- reading every statement and who signs it (§4);
- input statements and their actions (§5.1, §5.2), and the host's rules (§5.3);
- sealed actions (§5.5): the seal's form, its opening under the host's opening key, each
opening read and each commitment recomputed, at the opened level (§11.4);
- outcomes and their checks, the rules recomputed (§6);
- floor statements and the floor in effect (§7): a floor statement §7.2 refuses is stated with
its reason and changes nothing;
- latency records, their checks and their legs (§8);
- what a verification states (§11.3).
For every room it requires the outcome, reason and place, and everything else the corpus
states, at the commitment level and, under the corpus's opening key, at the opened level; of
the corpus, that every reason of the four tables of §12 appears in it and that no other file is
there. Then it mutates copies of the corpus, one defect each, and requires every copy to
fail; and it drops each of its own checks in turn and requires the corpus to catch the weakened
verifier.
python3 remote_desktop_verify.py <live-conformance/remote>
python3 remote_desktop_verify.py --room <room.json> [--opening-key <hex>]...
With `--room`, it verifies one room given in a vector's form (descriptor, grants, chains) and
prints what §11.3 states of it, as JSON: a session a deployment kept, checked from the text
alone; given opening keys (X25519 private keys, hexadecimal), at the opened level.
Exit status 0 only if every case verifies, every mutated copy fails and every weakened verifier
is caught. Python 3.8 or later; standard library only.
"""
import copy
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
import live_rooms_verify as LRV # noqa: E402
import telemetry_aggregate_verify as HPKE # noqa: E402
import _ed25519 as ED # noqa: E402
Refuse = LEV.Refuse
_verified = {}
_verify = ED.verify
def _memo_verify(pk, msg, sig, skip=()):
k = (bytes(pk), bytes(msg), bytes(sig), tuple(skip))
if k not in _verified:
_verified[k] = _verify(pk, msg, sig, skip)
return _verified[k]
ED.verify = _memo_verify
EXTENSION = os.path.join(HERE, "..", "extensions", "remote-desktop.md")
MAGICS = {b"WDS1": "screen", b"WDI1": "input", b"WDO1": "outcome", b"WDF1": "floor", b"WDL1": "latency"}
LATENCY_DOMAIN = b"wai:remote-latency\x01" # §8.1
SEAL_INFO = b"wai:remote-seal\x01" # §5.5
COMMIT_DOMAIN = b"wai:remote-commit\x01" # §5.5
SALT, TAG, TEXT_PAD = 16, 16, 64 # §5.5
ZERO = bytes(32)
NO_RTT = (1 << 32) - 1
META, JOIN, LEAVE, EXCHANGE = 0x01, 0x10, 0x11, 0x14
DIRECT = 2 # live-rooms §3.3, bit 1
SCREEN_PUBLISH = 4 # live-rooms §3.3, publish bit 2
TYPES = {1: "move", 2: "button", 3: "wheel", 4: "key", 5: "text", 6: "touch", 7: "clipboard"}
CLASS_OF = {1: 1, 2: 1, 3: 1, 4: 2, 5: 2, 6: 4, 7: 8} # §5.2: pointer, keys, touch, clipboard
SEALED = 8 # §5.2: a sealed action
SEALABLE = (4, 5, 7) # §5.5: key, text, clipboard
CLASS_NAMES = ((1, "pointer"), (2, "keys"), (4, "touch"), (8, "clipboard"))
REQUEST, GIVE, RELEASE, TAKE = 1, 2, 3, 4
DROP = set()
def on(name):
return name not in DROP
class Stop(Exception):
def __init__(self, reason, at):
super().__init__(reason)
self.reason, self.at = reason, at
def sha(*parts):
h = hashlib.sha256()
for p in parts:
h.update(p)
return h.digest()
def tables_of_extension():
"""§12: the codes of the first table (they refuse a room), the numbered reasons of the second
(a host states them), the codes of the third (a floor statement refused, which changes
nothing), and those of the fourth (why a seal is not opened, §5.5)."""
with open(EXTENSION, encoding="utf-8") as f:
text = f.read()
sec = text[text.index("\n## 12. Reasons"):]
sec = sec[:sec.index("\n## ", 1)]
tables = re.findall(r"((?:^\|.*\n)+)", sec, re.M)
if len(tables) != 4:
return [], {}, [], []
codes = re.findall(r"^\| `([a-z_0-9]+)` \|", tables[0], re.M)
numbered = {int(n): c for n, c in re.findall(r"^\| ([0-9]+) \| `([a-z_0-9]+)` \|", tables[1], re.M)}
floor_codes = re.findall(r"^\| `([a-z_0-9]+)` \|", tables[2], re.M)
seal_codes = re.findall(r"^\| `([a-z_0-9]+)` \|", tables[3], re.M)
return codes, numbered, floor_codes, seal_codes
REASONS, HOST_REASONS, FLOOR_REASONS, SEAL_REASONS = tables_of_extension()
ACTION_NAMES = {1: "request", 2: "give", 3: "release", 4: "take"}
class Rd:
def __init__(self, b):
self.b, self.p = b, 0
def take(self, n):
if self.p + n > len(self.b):
raise Refuse("rd_statement_malformed")
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("<H", self.take(2))[0]
def i16(self):
return struct.unpack("<h", self.take(2))[0]
def u32(self):
return struct.unpack("<I", self.take(4))[0]
def u64(self):
return struct.unpack("<Q", self.take(8))[0]
def short(self):
return self.take(self.u8())
def bad(cond):
if cond:
raise Refuse("rd_statement_malformed")
def utf8(b):
"""RFC 3629: Python's strict decoder refuses overlong forms, surrogates and anything above
U+10FFFF."""
try:
return b.decode("utf-8")
except UnicodeDecodeError:
raise Refuse("rd_statement_malformed")
def token(b):
"""live-rooms §2: one or more bytes of 0x21 to 0x7E."""
return len(b) > 0 and all(0x21 <= c <= 0x7E for c in b)
# ---------------------------------------------------------------- §3
def attrs_of(g):
"""§3.1: (hosts, control classes); Refuse when either attribute is malformed."""
host, control = False, 0
for k, v in LRV.read_attrs(g["attrs"]) or []:
if k == "remote_host":
if v not in ("true", "false"):
raise Refuse("rd_attr_malformed")
host = v == "true"
elif k == "remote_control":
if not re.fullmatch(r"[1-9][0-9]?", v) or int(v) > 15:
raise Refuse("rd_attr_malformed")
control = int(v)
return host, control
def screen_track(room, host):
"""§3.2: SHA-256 over `<room>-<host>-screen`, a zero byte, and `0`."""
return sha(("%s-%s-screen" % (room.hex(), host.hex())).encode(), b"\x00", b"0")
def rect(r):
return {"x": r.u16(), "y": r.u16(), "w": r.u16(), "h": r.u16()}
def read_screen(r):
"""§3.3."""
s = {"prev": r.take(32), "state": r.u8(), "track": r.take(32), "width": r.u16(), "height": r.u16(), "fps": r.u16(),
"scope": r.u8(), "region": rect(r)}
s["redactions"] = [rect(r) for _ in range(r.u8())]
s.update(pointer=r.u8(), accepts=r.u8(), clip_max=r.u16(), max_age_ms=r.u32(), floor=r.u8())
s["capability"], s["codec"], s["encoder"] = r.short(), r.short(), r.short()
s["api"], s["hardware"] = r.u8(), r.u8()
# §3.3: the statement ends after `hardware`, or after a 32-byte sealing key; read_statement
# refuses any other byte after its last field.
s["seal"] = r.take(32) if len(r.b) - r.p == 32 else None
if on("screen_fields"):
reg = s["region"]
bad(s["state"] not in (1, 2) or s["width"] == 0 or s["height"] == 0 or s["fps"] == 0 or s["scope"] not in (1, 2, 3))
bad((reg["w"] == 0 or reg["h"] == 0) if s["scope"] == 3 else any(reg.values()))
bad(len(s["redactions"]) > 16)
for x in s["redactions"]:
bad(x["w"] == 0 or x["h"] == 0 or x["x"] + x["w"] > s["width"] or x["y"] + x["h"] > s["height"])
bad(s["pointer"] > 1 or s["accepts"] & ~0x7F or (s["clip_max"] > 0) != bool(s["accepts"] & 0x40))
bad(s["max_age_ms"] == 0 or s["floor"] not in (1, 2) or s["api"] not in range(1, 6) or s["hardware"] > 2)
bad(not token(s["capability"]) or not token(s["codec"]))
e = s["encoder"]
bad(not token(e) or e.count(b"@") != 1 or e[:1] == b"@" or e[-1:] == b"@")
return s
# ---------------------------------------------------------------- §5
def read_action(r):
"""§5.2: (type, fields)."""
start = r.p
ty, a = read_action_fields(r)
a["raw"] = r.b[start:r.p]
return ty, a
def read_action_fields(r):
ty = r.u8()
if ty == 1:
return ty, {"x": r.u16(), "y": r.u16()}
if ty == 2:
a = {"button": r.u8(), "down": r.u8(), "x": r.u16(), "y": r.u16()}
bad(a["button"] > 4 or a["down"] > 1)
return ty, a
if ty == 3:
a = {"dx": r.i16(), "dy": r.i16(), "x": r.u16(), "y": r.u16()}
bad(a["dx"] == 0 and a["dy"] == 0)
return ty, a
if ty == 4:
a = {"usage": r.u16(), "down": r.u8(), "modifiers": r.u8()}
bad(not 0x04 <= a["usage"] <= 0xE7 or a["down"] > 1 or a["modifiers"] & ~0x0F)
return ty, a
if ty == 5:
b = r.short()
bad(not 1 <= len(b) <= 64)
return ty, {"text": utf8(b)}
if ty == 6:
a = {"id": r.u8(), "phase": r.u8(), "x": r.u16(), "y": r.u16()}
bad(not 1 <= a["phase"] <= 4)
return ty, a
if ty == 7:
b = r.take(r.u16())
return ty, {"text": utf8(b), "bytes": len(b)}
if ty == SEALED:
a = {"of": r.u8(), "length": r.u16(), "commit": r.take(32)}
if on("sealed_of"):
bad(a["of"] not in SEALABLE or (a["of"] != 7 and a["length"] != 0))
return ty, a
raise Refuse("rd_statement_malformed")
def opening_len(ty, a):
"""§5.5: the length of a sealed action's opening: the salt, then 4 for a key, 1 + 64 for a
text, a clipboard text's `length`."""
if ty != SEALED:
return 0
return SALT + {4: 4, 5: 1 + TEXT_PAD}.get(a["of"], a["length"])
def acts_as(ty, a):
"""§5.2: a sealed action is of the type it seals."""
return a["of"] if ty == SEALED else ty
def read_input(r):
"""§5.1, and the seal of §5.5."""
i = {"host": r.take(32), "screen": r.take(32), "actions": [], "seal": None}
n = r.u8()
bad(not 1 <= n <= 32)
for _ in range(n):
dt = r.u32()
if on("dt_order"):
bad(i["actions"] and dt > i["actions"][-1][0])
ty, a = read_action(r)
i["actions"].append((dt, ty, a))
i["clear"] = r.b[:r.p]
if any(ty == SEALED for _, ty, _ in i["actions"]):
enc = r.take(32)
want = TAG + sum(opening_len(ty, a) for _, ty, a in i["actions"])
if on("seal_length"):
bad(len(r.b) - r.p != want)
i["seal"] = (enc, r.take(len(r.b) - r.p))
return i
def read_outcome(r):
"""§6.1."""
o = {"input": r.take(32), "outcome": r.u8(), "reason": r.u8(), "applied_at": r.u64(), "captured_at": r.u64(),
"content": r.take(32), "rtt_us": r.u32()}
bad(o["outcome"] not in (1, 2) or o["reason"] > 9)
return o
def read_floor(r):
"""§7.2."""
f = {"host": r.take(32), "action": r.u8(), "to": r.take(32)}
bad(f["action"] not in (1, 2, 3, 4) or (f["to"] != ZERO) != (f["action"] == GIVE))
return f
def read_latency(r):
"""§8.1."""
start = r.p
l = {"room": r.take(32), "viewer": r.take(32), "host": r.take(32), "input": r.take(32), "action": r.u8(),
"outcome": r.take(32), "track": r.take(32), "group": r.u64(), "object": r.u64(), "content": r.take(32),
"input_at": r.u64(), "presented_at": r.u64(), "source": r.u8(), "latency_us": r.u32(), "rtt_us": r.u32()}
l["body"] = r.b[:r.p]
l["sig"] = r.take(64)
bad(l["source"] not in (1, 2, 3))
del start
return l
def read_statement(payload):
"""§4.1, §4.2: None for a payload not this extension's; else (kind, fields)."""
kind = MAGICS.get(payload[:4])
if kind is None:
return None
r = Rd(payload)
r.take(4)
bad(r.u8() != 1)
f = {"screen": read_screen, "input": read_input, "outcome": read_outcome, "floor": read_floor, "latency": read_latency}[kind](r)
bad(r.p != len(payload))
return kind, f
def coords(ty, a):
return (a["x"], a["y"]) if ty in (1, 2, 3, 6) else None
def rules(inp, screen_digest, screen, control, holds_floor, me):
"""§5.3 steps 1 to 6 at the input's place: the first reason's number, or 0."""
if on("rule_screen") and (inp["host"] != me or screen is None or inp["screen"] != screen_digest):
return 1
if screen is None:
return 1
classes = 0
for _, ty, a in inp["actions"]:
classes |= CLASS_OF[acts_as(ty, a)]
if on("rule_power") and classes & ~control:
return 2
if on("rule_floor") and screen["floor"] == 2 and not holds_floor:
return 3
if on("rule_paused") and screen["state"] == 2:
return 4
if on("rule_accepts") and any(not screen["accepts"] >> (acts_as(ty, a) - 1) & 1 for _, ty, a in inp["actions"]):
return 5
for _, ty, a in inp["actions"]:
c = coords(ty, a)
clip = a["bytes"] if ty == 7 else a["length"] if ty == SEALED and a["of"] == 7 else 0
if on("rule_bounds") and ((c and (c[0] >= screen["width"] or c[1] >= screen["height"])) or clip > screen["clip_max"]):
return 6
return 0
def unsealed(inp, screen):
"""§5.3 step 8: a key, text or clipboard action in the clear to a screen with a sealing key."""
return on("rule_unsealed") and screen is not None and screen["seal"] is not None and any(ty in SEALABLE for _, ty, _ in inp["actions"])
# ---------------------------------------------------------------- §5.5
_opened = {}
def open_seal(inp, room, author, screen, keys):
"""§5.5, rule 9 of §5.3, against the screen statement the input names: ("opened", the opened
statement), (a reason of §12's fourth table, None), or ("unknown", None) when no opening key
for its sealing key is given (the commitment level gives none)."""
if screen is None or screen["seal"] is None:
return ("seal_no_key", None) if on("seal_no_key") else ("unknown", None)
sk = next((k for k in keys if HPKE.x25519(k, HPKE.BASE) == screen["seal"]), None)
if sk is None:
return "unknown", None
enc, ct = inp["seal"]
memo = (sk, enc, ct, room, author, inp["clear"])
if memo in _opened and not DROP:
return _opened[memo]
out = _open(inp, room, author, sk, enc, ct)
if not DROP:
_opened[memo] = out
return out
def _open(inp, room, author, sk, enc, ct):
info = SEAL_INFO + room + inp["host"]
aad = author + inp["clear"]
try:
pt = HPKE.hpke_open(enc, sk, info, aad, ct)
except ValueError:
if on("seal_open"):
return "seal_not_opened", None
return "opened", inp["clear"]
at, actions, commits = 0, [], []
for k, (dt, ty, a) in enumerate(inp["actions"]):
if ty != SEALED:
actions.append(a["raw"])
continue
n = opening_len(ty, a)
opening = pt[at:at + n]
at += n
f = opening[SALT:]
raw = read_opening(a["of"], f, len(opening) == n)
if raw is None:
if on("seal_read"):
return "seal_unreadable", None
raw = bytes([a["of"]])
actions.append(raw)
commits.append((k, dt, a, opening))
if on("seal_read") and at != len(pt):
return "seal_unreadable", None
for k, dt, a, opening in commits:
c = sha(COMMIT_DOMAIN, room, author, inp["host"], inp["screen"], bytes([k]), struct.pack("<I", dt), bytes([a["of"]]), opening)
if on("seal_commit") and c != a["commit"]:
return "seal_commitment", None
opened = inp["clear"][:70] + b"".join(struct.pack("<I", dt) + raw for (dt, _, _), raw in zip(inp["actions"], actions))
return "opened", opened
def read_opening(of, f, whole):
"""§5.5: the action an opening's fields (after the salt) read as, in its clear form (§5.2), or
None when they do not read."""
if not whole:
return None
if of == 4:
usage, down, mods = struct.unpack("<HBB", f)
if not 0x04 <= usage <= 0xE7 or down > 1 or mods & ~0x0F:
return None
return bytes([4]) + f
if of == 5:
n = f[0]
if not 1 <= n <= 64 or any(f[1 + n:]):
return None
try:
f[1:1 + n].decode("utf-8")
except UnicodeDecodeError:
return None
return bytes([5, n]) + f[1:1 + n]
try:
f.decode("utf-8")
except UnicodeDecodeError:
return None
return bytes([7]) + struct.pack("<H", len(f)) + f
def stale(inp, t, screen, n):
"""§5.3 step 7."""
first = t - inp["actions"][0][0]
return on("rule_stale") and n - first > screen["max_age_ms"] * 1000
# ---------------------------------------------------------------- §11
def verify(descriptor, grants, chains, keys=None):
"""§11.2: a dict of what the corpus states (§11.3), at the commitment level, or with opening
keys (X25519 private keys) at the opened level (§11.4)."""
room = LRV.verify_room(descriptor, grants, None, chains) # step 1
if room["outcome"] == "refuse":
return {"outcome": "refuse", "reason": room["reason"], "at": ["room"] + room["at"]}
gap = room["outcome"] == "gap"
d = LRV.read_descriptor(descriptor)
room_id = d["room"]
held_grants, _ = LRV.check_grants(d, grants)
room_keys = (d["authority"], d["clock"])
try:
attrs = {}
for i, g in enumerate(held_grants): # step 2
try:
host, control = attrs_of(g)
except Refuse as e:
if on("attrs"):
raise Stop(e.reason, ["grant", i])
host, control = False, 0
attrs[g["digest"]] = {"host": host and g["publish"] & SCREEN_PUBLISH != 0, "control": control,
"direct": g["powers"] & DIRECT != 0}
records, seen = [], set()
for c, i, _ in room["order"]:
ev = LEV.read_record(chains[c][i])
if ev["digest"] in seen:
continue
seen.add(ev["digest"])
ev["chain"], ev["index"] = c, i
records.append(ev)
in_effect, hosts = {}, {}
screens, inputs, floor, floor_refused, later = [], [], [], [], []
named = {}
def attrs_now(p):
g = in_effect.get(p)
return attrs.get(g, {"host": False, "control": 0, "direct": False})
def lose(p, t):
for h, st in hosts.items():
if st["holder"] == p:
st["holder"] = None
floor.append({"host": h.hex(), "t": t, "holder": None})
for ev in records: # step 3
t, author = ev["t"], ev["principal"]
if author in room_keys and ev["kind"] in (JOIN, LEAVE, EXCHANGE):
pl = LEV.read_payload(ev["kind"], ev["payload"], 1 << 20)
x = LRV.read_detail(ev["kind"], pl["detail"])
who = pl["subject"]
if ev["kind"] == JOIN:
in_effect[who] = x["grant"]
elif ev["kind"] == EXCHANGE:
in_effect[who] = x["new"]
if on("floor_loss") and attrs_now(who)["control"] == 0:
lose(who, t)
else:
in_effect.pop(who, None)
if on("floor_loss"):
lose(who, t)
if who in hosts and hosts[who]["holder"] is not None:
hosts[who]["holder"] = None
floor.append({"host": who.hex(), "t": t, "holder": None})
continue
if ev["kind"] != META:
continue
at = ["record", ev["chain"], ev["index"]]
try:
s = read_statement(ev["payload"])
if s is None:
continue
kind, f = s
is_key = author in room_keys
me = attrs_now(author)
if kind == "screen":
if on("who_screen") and (is_key or not me["host"]):
raise Refuse("rd_not_permitted")
if on("track") and f["track"] != screen_track(room_id, author):
raise Refuse("screen_wrong_track")
st = hosts.setdefault(author, {"screen": None, "digest": ZERO, "holder": None})
if on("screen_link") and f["prev"] != st["digest"]:
raise Refuse("screen_unlinked")
st["screen"], st["digest"] = f, ev["digest"]
named[(author, ev["digest"])] = f
screens.append({"digest": ev["digest"].hex(), "host": author.hex(), "t": t,
"state": "live" if f["state"] == 1 else "paused", "width": f["width"], "height": f["height"],
"fps": f["fps"], "scope": f["scope"], "floor": "open" if f["floor"] == 1 else "floor",
"accepts": [TYPES[k] for k in range(1, 8) if f["accepts"] >> (k - 1) & 1],
"capability": f["capability"].decode(), "codec": f["codec"].decode(), "encoder": f["encoder"].decode(),
"api": f["api"], "hardware": f["hardware"]})
elif kind == "input":
if on("who_input") and is_key:
raise Refuse("rd_not_permitted")
st = hosts.get(f["host"])
scr = st["screen"] if st else None
first = rules(f, st["digest"] if st else None, scr, me["control"], bool(st) and st["holder"] == author, f["host"])
classes = 0
for _, ty, a in f["actions"]:
classes |= CLASS_OF[acts_as(ty, a)]
# Rule 9 against the screen statement the input names (§5.5).
seal = None
if f["seal"] is not None:
seal = open_seal(f, room_id, author, named.get((f["host"], f["screen"])), keys or [])
inputs.append({"digest": ev["digest"], "author": author, "t": t, "input": f, "first": first,
"unsealed": unsealed(f, scr), "seal": seal,
"sealed": sum(1 for _, ty, _ in f["actions"] if ty == SEALED),
"screen": scr, "classes": classes, "fate": {"outcome": "unanswered"}})
elif kind == "floor":
# §7.2: a statement its rules refuse is recorded, stated with its reason, and
# changes nothing; the floor is computed without it (§7.3, §11.3).
st = hosts.get(f["host"])
a = f["action"]
why = None
if on("floor_host") and (st is None or st["screen"] is None):
why = "floor_not_permitted"
else:
st = st or {"holder": None}
steward = is_key or author == f["host"] or me["direct"]
if a == REQUEST:
if on("floor_request") and (is_key or me["control"] == 0):
why = "floor_not_permitted"
elif a == GIVE:
if on("floor_give") and not (steward or st["holder"] == author):
why = "floor_not_permitted"
elif on("floor_eligible") and (f["to"] == f["host"] or attrs_now(f["to"])["control"] == 0):
why = "floor_not_eligible"
elif a == RELEASE:
if on("floor_release") and st["holder"] != author:
why = "floor_not_holder"
elif on("floor_take") and not steward:
why = "floor_not_permitted"
if why is not None:
floor_refused.append({"digest": ev["digest"].hex(), "author": author.hex(), "host": f["host"].hex(), "t": t,
"action": ACTION_NAMES[a], "reason": why})
elif a == GIVE:
st["holder"] = f["to"]
floor.append({"host": f["host"].hex(), "t": t, "holder": f["to"].hex()})
elif a == RELEASE:
st["holder"] = None
floor.append({"host": f["host"].hex(), "t": t, "holder": None})
elif a == TAKE and st["holder"] is not None:
st["holder"] = None
floor.append({"host": f["host"].hex(), "t": t, "holder": None})
elif kind == "outcome":
later.append(("outcome", at, ev, f))
else:
if on("who_latency") and is_key:
raise Refuse("rd_not_permitted")
if f["room"] != room_id:
raise Refuse("wrong_room")
if on("latency_key") and f["viewer"] != author:
raise Refuse("latency_wrong_key")
if on("latency_sig"):
why = ED.verify(f["viewer"], LATENCY_DOMAIN + f["body"], f["sig"])
if why:
raise Refuse(why)
later.append(("latency", at, ev, f))
except Refuse as e:
raise Stop(e.reason, at)
by_input = {x["digest"]: x for x in inputs}
outcomes = {ev["digest"]: (ev, f) for k, _, ev, f in later if k == "outcome"}
answered, latencies = set(), []
for k, at, ev, f in later: # §6.2, §8.2
try:
if k == "outcome":
x = by_input.get(f["input"])
if x is None:
if gap:
continue
raise Refuse("outcome_unknown_input")
if on("who_outcome") and ev["principal"] != x["input"]["host"]:
raise Refuse("rd_not_permitted")
if on("duplicate") and f["input"] in answered:
raise Refuse("outcome_duplicate")
answered.add(f["input"])
an = ev["anchor"]
if f["outcome"] == 1:
ok = (f["reason"] == 0 and an is not None and an[0] == screen_track(room_id, x["input"]["host"]) and an[2] == 0
and f["content"] != ZERO and x["t"] <= f["applied_at"] <= f["captured_at"] <= ev["t"])
else:
ok = (1 <= f["reason"] <= 9 and an is None and f["applied_at"] == 0 and f["captured_at"] == 0
and f["content"] == ZERO and ev["t"] >= x["t"])
if on("consistent") and not ok:
raise Refuse("outcome_invalid")
want = x["first"]
if want == 0 and stale(x["input"], x["t"], x["screen"], ev["t"]):
want = 7
if want == 0 and x["unsealed"]:
want = 8
# Rule 9: made where the seal's result is known, and otherwise either (§6.2 step 5).
allowed = {want}
if want == 0 and x["seal"] is not None:
allowed = {9} if x["seal"][0] not in ("opened", "unknown") else {0} if x["seal"][0] == "opened" else {0, 9}
stated = 0 if f["outcome"] == 1 else f["reason"]
if on("recompute") and stated not in allowed:
raise Refuse("outcome_mismatch")
x["fate"] = ({"outcome": "applied", "group": an[1], "object": an[2]} if f["outcome"] == 1 and an
else {"outcome": "refused", "reason": HOST_REASONS.get(f["reason"], "?")})
else:
x = by_input.get(f["input"])
if x is None and gap:
latencies.append({"digest": sha(f["body"]).hex(), "viewer": f["viewer"].hex(), "source": f["source"],
"latency_us": f["latency_us"], "legs": "unchecked"})
continue
if on("latency_input") and (x is None or x["author"] != f["viewer"] or x["input"]["host"] != f["host"]
or f["action"] >= len(x["input"]["actions"])):
raise Refuse("latency_unknown_input")
o = outcomes.get(f["outcome"])
if o is None and gap:
latencies.append({"digest": sha(f["body"]).hex(), "viewer": f["viewer"].hex(), "source": f["source"],
"latency_us": f["latency_us"], "legs": "unchecked"})
continue
dt = x["input"]["actions"][f["action"]][0]
if on("latency_frame") and not (o is not None and o[1]["input"] == f["input"] and o[1]["outcome"] == 1
and o[0]["anchor"] == (f["track"], f["group"], f["object"])
and o[1]["content"] == f["content"] and f["input_at"] == x["t"] - dt):
raise Refuse("latency_mismatch")
if on("latency_ahead") and f["presented_at"] > ev["t"]:
raise Refuse("latency_ahead")
if on("latency_sum") and (f["presented_at"] < f["input_at"] or f["presented_at"] - f["input_at"] != f["latency_us"]):
raise Refuse("latency_mismatch")
oo = o[1]
unc = None if f["rtt_us"] == NO_RTT or oo["rtt_us"] == NO_RTT else (f["rtt_us"] + oo["rtt_us"]) // 2
latencies.append({"digest": sha(f["body"]).hex(), "viewer": f["viewer"].hex(), "source": f["source"],
"latency_us": f["latency_us"],
"legs": {"up": oo["applied_at"] - f["input_at"], "host": oo["captured_at"] - oo["applied_at"],
"down": f["presented_at"] - oo["captured_at"], "uncertainty": unc}})
except Refuse as e:
raise Stop(e.reason, at)
except Stop as s:
return {"outcome": "refuse", "reason": s.reason, "at": s.at}
totals = {}
for x in inputs:
if gap and x["fate"]["outcome"] == "unanswered":
x["fate"] = {"outcome": "unchecked"}
h = x["input"]["host"].hex()
tt = totals.setdefault(h, {"host": h, "applied": 0, "refused": {HOST_REASONS[n]: 0 for n in sorted(HOST_REASONS)},
"unanswered": 0, "unchecked": 0})
fo = x["fate"]["outcome"]
if fo == "refused":
tt["refused"][x["fate"]["reason"]] += 1
else:
tt[fo] += 1
# §11.3: rules 1 to 7 always, rules 8 and 9 where they refused.
for tt in totals.values():
tt["refused"] = {k: v for k, v in tt["refused"].items() if v or k not in (HOST_REASONS.get(8), HOST_REASONS.get(9))}
out = {"outcome": "gap" if gap else "accept", "room": room_id.hex(),
"gaps": room.get("gaps", []) if gap else [],
"screens": screens,
"inputs": [input_fact(x, keys is not None) for x in inputs],
"floor": floor, "latencies": latencies, "totals": [totals[h] for h in sorted(totals)]}
if floor_refused:
out["floor_refused"] = floor_refused
return out
def input_fact(x, opened_level):
"""§11.3: an input as a verification states it; at the opened level, a sealed input's seal."""
o = {"digest": x["digest"].hex(), "author": x["author"].hex(), "host": x["input"]["host"].hex(), "t": x["t"],
"classes": [n for b, n in CLASS_NAMES if x["classes"] & b]}
if x["sealed"]:
o["sealed"] = x["sealed"]
o["fate"] = x["fate"]
if opened_level and x["seal"] is not None:
why, opened = x["seal"]
o["seal"] = "unopened" if why == "unknown" else why
if why == "opened":
o["opened"] = opened.hex()
return o
def run_vector(v, keys=None):
return verify(bytes.fromhex(v["descriptor"]), [bytes.fromhex(g) for g in v["grants"]], [[bytes.fromhex(r) for r in c] for c in v["chains"]], keys)
STATED = ("outcome", "reason", "at", "room", "gaps", "screens", "inputs", "floor", "floor_refused", "latencies", "totals")
def given(got, seen):
if got.get("outcome") == "refuse":
seen.add(got["reason"])
for x in got.get("inputs", []):
if x["fate"]["outcome"] == "refused":
seen.add(x["fate"]["reason"])
if x.get("seal", "opened") not in ("opened", "unopened"):
seen.add(x["seal"])
for x in got.get("floor_refused", []):
seen.add(x["reason"])
def check(doc, quiet=False, first=False):
"""Every vector of `doc` against this verifier, at both levels: the failures (with `first`, the
first alone), and every reason the corpus gives."""
fails, seen = [], set()
try:
keys = [bytes.fromhex(doc["opening_key"])]
except (KeyError, ValueError, TypeError):
return ["the corpus states no opening key"], seen
for v in doc["vectors"]:
if first and fails:
return fails, seen
try:
got = run_vector(v)
opened = run_vector(v, keys)
except Exception as e: # a copy that does not read is a failure to verify it
fails.append("%s: does not verify (%r)" % (v["name"], e))
continue
badk = [k for k in STATED if (k in got or k in v) and got.get(k) != v.get(k)]
# The opened level: what the vector states of it, or, where it states nothing of it, what
# the commitment level gives (§13).
want = v.get("opened", got)
bado = ["opened." + k for k in STATED if (k in opened or k in want) and opened.get(k) != (want.get(k) if isinstance(want, dict) else None)]
given(got, seen)
given(opened, seen)
if not quiet:
what = got["outcome"] + ("" if got["outcome"] != "refuse" else " " + got["reason"] + " at %s" % got["at"])
if "opened" in v:
what += "; opened: " + opened["outcome"] + ("" if opened["outcome"] != "refuse" else " " + opened["reason"] + " at %s" % opened["at"])
print(("PASS" if not badk and not bado else "FAIL") + " room %s: %s" % (v["name"], what))
for k in badk:
fails.append("%s: %s is %s, the corpus states %s" % (v["name"], k, json.dumps(got.get(k))[:200], json.dumps(v.get(k))[:200]))
for k in bado:
fails.append("%s: %s differs" % (v["name"], k))
return fails, seen
def mutations(doc):
"""Copies of the corpus, each with one defect a conforming verifier must catch."""
vn = {v["name"]: k for k, v in enumerate(doc["vectors"])}
fs, op, gp = vn["floor_session"], vn["open_handover"], vn["floor_session_gap"]
def mutate(label, f):
d = copy.deepcopy(doc)
f(d)
return label, d
def vs(d):
return d["vectors"]
def flip(h, i):
b = bytearray(bytes.fromhex(h))
b[i] ^= 1
return b.hex()
def flip_record(d, k, chain, rec, i):
vs(d)[k]["chains"][chain][rec] = flip(vs(d)[k]["chains"][chain][rec], i)
host = doc["principals"]["host"]
ss = vn["sealed_session"]
return [
mutate("an applied input stated refused", lambda d: vs(d)[fs]["inputs"][0].update(fate={"outcome": "refused", "reason": "input_no_floor"})),
mutate("a refusal's stated reason", lambda d: vs(d)[fs]["inputs"][3]["fate"].update(reason="input_not_permitted")),
mutate("an unanswered input stated applied", lambda d: vs(d)[fs]["inputs"][10].update(fate={"outcome": "applied", "group": 9, "object": 0})),
mutate("a stated frame", lambda d: vs(d)[fs]["inputs"][1]["fate"].update(group=7)),
mutate("a stated floor holder", lambda d: vs(d)[fs]["floor"][1].update(holder=host)),
mutate("a floor change unstated", lambda d: vs(d)[fs]["floor"].pop()),
mutate("a stated leg", lambda d: vs(d)[fs]["latencies"][0]["legs"].update(up=vs(d)[fs]["latencies"][0]["legs"]["up"] + 1)),
mutate("a stated uncertainty", lambda d: vs(d)[fs]["latencies"][1]["legs"].update(uncertainty=0)),
mutate("a stated total", lambda d: vs(d)[fs]["totals"][0].update(applied=5)),
mutate("a stated encoder", lambda d: vs(d)[fs]["screens"][0].update(encoder="another@2")),
mutate("the handover's second applied input stated refused", lambda d: vs(d)[op]["inputs"][3].update(fate={"outcome": "refused", "reason": "input_not_permitted"})),
mutate("the gap's unchecked input stated unanswered", lambda d: vs(d)[gp]["inputs"][-3].update(fate={"outcome": "unanswered"})),
mutate("a byte of a latency record's own signature", lambda d: flip_record(d, fs, 2, 1, -70)),
mutate("a refusal's reason", lambda d: vs(d)[vn["applies_stale"]].update(reason="outcome_invalid")),
mutate("a refusal's place", lambda d: vs(d)[vn["screen_unlinked"]].update(at=["record", 1, 5])),
mutate("a refused floor statement's reason", lambda d: vs(d)[vn["floor_late_release"]]["floor_refused"][0].update(reason="floor_not_permitted")),
mutate("a refused floor statement unstated", lambda d: vs(d)[vn["floor_given_by_viewer"]].pop("floor_refused")),
mutate("a floor statement stated refused that the rules accept", lambda d: vs(d)[fs].update(floor_refused=vs(d)[vn["floor_give_by_non_holder"]]["floor_refused"])),
mutate("a refused give stated as passing the floor", lambda d: vs(d)[vn["floor_give_by_non_holder"]]["floor"].insert(2, {"host": host, "t": vs(d)[vn["floor_give_by_non_holder"]]["floor_refused"][0]["t"], "holder": doc["principals"]["alice"]})),
mutate("a sealed input's count of sealed actions", lambda d: vs(d)[ss]["inputs"][0].update(sealed=4)),
mutate("a text in the clear to a sealing key stated applied", lambda d: vs(d)[ss]["inputs"][2].update(fate={"outcome": "applied", "group": 9, "object": 0})),
mutate("a refusal by rule 9 stated as rule 2's", lambda d: vs(d)[ss]["inputs"][3]["fate"].update(reason="input_not_permitted")),
mutate("a host's count of refusals by rule 9", lambda d: vs(d)[ss]["totals"][0]["refused"].update(input_bad_seal=4)),
mutate("a host's count of refusals by rule 8 left out", lambda d: vs(d)[ss]["totals"][0]["refused"].pop("input_unsealed")),
mutate("an opened seal stated not opened", lambda d: vs(d)[ss]["opened"]["inputs"][0].update(seal="seal_not_opened")),
mutate("a seal to the wrong key stated a commitment that does not match", lambda d: vs(d)[ss]["opened"]["inputs"][3].update(seal="seal_commitment")),
mutate("a byte of an opened statement", lambda d: vs(d)[ss]["opened"]["inputs"][0].update(opened=flip(vs(d)[ss]["opened"]["inputs"][0]["opened"], -1))),
mutate("a seal the opened level refuses stated accepted there", lambda d: vs(d)[vn["sealed_applies_wrong_key"]].update(opened={k: v for k, v in vs(d)[vn["sealed_applies_wrong_key"]].items() if k in STATED})),
mutate("the opened level stated of a vector with no seal", lambda d: vs(d)[fs].update(opened={"outcome": "refuse", "reason": "outcome_mismatch", "at": ["record", 1, 3]})),
mutate("another opening key", lambda d: d.update(opening_key="11" * 32)),
]
CHECKS = ("attrs", "screen_fields", "dt_order", "who_screen", "track", "screen_link", "who_input", "floor_host", "floor_request",
"floor_give", "floor_eligible", "floor_release", "floor_take", "floor_loss", "who_latency", "latency_key", "latency_sig",
"who_outcome", "duplicate", "consistent", "recompute", "rule_screen", "rule_power", "rule_floor", "rule_paused",
"rule_accepts", "rule_bounds", "rule_stale", "latency_input", "latency_frame", "latency_ahead", "latency_sum",
"sealed_of", "seal_length", "rule_unsealed", "seal_no_key", "seal_open", "seal_read", "seal_commit")
def main(corpus_dir):
fails = []
st = ED._self_test()
print(("PASS" if not st else "FAIL") + " Ed25519 strict rule self-test " + ",".join(st))
fails += ["ed25519 self-test"] if st else []
st = HPKE.crypto_self_test()
print(("PASS" if not st else "FAIL") + " X25519 (RFC 7748 §5.2) and HPKE base mode (RFC 9180 A.1.1) " + ",".join(st))
fails += ["HPKE self-test"] if st else []
ok = len(REASONS) == 13 and sorted(HOST_REASONS) == list(range(1, 10)) and len(FLOOR_REASONS) == 3 and len(SEAL_REASONS) == 4
print(("PASS" if ok else "FAIL") + " §12 read from the extension: %d reasons, %d host refusals, %d floor refusals, %d seal reasons" % (len(REASONS), len(HOST_REASONS), len(FLOOR_REASONS), len(SEAL_REASONS)))
fails += [] if ok else ["§12 does not read"]
names = sorted(n for n in os.listdir(corpus_dir) if n != "README.md")
ok = names == ["vectors.json"]
print(("PASS" if ok else "FAIL") + " the corpus holds vectors.json and README.md alone")
fails += [] if ok else ["other files: %s" % names]
with open(os.path.join(corpus_dir, "vectors.json"), encoding="utf-8") as f:
doc = json.load(f)
f2, seen = check(doc)
fails += f2
missing = [r for r in REASONS + [HOST_REASONS[n] for n in sorted(HOST_REASONS)] + FLOOR_REASONS + SEAL_REASONS if r not in seen]
print(("PASS" if not missing else "FAIL") + " every reason of the four tables of §12 appears in the corpus")
fails += ["the corpus never gives %s" % ", ".join(missing)] if missing else []
for label, d in mutations(doc):
caught, _ = check(d, quiet=True, first=True)
print(("PASS" if caught else "FAIL") + " mutated copy fails: %s%s" % (label, " (%s)" % caught[0][:90] if caught else ""))
if not caught:
fails.append("a mutated copy verifies: %s" % label)
for name in CHECKS:
DROP.add(name)
try:
caught, _ = check(doc, quiet=True, first=True)
finally:
DROP.discard(name)
print(("PASS" if caught else "FAIL") + " the corpus catches the check dropped: %s%s" % (name, " (%s)" % caught[0][:90] if caught else ""))
if not caught:
fails.append("the corpus does not catch the check dropped: %s" % name)
print("remote_desktop_verify: %s" % ("PASS" if not fails else "FAIL (%d)" % len(fails)))
for f in fails:
print(" " + f)
return 0 if not fails else 1
def room_main(path, keys):
with open(path, encoding="utf-8") as f:
v = json.load(f)
print(json.dumps(run_vector(v, [bytes.fromhex(k) for k in keys] if keys else None)))
return 0
if __name__ == "__main__":
if len(sys.argv) >= 3 and sys.argv[1] == "--room" and len(sys.argv) % 2 == 1 and all(a == "--opening-key" for a in sys.argv[3::2]):
sys.exit(room_main(sys.argv[2], sys.argv[4::2]))
if len(sys.argv) != 2:
print("usage: remote_desktop_verify.py <live-conformance/remote> | --room <room.json> [--opening-key <hex>]...", file=sys.stderr)
sys.exit(2)
sys.exit(main(sys.argv[1]))