open-standards/wai/live-conformance/stitched_presentation_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

1081 lines
44 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Independent verifier of the stitched-presentation corpus (extensions/stitched-presentation.md).
Written from the extension's text. It shares no code with wai-rs, which generates the corpus,
and never runs it. Ed25519 signatures are checked with ../claim-conformance/_ed25519.py, WAI's
strict rule (SPEC §7 *Signatures*), and BLAKE3 with ../claim-conformance/_blake3.py. From the
corpora's other second implementations it takes, by reference as the extension does: the spans'
quartile and run and RSASSA-PSS from ad_presentation_verify.py (ad-presentation §6, §7); a
form-1 receipt's leaves, Merkle root and class encoding from live_ads_verify.py (live-ads §8.3,
energy-measurement §2.7), and the consent signal's rules from it (live-ads §8.6, §4.7 here);
and an ad frame from live_compose_verify.py (live-compose §5.9).
Everything else is here, from the standard library:
- markers, their bodies and digests (§3.1), and reading them from a Media Playlist's date range
tags, an asset list and an MPD over the XML subset of §3.5, with a reader of its own;
- the pod-input record, its rules and signature (§4), a record of version 2's consent signal
in its layout (§4.7), the VAST check and a form-1 receipt's JSON carriage, payload, identity
and rules (§4.5, §7.2);
- the claim, its session, its rules and signature, and its attestation challenge (§5);
- each record's and claim's verdict, each ad's presentations and quartiles, and the spend by
currency (§7).
For every vector it requires every fact the corpus states; of the corpus, that every reason of
§9 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 -I stitched_presentation_verify.py <live-conformance/stitched>
Exit status 0 only if every vector verifies, every mutated copy fails and every weakened
verifier is caught. Python 3.8 or later; standard library only.
"""
import copy
import hashlib
import json
import os
import 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 _blake3 as B3 # noqa: E402
import _ed25519 as ED # noqa: E402
import ad_presentation_verify as APV # noqa: E402
import live_ads_verify as LAV # noqa: E402
import live_compose_verify as LCV # noqa: E402
EXTENSION = os.path.join(HERE, "..", "extensions", "stitched-presentation.md")
POD_MAGIC, CLAIM_MAGIC = b"WSI1", b"WSC1"
POD_DOMAIN = b"wai:stitched-pod-input\x01" # §4.4
CLAIM_DOMAIN = b"wai:stitched-presentation-claim\x01" # §5.3
SESSION_DOMAIN = b"wai:stitched-presentation-session\x01" # §5.2
ATTEST_DOMAIN = b"wai:stitched-presentation-attest\x01" # §5.5
MARKER_DOMAIN = b"wai:stitched-marker\x01" # §3.1
FORM1_DOMAIN = b"wai:video-ssai-receipt\x01" # §4.5
FORM1_ID_DOMAIN = b"wai:video-ssai-receipt-id\x01"
INTERSTITIAL = "com.apple.hls.interstitial" # allow-name: the interstitial date-range class, a protocol identifier
DATERANGE, DATERANGE_I, EVENT = 1, 2, 3 # §3.1
VAST, VMAP, FORM1 = 1, 2, 3 # §4.1
SERVING_ID, AD_ID, FORM1_ID = 1, 2, 3
PRICING, WINNING_BID, STATED = 1, 2, 3 # §4.2
CLAIM, ATTEST = 1, 2 # ad-presentation §8
MAX_PRICE = 10 ** 12
U64 = 2 ** 64 - 1
# The checks a weakened verifier may drop (main(), the last part).
SKIP = set()
def sha(*parts):
h = hashlib.sha256()
for p in parts:
h.update(p)
return h.digest()
def reasons():
"""§9's table of the codes this extension adds, from the text."""
with open(EXTENSION, encoding="utf-8") as f:
text = f.read()
at = text.index("\n## 9. Reasons")
end = text.index("\nA verification also", at)
return [line.split("`")[1] for line in text[at:end].splitlines() if line.startswith("| `")]
REASONS = reasons()
OTHER = ["bad_signature", "claim_key_unbound", "claim_no_decision", "claim_duplicate", "claim_asset_mismatch",
"claim_not_viewable", "attestation_absent", "attestation_invalid", "claim_absent"]
class Bad(Exception):
"""A verdict other than accepted or presented."""
# ---------------------------------------------------------------- §3.1 markers
def lng(b):
return struct.pack("<H", len(b)) + b
def marker_body(m):
if m["carriage"] in (DATERANGE, DATERANGE_I):
out = lng(m["id"]) + lng(m["start"])
if m["carriage"] == DATERANGE_I:
out += struct.pack("<H", m["index"])
return out
return lng(m["period"]) + lng(m["scheme"]) + lng(m["value"]) + lng(m["id"])
def marker_digest(carriage, body):
return sha(MARKER_DOMAIN, bytes([carriage]), body)
def read_body(carriage, b):
"""§3.1: the marker a body of a carriage holds, or None."""
p = 0
def field():
nonlocal p
if p + 2 > len(b):
raise ValueError
n = struct.unpack("<H", b[p:p + 2])[0]
if p + 2 + n > len(b):
raise ValueError
p += 2 + n
return b[p - n:p]
try:
if carriage == DATERANGE:
m = {"carriage": 1, "id": field(), "start": field()}
elif carriage == DATERANGE_I:
m = {"carriage": 2, "id": field(), "start": field()}
if p + 2 > len(b):
return None
m["index"] = struct.unpack("<H", b[p:p + 2])[0]
p += 2
elif carriage == EVENT:
m = {"carriage": 3, "period": field(), "scheme": field(), "value": field(), "id": field()}
else:
return None
except ValueError:
return None
return m if p == len(b) else None
# ---------------------------------------------------------------- §3.2, §3.3 HLS
def attribute_list(s):
"""§3.2: [(name, value, quoted)], or None."""
out, rest = [], s
while True:
if "=" not in rest:
return None
name, after = rest.split("=", 1)
if not name or any(not (c.isascii() and (c.isupper() or c.isdigit() or c == "-")) for c in name):
return None
if any(n == name for n, _, _ in out):
return None
if after.startswith('"'):
close = after.find('"', 1)
if close < 0:
return None
v = after[1:close]
if "\r" in v or "\n" in v:
return None
out.append((name, v, True))
after = after[close + 1:]
else:
end = after.find(",")
end = len(after) if end < 0 else end
v = after[:end]
if not v or any(c == '"' or c in " \t\r\n\x0b\x0c" for c in v):
return None
out.append((name, v, False))
after = after[end:]
if not after:
return out
if not after.startswith(",") or after == ",":
return None
rest = after[1:]
def daterange_tags(b):
"""§3.2: each date range tag that reads, as {name: (value, quoted)}; None when not a playlist."""
try:
s = b.decode("utf-8")
except UnicodeDecodeError:
return None
lines = [ln[:-1] if ln.endswith("\r") else ln for ln in s.split("\n")]
if lines[0] != "#EXTM3U":
return None
out = []
for ln in lines:
if not ln.startswith("#EXT-X-DATERANGE:"):
continue
a = attribute_list(ln[len("#EXT-X-DATERANGE:"):])
if a is None:
continue
d = {n: (v, q) for n, v, q in a}
if d.get("ID", ("", False))[1] and d.get("START-DATE", ("", False))[1]:
out.append(d)
return out
def hls_ads(b, attr):
tags = daterange_tags(b)
if tags is None:
return None
return [({"carriage": 1, "id": d["ID"][0].encode(), "start": d["START-DATE"][0].encode()}, d[attr][0].encode())
for d in tags if attr in d]
def interstitials(b):
tags = daterange_tags(b)
if tags is None:
return None
return [(d["ID"][0].encode(), d["START-DATE"][0].encode()) for d in tags if d.get("CLASS") == (INTERSTITIAL, True)]
def asset_uris(b):
try:
v = json.loads(b.decode("utf-8"))
except (UnicodeDecodeError, ValueError):
return None
if not isinstance(v, dict) or not isinstance(v.get("ASSETS"), list):
return None
out = []
for a in v["ASSETS"]:
if not isinstance(a, dict) or not isinstance(a.get("URI"), str):
return None
out.append(a["URI"])
return out
# ---------------------------------------------------------------- §3.5 the XML subset
WS = " \t\r\n"
def unescape(t):
out, i = [], 0
while True:
j = t.find("&", i)
if j < 0:
out.append(t[i:])
return "".join(out)
out.append(t[i:j])
k = t.find(";", j)
if k < 0:
raise ValueError("reference")
r = t[j + 1:k]
named = {"lt": "<", "gt": ">", "amp": "&", "quot": '"', "apos": "'"}
if r in named:
out.append(named[r])
elif r.startswith("#x") and len(r) > 2:
out.append(chr(int(r[2:], 16)))
elif r.startswith("#") and len(r) > 1 and r[1:].isdigit():
out.append(chr(int(r[1:])))
else:
raise ValueError("reference")
i = k + 1
def local(n):
return n.rsplit(":", 1)[-1]
class XmlReader:
def __init__(self, s):
self.s, self.p = s, 0
def rest(self):
return self.s[self.p:]
def ws(self):
while self.p < len(self.s) and self.s[self.p] in WS:
self.p += 1
def eat(self, t):
if self.s.startswith(t, self.p):
self.p += len(t)
return True
return False
def until(self, t):
j = self.s.find(t, self.p)
if j < 0:
raise ValueError("unterminated")
out = self.s[self.p:j]
self.p = j + len(t)
return out
def misc(self):
while True:
self.ws()
if self.eat("<!--"):
self.until("-->")
elif self.s.startswith("<!", self.p):
raise ValueError("outside the subset")
elif self.eat("<?"):
self.until("?>")
else:
return
def name(self):
j = self.p
while j < len(self.s) and self.s[j] not in WS + "/>=":
j += 1
n = self.s[self.p:j]
if not n or not (n[0].isalpha() or n[0] in "_:") or any(not (c.isalnum() or c in "_-.:") for c in n):
raise ValueError("name")
self.p = j
return n
def element(self, depth):
if depth > 64 or not self.eat("<"):
raise ValueError("element")
q = self.name()
attrs = {}
while True:
before = self.p
self.ws()
if self.eat("/>"):
return {"name": local(q), "attrs": attrs, "children": []}
if self.eat(">"):
break
if self.p == before:
raise ValueError("attribute")
an = self.name()
self.ws()
if not self.eat("="):
raise ValueError("attribute")
self.ws()
if self.eat('"'):
raw = self.until('"')
elif self.eat("'"):
raw = self.until("'")
else:
raise ValueError("attribute")
if "<" in raw or local(an) in attrs:
raise ValueError("attribute")
attrs[local(an)] = unescape(raw)
kids = []
while True:
if self.eat("</"):
c = self.name()
self.ws()
if c != q or not self.eat(">"):
raise ValueError("end tag")
return {"name": local(q), "attrs": attrs, "children": kids}
if self.eat("<![CDATA["):
kids.append(self.until("]]>"))
elif self.eat("<!--"):
self.until("-->")
elif self.s.startswith("<!", self.p):
raise ValueError("outside the subset")
elif self.eat("<?"):
self.until("?>")
elif self.s.startswith("<", self.p):
kids.append(self.element(depth + 1))
else:
j = self.s.find("<", self.p)
if j < 0:
raise ValueError("unterminated")
kids.append(unescape(self.s[self.p:j]))
self.p = j
def read_xml(b):
"""§3.5: the root element, or None."""
try:
s = b.decode("utf-8")
if s.startswith(""):
return None
x = XmlReader(s)
x.misc()
root = x.element(1)
x.misc()
return root if x.p == len(s) else None
except (UnicodeDecodeError, ValueError, OverflowError):
return None
def kids(e, name):
return [c for c in e["children"] if isinstance(c, dict) and c["name"] == name]
def text(e):
return "".join(c for c in e["children"] if isinstance(c, str)).strip(WS)
def mpd_ads(b, attr):
root = read_xml(b)
if root is None or root["name"] != "MPD":
return None
out = []
for period in kids(root, "Period"):
for es in kids(period, "EventStream"):
if "schemeIdUri" not in es["attrs"]:
continue
for ev in kids(es, "Event"):
if "id" in ev["attrs"] and attr in ev["attrs"]:
m = {"carriage": 3, "period": period["attrs"].get("id", "").encode(),
"scheme": es["attrs"]["schemeIdUri"].encode(), "value": es["attrs"].get("value", "").encode(),
"id": ev["attrs"]["id"].encode()}
out.append((m, ev["attrs"][attr].encode()))
return out
# ---------------------------------------------------------------- §4 the record
def is_text(b):
if not 1 <= len(b) <= 255:
return False
try:
b.decode("utf-8")
return True
except UnicodeDecodeError:
return False
class Cursor:
def __init__(self, b, why):
self.b, self.p, self.why = b, 0, why
def take(self, n):
if self.p + n > len(self.b):
raise Bad(self.why)
self.p += n
return self.b[self.p - n:self.p]
def u(self, fmt):
return struct.unpack("<" + fmt, self.take(struct.calcsize("<" + fmt)))[0]
def short(self):
return self.take(self.u("B"))
def long(self):
return self.take(self.u("H"))
def frame_ok(f):
"""§4.3."""
if f["kind"] == 0:
return all(f[k] == 0 for k in ("width", "height", "corner", "window_w", "window_h", "gap", "aspect"))
if f["kind"] not in (3, 4):
return False
w, h = f["width"], f["height"]
if not (16 <= w <= 1920 and 16 <= h <= 1920 and w % 2 == 0 and h % 2 == 0 and w * h <= 2073600):
return False
if f["kind"] == 3 and (f["gap"] or f["aspect"]):
return False
if f["kind"] == 4 and f["aspect"] > 3:
return False
return LCV.frame_fields_ok(w, h, f["kind"], f["corner"], f["window_w"], f["window_h"], f["gap"])
def frame_region(f):
"""§6: the ad region's rectangles, live-compose §5.9's."""
return LCV.ad_frame(f["width"], f["height"], f["kind"], f["corner"], f["window_w"], f["window_h"], f["gap"],
f["aspect"])[2]
def share_of(f):
"""§6: the region's share of the picture."""
if f["kind"] == 0:
return 10000
_, _, region = LCV.ad_frame(f["width"], f["height"], f["kind"], f["corner"], f["window_w"], f["window_h"], f["gap"],
f["aspect"])
return LCV.share_bp(region, f["width"], f["height"])
def terms_ok(q):
"""ad-presentation §8, and bit 0 set (§4.4 rule 7)."""
r, rule, tt, iss = q["require"], q["rule"], q["token_type"], q["issuer"]
claim = bool(r & CLAIM)
if r > 7 or (not claim and r) or rule > 2 or claim != (rule != 0):
return False
if r & ATTEST:
if tt not in (1, 2) or not iss:
return False
elif tt != 0 or iss:
return False
return claim
def xml_name(b):
return 1 <= len(b) <= 255 and (chr(b[0]).isalpha() and b[0] < 128 or b[0] == 0x5F) and \
all(chr(c).isalnum() and c < 128 or c in b"_-." for c in b)
def read_record(b):
"""§4.4: the record, or Bad(pod_input_malformed)."""
why = "pod_input_malformed"
c = Cursor(b, why)
if c.take(4) != POD_MAGIC:
raise Bad(why)
version = c.u("B")
if version not in (1, 2):
raise Bad(why)
r = {"carriage": c.u("B"), "session": c.take(16), "avail_id": c.short(), "avail_ms": c.u("I"),
"format": c.u("B"), "response": c.take(32), "id_kind": c.u("B"), "id_attr": c.short(),
"currency": c.take(3), "exponent": c.u("B")}
r["frame"] = {"kind": c.u("B"), "width": c.u("H"), "height": c.u("H"), "corner": c.u("B"), "window_w": c.u("H"),
"window_h": c.u("H"), "gap": c.u("H"), "aspect": c.u("B")}
r["terms"] = {"require": c.u("B"), "rule": c.u("B"), "token_type": c.u("H"), "issuer": c.long()}
r["ads"] = []
for _ in range(c.u("B")):
r["ads"].append({"slot": c.u("B"), "marker": c.take(32), "ad_id": c.short(), "serving_id": c.short(),
"creative_id": c.short(), "registry": c.short(), "creative_hash": c.take(32),
"duration_ms": c.u("I"), "price_micros": c.u("Q"), "price_source": c.u("B"),
"evidence": c.take(32)})
r["consent"] = None
if version == 2: # §4.7
gpp = c.long()
k = c.u("B")
sid = [c.u("H") for _ in range(k)]
try:
r["consent"] = {"gpp": gpp.decode("utf-8"), "gpp_sid": sid, "child_directed": c.u("B")}
except UnicodeDecodeError:
raise Bad(why)
r["signer"] = c.take(32)
r["body"] = b[:c.p]
r["sig"] = c.take(64)
if c.p != len(b):
raise Bad(why)
if "record_rules" in SKIP:
return r
ok = r["carriage"] in (1, 2, 3) and is_text(r["avail_id"]) and r["avail_ms"] >= 1
ok = ok and ((r["format"] in (VAST, VMAP) and r["id_kind"] in (SERVING_ID, AD_ID))
or (r["format"] == FORM1 and r["id_kind"] == FORM1_ID))
a = r["id_attr"]
if r["carriage"] == DATERANGE:
ok = ok and 1 <= len(a) <= 255 and all(ch in b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-" for ch in a)
elif r["carriage"] == EVENT:
ok = ok and xml_name(a)
else:
ok = ok and a == b""
ok = ok and all(65 <= ch <= 90 for ch in r["currency"]) and r["exponent"] <= 4
ok = ok and frame_ok(r["frame"]) and terms_ok(r["terms"]) and 1 <= len(r["ads"]) <= 16
for k, ad in enumerate(r["ads"]):
src = ad["price_source"]
ok = ok and ad["slot"] == k and is_text(ad["ad_id"]) and is_text(ad["serving_id"])
ok = ok and (ad["creative_id"] == b"") == (ad["registry"] == b"")
ok = ok and (ad["creative_id"] == b"" or (is_text(ad["creative_id"]) and is_text(ad["registry"])))
ok = ok and ad["duration_ms"] >= 1 and ad["price_micros"] <= MAX_PRICE and src in (1, 2, 3)
ok = ok and not (src == PRICING and r["format"] != VAST) and not (src == WINNING_BID and r["format"] == FORM1)
ok = ok and not (r["format"] == FORM1 and src != STATED)
ok = ok and (ad["evidence"] != bytes(32)) == (src == WINNING_BID)
ok = ok and all(x["marker"] != ad["marker"] for x in r["ads"][:k])
cs = r["consent"] # rule 10
if cs is not None and "consent" not in SKIP:
ok = ok and LAV.consent_ok(cs) and not (r["session"] == bytes(16) and cs["gpp"])
if not ok:
raise Bad(why)
return r
# ---------------------------------------------------------------- §4.5 a form-1 receipt
FORM1_MEMBERS = {"form", "avail_id", "manifest_hash", "pod_hash", "impressions", "merkle_root", "budget_spent_micros",
"work_decide_ops", "joules_micro", "energy_provenance", "grant", "parent_receipt_hash",
"signer_pubkey", "signer_id", "sig"}
def read_form1(o):
"""§4.5's JSON carriage, or None. Its integers, hexadecimal and class read as live-ads §8.2's."""
try:
if not isinstance(o, dict):
return None
label = o.get("energy_provenance")
if label is not None and label not in LAV.EVIDENCE:
return None
if set(o) != FORM1_MEMBERS | set(LAV.EVIDENCE.get(label, [])) or LAV.u(o["form"]) != 1:
return None
# A form-2 shaped copy holds the shared members, so live-ads' reader reads its class,
# grant and impressions; the form-1 members are read here.
shaped = {k: o[k] for k in o if k not in ("form", "avail_id", "manifest_hash", "parent_receipt_hash")}
shaped.update({"form": 2, "room": "00" * 32, "cue": "00" * 32, "session": "00" * 16, "start_offset_ms": 0,
"format": 1, "asset_list_sha256": "00" * 32, "avail_ms": 0, "slots": 0,
"funds_remaining_micros": 0})
r = LAV.read_receipt(shaped)
r["avail_id"] = LAV.text(o["avail_id"])
r["manifest_hash"] = LAV.hexn(o["manifest_hash"], 32)
p = o["parent_receipt_hash"]
r["parent"] = None if p is None else LAV.hexn(p, 32)
return r
except LAV.Refuse:
return None
def form1_payload(r):
g = r["grant"]
a, cap, sid = r["avail_id"].encode(), g["capability"].encode(), r["signer_id"].encode()
out = [FORM1_DOMAIN, struct.pack(">I", len(a)), a, r["manifest_hash"], r["pod_hash"], r["merkle_root"],
LAV.be64(r["budget_spent_micros"]), LAV.be64(r["work_decide_ops"]), LAV.be64(r["joules_micro"]),
LAV.class_bytes(r["class"]), g["grant_hash"], struct.pack(">I", len(cap)), cap,
LAV.be64(g["joule_ceiling_micro"])]
out.append(b"\x00" if g["funds_ceiling"] is None else b"\x01" + LAV.be64(g["funds_ceiling"]))
out.append(b"\x00" if r["parent"] is None else b"\x01" + r["parent"])
out.append(LAV.be64(len(r["impressions"])))
out += [LAV.leaf(i) for i in r["impressions"]]
out += [r["signer_pubkey"], struct.pack(">I", len(sid)), sid]
return b"".join(out)
def form1_identity(r):
return B3.blake3(FORM1_ID_DOMAIN + form1_payload(r) + r["sig"])
def form1_verifies(r):
"""§4.5: a form-1 receipt verifies."""
if ED.verify(r["signer_pubkey"], form1_payload(r), r["sig"]):
return False
imps = r["impressions"]
spend = sum(i["price_micros"] for i in imps if i["quartiles_reached"] >= 1)
if LAV.merkle([LAV.leaf(i) for i in imps]) != r["merkle_root"] or spend != r["budget_spent_micros"]:
return False
f = r["grant"]["funds_ceiling"]
if f is not None and spend > f:
return False
if r["work_decide_ops"] < len(imps) or any(i["slot"] != k or i["quartiles_reached"] > 5 for k, i in enumerate(imps)):
return False
c, j = r["class"], r["joules_micro"]
if c is None:
return j == 0
if j == 0:
return False
if c["label"] in ("OnChipCounter", "CalibratedInstrument") and (c["ppm"] == 0 or c["window"] == 0 or j * c["ppm"] < LAV.FLOOR):
return False
return not (c["label"] == "CalibratedInstrument" and all(ch in LAV.WHITE_SPACE for ch in c["ref"]))
# ---------------------------------------------------------------- §7.2 the VAST check
def decimal_scaled(s, scale):
if "." in s:
i, f = s.split(".", 1)
if not f:
return None
else:
i, f = s, ""
if not i or not (i.isascii() and i.isdigit()) or (f and not (f.isascii() and f.isdigit())):
return None
if len(f) > scale:
if f[scale:].strip("0"):
return None
f = f[:scale]
v = int(i + f + "0" * (scale - len(f)))
return v if v <= U64 else None
def vast_matches(rec, b):
root = read_xml(b)
if root is None or root["name"] != "VAST":
return False
for ad in rec["ads"]:
want = ad["serving_id"].decode()
def ident(a):
if rec["id_kind"] == SERVING_ID:
il = kids(a, "InLine")
sv = kids(il[0], "AdServingId") if il else []
return text(sv[0]) if sv else None
return a["attrs"].get("id")
hits = [a for a in kids(root, "Ad") if ident(a) == want]
if len(hits) != 1 or not kids(hits[0], "InLine"):
return False
inline = kids(hits[0], "InLine")[0]
if ad["creative_id"] and "vast_creative" not in SKIP:
cid, reg = ad["creative_id"].decode(), ad["registry"].decode()
if not any(text(u) == cid and u["attrs"].get("idRegistry") == reg
for cs in kids(inline, "Creatives") for c in kids(cs, "Creative") for u in kids(c, "UniversalAdId")):
return False
if ad["price_source"] == PRICING and "vast_price" not in SKIP:
ps = kids(inline, "Pricing")
if not ps:
return False
p = ps[0]
if p["attrs"].get("model") != "CPM" or p["attrs"].get("currency") != rec["currency"].decode():
return False
if decimal_scaled(text(p), rec["exponent"] + 3) != ad["price_micros"]:
return False
return True
def form1_matches(rec, r):
if not form1_verifies(r) or len(r["impressions"]) != len(rec["ads"]):
return False
return all(i["slot"] == a["slot"] and i["ad_id"].encode() == a["serving_id"] and i["creative_hash"] == a["creative_hash"]
and i["duration_ms"] == a["duration_ms"] and i["price_micros"] == a["price_micros"]
for i, a in zip(r["impressions"], rec["ads"]))
# ---------------------------------------------------------------- §5 the claim
def session_of(key):
return sha(SESSION_DOMAIN, key)[:16]
def read_claim(b):
"""§5.4: the claim, or Bad(stitched_claim_malformed)."""
why = "stitched_claim_malformed"
c = Cursor(b, why)
if c.take(4) != CLAIM_MAGIC or c.take(1) != b"\x01":
raise Bad(why)
x = {"carriage": c.u("B"), "manifest": c.take(32), "session": c.take(16), "creative_hash": c.take(32),
"player": c.take(32), "geometry": c.u("B"), "share_bp": c.u("H"), "frames": c.u("I"), "marker": c.long(),
"ad_id": c.short()}
n = c.u("H")
x["objects"] = [c.take(32) for _ in range(n)]
m = c.u("H")
x["spans"] = []
for _ in range(m):
start, length, vis, flags = struct.unpack("<IIHB", c.take(11))
x["spans"].append({"start": start, "length": length, "visible": vis, "flags": flags})
x["attestation"] = c.long()
x["body"] = b[:c.p]
x["sig"] = c.take(64)
if c.p != len(b):
raise Bad(why)
x["m"] = read_body(x["carriage"], x["marker"])
if "claim_rules" in SKIP:
return x
if x["m"] is None or x["geometry"] not in (1, 2, 3) or not 1 <= x["share_bp"] <= 10000 or not is_text(x["ad_id"]):
raise Bad(why)
if (n == 0) != (m == 0):
raise Bad(why)
prev = None
for s in x["spans"]:
if s["length"] == 0 or s["visible"] > 10000 or s["flags"] > 3:
raise Bad(why)
if prev is None and s["flags"] & APV.RESUMED:
raise Bad(why)
if prev is not None and (s["start"] < prev or (s["start"] > prev and not s["flags"] & APV.RESUMED)):
raise Bad(why)
prev = s["start"] + s["length"]
return x
def redemption_context(c):
return sha(ATTEST_DOMAIN, bytes([c["carriage"]]), marker_digest(c["carriage"], c["marker"]), c["session"],
c["creative_hash"], c["player"], bytes([len(c["ad_id"])]), c["ad_id"])
def attestation(c, q, issuers):
if not c["attestation"]:
return "attestation_absent"
t = APV.read_token(c["attestation"])
if t is None or t["type"] != q["token_type"]:
return "attestation_invalid"
if t["digest"] != sha(APV.token_challenge(q["token_type"], q["issuer"], redemption_context(c))):
return "attestation_invalid"
for i in issuers:
if i["name"] == q["issuer"] and i["token_type"] == t["type"]:
for k in i["keys"]:
if sha(k) == t["key_id"] and t["type"] == 2 and APV.pss_verify(k, t["input"], t["auth"]):
return None
return "attestation_invalid"
def marker_in_manifest(c, rec, man, manifests):
"""§7.3 step 6."""
m, attr = c["m"], rec["id_attr"].decode()
if m is None:
return False
if m["carriage"] == DATERANGE:
return any(f == m and a == c["ad_id"] for f, a in (hls_ads(man, attr) or []))
if m["carriage"] == EVENT:
return any(f == m and a == c["ad_id"] for f, a in (mpd_ads(man, attr) or []))
uris = asset_uris(man)
if uris is None or m["index"] >= len(uris) or uris[m["index"]].encode() != c["ad_id"]:
return False
return any((m["id"], m["start"]) in (interstitials(p) or []) for p in manifests)
# ---------------------------------------------------------------- §7 verification
def creative_digest(objects):
return APV.creative_digest(objects)
def verify(inp, through_attestation=True):
"""§7: what a verification states, as the corpus writes it. With `through_attestation` false,
§7.3 step 11 is not run (impression-aggregates §6.2 step 4 judges claims through step 10)."""
accepted, rfacts, stated = [], [], []
for b in inp["records"]:
f = {"digest": None, "verdict": "accepted"}
q_stated = None
try:
r = read_record(b)
f["digest"] = sha(r["body"]).hex()
bad = ED.verify(r["signer"], POD_DOMAIN + r["body"], r["sig"])
if bad:
raise Bad(bad)
if r["signer"] not in inp["signers"] and "signer" not in SKIP:
raise Bad("pod_signer_unknown")
if "pod_duplicate" not in SKIP and any(p is not None and p["session"] == r["session"] and
any(a["marker"] == x["marker"] for a in p["ads"] for x in r["ads"])
for p in accepted):
raise Bad("pod_duplicate")
checked, stated_spend = False, None
if r["format"] == VAST:
v = next((x for x in inp["responses"] if sha(x) == r["response"]), None)
if v is not None:
if not vast_matches(r, v) and "response" not in SKIP:
raise Bad("pod_response_mismatch")
checked = True
elif r["format"] == FORM1:
held = [x for x in (read_form1(o) for o in inp["receipts"]) if x is not None]
x = next((h for h in held if form1_identity(h) == r["response"]), None)
if x is not None:
if not form1_matches(r, x) and "response" not in SKIP:
raise Bad("pod_response_mismatch")
checked = True
q_stated = [i["quartiles_reached"] for i in x["impressions"]]
stated_spend = x["budget_spent_micros"]
region = None if r["frame"]["kind"] == 0 else [list(x) for x in frame_region(r["frame"])]
f.update(response_checked=checked, stated_spend_micros=stated_spend, share_bp=share_of(r["frame"]),
region=region)
if r["consent"] is not None: # §7.5
f["consent"] = dict(r["consent"])
accepted.append(r)
except Bad as e:
f = {"digest": f["digest"], "verdict": str(e)}
q_stated = None
accepted.append(None)
rfacts.append(f)
stated.append(q_stated)
cfacts, own, passed = [], {}, set()
by_hash = {sha(m): m for m in inp["manifests"]}
for j, b in enumerate(inp["claims"]):
f = {"digest": None, "verdict": "presented", "record": None, "slot": None, "quartile": None, "run_ms": None,
"viewable_ms": None}
try:
c = read_claim(b)
f["digest"] = sha(c["body"]).hex()
bad = ED.verify(c["player"], CLAIM_DOMAIN + c["body"], c["sig"])
if bad:
raise Bad(bad)
if session_of(c["player"]) != c["session"] and "key_bound" not in SKIP:
raise Bad("claim_key_unbound")
md = marker_digest(c["carriage"], c["marker"])
def names(p):
return p["carriage"] == c["carriage"] and any(a["marker"] == md and a["ad_id"] == c["ad_id"] for a in p["ads"])
i = next((k for k, p in enumerate(accepted) if p is not None and names(p) and
(p["session"] == bytes(16) or p["session"] == c["session"] or "session" in SKIP)), None)
if i is None:
raise Bad("claim_session_unbound" if any(p is not None and names(p) for p in accepted) else "claim_no_decision")
rec = accepted[i]
ad = next(a for a in rec["ads"] if a["marker"] == md and a["ad_id"] == c["ad_id"])
f["record"], f["slot"] = i, ad["slot"]
man = by_hash.get(c["manifest"])
if man is None and "manifest" not in SKIP:
raise Bad("claim_manifest_unknown")
if man is not None and "marker" not in SKIP and not marker_in_manifest(c, rec, man, inp["manifests"]):
raise Bad("claim_marker_absent")
key = (i, ad["slot"], c["session"])
if key in passed and "claim_duplicate" not in SKIP:
raise Bad("claim_duplicate")
passed.add(key)
own.setdefault((i, ad["slot"]), []).append(j)
q = APV.quartile(c["spans"], ad["duration_ms"])
run, viewable = APV.runs(c, rec["terms"])
f.update(quartile=q, run_ms=run, viewable_ms=viewable)
cr = next((x for x in inp["creatives"] if creative_digest(x) == ad["creative_hash"]), None)
asset_ok = c["creative_hash"] == ad["creative_hash"] and cr is not None and \
sum(o["duration_ms"] for o in cr) == ad["duration_ms"] and \
all(s["start"] + s["length"] <= ad["duration_ms"] for s in c["spans"]) and \
APV.touched(cr, c["spans"]) == c["objects"]
if not asset_ok and "asset" not in SKIP:
raise Bad("claim_asset_mismatch")
if c["share_bp"] != share_of(rec["frame"]) and "frame" not in SKIP:
raise Bad("claim_frame_mismatch")
if run < 2000 and "viewable" not in SKIP:
raise Bad("claim_not_viewable")
if through_attestation and rec["terms"]["require"] & ATTEST and "attest" not in SKIP:
why = attestation(c, rec["terms"], inp["issuers"])
if why:
raise Bad(why)
except Bad as e:
f["verdict"] = str(e)
cfacts.append(f)
spend = {}
for i, rec in enumerate(accepted):
if rec is None:
continue
ads, total = [], 0
for a in rec["ads"]:
mine = own.get((i, a["slot"]), [])
pres = [j for j in mine if cfacts[j]["verdict"] == "presented"]
qs = [cfacts[j]["quartile"] for j in pres]
if pres:
verdict = "presented"
else:
verdict = cfacts[mine[0]]["verdict"] if mine else "claim_absent"
s = a["price_micros"] * len(pres)
total += s
ads.append({"slot": a["slot"], "own_claims": mine, "presentations": len(pres), "verdict": verdict,
"quartiles": [sum(1 for x in qs if x >= k) for k in range(1, 6)], "presented_spend_micros": s,
"stated_quartile": None if stated[i] is None else stated[i][a["slot"]]})
rfacts[i].update(ads=ads, presented_spend_micros=total)
cur = rec["currency"].decode()
spend[cur] = spend.get(cur, 0) + total
return {"record_facts": rfacts, "claim_facts": cfacts, "spend": spend}
# ---------------------------------------------------------------- the corpus
def held(doc):
creatives = [[{"content_hash": bytes.fromhex(o["content_hash"]), "duration_ms": o["duration_ms"]} for o in c["objects"]]
for c in doc["creatives"]]
issuers = [{"name": i["name"].encode(), "token_type": i["token_type"], "keys": [bytes.fromhex(k) for k in i["keys"]]}
for i in doc["issuers"]]
return creatives, issuers, [bytes.fromhex(k) for k in doc["signers"]]
def inputs(doc, v, files):
cr, iss, sg = held(doc)
get = lambda k: [files[n] for n in v[k]] # noqa: E731
return {"records": [bytes.fromhex(x) for x in v["records"]], "claims": [bytes.fromhex(x) for x in v["claims"]],
"manifests": get("manifests"), "responses": get("responses"),
"receipts": [json.loads(x.decode("utf-8")) for x in get("receipts")],
"creatives": cr, "issuers": iss, "signers": sg}
def check(doc, files, quiet=False):
"""Every vector's facts; the reasons; the files. A list of failures."""
fails = []
names = {"vectors.json"}
seen = set()
for v in doc["vectors"]:
for k in ("manifests", "responses", "receipts", "bound"):
names.update(v[k])
try:
got = verify(inputs(doc, v, files))
except (KeyError, ValueError, TypeError, IndexError) as e:
fails.append("%s: its inputs do not read (%s)" % (v["name"], e))
continue
for k, want in got.items():
if v.get(k) != want:
fails.append("%s: %s differs" % (v["name"], k))
for r in got["record_facts"]:
seen.add(r["verdict"])
seen.update(a["verdict"] for a in r.get("ads", []))
seen.update(c["verdict"] for c in got["claim_facts"])
for r in REASONS + OTHER:
if r not in seen:
fails.append("no record, claim or ad gives %s" % r)
if set(files) - {"README.md"} != names:
fails.append("the files are not exactly the corpus's: %s" % sorted(set(files) ^ names - {"README.md"}))
if not quiet:
for f in fails:
print("FAIL", f)
return fails
def load(d):
files = {}
for n in sorted(os.listdir(d)):
with open(os.path.join(d, n), "rb") as f:
files[n] = f.read()
return json.loads(files["vectors.json"].decode("utf-8")), files
def flip(h, at):
b = bytearray(bytes.fromhex(h))
b[at % len(b)] ^= 0x01
return b.hex()
def vec(d, name):
return next(v for v in d["vectors"] if v["name"] == name)
def mutations(doc, files):
"""Copies of the corpus, one defect each: (what, doc, files)."""
out = []
def mut(what, f=None, g=None):
d, fs = copy.deepcopy(doc), dict(files)
if f:
f(d)
if g:
g(fs)
out.append((what, d, fs))
def edit(name, old, new):
def g(fs):
fs[name] = fs[name].replace(old, new, 1)
return g
def set_claim(name, k, h):
return lambda d: vec(d, name)["claims"].__setitem__(k, h(vec(d, name)["claims"][k]))
def set_record(name, k, h):
return lambda d: vec(d, name)["records"].__setitem__(k, h(vec(d, name)["records"][k]))
mut("hls_stitched: a record's signature flipped", set_record("hls_stitched", 0, lambda h: flip(h, len(h) // 2 - 3)))
mut("hls_stitched: a claim's span visible fraction changed", set_claim("hls_stitched", 1, lambda h: flip(h, 300)))
mut("hls_stitched: viewer 1's playlist carries another ad identity",
g=edit("hls_stitched.viewer1.m3u8", b'X-AD-ID="srv-v1-0"', b'X-AD-ID="srv-v1-9"'))
mut("hls_stitched: viewer 1's VAST prices ad-a at 25.01",
g=edit("hls_stitched.viewer1.vast.xml", b">25.00<", b">25.01<"))
mut("hls_stitched: viewer 2's VAST names another universal identifier",
g=edit("hls_stitched.viewer2.vast.xml", b">UID-C<", b">UID-X<"))
mut("hls_stitched: the stated spend", lambda d: vec(d, "hls_stitched")["spend"].__setitem__("USD", 7400000))
mut("hls_stitched: the prefetched ad stated presented",
lambda d: vec(d, "hls_stitched")["record_facts"][1]["ads"][1].update(verdict="presented"))
mut("hls_stitched: viewer 1's playlist missing", lambda d: vec(d, "hls_stitched")["manifests"].pop(0))
mut("hls_stitched: an unknown signer trusted", lambda d: d["signers"].append(vec(d, "hls_stitched")["records"][2][-192:-128]))
mut("hls_interstitial: an asset's URI changed", g=edit("hls_interstitial.assets.json", b'"URI":"https://ads.example/c/6', b'"URI":"https://ads.example/c/7'))
mut("hls_interstitial: the interstitial class changed", g=edit("hls_interstitial.m3u8", INTERSTITIAL.encode(), b"urn:example:other"))
mut("hls_interstitial: a token's byte flipped", set_claim("hls_interstitial", 0, lambda h: flip(h, len(h) // 2 - 70)))
mut("hls_interstitial: the issuer's key changed", lambda d: d["issuers"][0]["keys"].__setitem__(0, flip(d["issuers"][0]["keys"][0], 100)))
mut("squeezeback: the MPD's event identity changed", g=edit("squeezeback.mpd", b'messageData="srv-sq-1"', b'messageData="srv-sq-2"'))
mut("squeezeback: the region's side band moved", lambda d: vec(d, "squeezeback")["record_facts"][0]["region"][1].__setitem__(0, 1440))
mut("squeezeback: the quartile counts", lambda d: vec(d, "squeezeback")["record_facts"][0]["ads"][0].update(quartiles=[3, 3, 3, 3, 2]))
mut("squeezeback: a creative's duration", lambda d: d["creatives"][5]["objects"][0].update(duration_ms=5001))
mut("double_box: the stated currency's spend", lambda d: vec(d, "double_box")["spend"].update({"EUR": 1800001}))
mut("form1_prefetch: the receipt's stated quartile", g=edit("form1_prefetch.receipt.json", b'"quartiles_reached": 1', b'"quartiles_reached": 2'))
mut("form1_prefetch: the stated spend beside the presented", lambda d: vec(d, "form1_prefetch")["record_facts"][0].update(stated_spend_micros=3000000))
mut("consent: viewer 51's record with a byte of its GPP string changed",
set_record("consent", 0, lambda h: h.replace(b"1YNN".hex(), b"1YYN".hex(), 1)))
mut("consent: viewer 52's stated child-directed flag",
lambda d: vec(d, "consent")["record_facts"][1]["consent"].update(child_directed=0))
mut("consent: viewer 51's stated sections", lambda d: vec(d, "consent")["record_facts"][0]["consent"].update(gpp_sid=[]))
mut("a file the corpus does not name", g=lambda fs: fs.__setitem__("extra.m3u8", b"#EXTM3U\n"))
return out
CHECKS = ["record_rules", "signer", "pod_duplicate", "response", "vast_price", "vast_creative", "claim_rules",
"key_bound", "session", "manifest", "marker", "claim_duplicate", "asset", "frame", "viewable", "attest",
"consent"]
def main(corpus_dir):
doc, files = load(corpus_dir)
ok = True
fails = check(doc, files)
print("%s every vector gets the facts the corpus states, every reason of §9 is given, and no other file is there"
% ("FAIL" if fails else "PASS"))
ok &= not fails
caught = 0
for what, d, fs in mutations(doc, files):
try:
bad = check(d, fs, quiet=True)
except (KeyError, ValueError, TypeError, IndexError, StopIteration):
bad = ["does not read"]
if bad:
caught += 1
else:
print("FAIL the mutation was not caught:", what)
ok = False
print("%s every mutated copy of the corpus fails (%d)" % ("PASS" if ok else "FAIL", caught))
weak_ok = True
for k in CHECKS:
SKIP.clear()
SKIP.add(k)
try:
bad = check(doc, files, quiet=True)
except (KeyError, ValueError, TypeError, IndexError):
bad = ["does not run"]
if not bad:
print("FAIL the corpus does not catch a verifier without its check", k)
weak_ok = False
SKIP.clear()
print("%s the corpus catches the verifier without each of its checks" % ("PASS" if weak_ok else "FAIL"))
return 0 if ok and weak_ok else 1
if __name__ == "__main__":
if len(sys.argv) != 2:
print("usage: stitched_presentation_verify.py <live-conformance/stitched>", file=sys.stderr)
sys.exit(2)
sys.exit(main(sys.argv[1]))