open-standards/wai/live-conformance/stream_audit_verify.py
Transaction Science f2acb11dd4 wai: an unnamed acknowledgement refused, the collector's port, the stitched form
- The falsifications found the check that refuses an acknowledgement the
  record does not name guarded by no vector in either implementation:
  the corpus gains ack_not_named (audit_bundle_invalid), and the Python
  verifier can drop that check too, which the corpus now catches.
- The collector's default port moves to 9694: 9690 is the stitched
  presentation tool's.
- stream-audit §6.3 states that this revision claims every marked ad
  with WPC1, and that a stitched ad is claimed with the stitched
  presentation record where a deployment holds it, its bundle then
  carrying the manifests its claims name.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 22:05:49 -04:00

786 lines
32 KiB
Python

#!/usr/bin/env python3
"""Independent verifier of the stream audit corpus (extensions/stream-audit.md).
Written from the extension's text. It shares no code with wai-rs, which generates the corpus, and
never runs it. Acknowledgements and their item lists are read by the settlement corpus's second
implementation, settlement_verify.py (settlement §3.6, the tree root of jwp-receipts §2.2); claims,
their quartile and their run by the ad-presentation corpus's, ad_presentation_verify.py
(ad-presentation §5, §6); the observation log and a figure's acquisition class by the
live-metering corpus's, live_metering_verify.py (live-metering §3.6, §6.1); and every signature
with ../claim-conformance/_ed25519.py, WAI's strict rule (SPEC §7 *Signatures*) written from RFC
8032. Everything else is here, from the standard library:
- the channel id, the session and an origin's upstream (§3, settlement §14.2);
- the session record: reading, rules and signature (§5);
- the bundle and its verification in §10.2's order, with each refusal's place;
- each claim's verdict and facts (§10.2 step 9);
- the channel report, its bytes and its recomputation (§11).
For every session and report it requires the outcome the corpus states; of the corpus, that every
reason of §14 and every code it borrows 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 stream_audit_verify.py <live-conformance/audit>
Exit status 0 only if every case verifies, every mutated copy fails and every weakened verifier
fails. 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_metering_verify as LMV # noqa: E402
import settlement_verify as SV # noqa: E402
import ad_presentation_verify as APV # noqa: E402
import _ed25519 as ED # noqa: E402
EXTENSION = os.path.join(HERE, "..", "extensions", "stream-audit.md")
Refuse = LEV.Refuse
SESSION_MAGIC = b"WAS1" # §5.1
SESSION_DOMAIN = b"wai:audit-session\x01" # §5.4
CHANNEL_DOMAIN = b"wai:audit-channel\x01" # §3.1
MARKER_DOMAIN = b"wai:audit-marker\x01" # §6.1
REPORT_DOMAIN = b"wai:audit-report\x01" # §11.3
ORIGIN_DOMAIN = b"wai:settle-origin\x01" # settlement §14.2
SESSION_KEY_DOMAIN = b"wai:ad-presentation-session\x01" # §3.2, ad-presentation §3
NO_STARTUP = 0xFFFFFFFF
QOE_MIN = 50
LIVE, ON_DEMAND = 1, 2
ENDED = ["ended", "user", "hidden", "idle", "error"] # §8.2
ROLES = ["origin", "delivery", "sink_decode", "display"] # §5.3
CLASS_NAMES = {1: "live", 2: "fetch", 3: "fill", 4: "ancillary"} # settlement §3.3
LABELS = {0: "unmetered", 2: "ModelBased", 3: "Estimator", 4: "OnChipCounter", 5: "CalibratedInstrument"}
AUDIBLE, AD = 1, 2
ZERO = bytes(32)
SKIP = set()
def reasons_of_extension():
"""§14's codes, read from the text."""
with open(EXTENSION, encoding="utf-8") as f:
text = f.read()
at = text.index("\n## 14. Reasons")
end = text.index("\n## ", at + 1)
return [line.split("`")[1] for line in text[at:end].splitlines() if line.startswith("| `")]
REASONS = reasons_of_extension()
REUSED = ["truncated", "malformed", "unknown_version", "bad_key", "bad_signature", "record_missing", "energy_invalid",
"qoe_invalid", "ack_invalid", "self_acknowledged", "items_invalid", "items_mismatch", "ack_outside_period",
"ack_overlap", "ack_chain_broken", "claim_malformed", "claim_duplicate", "claim_asset_mismatch"]
class Stop(Exception):
"""A refusal and its place (§10.2)."""
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 given_digest(b):
return sha(b[:max(len(b) - 64, 0)])
def channel_id(name):
"""§3.1, or None for a name that is not 1 to 255 bytes of printable ASCII."""
b = name.encode("utf-8")
if not 1 <= len(b) <= 255 or any(c < 0x21 or c > 0x7E for c in b):
return None
return sha(CHANNEL_DOMAIN, b)
def session_of(channel, key):
return sha(SESSION_KEY_DOMAIN, channel, key)[:16]
def upstream_of(origin):
return sha(ORIGIN_DOMAIN, origin)
def marker_digest(marker):
return sha(MARKER_DOMAIN, marker)
def unhex(s):
if not isinstance(s, str) or len(s) % 2 or not re.fullmatch(r"[0-9a-f]*", s):
return None
return bytes.fromhex(s)
def check_sig(key, domain, body, sig):
bad = ED.verify(key, domain + body, sig)
if bad:
raise Refuse(bad)
class R:
"""A cursor for the record and for live-metering's class reader; running out is `truncated`."""
def __init__(self, b, p=0):
self.b, self.p = b, p
def take(self, n):
if self.p + n > len(self.b):
raise Refuse("truncated")
s = self.b[self.p:self.p + n]
self.p += n
return s
def u8(self):
return self.take(1)[0]
def u16(self):
return struct.unpack("<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 be32(self):
return struct.unpack(">I", self.take(4))[0]
def be64(self):
return struct.unpack(">Q", self.take(8))[0]
# ---------------------------------------------------------------- §5 the session record
def origin_ok(o):
"""§5.5."""
if not 1 <= len(o) <= 2048 or any(c < 0x21 or c > 0x7E for c in o):
return False
for scheme in (b"https://", b"http://"):
if o.startswith(scheme):
rest = o[len(scheme):]
return rest != b"" and not any(c in b"/?#@" for c in rest)
return False
def ascending(v):
return all(x < y for x, y in zip(v, v[1:]))
def read_session(b):
"""§5.5: the record, its rules, its energy entries, then its signature."""
if len(b) < 4:
raise Refuse("truncated")
if b[:4] != SESSION_MAGIC:
raise Refuse("malformed")
if len(b) < 5:
raise Refuse("truncated")
if b[4] != 1:
raise Refuse("unknown_version")
r = R(b, 5)
s = {"channel": r.take(32), "owner": r.take(32), "player": r.take(32), "session": r.take(16), "presentation": r.u8(),
"start": r.u64(), "end": r.u64(), "ended": r.u8()}
s["origins"] = [r.take(r.u16()) for _ in range(r.u8())]
s["acks"] = [r.take(32) for _ in range(r.u16())]
s["claims"] = [r.take(32) for _ in range(r.u16())]
s["spans"] = []
for _ in range(r.u32()):
start, length, vis, flags = struct.unpack("<IIHB", r.take(11))
s["spans"].append({"start": start, "length": length, "visible": vis, "flags": flags})
s["startup_ms"], s["stalls"], s["stall_ms"] = r.u32(), r.u32(), r.u64()
s["repeat_objects"], s["repeat_bytes"] = r.u32(), r.u64()
s["log"], s["notes"] = r.take(32), r.take(32)
s["energy"] = []
for _ in range(r.u8()):
role, joules = r.u8(), r.u64()
cls = LMV.read_class(r)
s["energy"].append({"role": role, "joules": joules, "class": cls})
s["attachments"] = [r.take(32) for _ in range(r.u16())]
sig = r.take(64)
if r.p != len(b):
raise Refuse("malformed")
if "session-rules" not in SKIP and not session_rules(s):
raise Refuse("audit_session_invalid")
if "energy" not in SKIP and not all(LMV.energy_ok(e["joules"], e["class"]) for e in s["energy"]):
raise Refuse("energy_invalid")
if "session-signature" not in SKIP:
check_sig(s["player"], SESSION_DOMAIN, b[:-64], sig)
s["digest"] = sha(b[:-64])
return s
def session_rules(s):
limit = (s["end"] - s["start"]) // 1000 if s["end"] > s["start"] else 0
prev = 0
for x in s["spans"]:
if x["length"] < 1 or x["visible"] > 10000 or x["flags"] > 3 or x["start"] < prev or x["start"] + x["length"] > limit:
return False
prev = x["start"] + x["length"]
roles = [e["role"] for e in s["energy"]]
return (s["presentation"] in (LIVE, ON_DEMAND) and s["start"] < s["end"] and 1 <= s["ended"] <= 5
and s["origins"] and all(origin_ok(o) for o in s["origins"]) and ascending(s["origins"])
and s["acks"] and ascending(s["acks"]) and ascending(s["claims"]) and ascending(s["attachments"])
and (s["startup_ms"] == NO_STARTUP) == (not s["spans"])
and (s["stalls"] != 0 or s["stall_ms"] == 0)
and (s["repeat_bytes"] == 0 if s["repeat_objects"] == 0 else s["repeat_bytes"] >= s["repeat_objects"])
and s["log"] != ZERO and ascending(roles) and all(1 <= x <= 4 for x in roles))
# ---------------------------------------------------------------- §9 the bundle
def exact(v, members):
return isinstance(v, dict) and set(v) == set(members)
def read_bundle(raw):
"""§9: the bundle, or Stop(audit_bundle_invalid)."""
def bad():
return Stop("audit_bundle_invalid", ["bundle"])
try:
v = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, ValueError):
raise bad()
if not exact(v, ["v", "session", "acks", "claims", "log", "notes", "attachments"]) or v["v"] != 1 or isinstance(v["v"], bool):
raise bad()
out = {"session": unhex(v["session"]), "acks": [], "claims": [], "attachments": []}
if out["session"] is None or not isinstance(v["acks"], list) or not isinstance(v["claims"], list) or not isinstance(v["attachments"], list):
raise bad()
for a in v["acks"]:
if not exact(a, ["record", "items"]) or not isinstance(a["items"], list):
raise bad()
rec, items = unhex(a["record"]), [unhex(i) for i in a["items"]]
if rec is None or any(i is None for i in items):
raise bad()
out["acks"].append((rec, items))
for c in v["claims"]:
if not exact(c, ["claim", "marker", "creative"]) or not isinstance(c["marker"], str) or not isinstance(c["creative"], list) or not c["creative"]:
raise bad()
objects = []
for o in c["creative"]:
if not exact(o, ["content_hash", "duration_ms"]):
raise bad()
h, d = unhex(o["content_hash"]), o["duration_ms"]
if h is None or len(h) != 32 or not isinstance(d, int) or isinstance(d, bool) or not 1 <= d <= 0xFFFFFFFF:
raise bad()
objects.append({"content_hash": h, "duration_ms": d})
claim = unhex(c["claim"])
if claim is None:
raise bad()
out["claims"].append({"claim": claim, "marker": c["marker"].encode("utf-8"), "creative": objects})
if not isinstance(v["log"], str) or not (v["notes"] is None or isinstance(v["notes"], str)):
raise bad()
out["log"] = v["log"].encode("utf-8")
out["notes"] = None if v["notes"] is None else v["notes"].encode("utf-8")
for a in v["attachments"]:
if not exact(a, ["name", "record"]) or not isinstance(a["name"], str) or unhex(a["record"]) is None:
raise bad()
out["attachments"].append(unhex(a["record"]))
return out
# ---------------------------------------------------------------- §10 verification
def stopped(fn, at):
try:
return fn()
except Refuse as e:
raise Stop(e.reason, at)
def read_one_log(b):
"""§10.2 step 6: a logs document of exactly one log: its sid and its metrics."""
try:
doc = json.loads(b.decode("utf-8"))
except (UnicodeDecodeError, ValueError):
raise Refuse("qoe_invalid")
if not isinstance(doc, list) or len(doc) != 1:
raise Refuse("qoe_invalid")
LMV.read_logs(b)
return doc[0][0]["sid"], LMV.session_metrics(doc[0])
def read_notes(b, acknowledged):
"""§7.3: a notes document's count of responses, or None for one that does not read."""
try:
doc = json.loads(b.decode("utf-8"))
except (UnicodeDecodeError, ValueError):
return None
if not isinstance(doc, list):
return None
for n in doc:
if not exact(n, ["content_hash", "cmsd_static", "cmsd_dynamic"]):
return None
h = n["content_hash"]
if not isinstance(h, str) or not re.fullmatch(r"[0-9a-f]{64}", h):
return None
if "notes-acked" not in SKIP and bytes.fromhex(h) not in acknowledged:
return None
if any(not (n[k] is None or isinstance(n[k], str)) for k in ("cmsd_static", "cmsd_dynamic")):
return None
return len(doc)
def verify_bundle(raw, owner, channels):
"""§10.2: the outcome of a bundle, as §10.3 states it."""
try:
return accept_json(verify(raw, owner, channels))
except Stop as e:
return {"outcome": "refuse", "reason": e.reason, "at": e.at}
def verify(raw, owner, channels):
# 1. The bundle.
b = read_bundle(raw)
# 2. The record.
s = stopped(lambda: read_session(b["session"]), ["session"])
# 3. Its binding.
if "owner" not in SKIP and s["owner"] != owner:
raise Stop("audit_wrong_owner", ["session"])
if "channel" not in SKIP and not any(channel_id(c) == s["channel"] for c in channels):
raise Stop("audit_unknown_channel", ["session"])
if "session-bound" not in SKIP and session_of(s["channel"], s["player"]) != s["session"]:
raise Stop("audit_session_unbound", ["session"])
# 4. Each acknowledgement.
upstreams = {upstream_of(o): o for o in s["origins"]}
by_digest = {}
for k, (rec, _) in enumerate(b["acks"]):
by_digest.setdefault(given_digest(rec), k)
media = 1 if s["presentation"] == LIVE else 2
acks, delivery, acknowledged = [], {}, set()
for i, d in enumerate(s["acks"]):
if d not in by_digest:
raise Stop("record_missing", ["ack", i])
rec, lists = b["acks"][by_digest[d]]
a = stopped(lambda: SV.read_ack(rec), ["ack", i])
if "ack-bound" not in SKIP and (a["mode"] != 1 or a["downstream"] != s["player"]):
raise Stop("audit_ack_unbound", ["ack", i])
if a["upstream"] not in upstreams and "origin" not in SKIP:
raise Stop("audit_origin_unknown", ["ack", i])
origin = upstreams.get(a["upstream"], b"?")
if "ack-window" not in SKIP and (a["start"] < s["start"] or a["end"] > s["end"]):
raise Stop("ack_outside_period", ["ack", i])
if "ack-classes" not in SKIP and (a["role"] != 2 or any(e["grant"] != ZERO or e["proof"] != ZERO or e["class"] not in (4, media) for e in a["entries"])):
raise Stop("audit_ack_invalid", ["ack", i])
if len(lists) > len(a["entries"]):
raise Stop("audit_bundle_invalid", ["ack", i])
for j, e in enumerate(a["entries"]):
at = ["ack", i, "entry", j]
if j >= len(lists):
raise Stop("record_missing", at)
items = stopped(lambda: SV.read_items(lists[j]), at)
if not SV.recomputes(e, items, lists[j]):
raise Stop("items_mismatch", at)
if "items-start" not in SKIP and any(st != 0 for _, st, _ in items):
raise Stop("audit_ack_invalid", at)
key = (origin, e["class"])
o, n = delivery.get(key, (0, 0))
delivery[key] = (o + e["objects"], n + e["bytes"])
acknowledged.update(h for h, _, _ in items)
acks.append((i, a))
named, once = set(s["acks"]), set()
for k, (rec, _) in enumerate(b["acks"]):
d = given_digest(rec)
if "bundle-extra-ack" not in SKIP and (d not in named or d in once):
raise Stop("audit_bundle_invalid", ["ack", k])
once.add(d)
# 5. The chains.
chains = {}
for i, a in acks:
chains.setdefault(a["upstream"], []).append((i, a))
breaks = []
for chain in chains.values():
chain.sort(key=lambda x: (x[1]["start"], x[0]))
for n, (i, a) in enumerate(chain):
if n == 0:
if "chain-first" not in SKIP and a["prev"] != ZERO:
breaks.append((i, "ack_chain_broken"))
continue
p = chain[n - 1][1]
if "chain-overlap" not in SKIP and p["end"] > a["start"]:
breaks.append((i, "ack_overlap"))
elif "chain-prev" not in SKIP and a["prev"] != p["digest"]:
breaks.append((i, "ack_chain_broken"))
if breaks:
i, r = min(breaks)
raise Stop(r, ["ack", i])
# 6. The log.
if "log-digest" not in SKIP and sha(b["log"]) != s["log"]:
raise Stop("audit_document_mismatch", ["log"])
sid, m = stopped(lambda: read_one_log(b["log"]), ["log"])
startup = None if s["startup_ms"] == NO_STARTUP else s["startup_ms"]
if "qoe" not in SKIP and (sid != s["session"].hex() or m["vst"] != startup or m["stalls"] != s["stalls"] or m["stall_ms"] != s["stall_ms"]):
raise Stop("audit_qoe_mismatch", ["log"])
# 7. The notes.
if s["notes"] == ZERO and b["notes"] is None:
notes = 0
elif s["notes"] != ZERO and b["notes"] is not None and (sha(b["notes"]) == s["notes"] or "notes-digest" in SKIP):
notes = read_notes(b["notes"], acknowledged)
if notes is None:
raise Stop("audit_notes_invalid", ["notes"])
else:
raise Stop("audit_document_mismatch", ["notes"])
# 8. The attachments.
given = [sha(x) for x in b["attachments"]]
for i, d in enumerate(s["attachments"]):
if "attach" not in SKIP and d not in given:
raise Stop("record_missing", ["attachment", i])
for j, d in enumerate(given):
if d not in s["attachments"]:
raise Stop("audit_document_mismatch", ["attachment", j])
# 9. The claims.
by_claim = {}
for k, c in enumerate(b["claims"]):
by_claim.setdefault(given_digest(c["claim"]), k)
order = []
for i, d in enumerate(s["claims"]):
if d not in by_claim:
raise Stop("record_missing", ["claim", i])
order.append(by_claim[d])
named, once = set(s["claims"]), set()
for k, c in enumerate(b["claims"]):
d = given_digest(c["claim"])
if "bundle-extra" not in SKIP and (d not in named or d in once):
raise Stop("audit_bundle_invalid", ["claim", k])
once.add(d)
seen = set()
verdicts = [judge(b["claims"][k], s, acknowledged, seen) for k in order]
return {"session": s, "delivery": delivery, "claims": verdicts, "notes": notes}
def judge(c, s, acknowledged, seen):
"""§10.2 step 9: one claim's verdict and facts."""
try:
x = APV.read_claim(c["claim"])
except APV.Bad as e:
return {"verdict": str(e)}
bad = ED.verify(x["player"], APV.CLAIM_DOMAIN + x["body"], x["sig"])
if bad:
return {"verdict": bad}
if "claim-bound" not in SKIP and (x["room"] != s["channel"] or x["session"] != s["session"] or x["player"] != s["player"]):
return {"verdict": "audit_claim_unbound"}
if "marker" not in SKIP and x["cue"] != marker_digest(c["marker"]):
return {"verdict": "audit_marker_mismatch"}
if "duplicate" not in SKIP and (x["cue"], x["slot"]) in seen:
return {"verdict": "claim_duplicate"}
seen.add((x["cue"], x["slot"]))
d = sum(o["duration_ms"] for o in c["creative"])
if "asset" not in SKIP and (x["creative_hash"] != APV.creative_digest(c["creative"]) or any(sp["start"] + sp["length"] > d for sp in x["spans"])
or x["objects"] != APV.touched(c["creative"], x["spans"])):
return {"verdict": "claim_asset_mismatch"}
if "acked" not in SKIP and any(h not in acknowledged for h in x["objects"]):
return {"verdict": "audit_ad_not_acknowledged"}
run, viewable = APV.runs(x, {"rule": APV.VIDEO, "require": APV.CLAIM})
return {"verdict": "presented", "quartile": APV.quartile(x["spans"], d), "run_ms": run, "viewable_ms": viewable, "viewable": run >= 2000}
def presented(s):
f = [0, 0, 0, 0]
for x in s["spans"]:
f[0] += x["length"]
f[1] += x["length"] if x["visible"] >= 5000 else 0
f[2] += x["length"] if x["flags"] & AUDIBLE else 0
f[3] += x["length"] if x["flags"] & AD else 0
return f
def tag(cls):
return 0 if cls is None else cls[0]
def accept_json(v):
s = v["session"]
p = presented(s)
return {
"outcome": "accept", "digest": s["digest"].hex(), "channel": s["channel"].hex(),
"presentation": "live" if s["presentation"] == LIVE else "on_demand", "start": s["start"], "end": s["end"],
"ended": ENDED[s["ended"] - 1], "presented_ms": p[0], "viewable_ms": p[1], "audible_ms": p[2], "ad_ms": p[3],
"delivery": [{"origin": o.decode(), "class": CLASS_NAMES[c], "objects": n, "bytes": by} for (o, c), (n, by) in sorted(v["delivery"].items())],
"repeats": {"objects": s["repeat_objects"], "bytes": s["repeat_bytes"]},
"startup_ms": None if s["startup_ms"] == NO_STARTUP else s["startup_ms"], "stalls": s["stalls"], "stall_ms": s["stall_ms"],
"energy": [{"role": ROLES[e["role"] - 1], "class": LABELS[tag(e["class"])], "joules_micro": e["joules"]} for e in s["energy"]],
"claims": v["claims"], "notes": v["notes"], "attachments": len(s["attachments"]),
}
# ---------------------------------------------------------------- §11 the report
def report(name, owner, frm, to, accepted):
"""§11.1, §11.2: the report over the accepted sessions of the channel, the owner and the period."""
channel = channel_id(name)
sessions = {}
for a in accepted:
s = a["session"]
if s["channel"] == channel and s["owner"] == owner and frm <= s["start"] < to:
sessions[s["digest"]] = a
sessions = [sessions[d] for d in sorted(sessions)]
if "report-dup" not in SKIP and len({a["session"]["session"] for a in sessions}) != len(sessions):
raise Stop("audit_session_duplicate", ["report"])
pres, ends = [0, 0, 0, 0], [0] * 5
claims = shown = viewable = 0
quartiles, refused = [0] * 5, {}
with_span = startup = stalls = stall_ms = 0
delivery, rep, energy = {}, [0, 0], {}
unmetered, absent = [0] * 4, [0] * 4
for a in sessions:
s = a["session"]
pres = [x + y for x, y in zip(pres, presented(s))]
ends[s["ended"] - 1] += 1
for c in a["claims"]:
claims += 1
if c["verdict"] == "presented":
shown += 1
if 1 <= c["quartile"] <= 5:
quartiles[c["quartile"] - 1] += 1
viewable += 1 if c["viewable"] else 0
else:
refused[c["verdict"]] = refused.get(c["verdict"], 0) + 1
if s["spans"]:
with_span += 1
startup += s["startup_ms"]
stalls += s["stalls"]
stall_ms += s["stall_ms"]
for k, (n, by) in a["delivery"].items():
o0, b0 = delivery.get(k, (0, 0))
delivery[k] = (o0 + n, b0 + by)
rep[0] += s["repeat_objects"]
rep[1] += s["repeat_bytes"]
for r in range(1, 5):
es = [e for e in s["energy"] if e["role"] == r]
if not es:
absent[r - 1] += 1
elif es[0]["class"] is None and "unmetered-apart" not in SKIP:
unmetered[r - 1] += 1
else:
k = (r, tag(es[0]["class"]))
f, j = energy.get(k, (0, 0))
energy[k] = (f + 1, j + es[0]["joules"])
n = len(sessions)
sup = n < QOE_MIN and "suppress" not in SKIP
def q(v):
return None if sup else v
return {
"v": 1, "channel": channel.hex(), "owner": owner.hex(), "from": frm, "to": to,
"sessions": [a["session"]["digest"].hex() for a in sessions],
"presented": {"presented_ms": pres[0], "viewable_ms": pres[1], "audible_ms": pres[2], "ad_ms": pres[3]},
"ended": dict(zip(ENDED, ends)),
"ads": {"claims": claims, "presented": shown, "quartiles": quartiles, "viewable": viewable, "refused": refused},
"rebuffering": {"sessions": n, "suppressed": sup, "presented": q(with_span), "startup_ms": q(startup), "stalls": q(stalls), "stall_ms": q(stall_ms)},
"delivery": [{"origin": o.decode(), "class": CLASS_NAMES[c], "objects": k, "bytes": by} for (o, c), (k, by) in sorted(delivery.items())],
"repeats": {"objects": rep[0], "bytes": rep[1]},
"energy": [{"role": ROLES[r - 1], "class": LABELS[t], "figures": f, "joules_micro": j} for (r, t), (f, j) in sorted(energy.items())],
"unmetered": dict(zip(ROLES, unmetered)), "absent": dict(zip(ROLES, absent)),
}
def report_bytes(v):
"""§11.3: members in bytewise order, no white space, no escape but \\" and \\\\ (the report is ASCII)."""
return json.dumps(v, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("ascii")
def check_report(served, bundles, name, owner, channels):
"""§11.4."""
accepted = []
for k, raw in enumerate(bundles):
try:
accepted.append(verify(raw, owner, channels))
except Stop as e:
raise Stop(e.reason, ["bundle", k] + e.at)
if not exact(served, ["report", "sig"]):
raise Stop("audit_report_mismatch", ["report"])
r, sig = served["report"], unhex(served["sig"])
frm, to, names = r.get("from") if isinstance(r, dict) else None, r.get("to") if isinstance(r, dict) else None, r.get("sessions") if isinstance(r, dict) else None
if sig is None or len(sig) != 64 or not isinstance(frm, int) or not isinstance(to, int) or not isinstance(names, list):
raise Stop("audit_report_mismatch", ["report"])
names = [unhex(x) for x in names]
if any(x is None or len(x) != 32 for x in names):
raise Stop("audit_report_mismatch", ["report"])
recomputed = report(name, owner, frm, to, accepted)
digests = {a["session"]["digest"] for a in accepted}
for i, d in enumerate(names):
if "report-missing" not in SKIP and d not in digests:
raise Stop("record_missing", ["report", "session", i])
if "report-bytes" not in SKIP and report_bytes(r) != report_bytes(recomputed):
raise Stop("audit_report_mismatch", ["report"])
if "report-signature" not in SKIP and ED.verify(owner, REPORT_DOMAIN + report_bytes(r), sig):
raise Stop("bad_signature", ["report"])
# ---------------------------------------------------------------- the corpus
def bundle_bytes(v):
if "bundle_text" in v:
return v["bundle_text"].encode("utf-8")
return json.dumps(v["bundle"]).encode("utf-8")
def report_outcome(doc, v, owner, channels):
bundles = []
for n in v["sessions"]:
for s in doc["sessions"]:
if s["name"] == n:
bundles.append(bundle_bytes(s))
break
try:
check_report(v["served"], bundles, v["channel"], owner, channels)
return {"outcome": "accept", "digest": sha(report_bytes(v["served"]["report"])).hex()}
except Stop as e:
return {"outcome": "refuse", "reason": e.reason, "at": e.at}
def check(doc, quiet=False):
"""Every vector's outcome: the failures."""
fails = []
owner, channels = bytes.fromhex(doc["owner"]), doc["channels"]
for v in doc["sessions"]:
got = verify_bundle(bundle_bytes(v), owner, channels)
if got != v["outcome"]:
fails.append("session %s: stated %s, got %s" % (v["name"], json.dumps(v["outcome"])[:300], json.dumps(got)[:300]))
for v in doc["reports"]:
got = report_outcome(doc, v, owner, channels)
if got != v["outcome"]:
fails.append("report %s: stated %s, got %s" % (v["name"], v["outcome"], got))
if not quiet:
for f in fails:
print("FAIL " + f)
return fails
def reasons_given(doc):
seen = set()
for section in ("sessions", "reports"):
for v in doc[section]:
if v["outcome"].get("reason"):
seen.add(v["outcome"]["reason"])
for c in v["outcome"].get("claims", []):
seen.add(c["verdict"])
return seen
def flip_hex(h, at):
b = bytearray(bytes.fromhex(h))
b[at % len(b)] ^= 1
return b.hex()
def mutations(doc):
"""Copies of the corpus, each with one defect a conforming verifier notices."""
def find(d, section, name):
return next(v for v in d[section] if v["name"] == name)
def m_sig(d):
v = find(d, "sessions", "live_two_origins")
v["bundle"]["session"] = flip_hex(v["bundle"]["session"], -1)
def m_figure(d):
find(d, "sessions", "live_two_origins")["outcome"]["presented_ms"] += 1
def m_verdict(d):
find(d, "sessions", "claim_verdicts")["outcome"]["claims"][0]["quartile"] = 5
def m_drop_ack(d):
find(d, "sessions", "on_demand_hidden")["bundle"]["acks"].pop()
def m_item(d):
v = find(d, "sessions", "live_two_origins")
v["bundle"]["acks"][0]["items"][0] = flip_hex(v["bundle"]["acks"][0]["items"][0], 5)
def m_log(d):
find(d, "sessions", "nothing_presented")["bundle"]["log"] += " "
def m_marker(d):
find(d, "sessions", "live_two_origins")["bundle"]["claims"][0]["marker"] += "x"
def m_reason(d):
find(d, "sessions", "ack_chain_broken")["outcome"]["reason"] = "ack_overlap"
def m_place(d):
find(d, "sessions", "items_not_whole")["outcome"]["at"] = ["ack", 0]
def m_channels(d):
d["channels"] = [c for c in d["channels"] if c != "film-library"]
def m_owner(d):
d["owner"] = flip_hex(d["owner"], 3)
def m_report(d):
find(d, "reports", "news_east")["served"]["report"]["ads"]["presented"] += 1
def m_report_sessions(d):
find(d, "reports", "fifty")["sessions"].pop()
def m_notes(d):
v = find(d, "sessions", "live_two_origins")
v["bundle"]["notes"] = v["bundle"]["notes"].replace("rtt=40", "rtt=41")
def m_creative(d):
find(d, "sessions", "live_two_origins")["bundle"]["claims"][1]["creative"][0]["duration_ms"] += 1
return [m_sig, m_figure, m_verdict, m_drop_ack, m_item, m_log, m_marker, m_reason, m_place, m_channels, m_owner,
m_report, m_report_sessions, m_notes, m_creative]
CHECKS = ["session-rules", "energy", "session-signature", "owner", "channel", "session-bound", "ack-bound", "origin",
"ack-window", "ack-classes", "items-start", "chain-first", "chain-overlap", "chain-prev", "log-digest", "qoe",
"notes-digest", "notes-acked", "attach", "bundle-extra", "bundle-extra-ack", "claim-bound", "marker", "duplicate", "asset", "acked",
"report-dup", "report-missing", "report-bytes", "report-signature", "suppress", "unmetered-apart"]
def main(corpus_dir):
with open(os.path.join(corpus_dir, "vectors.json"), encoding="utf-8") as f:
doc = json.load(f)
ok = True
files = sorted(os.listdir(corpus_dir))
if files != ["README.md", "vectors.json"]:
print("FAIL the corpus directory holds %s" % files)
ok = False
fails = check(doc)
print("%s %d sessions and %d reports get the outcomes the corpus states" % ("PASS" if not fails else "FAIL", len(doc["sessions"]), len(doc["reports"])))
ok = ok and not fails
seen = reasons_given(doc)
missing = [r for r in REASONS + REUSED if r not in seen]
print("%s every reason of §14 and every borrowed code appears%s" % ("PASS" if not missing else "FAIL", "" if not missing else ": missing " + ", ".join(missing)))
ok = ok and not missing
survived = []
for m in mutations(doc):
d = copy.deepcopy(doc)
m(d)
if not check(d, quiet=True):
survived.append(m.__name__)
n = len(mutations(doc))
print("%s %d of %d mutated copies of the corpus fail%s" % ("PASS" if not survived else "FAIL", n - len(survived), n, "" if not survived else ": " + ", ".join(survived)))
ok = ok and not survived
uncaught = []
for c in CHECKS:
SKIP.add(c)
try:
if not check(doc, quiet=True):
uncaught.append(c)
finally:
SKIP.discard(c)
print("%s the corpus catches each of %d checks dropped%s" % ("PASS" if not uncaught else "FAIL", len(CHECKS), "" if not uncaught else ": not caught " + ", ".join(uncaught)))
ok = ok and not uncaught
return 0 if ok else 1
if __name__ == "__main__":
if len(sys.argv) != 2:
print("usage: stream_audit_verify.py <live-conformance/audit>")
sys.exit(2)
sys.exit(main(sys.argv[1]))