open-standards/wai/live-conformance/live_ads_verify.py
Transaction Science d6073e2000 wai/ad-consent-supply-chain, converted to the layout of the fix: no shared append point
The change of wai/ad-consent-supply-chain (44d5f2264..110c53553, 2 commits), on the fix branch: its
changelog entries as files of wai/changes/, its wai-conformance.yml edits as step files,
corpus files and test tags, and every other file as the branch has it.

- f8fe39cba WAI: an ad decision's consent signal and supply chain, bound into what the decider signs
- 110c53553 WAI: a GPP string links what carries it; a chain per impression; sellers files read exactly

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 02:00:58 -05:00

1131 lines
51 KiB
Python

#!/usr/bin/env python3
"""Independent verifier of the live-ads corpus (extensions/live-ads.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 with ../claim-conformance/_blake3.py, written from its specification. 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. Everything else is here:
- the ad policy (§3): its layout, rules and signature, and the decider grant;
- the break annotation and the schedule (§4), with its refusals;
- the HLS interstitial, closing and preload tags (§5) and the DASH events (§6), byte for byte;
- the decision and the asset lists (§7), byte for byte;
- the break form of the wai.video.ssai receipt (§8): its JSON carriage, hashes, signing
payload, identity and rules, and a receipt of version 2's request terms: the consent signal
(§8.6) and the supply chain of each impression (§8.7), their rules and signed encodings, and
the sellers files and authorised-sellers files a chain is checked against, read here by
§8.7: a sellers file by RFC 8259's grammar outside what §8.7 reads, its numbers never
converted;
- verification, from the room to the spend (§9), and steps 13 and 14 under a policy of
version 3 and for each chain a receipt states.
A policy of version 2 or 3 is read here with its presentation fields (§3.1, ad-presentation §8),
and one of version 3 with its audience. A
room whose policy requires presentation claims is verified with ad_presentation_verify.py,
which gives accept() the judge of those claims (ad-presentation §9).
For every room it requires the outcome, reason and place, and everything else the corpus
states; every HLS tag file, byte for byte; of the corpus, that every reason of §10 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.
python3 live_ads_verify.py <live-conformance/ads>
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
import _blake3 as B3 # noqa: E402
Refuse = LEV.Refuse
EXTENSION = os.path.join(HERE, "..", "extensions", "live-ads.md")
POLICY_MAGIC, ANNOTATION_MAGIC = b"WAP1", b"WAB1"
POLICY_DOMAIN = b"wai:live-ads-policy\x01" # §3.2
BREAK_DOMAIN = b"wai:video-ssai-break-receipt\x01" # §8.4
TERMS_DOMAIN = b"wai:video-ssai-break-receipt\x02" # §8.4, version 2
ID_DOMAIN = b"wai:video-ssai-receipt-id\x01"
POD_DOMAIN, IMP_DOMAIN = b"wai:video-ssai-pod\x01", b"wai:video-ssai-imp\x01" # §8.3
LEAF, NODE = b"moq-jwp:merkle-leaf\x01", b"moq-jwp:merkle-node\x01" # jwp-receipts §2.2
INTERSTITIAL = "com.apple.hls.interstitial" # allow-name: the interstitial date-range class, a protocol identifier
PRELOAD = "com.apple.hls.preload" # allow-name: the preload date-range class, a protocol identifier
REPLACE = "urn:mpeg:dash:event:alternativeMPD:replace:2025" # §6.1
INSERT = "urn:mpeg:dash:event:alternativeMPD:insert:2025"
OUT, IN, CHAPTER, BREAK = 1, 2, 3, 4
META, CUE = 0x01, 0x02
MID, POST = 1, 2
HLS, DASH = 1, 2
NO_SKIP = 65535
NO_CEILING = 2 ** 64 - 1
U64 = 2 ** 64 - 1
FLOOR = 288676 # §8.5 rule 5
# Unicode White_Space, the code points a calibration reference must hold more than (§8.5).
WHITE_SPACE = set(map(chr, [0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x20, 0x85, 0xA0, 0x1680] + list(range(0x2000, 0x200B))
+ [0x2028, 0x2029, 0x202F, 0x205F, 0x3000]))
def sha(*parts):
h = hashlib.sha256()
for p in parts:
h.update(p)
return h.digest()
# ---------------------------------------------------------------- the extension's own table
def reasons():
"""§10's codes, from the text."""
with open(EXTENSION, encoding="utf-8") as f:
text = f.read()
at = text.index("\n## 10. Reasons")
end = text.index("\n## ", at + 1)
return [line.split("`")[1] for line in text[at:end].splitlines() if line.startswith("| `")]
REASONS = reasons()
# ---------------------------------------------------------------- §3 the ad policy
def is_base(b):
"""§3.1."""
if not b.startswith(b"https://"):
return False
rest = b[8:]
return (1 <= len(rest) <= 492 and rest[-1:] == b"/"
and all(0x21 <= c <= 0x7E and c not in b"\"#&'<>?\\" for c in rest))
def read_policy(b):
"""§3.3: the policy, or Refuse."""
if len(b) < 4:
raise Refuse("truncated")
if b[:4] != POLICY_MAGIC:
raise Refuse("malformed")
if len(b) < 5:
raise Refuse("truncated")
if b[4] not in (1, 2, 3):
raise Refuse("unknown_version")
r = LEV.Reader(b, "truncated")
r.take(5)
p = {"room": r.take(32), "decider_grant": r.take(32), "slots": r.u8(), "open_avail_ms": r.u32(),
"preload_ms": r.u32(), "restrict": r.u8(), "skip_after_s": r.u16(), "snap": r.u8()}
p["asset_base"] = r.take(r.u16())
p["creative_base"] = r.take(r.u16())
p["presentation"] = None
if b[4] >= 2: # ad-presentation §8
p["presentation"] = {"require": r.u8(), "rule": r.u8(), "token_type": r.u16(), "issuer": r.take(r.u16())}
p["audience"] = r.u8() if b[4] == 3 else None # §3.1
p["body"] = b[:r.p]
p["sig"] = r.take(64)
if not r.done():
raise Refuse("malformed")
if (not 1 <= p["slots"] <= 16 or p["restrict"] > 3 or p["snap"] > 3
or not is_base(p["asset_base"]) or not is_base(p["creative_base"])
or (p["presentation"] is not None and not presentation_ok(p["presentation"]))
or (p["audience"] is not None and p["audience"] > 1)):
raise Refuse("ad_policy_malformed")
p["asset_base"], p["creative_base"] = p["asset_base"].decode(), p["creative_base"].decode()
return p
def presentation_ok(q):
"""ad-presentation §8's rules for a policy of version 2."""
req, claim, attest = q["require"], q["require"] & 1, q["require"] & 2
if req > 7 or (not claim and req != 0) or q["rule"] > 2 or bool(claim) != (q["rule"] != 0):
return False
if attest:
return q["token_type"] in (1, 2) and len(q["issuer"]) > 0
return q["token_type"] == 0 and q["issuer"] == b""
def requires_claims(p):
return p["presentation"] is not None and p["presentation"]["require"] & 1 == 1
def child_room(p):
"""§3.1: the policy's audience sets child."""
return p["audience"] is not None and p["audience"] & 1 == 1
# ---------------------------------------------------------------- §4 breaks
def read_annotation(b):
"""§4.2."""
if len(b) != 38 or b[:4] != ANNOTATION_MAGIC or b[4] != 1 or b[37] not in (MID, POST):
raise Refuse("annotation_malformed")
return {"cue": b[5:37], "placement": b[37]}
def cue_of(ev):
return LEV.read_payload(CUE, ev["payload"], 65535)
def schedule(held, p):
"""§4.3 over the held events in the merged order: (breaks, chapters, written) or Stop."""
breaks, chapters, written = [], [], []
open_, last_end, closed = None, None, False
for k, (c, i, ev) in enumerate(held):
where = ["record", c, i]
if ev["kind"] == CUE:
q = cue_of(ev)
if closed:
raise Stop("cue_after_close", where)
t = ev["t"]
overlaps = open_ is not None or (last_end is not None and t < last_end)
if q["type"] == OUT:
if overlaps:
raise Stop("break_overlap", where)
avail = q["duration_ms"] or p["open_avail_ms"]
if avail == 0:
raise Stop("break_duration_unknown", where)
open_ = len(breaks)
written.append(("open", len(breaks)))
breaks.append({"digest": ev["digest"], "author": ev["principal"], "cue_id": q["cue_id"], "t": t,
"type": OUT, "placement": MID, "planned_ms": q["duration_ms"], "avail_ms": avail,
"duration_ms": None, "closed_by": None, "end": None})
elif q["type"] == IN:
b = breaks[open_] if open_ is not None else None
if b is None or b["cue_id"] != q["cue_id"] or b["author"] != ev["principal"]:
raise Stop("cue_in_unmatched", where)
duration = (t - b["t"]) // 1000
if duration == 0:
raise Stop("break_zero_duration", where)
if q["duration_ms"] != 0 and q["duration_ms"] != duration:
raise Stop("cue_duration_mismatch", where)
b.update(duration_ms=duration, closed_by=ev["digest"], end=t)
written.append(("close", open_))
open_, last_end = None, t
elif q["type"] == CHAPTER:
chapters.append({"digest": ev["digest"], "t": t, "duration_ms": q["duration_ms"]})
else:
if q["duration_ms"] == 0:
raise Stop("break_zero_duration", where)
if overlaps:
raise Stop("break_overlap", where)
last_end = t + q["duration_ms"] * 1000
written.append(("open", len(breaks)))
breaks.append({"digest": ev["digest"], "author": ev["principal"], "cue_id": q["cue_id"], "t": t,
"type": BREAK, "placement": MID, "planned_ms": q["duration_ms"],
"avail_ms": q["duration_ms"], "duration_ms": q["duration_ms"], "closed_by": None,
"end": last_end})
elif ev["kind"] == META and ev["payload"][:4] == ANNOTATION_MAGIC:
try:
a = read_annotation(ev["payload"])
except Refuse as e:
raise Stop(e.reason, where)
before = next((x for _, _, x in reversed(held[:k])
if x["principal"] == ev["principal"] and x["seq"] == ev["seq"] - 1), None)
if (before is None or before["digest"] != a["cue"] or before["kind"] != CUE or before["t"] != ev["t"]
or cue_of(before)["type"] != BREAK):
raise Stop("annotation_misplaced", where)
if a["placement"] == POST:
for b in breaks:
if b["digest"] == a["cue"]:
b["placement"] = POST
closed = True
return breaks, chapters, written
# ---------------------------------------------------------------- §4.4, §5, §6, §7
def break_id(d):
return "wai-ad-" + d.hex()
def event_id(d):
return int.from_bytes(d[:8], "big") & ((1 << 53) - 1)
def secs(ms):
return "%d.%03d" % (ms // 1000, ms % 1000)
def hls_uri(p, d):
i = break_id(d)
return "%s%s.json?_HLS_interstitial_id=%s" % (p["asset_base"], i, i)
def dash_uri(p, d):
return "%s%s.mpd" % (p["asset_base"], break_id(d))
def date(t0, t, less=0):
return LEV.iso((t0 + t) // 1000 - less)
def bits(v, names):
return ",".join(n for k, n in enumerate(names) if v & (1 << k))
def interstitial(p, t0, b):
"""§5.1."""
post = b["placement"] == POST
a = ['ID="%s"' % break_id(b["digest"]), 'CLASS="%s"' % INTERSTITIAL, 'START-DATE="%s"' % date(t0, b["t"]),
'CUE="%s"' % ("POST,ONCE" if post else "ONCE")]
known = b["planned_ms"] if b["type"] == BREAK else None
if known is not None:
a.append("DURATION=" + secs(known))
elif b["planned_ms"]:
a.append("PLANNED-DURATION=" + secs(b["planned_ms"]))
a.append('X-ASSET-LIST="%s"' % hls_uri(p, b["digest"]))
if known is not None:
if not post:
a.append("X-RESUME-OFFSET=" + secs(known))
a.append("X-PLAYOUT-LIMIT=" + secs(known))
if p["snap"]:
a.append('X-SNAP="%s"' % bits(p["snap"], ["OUT", "IN"]))
if p["restrict"]:
a.append('X-RESTRICT="%s"' % bits(p["restrict"], ["SKIP", "JUMP"]))
if p["skip_after_s"] != NO_SKIP:
a.append("X-SKIP-CONTROL-OFFSET=%d" % p["skip_after_s"])
return "#EXT-X-DATERANGE:" + ",".join(a)
def closing(b):
"""§5.2."""
s = secs(b["duration_ms"])
return '#EXT-X-DATERANGE:ID="%s",CLASS="%s",DURATION=%s,X-RESUME-OFFSET=%s,X-PLAYOUT-LIMIT=%s' % (
break_id(b["digest"]), INTERSTITIAL, s, s, s)
def preload(p, t0, b):
"""§5.3."""
i = break_id(b["digest"])
return ('#EXT-X-DATERANGE:ID="%s-preload",CLASS="%s",START-DATE="%s",DURATION=%s,X-URI="%s",X-TARGET-ID="%s",'
'X-TARGET-CLASS="%s"' % (i, PRELOAD, date(t0, b["t"], p["preload_ms"]), secs(p["preload_ms"]),
hls_uri(p, b["digest"]), i, INTERSTITIAL))
def hls_tags(p, t0, breaks, written):
"""§5.4: in the order a packager writes them."""
out = []
for what, i in written:
if what == "open":
if p["preload_ms"]:
out.append(preload(p, t0, breaks[i]))
out.append(interstitial(p, t0, breaks[i]))
else:
out.append(closing(breaks[i]))
return out
def dash_event(p, pt0, b, closed):
"""§6.2."""
known = b["duration_ms"] if closed else None
post = b["placement"] == POST
e = '<Event presentationTime="%d"' % (pt0 + b["t"] // 1000)
if known is not None:
e += ' duration="%d"' % known
e += ' id="%d">' % event_id(b["digest"])
child = "InsertPresentation" if post else "ReplacePresentation"
e += '<%s uri="%s"' % (child, dash_uri(p, b["digest"]))
if p["preload_ms"]:
e += ' earliestResolutionTimeOffset="%d"' % p["preload_ms"]
if known is not None:
e += ' maxDuration="%d"' % known
e += ' executeOnce="true"'
skip = p["skip_after_s"] if p["skip_after_s"] != NO_SKIP else -(-b["avail_ms"] // 1000)
e += ' skipAfter="PT%dS"' % skip
if known is not None and not post:
e += ' returnOffset="%d"' % known
return e + "/></Event>"
def dash_streams(p, pt0, breaks):
"""§6.3."""
out = ""
for scheme, post in ((REPLACE, False), (INSERT, True)):
evs = [dash_event(p, pt0, b, True) for b in breaks if (b["placement"] == POST) == post]
if evs:
out += '<EventStream schemeIdUri="%s" timescale="1000">%s</EventStream>' % (scheme, "".join(evs))
return out
def decide(pod, slots, avail, funds):
"""§7.1: the fill, each (slot, ad_id, creative_hash, duration_ms, price_micros)."""
fill = []
for c in sorted(pod, key=lambda c: (-c["price_micros"], c["ad_id"].encode())):
if len(fill) >= slots:
break
if c["duration_ms"] <= avail and c["price_micros"] <= funds:
fill.append((len(fill), c["ad_id"], c["creative_hash"], c["duration_ms"], c["price_micros"]))
avail -= c["duration_ms"]
funds -= c["price_micros"]
return fill
def asset_list(p, fmt, fill):
"""§7.2, §7.3: the exact bytes."""
if fmt == HLS:
assets = ['{"URI":"%s%s.m3u8","DURATION":%s}' % (p["creative_base"], h.hex(), secs(d)) for _, _, h, d, _ in fill]
return ('{"ASSETS":[%s]}' % ",".join(assets)).encode()
if not fill:
return b""
periods = "".join('<Period id="%d" duration="PT%sS"><ImportedMPD>%s%s.mpd</ImportedMPD></Period>'
% (s, secs(d), p["creative_base"], h.hex()) for s, _, h, d, _ in fill)
return ('<?xml version="1.0" encoding="UTF-8"?>\n<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" '
'profiles="urn:mpeg:dash:profile:list:2024" type="list" minBufferTime="PT1S">%s</MPD>' % periods).encode()
# ---------------------------------------------------------------- §8 the break form
MEMBERS = ["form", "room", "cue", "session", "start_offset_ms", "format", "asset_list_sha256", "avail_ms", "slots",
"funds_remaining_micros", "pod_hash", "impressions", "merkle_root", "budget_spent_micros",
"work_decide_ops", "joules_micro", "energy_provenance", "grant", "signer_pubkey", "signer_id", "sig"]
EVIDENCE = {"OnChipCounter": ["energy_uncertainty", "energy_counter_filtering"],
"CalibratedInstrument": ["energy_uncertainty", "energy_calibration_ref"],
"ModelBased": [], "Estimator": []}
CLASS_TAG = {"ModelBased": 2, "Estimator": 3, "OnChipCounter": 4, "CalibratedInstrument": 5}
FILTERING = {"Unknown": 0, "Off": 1, "On": 2}
def u(v, top=U64):
if not isinstance(v, int) or isinstance(v, bool) or not 0 <= v <= top:
raise Refuse("receipt_malformed")
return v
def hexn(v, n):
if not isinstance(v, str) or len(v) != 2 * n or any(c not in "0123456789abcdef" for c in v):
raise Refuse("receipt_malformed")
return bytes.fromhex(v)
def text(v):
if not isinstance(v, str):
raise Refuse("receipt_malformed")
return v
def read_receipt(o):
"""§8.2: the receipt, or Refuse receipt_malformed."""
if not isinstance(o, dict):
raise Refuse("receipt_malformed")
label = o.get("energy_provenance")
if label is not None and label not in EVIDENCE:
raise Refuse("receipt_malformed")
want = set(MEMBERS) | set(EVIDENCE.get(label, []))
v2 = "version" in o # §8.2: version 2
if v2:
want |= {"version", "consent", "supply_chains"}
if set(o) != want or u(o["form"]) != 2 or (v2 and u(o["version"]) != 2):
raise Refuse("receipt_malformed")
r = {"room": hexn(o["room"], 32), "cue": hexn(o["cue"], 32), "session": hexn(o["session"], 16),
"start_offset_ms": u(o["start_offset_ms"]), "format": u(o["format"], 255),
"asset_list_sha256": hexn(o["asset_list_sha256"], 32), "avail_ms": u(o["avail_ms"]), "slots": u(o["slots"]),
"funds_remaining_micros": u(o["funds_remaining_micros"]), "pod_hash": hexn(o["pod_hash"], 32),
"merkle_root": hexn(o["merkle_root"], 32), "budget_spent_micros": u(o["budget_spent_micros"]),
"work_decide_ops": u(o["work_decide_ops"]), "joules_micro": u(o["joules_micro"]),
"signer_pubkey": hexn(o["signer_pubkey"], 32), "signer_id": text(o["signer_id"]), "sig": hexn(o["sig"], 64)}
if not isinstance(o["impressions"], list):
raise Refuse("receipt_malformed")
r["impressions"] = []
for i in o["impressions"]:
if not isinstance(i, dict) or set(i) != {"slot", "ad_id", "creative_hash", "duration_ms", "price_micros",
"quartiles_reached"}:
raise Refuse("receipt_malformed")
r["impressions"].append({"slot": u(i["slot"]), "ad_id": text(i["ad_id"]),
"creative_hash": hexn(i["creative_hash"], 32), "duration_ms": u(i["duration_ms"]),
"price_micros": u(i["price_micros"]), "quartiles_reached": u(i["quartiles_reached"], 255)})
g = o["grant"]
if not isinstance(g, dict) or set(g) != {"grant_hash", "capability", "joule_ceiling_micro", "funds_ceiling"}:
raise Refuse("receipt_malformed")
r["grant"] = {"grant_hash": hexn(g["grant_hash"], 32), "capability": text(g["capability"]),
"joule_ceiling_micro": u(g["joule_ceiling_micro"]),
"funds_ceiling": None if g["funds_ceiling"] is None else u(g["funds_ceiling"])}
r["class"] = None
if label is not None:
c = {"label": label}
if label in ("OnChipCounter", "CalibratedInstrument"):
unc = o["energy_uncertainty"]
if not isinstance(unc, dict) or set(unc) != {"relative_ppm", "window_us"}:
raise Refuse("receipt_malformed")
c["ppm"], c["window"] = u(unc["relative_ppm"], 2 ** 32 - 1), u(unc["window_us"])
if label == "OnChipCounter":
if o["energy_counter_filtering"] not in FILTERING:
raise Refuse("receipt_malformed")
c["filtering"] = FILTERING[o["energy_counter_filtering"]]
if label == "CalibratedInstrument":
c["ref"] = text(o["energy_calibration_ref"])
r["class"] = c
r["terms"] = read_terms(o["consent"], o["supply_chains"]) if v2 else None
return r
def exactly(o, members):
if not isinstance(o, dict) or set(o) != set(members):
raise Refuse("receipt_malformed")
return o
def opt_text(v):
return None if v is None else text(v)
def read_terms(consent, chains):
"""§8.6, §8.7: the JSON carriage of a receipt of version 2's request terms."""
c = exactly(consent, ["gpp", "gpp_sid", "child_directed"])
if not isinstance(c["gpp_sid"], list) or not isinstance(chains, list):
raise Refuse("receipt_malformed")
return {"consent": {"gpp": text(c["gpp"]), "gpp_sid": [u(x, 65535) for x in c["gpp_sid"]],
"child_directed": u(c["child_directed"], 255)},
"supply_chains": [None if ch is None else read_chain(ch) for ch in chains]}
def read_chain(chain):
"""§8.7: one entry of supply_chains that is not null."""
ch = exactly(chain, ["ver", "complete", "nodes", "authorised"])
if not isinstance(ch["nodes"], list):
raise Refuse("receipt_malformed")
nodes = []
for n in ch["nodes"]:
n = exactly(n, ["asi", "sid", "hp", "rid", "name", "domain", "sellers"])
nodes.append({"asi": text(n["asi"]), "sid": text(n["sid"]), "hp": u(n["hp"], 255), "rid": opt_text(n["rid"]),
"name": opt_text(n["name"]), "domain": opt_text(n["domain"]),
"sellers": None if n["sellers"] is None else hexn(n["sellers"], 32)})
a = ch["authorised"]
if a is not None:
a = exactly(a, ["file", "domain", "sha256"])
a = {"file": text(a["file"]), "domain": text(a["domain"]), "sha256": hexn(a["sha256"], 32)}
return {"ver": text(ch["ver"]), "complete": u(ch["complete"], 255), "nodes": nodes, "authorised": a}
GPP_CHARS = set(b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~")
def consent_ok(c):
"""§8.6's rules."""
g = c["gpp"].encode("utf-8")
if len(g) > 65535 or any(ch not in GPP_CHARS for ch in g):
return False
if g and any(part == b"" for part in g.split(b"~")):
return False
sid = c["gpp_sid"]
if len(sid) > 16 or any(x < 1 for x in sid) or len(set(sid)) != len(sid) or (not g and sid):
return False
return c["child_directed"] in (0, 1)
def domain_ok(d):
"""§8.7: a domain name."""
b = d.encode("utf-8")
if not 1 <= len(b) <= 253:
return False
labels = b.split(b".")
alnum = set(b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-")
return len(labels) >= 2 and all(1 <= len(lb) <= 63 and all(ch in alnum for ch in lb) and lb[:1] != b"-"
and lb[-1:] != b"-" for lb in labels)
def text_ok(v):
return 1 <= len(v.encode("utf-8")) <= 255
def chain_ok(ch):
"""§8.7's rules."""
major, dot, minor = ch["ver"].partition(".")
digits = set("0123456789")
if not (dot and major and minor and set(major) <= digits and set(minor) <= digits and len(ch["ver"]) <= 15):
return False
if ch["complete"] not in (0, 1) or not 1 <= len(ch["nodes"]) <= 32:
return False
for n in ch["nodes"]:
if not (domain_ok(n["asi"]) and text_ok(n["sid"]) and n["hp"] in (0, 1)
and (n["rid"] is None or text_ok(n["rid"])) and (n["name"] is None or text_ok(n["name"]))
and (n["domain"] is None or domain_ok(n["domain"]))):
return False
a = ch["authorised"]
return a is None or (a["file"] in ("ads.txt", "app-ads.txt") and domain_ok(a["domain"]))
def text_bytes(v):
b = v.encode("utf-8")
return struct.pack(">I", len(b)) + b
def terms_bytes(t):
"""§8.6, §8.7: the signed encodings of the consent signal and the supply chains."""
c = t["consent"]
out = [text_bytes(c["gpp"]), be64(len(c["gpp_sid"]))] + [struct.pack(">H", x) for x in c["gpp_sid"]]
out.append(bytes([c["child_directed"]]))
out.append(be64(len(t["supply_chains"])))
for ch in t["supply_chains"]:
if ch is None:
out.append(b"\x00")
continue
out += [b"\x01", text_bytes(ch["ver"]), bytes([ch["complete"]]), be64(len(ch["nodes"]))]
for n in ch["nodes"]:
out += [text_bytes(n["asi"]), text_bytes(n["sid"]), bytes([n["hp"]])]
out += [b"\x00" if n[k] is None else b"\x01" + text_bytes(n[k]) for k in ("rid", "name", "domain")]
out.append(b"\x00" if n["sellers"] is None else b"\x01" + n["sellers"])
a = ch["authorised"]
out.append(b"\x00" if a is None else b"\x01" + text_bytes(a["file"]) + text_bytes(a["domain"]) + a["sha256"])
return b"".join(out)
def ascii_lower(v):
return "".join(chr(ord(ch) + 32) if "A" <= ch <= "Z" else ch for ch in v)
def ascii_upper(v):
return "".join(chr(ord(ch) - 32) if "a" <= ch <= "z" else ch for ch in v)
def reject_constant(name):
raise ValueError("not a JSON number: " + name)
SELLERS_DEPTH = 64 # §8.7
class Number:
"""A JSON number as RFC 8259's grammar reads it, never converted to a value (§8.7)."""
def __init__(self, written):
self.written = written
class Object:
"""A JSON object: its members in order, a name given twice kept twice (§8.7)."""
def __init__(self, members):
self.members = members
def nests_within(b, limit):
"""§8.7: do the arrays and objects of the text nest at most limit deep, the outermost the
first? Brackets in strings are not counted; on a text that is not JSON it does not matter."""
depth, in_string, escaped = 0, False, False
for c in b:
if in_string:
if escaped:
escaped = False
elif c == 0x5C:
escaped = True
elif c == 0x22:
in_string = False
elif c == 0x22:
in_string = True
elif c in (0x5B, 0x7B):
depth += 1
if depth > limit:
return False
elif c in (0x5D, 0x7D):
depth = max(depth - 1, 0)
return True
def scalar_text(v):
"""A string of Unicode scalar values: the JSON string escaped no lone surrogate (RFC 8259
§7; a pair escapes one character, and UTF-8 text holds no surrogate)."""
return isinstance(v, str) and not any(0xD800 <= ord(ch) <= 0xDFFF for ch in v)
def read_sellers(b):
"""§8.7: a sellers file's (seller_id, seller_type) pairs, or None when it does not read.
What §8.7 does not read is read by the grammar alone: numbers are kept as written, and a
string the reader skips may escape a lone surrogate."""
try:
t = b.decode("utf-8")
except UnicodeDecodeError:
return None
if t.startswith("\ufeff") or not nests_within(b, SELLERS_DEPTH):
return None
try:
v = json.loads(t, parse_int=Number, parse_float=Number, parse_constant=reject_constant, object_pairs_hook=Object)
except ValueError:
return None
if not isinstance(v, Object) or not all(scalar_text(k) for k, _ in v.members):
return None
sellers = [x for k, x in v.members if k == "sellers"]
if len(sellers) != 1 or not isinstance(sellers[0], list):
return None
out = []
for x in sellers[0]:
if not isinstance(x, Object) or not all(scalar_text(k) for k, _ in x.members):
return None
ids = [s for k, s in x.members if k == "seller_id"]
kinds = [s for k, s in x.members if k == "seller_type"]
if len(ids) != 1 or len(kinds) > 1 or not all(scalar_text(s) for s in ids + kinds):
return None
out.append((ids[0], kinds[0] if kinds else None))
return out
def read_authorised(b):
"""§8.7: an authorised-sellers file's data records, or None when it is not UTF-8."""
try:
t = b.decode("utf-8")
except UnicodeDecodeError:
return None
out = []
for line in t.replace("\r\n", "\n").replace("\r", "\n").split("\n"):
line = line.split("#", 1)[0].strip(" \t")
if not line:
continue
name, eq, _ = line.partition("=")
if eq and name and not any(ch in name for ch in ", \t"):
continue # a variable record
fields = [f.strip(" \t") for f in line.split(";", 1)[0].split(",")]
if 3 <= len(fields) <= 4 and all(fields):
out.append((fields[0], fields[1], fields[2]))
return out
def check_chain(ch, held):
"""§9.3 step 14: what a verification states of the chain, or Refuse."""
nodes = []
for n in ch["nodes"]:
f = {"asi": n["asi"], "sid": n["sid"], "sellers_checked": False, "seller_type": None, "relationship": None}
b = held.get(n["sellers"]) if n["sellers"] is not None else None
if b is not None:
sellers = read_sellers(b)
if sellers is None:
raise Refuse("supply_file_malformed")
hit = [kind for sid, kind in sellers if sid == n["sid"]]
if not hit:
raise Refuse("supply_seller_unlisted")
f["sellers_checked"], f["seller_type"] = True, hit[0]
nodes.append(f)
checked, a = False, ch["authorised"]
if a is not None and a["sha256"] in held:
records = read_authorised(held[a["sha256"]])
if records is None:
raise Refuse("supply_file_malformed")
for k, (n, f) in enumerate(zip(ch["nodes"], nodes)):
direct_only = k == 0 and ch["complete"] == 1
rel = [ascii_upper(r) for system, account, r in records
if ascii_lower(system) == ascii_lower(n["asi"]) and account == n["sid"]]
rel = [x for x in rel if x == "DIRECT" or (not direct_only and x == "RESELLER")]
if not rel:
raise Refuse("supply_seller_unauthorised")
f["relationship"] = rel[0]
checked = True
return {"complete": ch["complete"], "nodes": nodes, "authorised_checked": checked}
def b3(*parts):
return B3.blake3(b"".join(parts))
def be64(v):
return struct.pack(">Q", v)
def pod_hash(pod):
"""§8.3."""
out = [POD_DOMAIN, be64(len(pod))]
for c in pod:
a = c["ad_id"].encode()
out += [struct.pack(">I", len(a)), a, c["creative_hash"], be64(c["duration_ms"]), be64(c["price_micros"])]
return b3(*out)
def leaf(i):
a = i["ad_id"].encode()
return b3(IMP_DOMAIN, be64(i["slot"]), struct.pack(">I", len(a)), a, i["creative_hash"], be64(i["duration_ms"]),
be64(i["price_micros"]), bytes([i["quartiles_reached"]]))
def merkle(hashes):
"""jwp-receipts §2.2, each leaf of its first form."""
if not hashes:
return bytes(32)
level = [b3(LEAF, h) for h in hashes]
while len(level) > 1:
if len(level) % 2:
level.append(level[-1])
level = [b3(NODE, level[k], level[k + 1]) for k in range(0, len(level), 2)]
return level[0]
def class_bytes(c):
"""energy-measurement §2.7's signed encoding."""
if c is None:
return b"\x00"
out = bytes([CLASS_TAG[c["label"]]])
if c["label"] == "OnChipCounter":
out += struct.pack(">IQ", c["ppm"], c["window"]) + bytes([c["filtering"]])
elif c["label"] == "CalibratedInstrument":
ref = c["ref"].encode()
out += struct.pack(">IQ", c["ppm"], c["window"]) + struct.pack(">I", len(ref)) + ref
return out
def payload(r):
"""§8.4; a receipt of version 2 under its own domain, with its request terms."""
g = r["grant"]
cap, sid = g["capability"].encode(), r["signer_id"].encode()
out = [BREAK_DOMAIN if r.get("terms") is None else TERMS_DOMAIN, r["room"], r["cue"], r["session"], be64(r["start_offset_ms"]), bytes([r["format"]]),
r["asset_list_sha256"], be64(r["avail_ms"]), be64(r["slots"]), be64(r["funds_remaining_micros"]),
r["pod_hash"], r["merkle_root"], be64(r["budget_spent_micros"]), be64(r["work_decide_ops"]),
be64(r["joules_micro"]), class_bytes(r["class"]), g["grant_hash"], struct.pack(">I", len(cap)), cap,
be64(g["joule_ceiling_micro"])]
out.append(b"\x00" if g["funds_ceiling"] is None else b"\x01" + be64(g["funds_ceiling"]))
out.append(be64(len(r["impressions"])))
out += [leaf(i) for i in r["impressions"]]
if r.get("terms") is not None:
out.append(terms_bytes(r["terms"]))
out += [r["signer_pubkey"], struct.pack(">I", len(sid)), sid]
return b"".join(out)
def own_rules(r):
"""§8.5: None, or the refusal; rule 6, a receipt of version 2's request terms, last."""
bad = rules_one_to_five(r)
if bad or r.get("terms") is None:
return bad
if not consent_ok(r["terms"]["consent"]):
return "consent_invalid"
chains = r["terms"]["supply_chains"]
if len(chains) != len(r["impressions"]) or any(ch is not None and not chain_ok(ch) for ch in chains):
return "supply_chain_invalid" # §8.7 rules 1 to 4
return None
def rules_one_to_five(r):
"""§8.5 rules 1 to 5."""
if r["format"] not in (HLS, DASH):
return "receipt_invalid"
bad = ED.verify(r["signer_pubkey"], payload(r), r["sig"])
if bad:
return bad
imps = r["impressions"]
spend = sum(i["price_micros"] for i in imps if i["quartiles_reached"] >= 1)
if merkle([leaf(i) for i in imps]) != r["merkle_root"] or spend != r["budget_spent_micros"]:
return "receipt_invalid"
f = r["grant"]["funds_ceiling"]
if f is not None and spend > f * 10 ** 6:
return "receipt_invalid"
if any(i["slot"] != k or i["quartiles_reached"] > 5 for k, i in enumerate(imps)) or r["work_decide_ops"] < len(imps):
return "receipt_invalid"
c, j = r["class"], r["joules_micro"]
if c is None:
return None if j == 0 else "receipt_invalid"
if j == 0:
return "receipt_invalid"
if c["label"] in ("OnChipCounter", "CalibratedInstrument"):
if c["ppm"] == 0 or c["window"] == 0 or j * c["ppm"] < FLOOR:
return "receipt_invalid"
if c["label"] == "CalibratedInstrument" and all(ch in WHITE_SPACE for ch in c["ref"]):
return "receipt_invalid"
return None
def identity(r):
return b3(ID_DOMAIN, payload(r), r["sig"])
# ---------------------------------------------------------------- §9 verification
class Stop(Exception):
def __init__(self, reason, at):
super().__init__(reason)
self.reason, self.at = reason, at
def ads(v, judge=None, files=None):
"""§9: a dict of what the corpus states for vector v, and the run's HLS tags. judge, when
given, makes the judge of a policy's presentation claims (ad-presentation §9); files holds
the corpus's files by name, among them the supply files the vector names (§9.1)."""
descriptor = bytes.fromhex(v["descriptor"])
grants = [bytes.fromhex(g) for g in v["grants"]]
chains = [[bytes.fromhex(h) for h in c] for c in v["chains"]]
room = LRV.verify_room(descriptor, grants, v["key_log"], chains) # step 1
if room["outcome"] == "refuse":
return {"outcome": "refuse", "reason": room["reason"], "at": ["room"] + room["at"]}, None
if room["outcome"] == "gap":
return {"outcome": "gap", "gaps": room["gaps"]}, None
supply = {sha(files[n]): files[n] for n in v.get("supply_files", [])} if files is not None else {}
try:
return accept(v, descriptor, grants, chains, room, judge, supply)
except Stop as e:
return {"outcome": "refuse", "reason": e.reason, "at": e.at}, None
def accept(v, descriptor, grants, chains, room, judge=None, supply=None):
d = LRV.read_descriptor(descriptor)
held_grants, _ = LRV.check_grants(d, grants)
try: # step 2
p = read_policy(bytes.fromhex(v["policy"]))
if p["room"] != d["room"]:
raise Refuse("wrong_room")
bad = ED.verify(d["authority"], POLICY_DOMAIN + p["body"], p["sig"])
if bad:
raise Refuse(bad)
decider = next((g for g in held_grants if g["digest"] == p["decider_grant"]), None)
if decider is None:
raise Refuse("decider_grant_unknown")
if LRV.revoked(decider, LRV.check_key_log(d, v["key_log"])):
raise Refuse("grant_revoked")
except Refuse as e:
raise Stop(e.reason, ["policy"])
held, seen = [], set() # step 3
for c, i, _ in room["order"]:
ev = LEV.read_record(chains[c][i])
if ev["digest"] not in seen:
seen.add(ev["digest"])
held.append((c, i, ev))
breaks, chapters, written = schedule(held, p)
t0, pt0 = d["t0"], v["pt0_ms"] # step 4
tags = hls_tags(p, t0, breaks, written)
funds = None if decider["funds"] == NO_CEILING else decider["funds"]
claims = None
if requires_claims(p): # ad-presentation §9.2 steps 1, 2
if judge is None:
raise ValueError("a policy that requires presentation claims: verify it with ad_presentation_verify.py")
claims = judge(v, d, p)
stated, sessions, total = [], set(), 0 # step 5
for k, (o, pod_json) in enumerate(zip(v["receipts"], v["pods"] + [None] * len(v["receipts"]))):
at = ["receipt", k]
try:
r = read_receipt(o)
except Refuse as e:
raise Stop(e.reason, at)
if r["room"] != d["room"]:
raise Stop("wrong_room", at)
bad = own_rules(r)
if bad:
raise Stop(bad, at)
if r["signer_pubkey"] != decider["principal"]:
raise Stop("receipt_not_decider", at)
g = r["grant"]
if (g["grant_hash"] != decider["digest"] or g["capability"] != "wai.live.room"
or g["joule_ceiling_micro"] != decider["joules"] or g["funds_ceiling"] != funds):
raise Stop("receipt_grant_mismatch", at)
b = next((x for x in breaks if x["digest"] == r["cue"]), None)
if b is None:
raise Stop("receipt_break_unknown", at)
if not LRV.good_at(decider, d, b["t"]):
raise Stop("outside_grant_window", at)
pod = [{"ad_id": c["ad_id"], "creative_hash": bytes.fromhex(c["creative_hash"]), "duration_ms": c["duration_ms"],
"price_micros": c["price_micros"]} for c in (pod_json or [])]
if r["pod_hash"] != pod_hash(pod):
raise Stop("pod_mismatch", at)
fill = decide(pod, p["slots"], max(b["avail_ms"] - r["start_offset_ms"], 0), r["funds_remaining_micros"])
got = [(i["slot"], i["ad_id"], i["creative_hash"], i["duration_ms"], i["price_micros"]) for i in r["impressions"]]
if r["avail_ms"] != b["avail_ms"] or r["slots"] != p["slots"] or got != fill:
raise Stop("decision_mismatch", at)
listed = asset_list(p, r["format"], fill)
if r["asset_list_sha256"] != sha(listed):
raise Stop("asset_list_mismatch", at)
if (r["cue"], r["session"]) in sessions:
raise Stop("receipt_duplicate", at)
sessions.add((r["cue"], r["session"]))
counted, extra = r["budget_spent_micros"], {} # step 12
if claims is not None:
counted, extra = claims.receipt(k, r)
total += counted
if funds is not None and (r["funds_remaining_micros"] > funds * 10 ** 6 or total > funds * 10 ** 6):
raise Stop("funds_exceeded", at)
t = r["terms"]
if p["audience"] is not None: # step 13
if t is None:
raise Stop("request_terms_absent", at)
if child_room(p) and t["consent"]["child_directed"] != 1:
raise Stop("child_directed_unset", at)
fact = {"identity": identity(r).hex(), "asset_list": listed.decode(), "spend_micros": r["budget_spent_micros"]}
fact.update(extra)
if t is not None: # step 14
chains = []
for ch in t["supply_chains"]:
try:
chains.append(None if ch is None else check_chain(ch, supply or {}))
except Refuse as e:
raise Stop(e.reason, at)
fact["consent"] = dict(t["consent"])
fact["supply_chains"] = chains
stated.append(fact)
out = {
"outcome": "accept",
"room": d["room"].hex(),
"breaks": [{"digest": b["digest"].hex(), "id": break_id(b["digest"]), "event_id": event_id(b["digest"]),
"author": b["author"].hex(), "cue_id": b["cue_id"], "t": b["t"],
"type": "out" if b["type"] == OUT else "break", "placement": "post" if b["placement"] == POST else "mid",
"planned_ms": b["planned_ms"], "avail_ms": b["avail_ms"], "duration_ms": b["duration_ms"],
"closed_by": b["closed_by"].hex() if b["closed_by"] else None, "end": b["end"],
"hls_list_uri": hls_uri(p, b["digest"]), "dash_list_uri": dash_uri(p, b["digest"])} for b in breaks],
"chapters": [{"digest": c["digest"].hex(), "t": c["t"], "duration_ms": c["duration_ms"]} for c in chapters],
"hls": v["name"] + ".m3u8",
"dash": dash_streams(p, pt0, breaks),
"dash_open": [{"break": k, "event": dash_event(p, pt0, b, False)} for k, b in enumerate(breaks) if b["type"] == OUT],
"receipt_facts": stated,
"spend_micros": min(total, U64),
}
if claims is not None:
out["claim_facts"] = claims.finish()
return out, "".join(t + "\n" for t in tags).encode()
# ---------------------------------------------------------------- the corpus
def check(doc, files, quiet=False):
"""Every room against its statement, and every tag file: the failures, and the files written."""
fails, written = [], {}
for v in doc.get("vectors", []):
try:
got, tags = ads(v, files=files)
except Exception as e: # a crash on a mutated copy is a failure to read it
got, tags = None, None
bad = ["the vector does not verify: %s: %s" % (type(e).__name__, e)]
else:
bad = [k for k in got if v.get(k) != got[k]]
bad += [k for k in ("outcome", "reason", "at", "gaps", "breaks", "hls", "dash", "receipt_facts")
if k in v and k not in got]
if tags is not None:
name = v["name"] + ".m3u8"
written[name] = tags
if files.get(name) != tags:
bad.append("its tag file %s" % name)
if not quiet:
what = "?" if got is None else got["outcome"] + (" " + got["reason"] if got["outcome"] == "refuse" else "")
print(("PASS" if not bad else "FAIL") + " room %s: %s" % (v.get("name"), what))
fails += ["room %s: %s differs from the corpus" % (v.get("name"), k) for k in bad]
return fails, written
def flip(h, at):
b = bytearray(bytes.fromhex(h))
b[at] ^= 0x01
return b.hex()
def mutations(doc, files):
"""Copies of the corpus, each with one defect a conforming verifier must catch."""
vn = {v["name"]: k for k, v in enumerate(doc["vectors"])}
live, mid, post, chap, dup = vn["live_break"], vn["mid_roll"], vn["post_roll"], vn["chapters"], vn["receipt_duplicate"]
cc, kids = vn["consent_and_chain"], vn["child_room"]
def mutate(label, f):
d, fs = copy.deepcopy(doc), dict(files)
f(d["vectors"], fs)
return label, d, fs
def drop_last(vs, chain_of):
"""The last record of the chain holding a producer's cues."""
ch = vs[chain_of]["chains"]
longest = max(range(len(ch)), key=lambda c: len(ch[c]))
ch[longest].pop()
def swap_pod(vs, fs):
pod = vs[mid]["pods"][0]
pod[1], pod[3] = pod[3], pod[1]
def edit_file(name, a, b):
return lambda vs, fs: fs.update({name: fs[name].replace(a, b, 1)})
return [
mutate("a byte of the live break's policy signature", lambda vs, fs: vs[live].update(policy=flip(vs[live]["policy"], -2))),
mutate("the live break's in withheld from the end of its chain", lambda vs, fs: drop_last(vs, live)),
mutate("the post-roll's annotation withheld", lambda vs, fs: drop_last(vs, post)),
mutate("a stated asset list with one ad's duration changed",
lambda vs, fs: vs[live]["receipt_facts"][0].update(asset_list=vs[live]["receipt_facts"][0]["asset_list"].replace("15.000", "14.000", 1))),
mutate("the DASH events stated with executeOnce false",
lambda vs, fs: vs[live].update(dash=vs[live]["dash"].replace('executeOnce="true"', 'executeOnce="false"'))),
mutate("the open out's DASH event stated with its duration",
lambda vs, fs: vs[live]["dash_open"][0].update(event=vs[live]["dash"].split(">", 1)[1].rsplit("</EventStream>", 1)[0])),
mutate("a byte of the mid-roll's tag file", edit_file("mid_roll.m3u8", b"X-RESUME-OFFSET=30.000", b"X-RESUME-OFFSET=31.000")),
mutate("the post-roll's tag stated without POST", edit_file("post_roll.m3u8", b'CUE="POST,ONCE"', b'CUE="ONCE"')),
mutate("the mid-roll pod given in another order", swap_pod),
mutate("the mid-roll's presentation time of t0 changed", lambda vs, fs: vs[mid].update(pt0_ms=vs[mid]["pt0_ms"] + 1)),
mutate("a live break receipt's quartile raised",
lambda vs, fs: vs[live]["receipts"][0]["impressions"][1].update(quartiles_reached=4)),
mutate("a live break receipt's session changed",
lambda vs, fs: vs[live]["receipts"][1].update(session="00" * 16)),
mutate("the stated spend", lambda vs, fs: vs[live].update(spend_micros=vs[live]["spend_micros"] + 1)),
mutate("a chapter's stated length", lambda vs, fs: vs[chap]["chapters"][0].update(duration_ms=59999)),
mutate("the duplicated receipt given once", lambda vs, fs: vs[dup]["receipts"].pop()),
mutate("the funds control's second receipt given once",
lambda vs, fs: vs[vn["funds_exceeded"]]["receipts"].pop()),
mutate("a tag file removed", lambda vs, fs: fs.pop("chapters.m3u8")),
# The request terms (§8.6, §8.7, §9.3 steps 13 and 14).
mutate("the first viewer's GPP string with a section changed",
lambda vs, fs: vs[cc]["receipts"][0]["consent"].update(gpp=vs[cc]["receipts"][0]["consent"]["gpp"].replace("1YNN", "1NNN"))),
mutate("the first viewer's applicable sections stated as 2 and 6",
lambda vs, fs: vs[cc]["receipts"][0]["consent"].update(gpp_sid=[2, 6])),
mutate("the stated child-directed flag of the joining viewer",
lambda vs, fs: vs[cc]["receipt_facts"][1]["consent"].update(child_directed=0)),
mutate("a node's seller type stated as BOTH",
lambda vs, fs: vs[cc]["receipt_facts"][0]["supply_chains"][0]["nodes"][0].update(seller_type="BOTH")),
mutate("the second node's relationship stated as DIRECT",
lambda vs, fs: vs[cc]["receipt_facts"][0]["supply_chains"][0]["nodes"][1].update(relationship="DIRECT")),
mutate("the second impression's chain stated as none",
lambda vs, fs: vs[cc]["receipt_facts"][0]["supply_chains"].__setitem__(1, None)),
mutate("the first viewer's two chains given in the other order",
lambda vs, fs: vs[cc]["receipts"][0]["supply_chains"].reverse()),
mutate("the liberal ads.txt's lone CR made a space", edit_file(
"supply.channel.example.ads.txt", b"reseller\r", b"reseller ")),
mutate("the gateway's sellers file nested one deeper",
lambda vs, fs: fs.update({"supply.gateway.example.sellers.json": fs["supply.gateway.example.sellers.json"]
.replace(b"[" * 63, b"[" * 64, 1).replace(b"]" * 63, b"]" * 64, 1)})),
mutate("the app-ads.txt lists the SSP's account as a reseller", edit_file(
"supply.channel.example.app-ads.txt", b"ssp.example, pub-1001, DIRECT", b"ssp.example, pub-1001, RESELLER")),
mutate("the SSP's sellers file no longer lists the channel",
edit_file("supply.ssp.example.sellers.json", b'"seller_id": "pub-1001"', b'"seller_id": "pub-1009"')),
mutate("the supply files withheld from the chain's vector", lambda vs, fs: vs[cc].pop("supply_files")),
mutate("the child room's policy stated with no audience bit",
lambda vs, fs: vs[kids].update(policy=vs[kids]["policy"][:-130] + "00" + vs[kids]["policy"][-128:])),
mutate("the cleared child-directed flag set back", lambda vs, fs: vs[vn["receipt_child_flag_cleared"]]["receipts"][0]["consent"].update(child_directed=1)),
mutate("a version 2 receipt read as version 1",
lambda vs, fs: [vs[cc]["receipts"][0].pop(k) for k in ("version", "consent", "supply_chains")]),
]
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 []
ok = len(REASONS) == 27
print(("PASS" if ok else "FAIL") + " §10 read from the extension: %d reasons" % len(REASONS))
fails += [] if ok else ["§10 does not read"]
with open(os.path.join(corpus_dir, "vectors.json"), encoding="utf-8") as f:
doc = json.load(f)
files = {}
supply = {n for v in doc["vectors"] for n in v.get("supply_files", [])}
for name in sorted(os.listdir(corpus_dir)):
if name.endswith(".m3u8") or name in supply:
with open(os.path.join(corpus_dir, name), "rb") as f:
files[name] = f.read()
got, written = check(doc, files)
fails += got
extra = sorted(set(os.listdir(corpus_dir)) - set(written) - supply - {"vectors.json", "README.md"})
print(("PASS" if not extra else "FAIL") + " no file but vectors.json, README.md, the tag files the rooms write and the "
"supply files the vectors name")
fails += ["an extra file: %s" % n for n in extra]
seen = {v.get("reason") for v in doc["vectors"]}
missing = [r for r in REASONS if r not in seen]
print(("PASS" if not missing else "FAIL") + " every reason of §10 appears in the corpus")
fails += ["no case gives %s" % r for r in missing]
for label, d, fs in mutations(doc, files):
caught, _ = check(d, fs, quiet=True)
print(("PASS" if caught else "FAIL") + " mutated copy fails: %s%s" % (label, " (%s)" % caught[0][:100] 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") + " live-ads corpus: %d rooms, %d tag files" % (len(doc["vectors"]), len(files)))
return 1 if fails else 0
if __name__ == "__main__":
if len(sys.argv) != 2:
print("usage: live_ads_verify.py <live-conformance/ads>", file=sys.stderr)
sys.exit(2)
sys.exit(main(sys.argv[1]))