#!/usr/bin/env python3 """Independent verifier of the capture-binding corpus (extensions/capture-binding.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. The room is verified by the live-rooms corpus's own second implementation, live_rooms_verify.py (live-rooms §9), which verifies every event with live_events_verify.py (live-events §6); both are likewise written from their texts alone. Everything else is here: - the take tree (§4): leaves of the take's domain, the Merkle tree hash of RFC 6962 §2.1 with staged-measurement Appendix C's node, and consistency checked as RFC 9162 §2.1.4.2 checks it; - the take_commit payload and its chain (§5), the upload receipts and their state machine (§6), the take manifest (§7) and the consent record (§8), each read and checked here; - take verification (§9): the take in the room, the store, the receipts, the consents, the verdicts of single-take, aligned, complete and publishable, and whether the manifest stands; the master position of every marker (§10); the stream id (§11.1). For every vector it requires the outcome, reason and place, and for one with verdicts the take id, stream id, commitments, size, root, verdicts, standing and markers the corpus states; of the corpus, that every reason of §12 and delivery_gap appear in it. Then it mutates copies of the corpus, one defect each, and requires every copy to fail. python3 capture_take_verify.py Exit status 0 only if every vector verifies and every mutated copy fails. Python 3.8 or later; standard library only. """ import copy import hashlib import json import os 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 TAKE_DOMAIN = b"wai:capture-take\x01" LEAF_DOMAIN = b"wai:take-leaf\x01" NODE_DOMAIN = b"wai:out-node\x01" MANIFEST_DOMAIN = b"wai:capture-manifest\x01" RECEIPT_DOMAIN = b"wai:take-upload\x01" CONSENT_DOMAIN = b"wai:capture-consent\x01" STORE_AUTHORITY = "capture/store" ZERO = bytes(32) OPEN, RUNNING, CLOSE = 1, 2, 3 R_OPEN, R_PROGRESS, R_RESUME, R_SEALED = 1, 2, 3, 4 PURPOSES = [(1, "live"), (2, "record"), (4, "distribute"), (8, "train")] RECORD = 2 SINGLE_TAKE, ALIGNED, COMPLETE = 1, 2, 4 TAKE_COMMIT, MARKER, CONTROL = 0x0C, 0x08, 0x09 # §12, in its order. REASONS = [ "recording_not_permitted", "take_unknown", "commit_malformed", "device_not_bound", "manifest_invalid", "manifest_mismatch", "store_not_listed", "upload_receipt_invalid", "upload_receipt_mismatch", "store_outside_region", "consent_invalid", "commit_chain_broken", "root_not_extending", "take_not_closed", "manifest_not_committed", "clock_unstable", "capture_discontinuous", "chunk_too_long", "commit_early", "commit_late", "chunk_extra", "chunk_reordered", "chunk_altered", "misaligned", "chunk_missing", "upload_not_sealed", "consent_missing", "guardian_missing", "consent_revoked", "consent_expired", "purpose_not_consented", ] def sha(*parts): h = hashlib.sha256() for p in parts: h.update(p) return h.digest() class Reader: """Reads little-endian fields in order; running out is `out`.""" def __init__(self, b, p, out): self.b, self.p, self.out = b, p, out def take(self, n): if self.p + n > len(self.b): raise Refuse(self.out) s = self.b[self.p:self.p + n] self.p += n return s def u8(self): return self.take(1)[0] def uint(self, n): return int.from_bytes(self.take(n), "little") def i64(self): return int.from_bytes(self.take(8), "little", signed=True) def done(self): return self.p == len(self.b) def read_head(b, magic): """live-events §3.3's first steps, with `magic` for WEV1.""" if len(b) < 4: raise Refuse("truncated") if b[:4] != magic: raise Refuse("malformed") if len(b) < 5: raise Refuse("truncated") if b[4] != 1: raise Refuse("unknown_version") def check_sig(key, domain, body, sig): bad = ED.verify(key, domain + body, sig) if bad: raise Refuse(bad) # ---------------------------------------------------------------- §4 the take tree def leaf(desc): return sha(LEAF_DOMAIN, desc) def node(left, right): return sha(NODE_DOMAIN, left, right) def split(n): """The largest power of two below n, n >= 2.""" k = 1 while k * 2 < n: k *= 2 return k def mth(ls): """RFC 6962 §2.1, n >= 1.""" if len(ls) == 1: return ls[0] k = split(len(ls)) return node(mth(ls[:k]), mth(ls[k:])) def root_of(chunks): return ZERO if not chunks else mth([leaf(c["desc"]) for c in chunks]) def consistent(first, second, first_root, second_root, path): """RFC 9162 §2.1.4.2, with node() for HASH(0x01 || ...); 0 < first < second.""" if not path: return False if first & (first - 1) == 0: path = [first_root] + list(path) fn, sn = first - 1, second - 1 while fn & 1: fn >>= 1 sn >>= 1 fr = sr = path[0] for c in path[1:]: if sn == 0: return False if fn & 1 or fn == sn: fr = node(c, fr) sr = node(c, sr) if not fn & 1: while not fn & 1 and fn != 0: fn >>= 1 sn >>= 1 else: sr = node(sr, c) fn >>= 1 sn >>= 1 return fr == first_root and sr == second_root and sn == 0 def extends(first, old, size, new, proof): """§4.3, with the two cases RFC 9162 leaves to the caller.""" if first == 0: return not proof if first == size: return not proof and old == new return first < size and consistent(first, size, old, new, proof) # ---------------------------------------------------------------- §5, §6, §7, §8 records def read_commit(b): """§5.2.""" r = Reader(b, 0, "commit_malformed") c = {"take": r.take(32), "n": r.uint(4), "phase": r.u8()} if c["phase"] not in (OPEN, RUNNING, CLOSE): raise Refuse("commit_malformed") c["prev"], c["first"], c["size"] = r.take(32), r.uint(8), r.uint(8) if c["size"] < c["first"]: raise Refuse("commit_malformed") c["root"] = r.take(32) if c["size"] == 0 and c["root"] != ZERO: raise Refuse("commit_malformed") c["offset"] = r.i64() k = r.u8() if k > 64: raise Refuse("commit_malformed") c["proof"] = [r.take(32) for _ in range(k)] if not r.done(): raise Refuse("commit_malformed") return c def is_opening(c): return (c["n"] == 0 and c["phase"] == OPEN and c["prev"] == ZERO and c["first"] == 0 and c["size"] == 0 and not c["proof"]) def read_chunk(r): desc = r.take(64) return {"desc": desc, "index": int.from_bytes(desc[0:8], "little"), "start": int.from_bytes(desc[8:16], "little"), "end": int.from_bytes(desc[16:24], "little"), "len": int.from_bytes(desc[24:32], "little"), "hash": desc[32:]} def is_chunk(c, b): return len(b) == c["len"] and sha(b) == c["hash"] def read_receipt(b): """§6.2.""" read_head(b, b"WUR1") r = Reader(b, 5, "truncated") x = {"store": r.take(32), "take": r.take(32), "seq": r.uint(8), "prev": r.take(32), "at": r.uint(8), "state": r.u8(), "held": r.uint(8), "root": r.take(32), "region": r.take(2), "sig": r.take(64)} if not r.done(): raise Refuse("malformed") x["body"] = b[:-64] x["digest"] = sha(x["body"]) return x def read_manifest(b, room): """§7.2: the manifest, or Refuse.""" read_head(b, b"WTM1") r = Reader(b, 5, "truncated") m = {k: r.take(32) for k in ("room", "device", "principal", "start", "take", "store")} m["claims"], m["purposes"] = r.u8(), r.u8() m["chunks"] = [read_chunk(r) for _ in range(r.uint(4))] m["root"] = r.take(32) m["commits"] = [r.take(32) for _ in range(r.uint(4))] m["depicted"] = [(r.take(32), r.take(32)) for _ in range(r.u8())] m["sig"] = r.take(64) if not r.done(): raise Refuse("malformed") keys = [p for p, _ in m["depicted"]] ok = (m["claims"] & ~0b111 == 0 and m["purposes"] & ~0b1111 == 0 and len(m["chunks"]) >= 1 and all(c["index"] == i and c["len"] >= 1 for i, c in enumerate(m["chunks"])) and m["root"] == root_of(m["chunks"]) and len(m["commits"]) >= 1 and len(set(m["commits"])) == len(m["commits"]) and 1 <= len(keys) <= 32 and all(a < z for a, z in zip(keys, keys[1:])) and m["principal"] in keys and m["take"] == sha(TAKE_DOMAIN, m["room"], m["device"], m["start"])) if not ok: raise Refuse("manifest_invalid") if m["room"] != room: raise Refuse("wrong_room") check_sig(m["device"], MANIFEST_DOMAIN, b[:-64], m["sig"]) return m def read_consent(b): """§8.1, §8.2.""" read_head(b, b"WCN1") r = Reader(b, 5, "truncated") c = {"subject": r.take(32), "minor": r.u8(), "guardian": r.take(32), "purposes": r.u8(), "room": r.take(32), "nbf": r.uint(8), "exp": r.uint(8), "nonce": r.take(16), "sig": r.take(64)} if not r.done(): raise Refuse("malformed") ok = (c["minor"] in (0, 1) and (c["minor"] == 1 or c["guardian"] == ZERO) and c["guardian"] != c["subject"] and c["purposes"] != 0 and c["purposes"] & ~0b1111 == 0 and c["nbf"] < c["exp"]) if not ok: raise Refuse("consent_invalid") signer = c["subject"] if c["guardian"] == ZERO else c["guardian"] check_sig(signer, CONSENT_DOMAIN, b[:-64], c["sig"]) c["digest"] = sha(b[:-64]) return c def stream_id(take): """§11.1: an RFC 9562 version-8 UUID of the take id's first 16 bytes.""" u = bytearray(take[:16]) u[6] = (u[6] & 0x0F) | 0x80 u[8] = (u[8] & 0x3F) | 0x80 h = u.hex() return "%s-%s-%s-%s-%s" % (h[:8], h[8:12], h[12:16], h[16:20], h[20:]) # ---------------------------------------------------------------- §9 verification class Stop(Exception): def __init__(self, at, reason): Exception.__init__(self, reason) self.at, self.reason = at, reason def attrs_of(g): return dict(LRV.read_attrs(g["attrs"]) or []) def binds(g, principal): """§3.1.""" a = attrs_of(g) return a["subject"] == principal.hex() if "subject" in a else g["principal"] == principal def control(ev, device, command): """The control payload of `ev`, when it is `command` addressed to `device`.""" if ev["kind"] != CONTROL: return None try: p = LEV.read_payload(CONTROL, ev["payload"], 65535) except Refuse: return None return p if p["target"] == device and p["command"] == command else None def find(d, grants, seen, gaps, m): """§9.2: the start, its count-in, the stop and the held commitments.""" if d["mode"] == 0: raise Stop(["manifest"], "recording_not_permitted") pos = next((k for k, s in enumerate(seen) if s["ev"]["digest"] == m["start"]), None) start = None if pos is None else control(seen[pos]["ev"], m["device"], 1) if start is None: raise Stop(["manifest"], "take_unknown") stop = next((s for s in seen[pos + 1:] if control(s["ev"], m["device"], 2) is not None), None) mine = sorted((s for s in seen if s["ev"]["principal"] == m["device"] and s["ev"]["kind"] == TAKE_COMMIT), key=lambda s: s["ev"]["seq"]) commits = [] for s in mine: try: c = read_commit(s["ev"]["payload"]) except Refuse as e: raise Stop(["record", s["chain"], s["record"]], e.reason) if c["take"] == m["take"]: commits.append((s, c)) for s, _ in commits: g = grants.get(s["grant"]) if g is None or not binds(g, m["principal"]): raise Stop(["record", s["chain"], s["record"]], "device_not_bound") device_gap = any(g["principal"] == m["device"].hex() for g in gaps) held = [s["ev"]["digest"] for s, _ in commits] every = {s["ev"]["digest"] for s in seen} listed_held = [x for x in m["commits"] if x in held] unheld = [x for x in m["commits"] if x not in held] if listed_held != held or any(x in every or not device_gap for x in unheld): raise Stop(["manifest"], "manifest_mismatch") return seen[pos], start["count_in_ms"], stop, commits def head_of(text): return json.loads(text) if text is not None else None def check_upload(d, head, m, receipts): """§6.4, §6.3: the last receipt, or Stop.""" if d["origin"] != b"": h = head_of(head) listed = h is not None and any( e["key"] == m["store"].hex() and STORE_AUTHORITY in e["may_sign"] and e["status"] != "revoked" for e in h["keys"]) if not listed: raise Stop(["store"], "store_not_listed") prev = None for i, b in enumerate(receipts): at = ["receipt", i] try: x = read_receipt(b) except Refuse as e: raise Stop(at, e.reason) fields = (x["state"] in (1, 2, 3, 4) and all(0x41 <= c <= 0x5A for c in x["region"]) and (x["held"] != 0 or x["root"] == ZERO)) if not fields or x["store"] != m["store"] or x["take"] != m["take"]: raise Stop(at, "upload_receipt_invalid") bad = ED.verify(x["store"], RECEIPT_DOMAIN + x["body"], x["sig"]) if bad: raise Stop(at, bad) if prev is None: chained = x["seq"] == 0 and x["prev"] == ZERO and x["state"] == R_OPEN and x["held"] == 0 else: step = {R_PROGRESS: x["held"] > prev["held"], R_RESUME: x["held"] == prev["held"], R_SEALED: x["held"] == prev["held"]}.get(x["state"], False) chained = (x["seq"] == i and x["prev"] == prev["digest"] and x["at"] >= prev["at"] and x["region"] == prev["region"] and prev["state"] != R_SEALED and step) if not chained: raise Stop(at, "upload_receipt_invalid") if x["held"] > len(m["chunks"]) or x["root"] != root_of(m["chunks"][:x["held"]]): raise Stop(at, "upload_receipt_mismatch") if d["regions"] and x["region"] not in d["regions"]: raise Stop(at, "store_outside_region") prev = x return prev def single_take(f, m, clock, parts): """§9.4 single-take: None when it holds, else its reason.""" start, _, stop, commits = f interval, jitter, o = clock held = {s["ev"]["digest"] for s, _ in commits} if any(x not in held for x in m["commits"]): return "delivery_gap" cs = [c for _, c in commits] if not cs or not is_opening(cs[0]): return "commit_chain_broken" for (ps, p), (_, c) in zip(commits, commits[1:]): if p["phase"] == CLOSE or c["phase"] == OPEN or c["n"] != p["n"] + 1 or c["prev"] != ps["ev"]["digest"] or c["first"] != p["size"]: return "commit_chain_broken" for p, c in zip(cs, cs[1:]): if not extends(c["first"], p["root"], c["size"], c["root"], c["proof"]): return "root_not_extending" last = cs[-1] if last["phase"] != CLOSE or stop is None: return "take_not_closed" ch = m["chunks"] if len(ch) != last["size"] or any(c["size"] > 0 and root_of(ch[:c["size"]]) != c["root"] for c in cs): return "manifest_not_committed" if any(abs(c["offset"] - o) > jitter for c in cs): return "clock_unstable" if any(c["start"] >= c["end"] for c in ch) or any(b["start"] != a["end"] for a, b in zip(ch, ch[1:])): return "capture_discontinuous" if any(c["end"] - c["start"] > interval for c in ch): return "chunk_too_long" added = [(s["ev"]["t"], ch[c["first"]:c["size"]]) for s, c in commits] if (commits[0][0]["ev"]["t"] < start["ev"]["t"] or commits[-1][0]["ev"]["t"] < stop["ev"]["t"] or any(t + jitter < x["end"] + o for t, xs in added for x in xs)): return "commit_early" if any(t > x["end"] + o + interval + jitter for t, xs in added for x in xs): return "commit_late" for index, b in parts: if index >= len(ch): return "chunk_extra" if not is_chunk(ch[index], b): return "chunk_reordered" if any(c["index"] != index and is_chunk(c, b) for c in ch) else "chunk_altered" return None def aligned(f, m, clock): """§9.4 aligned.""" start, count_in_ms, _, commits = f _, jitter, o = clock if not commits or commits[0][0]["ev"]["digest"] != m["commits"][0]: return "delivery_gap" if not is_opening(commits[0][1]): return "commit_chain_broken" adds = next((c for _, c in commits if c["size"] > 0), None) if adds is None or adds["size"] > len(m["chunks"]) or root_of(m["chunks"][:adds["size"]]) != adds["root"]: return "manifest_not_committed" if abs(m["chunks"][0]["start"] + o - (start["ev"]["t"] + count_in_ms * 1000)) > jitter: return "misaligned" return None def complete(f, m, parts, last_receipt): """§9.4 complete.""" commits = f[3] s, r = (commits[-1][1]["size"], commits[-1][1]["root"]) if commits else (0, ZERO) if s == 0 or s > len(m["chunks"]) or root_of(m["chunks"][:s]) != r: return "manifest_not_committed" for c in m["chunks"][:s]: if not any(i == c["index"] and is_chunk(c, b) for i, b in parts): return "chunk_missing" if last_receipt is None or last_receipt["state"] != R_SEALED or last_receipt["held"] != s: return "upload_not_sealed" return None def publishable(p, m, consents, room, revoked, principal_minor, start_s, at): """§9.4 publishable for purpose bit p.""" for subject, digest in m["depicted"]: c = next((c for c in consents if digest != ZERO and c["digest"] == digest and c["subject"] == subject and c["room"] in (ZERO, room)), None) if c is None: return "consent_missing" if (c["minor"] == 1 or (subject == m["principal"] and principal_minor)) and c["guardian"] == ZERO: return "guardian_missing" if c["digest"] in revoked: return "consent_revoked" if not (c["nbf"] <= start_s < c["exp"]) or not (c["nbf"] <= at < c["exp"]): return "consent_expired" if not c["purposes"] & RECORD or not c["purposes"] & p: return "purpose_not_consented" return None def position(chunks, o, t): """§10: (chunk, into the chunk, into the master), or None outside the master.""" dv = t - o for c in chunks: if c["start"] <= dv < c["end"]: return c["index"], dv - c["start"], dv - chunks[0]["start"] return None def verify_take(v): """§9: a dict of what the corpus states for the vector.""" descriptor = bytes.fromhex(v["descriptor"]) grants = [bytes.fromhex(g) for g in v["grants"]] head = v["key_log_head"] chains = [[bytes.fromhex(r) for r in c] for c in v["chains"]] room_out = LRV.verify_room(descriptor, grants, head, chains) if room_out["outcome"] == "refuse": return {"outcome": "refuse", "reason": room_out["reason"], "at": ["room"] + room_out["at"]} d = LRV.read_descriptor(descriptor) held_grants, _ = LRV.check_grants(d, grants) by_digest = {g["digest"]: g for g in held_grants} seen, digests = [], set() for c, i, g in room_out["order"]: ev = LEV.read_record(chains[c][i]) if ev["digest"] not in digests: digests.add(ev["digest"]) seen.append({"chain": c, "record": i, "ev": ev, "grant": bytes.fromhex(g) if g else None}) try: try: m = read_manifest(bytes.fromhex(v["manifest"]), d["room"]) except Refuse as e: raise Stop(["manifest"], e.reason) f = find(d, by_digest, seen, room_out.get("gaps", []), m) last_receipt = check_upload(d, head, m, [bytes.fromhex(r) for r in v["receipts"]]) consents = [] for k, b in enumerate(v["consents"]): try: consents.append(read_consent(bytes.fromhex(b))) except Refuse as e: raise Stop(["consent", k], e.reason) except Stop as e: return {"outcome": "refuse", "reason": e.reason, "at": e.at} _, _, _, commits = f o = commits[0][1]["offset"] if commits else 0 clock = (d["commit_interval_ms"] * 1000, d["jitter_ms"] * 1000, o) parts = [(p["index"], bytes.fromhex(p["bytes"])) for p in v["parts"]] revoked = set() if d["origin"] != b"": revoked = {bytes.fromhex(x) for x in head_of(head)["revoked_modules"]} first_grant = by_digest.get(commits[0][0]["grant"]) if commits else None principal_minor = first_grant is not None and attrs_of(first_grant).get("minor") == "true" start_s = (d["t0"] + f[0]["ev"]["t"]) // 1000000 verdicts = { "single_take": single_take(f, m, clock, parts) or "holds", "aligned": aligned(f, m, clock) or "holds", "complete": complete(f, m, parts, last_receipt) or "holds", "publishable": {n: publishable(b, m, consents, d["room"], revoked, principal_minor, start_s, v["evaluated_at"]) or "holds" for b, n in PURPOSES}, } claimed = [(SINGLE_TAKE, "single_take"), (ALIGNED, "aligned"), (COMPLETE, "complete")] stands = (all(not m["claims"] & b or verdicts[k] == "holds" for b, k in claimed) and all(not m["purposes"] & b or verdicts["publishable"][n] == "holds" for b, n in PURPOSES)) markers = [] if verdicts["aligned"] == "holds": for s in seen: if s["ev"]["kind"] == MARKER: at = position(m["chunks"], o, s["ev"]["t"]) if at is not None: markers.append({"event": s["ev"]["digest"].hex(), "t": s["ev"]["t"], "chunk": at[0], "into_chunk": at[1], "into_master": at[2]}) last = commits[-1][1] if commits else None return {"outcome": "verdicts", "take": m["take"].hex(), "stream_id": stream_id(m["take"]), "commits": [s["ev"]["digest"].hex() for s, _ in commits], "size": last["size"] if last else 0, "root": (last["root"] if last else ZERO).hex(), "verdicts": verdicts, "stands": stands, "markers": markers} # ---------------------------------------------------------------- the corpus def problems_of(v): """What differs between the vector's statement and this verifier's outcome.""" try: got = verify_take(v) except Exception as e: # a crash on a mutated copy is a failure to read it return ["the vector does not verify: %s: %s" % (type(e).__name__, e)], None out = [k for k in got if v.get(k) != got[k]] out += [k for k in ("outcome", "reason", "at") if k in v and k not in got] return out, got def check(doc, quiet=False): """Every vector against its statement: the failures.""" fails = [] for v in doc["vectors"]: problems, got = problems_of(v) if not quiet: what = "?" if got is None else got["outcome"] + (" " + got["reason"] if got["outcome"] == "refuse" else "") print(("PASS" if not problems else "FAIL") + " vector %s: %s" % (v["name"], what)) for k in problems: fails.append("%s: %s is %s, the corpus states %s" % ( v["name"], k, json.dumps(got.get(k))[:300] if got else k, json.dumps(v.get(k))[:300])) return fails def flip(h, at): """Hexadecimal h with the byte at `at` (from the end when negative) changed.""" b = bytearray(bytes.fromhex(h)) b[at] ^= 0x01 return b.hex() def mutations(doc): """Copies of the corpus, each with one defect a conforming verifier must catch.""" names = {v["name"]: k for k, v in enumerate(doc["vectors"])} lesson = names["lesson"] def mutate(label, f): d = copy.deepcopy(doc) f(d["vectors"]) return label, d def device_chain(vs, k): dev = doc["principals"]["device"] return next(c for c in vs[k]["chains"] if LEV.read_record(bytes.fromhex(c[0]))["principal"].hex() == dev) out = [ mutate("a byte of the lesson's chunk 2 part", lambda vs: vs[lesson]["parts"][2].update(bytes=flip(vs[lesson]["parts"][2]["bytes"], 5))), mutate("the lesson's parts 0 and 1 swapped", lambda vs: vs[lesson]["parts"][0].update(bytes=vs[lesson]["parts"][1]["bytes"])), mutate("the lesson's last part dropped", lambda vs: vs[lesson]["parts"].pop()), mutate("a byte of the lesson's manifest signature", lambda vs: vs[lesson].update(manifest=flip(vs[lesson]["manifest"], -10))), # Chunk 0's descriptor begins at byte 203 of a manifest (§7.1); byte 8 of it is `start`'s. mutate("a byte of the lesson's chunk 0 descriptor", lambda vs: vs[lesson].update(manifest=flip(vs[lesson]["manifest"], 203 + 8))), mutate("the lesson's sealed receipt removed", lambda vs: vs[lesson]["receipts"].pop()), mutate("a byte of the lesson's consent", lambda vs: vs[lesson]["consents"].__setitem__(0, flip(vs[lesson]["consents"][0], 70))), mutate("the lesson's second commitment not delivered", lambda vs: device_chain(vs, lesson).pop(1)), mutate("the lesson evaluated a year on", lambda vs: vs[lesson].update(evaluated_at=vs[lesson]["evaluated_at"] + 365 * 86400)), mutate("the lesson's stated root", lambda vs: vs[lesson].update(root="00" * 32)), mutate("the lesson's stated stream id", lambda vs: vs[lesson].update(stream_id="0" + vs[lesson]["stream_id"][1:])), mutate("the lesson's stated marker position", lambda vs: vs[lesson]["markers"][0].update(into_master=vs[lesson]["markers"][0]["into_master"] + 1)), mutate("the missing chunk stated complete", lambda vs: vs[names["missing_chunk"]]["verdicts"].update(complete="holds")), mutate("the late commitment stated single", lambda vs: vs[names["late_commit"]]["verdicts"].update(single_take="holds")), mutate("the misaligned take stated aligned", lambda vs: vs[names["misaligned"]]["verdicts"].update(aligned="holds")), mutate("the revoked consent stated publishable", lambda vs: vs[names["consent_revoked"]]["verdicts"]["publishable"].update(record="holds")), mutate("the impostor's refusal at the manifest", lambda vs: vs[names["device_not_bound"]].update(at=["manifest"])), mutate("the store outside its region stated listed", lambda vs: vs[names["store_outside_region"]].update(reason="store_not_listed")), ] return out 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) fails += check(doc) seen = set() for v in doc["vectors"]: if v.get("reason"): seen.add(v["reason"]) if "verdicts" in v: vd = v["verdicts"] seen |= {vd["single_take"], vd["aligned"], vd["complete"]} | set(vd["publishable"].values()) missing = [r for r in REASONS + ["delivery_gap"] if r not in seen] if missing: fails.append("no vector gives %s" % ", ".join(missing)) takes = ["lesson", "resumed_upload", "minor_guardian"] absent = [t for t in takes if not any(v["name"] == t and v.get("stands") for v in doc["vectors"])] if absent: fails.append("good takes that do not stand: %s" % ", ".join(absent)) print("%s every reason of §12 appears, and every good take of §13 stands" % ("PASS" if not (missing or absent) else "FAIL")) for label, mutated in mutations(doc): changed = [v for v, o in zip(mutated["vectors"], doc["vectors"]) if v != o] caught = check({"vectors": changed}, quiet=True) if changed else [] 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) print(("PASS" if not fails else "FAIL") + " capture-binding corpus: %d vectors" % len(doc["vectors"])) return 1 if fails else 0 if __name__ == "__main__": if len(sys.argv) != 2: print("usage: capture_take_verify.py ", file=sys.stderr) sys.exit(2) sys.exit(main(sys.argv[1]))