open-standards/wai/live-conformance/synthesis_verify.py
Transaction Science 495651ddd2
Some checks failed
wai key · the real keychain / does this change touch it (push) Successful in 18s
wai-quantum energy-landscape jobs / does this change touch it (push) Successful in 12s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 22s
wai key · the real keychain / the real keychain · macos (push) Successful in 39s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Failing after 57s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Failing after 2m6s
wai-quantum energy-landscape jobs / the module under wasm32-wasip2 (wasmtime) (push) Successful in 2m17s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 4m28s
wai-quantum energy-landscape jobs / the same bits · macos (push) Successful in 4m30s
wai-quantum energy-landscape jobs / native against WebAssembly, over many seeds (push) Successful in 4m44s
wai-quantum energy-landscape jobs / reader, kernels, answers and the stated result · linux, release (push) Successful in 7m36s
wai-quantum energy-landscape jobs / the same bits · linux-arm64 (push) Successful in 7m42s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Has been cancelled
joule-moq-room room service / joule-moq-room · does this change touch it (push) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (push) Has been cancelled
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Has been cancelled
wai-app and Create / wai-app · does this change touch it (push) Has been cancelled
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Has been cancelled
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Has been cancelled
wai-audit collector and player integration / does this change touch it (push) Has been cancelled
wai-audit collector and player integration / collector · linux (push) Has been cancelled
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Has been cancelled
WAI operation catalogue / does this change touch it (push) Has been cancelled
WAI operation catalogue / the catalogue and every door · linux (push) Has been cancelled
wai-fleet service / does this change touch it (push) Has been cancelled
wai-fleet service / service · linux (push) Has been cancelled
wai-fleet service / service · macos (push) Has been cancelled
wai-ledger service / does this change touch it (push) Has been cancelled
wai-ledger service / service · linux (push) Has been cancelled
wai-ledger service / service · macos (push) Has been cancelled
wai-quantum for JavaScript / does this change touch it (push) Has been cancelled
wai-quantum for JavaScript / javascript · linux (push) Has been cancelled
wai-quantum studio and desktop / does this change touch it (push) Has been cancelled
wai-quantum studio and desktop / studio · browser (push) Has been cancelled
wai-quantum studio and desktop / desktop app · macos (push) Has been cancelled
wai-quantum studio and desktop / desktop app · linux (push) Has been cancelled
wai-quantum studio and desktop / desktop app · windows (push) Has been cancelled
wai-sink-android native sink library / does this change touch it (push) Has been cancelled
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Has been cancelled
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Has been cancelled
wai-sink-swift native sink package / does this change touch it (push) Has been cancelled
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Has been cancelled
WAI Studio / does this change touch it (push) Has been cancelled
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Has been cancelled
WAI Studio / Studio in Chromium and Firefox · macos (push) Has been cancelled
WAI Studio / Studio in WebKit · linux (push) Has been cancelled
wai-key-proxy / does this change touch it (push) Has been cancelled
wai-key-proxy / proxy · linux (push) Has been cancelled
wai key · Windows / does this change touch it (push) Successful in 13s
wai key · Windows / wai key and the toolchain's tests · windows (push) Successful in 2m10s
wai: operation receipts on every developer-facing write, and a signed origin claim for each generated picture
No new record type. An operation receipt is an asset-provenance receipt
(wai::provenance::AssetReceipt), unchanged: its steps are the project by the
canonical digest of the wai.json the operation ran under, each file it read,
and each file it wrote, by content hash and path. wai create, lock, build,
decode, wrap, unwrap -o, generate and key create|import|rotate|revoke|export
each write one in a build with quickstart, kept in a project's .wai/receipts/
(every template's .gitignore keeps it out) or beside the file written. --key
signs it; a key operation signs with the key it makes; otherwise a key made
for the run, and the receipt says so. A --key that does not sign stops the
operation before it writes anything.

wai verify reads every *.wai-receipt.json under its paths and refuses a
receipt that does not verify, a written file that is missing or changed, a
receipt kept in a project whose wai.json is not the one it names, and a built
tree that is not the one its build record lists.

wai generate, with synthesis, signs for each picture a wai-origin claim
(subject the picture's samples, step generate, operation
wai.generate.spectral, the model file by SHA-256 and type algorithmic,
oversight prompt_guided, source algorithmicMedia) whose record is the
picture's receipt, signed by the same key. extensions/synthesis.md §3.5 states
it, and how a claim names a model no prior pins, such as a person's own model
reached at an endpoint from Create: by the SHA-256 of the endpoint's URL and
the model's name, read as a name, never as a pin. wai verify checks the claim;
so does synthesis_verify.py claim, the second implementation.

The catalogue marks project.create, project.lock, project.build,
media.decode, media.generate, envelope.wrap, envelope.unwrap and key.* with
receipt and adds their --key inputs; ops.json and Studio's actions are
regenerated. Create's end-to-end test runs wai check and wai dev --check on
the exported project, reads its receipt with wai verify, and requires wai
verify to refuse each changed copy.

tests/cli_receipts.rs, in wai-conformance's project step, plants the faults:
a changed picture, decoded file, built file and key record; another project's
receipt; a claim beside another picture's receipt; an edited claim.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 15:26:12 -05:00

1122 lines
49 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.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Second implementation of extensions/synthesis.md (`wai.media.synthesis`), over the corpus
`live-conformance/synthesis/`.
Written from the extension's text. It shares no code with wai-rs: it reads every record as §2
has it read (strict JSON, the encodings of jwp-receipts §2.4.3), verifies every signature with
WAI's strict Ed25519 rule (claim-conformance/_ed25519.py) and every link hash with BLAKE3
written from its specification (claim-conformance/_blake3.py), runs the two pinned derivations
of §4 in its own arithmetic, writes the digital source type of §5 and the C2PA statement of §6
itself, and verifies each room with the live-rooms corpus's second implementation
(live_rooms_verify.py). It reads §10.2's pinned icons and §12's codes from the extension.
python3 -I synthesis_verify.py <live-conformance/synthesis>
python3 -I synthesis_verify.py claim <claim.json> [<samples>]
It gives every vector of the corpus the outcome, the reason, the place and the statements the
corpus states, checks that every reason of §12 refuses a vector, and then mutates copies of the
corpus and requires every copy to fail. Exit status 0 only if all of that holds. Python 3.8 or
later; standard library only.
`claim` verifies one origin claim, such as the one `wai generate` signs (§3.5), as §11 verifies a
case that holds it alone, and prints its outcome as one line of JSON: `accept` with the statement
§11.3 gives it, or `refuse` with the reason. Given the samples it is about, it also requires its
subject to be those samples by SHA-256 (`samples` with another digest, or another subject, is
refused as `subject_not_these_samples`). Exit status 0 only for `accept`.
"""
import copy
import hashlib
import json
import os
import re
import sys
import unicodedata
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 _int_sr as SR # noqa: E402
import live_rooms_verify as LRV # noqa: E402
import _wai_mark as MARK # noqa: E402
import _wai_audio_mark as AMARK # noqa: E402
import _ed25519 as ED # noqa: E402
from _blake3 import blake3 # noqa: E402
EXTENSION = os.path.join(HERE, "..", "extensions", "synthesis.md")
REGISTRY = os.path.join(HERE, "..", "registry", "capabilities.json")
SIGN_DOMAIN = b"wai:synthesis\x01" # §2
LINK_DOMAIN = b"wai:synthesis-link\x01" # §2
OBJECT_DOMAIN = b"moq-jwp:object\x01" # jwp-receipts §2.1
IPTC = "http://cv.iptc.org/newscodes/digitalsourcetype/"
MODEL_ID = "tag:transaction.science,2026:wai/model/" # §6
UPSCALE2, DENOISE3 = "wai-synthesis:upscale2", "wai-synthesis:denoise3"
MAX_INT = (1 << 53) - 1
CLASSES = ["captured", "reconstructed", "assistive", "synthesized"] # §3.2, in order
STEP_CLASS = {"capture": "captured", "convert": "reconstructed", "assist": "assistive", "generate": "synthesized"}
CAPTURE_SOURCES = ["digitalCapture", "computationalCapture", "screenCapture", "digitalCreation"]
SOURCES = CAPTURE_SOURCES + ["algorithmicallyEnhanced", "trainedAlgorithmicMedia", "compositeWithTrainedAlgorithmicMedia",
"algorithmicMedia", "composite", "compositeCapture", "compositeSynthetic"] # §5
OVERSIGHT = ["fully_autonomous", "prompt_guided", "human_validated"]
MEDIA = ["audio", "video", "image", "text"]
ORIGIN_MEDIA = MEDIA + ["volumetric"] # §3.1
PATH_TIERS = ["decode-equivalence", "generation-equivalence", "entropy-consistency", "none"]
REVIEW_TIERS = ["decode-equivalence", "generation-equivalence"]
# Unicode's White_Space property (a term holds none of these, jwp-receipts §2.4.3).
WHITE_SPACE = set("\t\n\x0b\x0c\r \x85\xa0         "
"   

   ")
class Refuse(Exception):
def __init__(self, reason, at=None):
super().__init__(reason)
self.reason, self.at = reason, at
class Bad(Exception):
"""A record that does not read: `record_malformed`."""
# ---------------------------------------------------------------- §2 records
def strict_json(text):
"""RFC 8259, no member named twice, no number but an unsigned integer, no unpaired
surrogate, at most 32 levels."""
def pairs(ps):
d = {}
for k, v in ps:
if k in d:
raise Bad("a member named twice")
d[k] = v
return d
def no_float(s):
raise Bad("a number that is not an unsigned integer")
def integer(s):
if s.startswith("-"):
raise Bad("a negative number")
return int(s)
try:
v = json.loads(text, object_pairs_hook=pairs, parse_float=no_float, parse_int=integer, parse_constant=no_float)
except (ValueError, RecursionError):
raise Bad("not JSON")
def walk(x, depth):
if isinstance(x, (dict, list)):
if depth > 32:
raise Bad("too deep")
for y in (x.values() if isinstance(x, dict) else x):
walk(y, depth + 1)
if isinstance(x, dict):
for k in x:
walk(k, depth)
elif isinstance(x, str):
if any(0xD800 <= ord(c) <= 0xDFFF for c in x):
raise Bad("an unpaired surrogate")
elif isinstance(x, bool):
raise Bad("a boolean")
elif isinstance(x, int):
if x > MAX_INT:
raise Bad("an integer past 2^53 - 1")
walk(v, 1)
return v
def jstr(s):
out = ['"']
for c in s:
o = ord(c)
if c == '"':
out.append('\\"')
elif c == "\\":
out.append("\\\\")
elif c == "\b":
out.append("\\b")
elif c == "\f":
out.append("\\f")
elif c == "\n":
out.append("\\n")
elif c == "\r":
out.append("\\r")
elif c == "\t":
out.append("\\t")
elif o < 0x20:
out.append("\\u%04x" % o)
else:
out.append(c)
out.append('"')
return "".join(out)
def jcs(v):
"""RFC 8785 on the values a record carries."""
if v is None:
return "null"
if isinstance(v, bool):
raise Bad("a boolean")
if isinstance(v, int):
return str(v)
if isinstance(v, str):
return jstr(v)
if isinstance(v, list):
return "[" + ",".join(jcs(x) for x in v) + "]"
keys = sorted(v, key=lambda k: k.encode("utf-16-be"))
return "{" + ",".join(jstr(k) + ":" + jcs(v[k]) for k in keys) + "}"
def link_hash(v):
return blake3(LINK_DOMAIN + jcs(v).encode("utf-8"))
def signing_bytes(v):
return SIGN_DOMAIN + jcs({k: x for k, x in v.items() if k != "sig"}).encode("utf-8")
def need(cond):
if not cond:
raise Bad("a member breaks its rule")
def hexb(v, n):
need(isinstance(v, str) and len(v) == 2 * n and re.fullmatch(r"[0-9a-f]*", v) is not None)
return bytes.fromhex(v)
def opt_hex(v, n):
return None if v is None else hexb(v, n)
def integer(v):
need(isinstance(v, int) and not isinstance(v, bool) and 0 <= v <= MAX_INT)
return v
def term(v):
need(isinstance(v, str) and 0 < len(v.encode("utf-8")) <= 2048
and not any(c in WHITE_SPACE or unicodedata.category(c) == "Cc" for c in v))
return v
def opt_term(v):
return None if v is None else term(v)
def one_of(v, values):
need(isinstance(v, str) and v in values)
return v
def members(v, names):
need(isinstance(v, dict) and set(v) == set(names))
return v
def date_time(s):
"""RFC 3339 §5.6 date-time, with §5.7's days (jwp-receipts §2.4.3)."""
m = re.fullmatch(r"([0-9]{4})-([0-9]{2})-([0-9]{2})[Tt]([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]+)?([Zz]|[+-]([0-9]{2}):([0-9]{2}))", s or "")
need(m is not None)
y, mo, d, h, mi, sec = (int(m.group(i)) for i in range(1, 7))
leap = y % 4 == 0 and (y % 100 != 0 or y % 400 == 0)
days = [31, 29 if leap else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
need(1 <= mo <= 12 and 1 <= d <= days[mo - 1] and h <= 23 and mi <= 59 and sec <= 60)
if m.group(9) is not None:
need(int(m.group(9)) <= 23 and int(m.group(10)) <= 59)
def read(text, kind, names, fields):
"""§2: read one record, then its signature. `fields` reads the kind's own members."""
try:
v = strict_json(text)
members(v, names)
need(v["kind"] == kind)
signer = hexb(v["signer_pubkey"], 32)
sig = hexb(v["sig"], 64)
if "issued_at" in v:
date_time(v["issued_at"])
r = fields(v)
except (Bad, KeyError, TypeError, AttributeError):
raise Refuse("record_malformed")
bad = ED.verify(signer, signing_bytes(v), sig)
if bad:
raise Refuse(bad)
r.update(value=v, signer=signer, link=link_hash(v))
return r
# ---------------------------------------------------------------- §3 the origin claim
ORIGIN = ["kind", "subject", "media", "class", "step", "operation", "record", "source", "models", "oversight", "inputs",
"evidence", "issued_at", "signer_pubkey", "sig"]
def subject_of(v):
t = v.get("type")
if t == "object":
members(v, ["type", "content_hash"])
return ("object", hexb(v["content_hash"], 32))
if t == "group":
members(v, ["type", "receipt"])
return ("group", hexb(v["receipt"], 32))
if t == "track":
members(v, ["type", "track", "from_group"])
integer(v["from_group"])
return ("track", hexb(v["track"], 32))
if t == "samples":
members(v, ["type", "samples"])
return ("samples", hexb(v["samples"], 32))
raise Bad("a subject of no type")
def origin_fields(v):
c = {"subject": subject_of(v["subject"]), "media": one_of(v["media"], ORIGIN_MEDIA), "class": one_of(v["class"], CLASSES),
"step": one_of(v["step"], list(STEP_CLASS)), "operation": term(v["operation"]), "record": opt_hex(v["record"], 32),
"source": one_of(v["source"], SOURCES),
"oversight": None if v["oversight"] is None else one_of(v["oversight"], OVERSIGHT)}
need(isinstance(v["models"], list) and len(v["models"]) <= 16)
c["models"] = []
for m in v["models"]:
members(m, ["digest", "type"])
c["models"].append({"digest": hexb(m["digest"], 32), "trained": one_of(m["type"], ["trained", "algorithmic"]) == "trained"})
need(len({m["digest"] for m in c["models"]}) == len(c["models"]))
need(isinstance(v["inputs"], list) and len(v["inputs"]) <= 16)
c["inputs"] = []
for i in v["inputs"]:
members(i, ["content_hash", "origin", "class", "source"])
c["inputs"].append({"content_hash": hexb(i["content_hash"], 32), "origin": opt_hex(i["origin"], 32),
"class": one_of(i["class"], CLASSES), "source": one_of(i["source"], SOURCES)})
need(len({i["content_hash"] for i in c["inputs"]}) == len(c["inputs"]))
e = v["evidence"]
if e is None:
c["evidence"] = None
else:
members(e, ["tier", "derivation", "params", "width", "height", "input_samples", "output_samples"])
need(isinstance(e["params"], dict))
c["evidence"] = {"tier": one_of(e["tier"], ["replicate", "attest"]), "derivation": term(e["derivation"]),
"params": e["params"], "width": integer(e["width"]), "height": integer(e["height"]),
"input_samples": hexb(e["input_samples"], 32), "output_samples": hexb(e["output_samples"], 32)}
need(c["evidence"]["width"] >= 1 and c["evidence"]["height"] >= 1)
n, m, s = len(c["inputs"]), len(c["models"]), c["step"]
plain = c["oversight"] is None and c["evidence"] is None
if s == "capture":
need(n == 0 and m == 0 and plain and c["source"] in CAPTURE_SOURCES)
elif s == "convert":
need(n >= 1 and m == 0 and plain)
elif s == "assist":
need(n == 1 and c["oversight"] is None and c["evidence"] is not None)
else:
need(m >= 1 and c["oversight"] is not None and c["evidence"] is None)
return c
def class_of(c):
"""§3.2: the greatest of the step's class and the inputs'."""
return max([STEP_CLASS[c["step"]]] + [i["class"] for i in c["inputs"]], key=CLASSES.index)
def generated_source(c):
"""§5."""
if not any(m["trained"] for m in c["models"]):
return "algorithmicMedia"
if c["oversight"] == "human_validated" or any(i["class"] != "synthesized" for i in c["inputs"]):
return "compositeWithTrainedAlgorithmicMedia"
return "trainedAlgorithmicMedia"
def source_of(c):
"""§5's table."""
ins = c["inputs"]
if c["step"] == "capture":
return c["source"]
if c["step"] == "convert":
if len(ins) == 1:
return ins[0]["source"]
if any(i["class"] == "synthesized" for i in ins):
return "compositeSynthetic"
if all(i["source"] in ("digitalCapture", "computationalCapture") for i in ins):
return "compositeCapture"
return "composite"
if c["step"] == "assist":
return ins[0]["source"] if ins[0]["class"] == "synthesized" else "algorithmicallyEnhanced"
return generated_source(c)
def statement(c):
"""§6, as its JCS text."""
if len(c["inputs"]) != 1:
actions = [{"action": "c2pa.created", "digitalSourceType": IPTC + c["source"]}]
elif c["step"] == "convert":
actions = [{"action": "c2pa.opened"}, {"action": "c2pa.converted"}]
elif c["step"] == "assist":
actions = [{"action": "c2pa.opened"}, {"action": "c2pa.edited", "digitalSourceType": IPTC + "algorithmicallyEnhanced"}]
else:
actions = [{"action": "c2pa.opened"}, {"action": "c2pa.edited", "digitalSourceType": IPTC + generated_source(c)}]
disclosures = []
for m in [m for m in c["models"] if m["trained"]]:
data = {"modelType": "c2pa.types.model", "modelIdentifier": MODEL_ID + m["digest"].hex()}
if c["step"] == "generate":
data["contentProfile"] = {"humanOversightLevel": c["oversight"]}
k = len(disclosures)
disclosures.append({"label": "c2pa.ai-disclosure" if k == 0 else "c2pa.ai-disclosure__%d" % k, "data": data})
return jcs({"actions": actions, "assertions": disclosures})
def payload_of(c):
"""§7.2: the class's code, then seven bytes of the claim's link hash."""
return bytes([CLASSES.index(c["class"])]) + c["link"][:7]
# ---------------------------------------------------------------- §4 the derivations
def upscale2(px, w, h):
out = bytearray(4 * w * h * 3)
ow = 2 * w
def at(x, y, k):
return px[(y * w + x) * 3 + k]
for y in range(h):
for x in range(w):
x1, y1 = min(x + 1, w - 1), min(y + 1, h - 1)
for k in range(3):
a, b, c, d = at(x, y, k), at(x1, y, k), at(x, y1, k), at(x1, y1, k)
out[((2 * y) * ow + 2 * x) * 3 + k] = a
out[((2 * y) * ow + 2 * x + 1) * 3 + k] = (a + b + 1) >> 1
out[((2 * y + 1) * ow + 2 * x) * 3 + k] = (a + c + 1) >> 1
out[((2 * y + 1) * ow + 2 * x + 1) * 3 + k] = (a + b + c + d + 2) >> 2
return bytes(out)
def denoise3(px, w, h):
out = bytearray(w * h * 3)
for y in range(h):
for x in range(w):
for k in range(3):
s = 8
for j in (-1, 0, 1):
for i in (-1, 0, 1):
weight = (2 if i == 0 else 1) * (2 if j == 0 else 1)
xx, yy = min(max(x + i, 0), w - 1), min(max(y + j, 0), h - 1)
s += weight * px[(yy * w + xx) * 3 + k]
out[(y * w + x) * 3 + k] = s >> 4
return bytes(out)
DERIVATIONS = {UPSCALE2: (upscale2, 4), DENOISE3: (denoise3, 1)}
# The derivations of exact-upscale §3, each with the factor its output's samples hold over its
# input's (§4.3), checked with the prior its evidence names.
SR_DERIVATIONS = {SR.SR2: 4, SR.SR4: 16}
def evidence_prior(e):
"""§4.3: the prior an exact-upscale derivation's `params` names, `{"prior"}`, or None."""
p = e["params"]
if not isinstance(p, dict) or set(p) != {"prior"} or not isinstance(p["prior"], str):
return None
try:
return hexb(p["prior"], 32)
except Bad:
return None
def replicate_admitted(e):
"""§4.3: a pinned derivation with its parameters, and an output of at most 2^26 bytes."""
w, h = e["width"], e["height"]
if e["derivation"] in DERIVATIONS:
return e["params"] == {} and DERIVATIONS[e["derivation"]][1] * w * h * 3 <= (1 << 26)
if e["derivation"] in SR_DERIVATIONS:
return evidence_prior(e) is not None and SR_DERIVATIONS[e["derivation"]] * w * h * 3 <= (1 << 26)
return False
# ---------------------------------------------------------------- §7, §9, §10 the other records
def positive(v):
need(integer(v) >= 1)
return v
def frames_of(v, digests):
"""§7.2, §7.3: an embedding or a read: the frames' count and size, or the audio's rate,
channels and length, and digests."""
if v is None:
return None
if isinstance(v, dict) and set(v) == set(["rate", "channels", "length"] + digests):
f = {k: positive(v[k]) for k in ("rate", "channels", "length")}
f["audio"] = True
f["bytes"] = f["length"] * f["channels"] * 2
else:
members(v, ["frames", "width", "height"] + digests)
f = {k: positive(v[k]) for k in ("frames", "width", "height")}
f["audio"] = False
f["bytes"] = f["frames"] * f["width"] * f["height"] * 3
f.update({k: hexb(v[k], 32) for k in digests})
return f
def admitted(scheme, f):
"""§7.5.1, §7.10.1: whether the scheme admits what the record states."""
if scheme == AMARK.SCHEME:
return f["audio"] and AMARK.admits(f["rate"], f["channels"], f["length"])
return not f["audio"] and MARK.admits(f["width"], f["height"], f["frames"])
def marking_fields(v):
return {"subject": hexb(v["subject"], 32), "origin": hexb(v["origin"], 32), "scheme": term(v["scheme"]),
"scheme_digest": hexb(v["scheme_digest"], 32), "payload": hexb(v["payload"], 8),
"embedding": frames_of(v["embedding"], ["unmarked", "marked"])}
def detection_fields(v):
return {"subject": hexb(v["subject"], 32), "scheme": term(v["scheme"]), "scheme_digest": hexb(v["scheme_digest"], 32),
"payload": opt_hex(v["payload"], 8), "read": frames_of(v["read"], ["samples"])}
def scheme_check(scheme, digest, frames):
"""§7.4: the registered scheme a record names, or None; refuses a registered term under
another digest, a record of it with no frames the scheme admits, and frames stated for
wai-mark:conformance."""
if scheme in SCHEME_DIGESTS:
if digest != SCHEME_DIGESTS[scheme]:
raise Refuse("scheme_mismatch")
if frames is None or not admitted(scheme, frames):
raise Refuse("mark_evidence_invalid")
return scheme
if scheme == "wai-mark:conformance" and frames is not None:
raise Refuse("mark_evidence_invalid")
return None
def marking_statement(m):
"""§7.7: the marking's C2PA statement, as its JCS text."""
return jcs({"actions": [{"action": "c2pa.watermarked.bound"}],
"assertions": [{"label": "c2pa.soft-binding",
"data": {"alg": m["scheme"], "alg-params": m["scheme_digest"].hex(),
"blocks": [{"scope": {}, "value": m["payload"].hex()}]}}]})
_EMBEDDED, _DETECTED = {}, {}
def embedded(px, e, payload, scheme=MARK.SCHEME):
"""§7.5.4, §7.9.3 or §7.10.4, memoised: the SHA-256 of the marked samples."""
if e["audio"]:
k = (scheme, hashlib.sha256(px).digest(), e["rate"], e["channels"], e["length"], payload)
if k not in _EMBEDDED:
_EMBEDDED[k] = hashlib.sha256(AMARK.embed_bytes(e["rate"], e["channels"], px, payload)[0]).digest()
return _EMBEDDED[k]
k = (scheme, hashlib.sha256(px).digest(), e["width"], e["height"], e["frames"], payload)
if k not in _EMBEDDED:
fn = MARK.embed_s if scheme == MARK.SCHEME_S else MARK.embed
_EMBEDDED[k] = hashlib.sha256(fn(px, e["width"], e["height"], e["frames"], payload)[0]).digest()
return _EMBEDDED[k]
def detected(px, r, scheme=MARK.SCHEME):
"""§7.5.5, §7.9.4 or §7.10.5, memoised: the payload read, or None."""
if r["audio"]:
k = (scheme, hashlib.sha256(px).digest(), r["rate"], r["channels"], r["length"])
if k not in _DETECTED:
got = AMARK.detect(r["rate"], r["channels"], px)
_DETECTED[k] = None if got is None else got[0]
return _DETECTED[k]
k = (scheme, hashlib.sha256(px).digest(), r["width"], r["height"], r["frames"])
if k not in _DETECTED:
fn = MARK.detect_s if scheme == MARK.SCHEME_S else MARK.detect
got = fn(px, r["width"], r["height"], r["frames"])
_DETECTED[k] = None if got is None else got[0]
return _DETECTED[k]
def recomputes(px, e, payload, scheme=MARK.SCHEME):
"""§7.6 step 2: the embedding of the unmarked frames is the marked frames."""
return embedded(px, e, payload, scheme) == e["marked"]
def reproduces(px, r, payload, scheme=MARK.SCHEME):
"""§7.6: the detector, run again, reads the detection's payload."""
return detected(px, r, scheme) == payload
def inherits(c, p, by_link):
"""§7.3 step 4: p is the payload of a held claim c reaches through inputs along steps
convert and assist."""
seen, stack = set(), [c]
while stack:
x = stack.pop()
if x["step"] not in ("convert", "assist") or x["link"] in seen:
continue
seen.add(x["link"])
for i in x["inputs"]:
held = by_link.get(i["origin"])
if held is not None:
if payload_of(held) == p:
return True
stack.append(held)
return False
def review_fields(v):
verdict = one_of(v["verdict"], ["approved", "approved-with-notes", "rejected"])
notes = opt_hex(v["notes"], 32)
need((notes is None) == (verdict == "approved"))
term(v["decoder"])
opt_term(v["variant"])
opt_term(v["role"])
one_of(v["scope"], ["accuracy", "rendering", "translation"])
return {"subject": hexb(v["subject"], 32), "origin": hexb(v["origin"], 32), "tier": one_of(v["tier"], REVIEW_TIERS),
"reconstruction": hexb(v["reconstruction"], 32), "verdict": verdict}
def policy_fields(v):
need(isinstance(v["classes"], list) and len(v["classes"]) >= 1)
idx = [CLASSES.index(one_of(c, CLASSES)) for c in v["classes"]]
need(all(a < b for a, b in zip(idx, idx[1:])))
interval = None if v["interval_us"] is None else integer(v["interval_us"])
need(interval != 0)
return {"classes": list(v["classes"]), "visual": hexb(v["visual"], 32), "audio": opt_hex(v["audio"], 32),
"interval_us": interval, "grace_us": integer(v["grace_us"])}
def entry_of(e):
t = e.get("type")
if t == "present":
members(e, ["type", "subject", "origin", "class", "media", "tier", "decoder", "variant", "output"])
term(e["decoder"])
opt_term(e["variant"])
return {"type": t, "subject": hexb(e["subject"], 32), "origin": opt_hex(e["origin"], 32),
"class": None if e["class"] is None else one_of(e["class"], CLASSES), "media": one_of(e["media"], MEDIA),
"tier": one_of(e["tier"], PATH_TIERS), "output": opt_hex(e["output"], 32)}
if t == "shown":
members(e, ["type", "label", "asset", "trigger"])
return {"type": t, "audio": one_of(e["label"], ["visual", "audio"]) == "audio", "asset": hexb(e["asset"], 32),
"trigger": one_of(e["trigger"], ["first-exposure", "resume", "interval"])}
if t == "resume":
members(e, ["type", "after"])
one_of(e["after"], ["ad-break", "other"])
return {"type": t}
if t == "review":
members(e, ["type", "subject", "review"])
return {"type": t, "subject": hexb(e["subject"], 32), "review": hexb(e["review"], 32)}
raise Bad("an entry of no type")
def player_fields(v):
return {"session": hexb(v["session"], 32), "seq": integer(v["seq"]), "prev": opt_hex(v["prev"], 32), "at": integer(v["at"]),
"entry": entry_of(v["entry"])}
# ---------------------------------------------------------------- §11 verification
def generates(env, classes):
"""§3.3: an envelope whose capability's registry class is generative-exact or behavioral, or
whose intent is create."""
if len(env) < 8 or env[:4] != b"WAI1":
return False
n = int.from_bytes(env[4:8], "little")
try:
m = json.loads(env[8:8 + n])
except ValueError:
return False
cap = m.get("model_requirement", {}).get("capability", "")
return classes.get(cap) in ("generative-exact", "behavioral") or m.get("intent") == "create"
def track_digest(room, owner, media, layer):
"""live-rooms §6.2."""
return hashlib.sha256(("-".join([room.hex(), owner, media]) + "\x00" + str(layer)).encode()).digest()
def verify(case, classes):
"""§11.2: the statements of §11.3, or Refuse with its reason and place."""
st = {"claims": [], "markings": [], "detections": [], "ai_tracks": [], "reviews_presented": [], "labels": [], "visual_icon": None,
"room": None}
def each(key, kind, names, fields, place):
out = []
for i, t in enumerate(case[key]):
try:
out.append(read(t, kind, names, fields))
except Refuse as e:
raise Refuse(e.reason, [place, i])
return out
# Step 1.
claims = each("claims", "wai-origin", ORIGIN, origin_fields, "claim")
markings = each("markings", "wai-marking", ["kind", "subject", "origin", "scheme", "scheme_digest", "payload", "embedding", "issued_at",
"signer_pubkey", "sig"], marking_fields, "marking")
detections = each("detections", "wai-detection", ["kind", "subject", "scheme", "scheme_digest", "payload", "read", "issued_at",
"signer_pubkey", "sig"], detection_fields, "detection")
reviews = each("reviews", "wai-review", ["kind", "subject", "origin", "tier", "reconstruction", "decoder", "variant", "scope", "verdict", "notes",
"role", "issued_at", "signer_pubkey", "sig"], review_fields, "review")
policy = None
if case["label_policy"] is not None:
try:
policy = read(case["label_policy"], "wai-label-policy", ["kind", "classes", "visual", "audio", "interval_us", "grace_us", "issued_at",
"signer_pubkey", "sig"], policy_fields)
except Refuse as e:
raise Refuse(e.reason, ["label_policy"])
log = each("player", "wai-player", ["kind", "session", "seq", "prev", "at", "entry", "signer_pubkey", "sig"], player_fields, "player")
by_link = {c["link"]: c for c in claims}
objects = {blake3(OBJECT_DOMAIN + bytes.fromhex(o)): bytes.fromhex(o) for o in case["objects"]}
decoded = {bytes.fromhex(d["object"]): bytes.fromhex(d["samples"]) for d in case["decoded"]}
unmarked = {bytes.fromhex(d["object"]): bytes.fromhex(d["samples"]) for d in case["unmarked"]}
prove = case["policy"]["second_layer"] == "require-proof"
sha = lambda b: hashlib.sha256(b).digest() # noqa: E731
priors = {sha(bytes.fromhex(p)): bytes.fromhex(p) for p in case["priors"]}
# Step 2.
seen = set()
for i, c in enumerate(claims):
def no(reason):
raise Refuse(reason, ["claim", i])
key = jcs(c["value"]["subject"])
if key in seen:
no("claim_conflict")
seen.add(key)
if c["class"] != class_of(c):
no("class_mismatch")
if c["source"] != source_of(c):
no("source_mismatch")
for inp in c["inputs"]:
held = by_link.get(inp["origin"])
if held is not None and (held["subject"] != ("object", inp["content_hash"]) or held["class"] != inp["class"]
or held["source"] != inp["source"]):
no("input_mismatch")
kind, h = c["subject"]
if kind == "object" and h in objects and generates(objects[h], classes) and c["step"] != "generate":
no("class_understated")
status = None
e = c["evidence"]
if e is not None:
w, hh = e["width"], e["height"]
if e["tier"] == "replicate":
if not replicate_admitted(e):
no("evidence_invalid")
status = "not-recomputed"
px = decoded.get(c["inputs"][0]["content_hash"])
if px is not None:
if len(px) != w * hh * 3 or sha(px) != e["input_samples"]:
no("input_samples_mismatch")
if e["derivation"] in DERIVATIONS:
out = DERIVATIONS[e["derivation"]][0](px, w, hh)
else:
prior = priors.get(evidence_prior(e))
try:
out = None if prior is None else SR.derive(e["derivation"], prior, px, w, hh)
except SR.Unreadable:
no("evidence_invalid")
if out is not None:
if sha(out) != e["output_samples"]:
no("assistive_recompute_mismatch")
status = "proved"
else:
status = "asserted"
subject = sha(decoded[h]) if kind == "object" and h in decoded else h if kind == "samples" else None
if subject is not None and subject != e["output_samples"]:
no("samples_mismatch")
if case["policy"]["assistive"] == "require-proof" and status != "proved":
no("assistive_not_proved")
st["claims"].append({"link": c["link"].hex(), "class": c["class"], "source": c["source"], "c2pa": statement(c), "assistive": status})
# Step 3: §7.2 and §7.6 for the markings.
for i, m in enumerate(markings):
def no(reason):
raise Refuse(reason, ["marking", i])
c = by_link.get(m["origin"])
if c is None:
no("claim_unknown")
if c["subject"] != ("object", m["subject"]) or payload_of(c) != m["payload"]:
no("marking_mismatch")
e = m["embedding"]
try:
registered = scheme_check(m["scheme"], m["scheme_digest"], e)
except Refuse as r:
no(r.reason)
status = "stated"
if registered and m["subject"] in unmarked:
px = unmarked[m["subject"]]
if len(px) != e["bytes"] or sha(px) != e["unmarked"]:
no("mark_input_mismatch")
if not recomputes(px, e, m["payload"], registered):
no("mark_recompute_mismatch")
status = "proved"
if e is not None and m["subject"] in decoded and sha(decoded[m["subject"]]) != e["marked"]:
no("samples_mismatch")
if prove and status != "proved":
no("layer_not_proved")
st["markings"].append({"subject": m["subject"].hex(), "scheme": m["scheme"], "status": status, "c2pa": marking_statement(m)})
# §7.3 and §7.6 for the detections.
for i, d in enumerate(detections):
def no(reason):
raise Refuse(reason, ["detection", i])
claim = next((c for c in claims if c["subject"] == ("object", d["subject"])), None)
marking = next((m for m in markings if m["subject"] == d["subject"]), None)
if marking is not None and (marking["scheme"], marking["scheme_digest"]) != (d["scheme"], d["scheme_digest"]):
no("detection_mismatch")
r = d["read"]
try:
registered = scheme_check(d["scheme"], d["scheme_digest"], r)
except Refuse as x:
no(x.reason)
rerun = "not-rerun"
if r is not None and d["subject"] in decoded:
px = decoded[d["subject"]]
if len(px) != r["bytes"] or sha(px) != r["samples"]:
no("samples_mismatch")
if registered:
if not reproduces(px, r, d["payload"], registered):
no("detection_irreproducible")
rerun = "reproduced"
if prove and rerun != "reproduced":
no("layer_not_proved")
if d["payload"] is None:
if marking is not None:
no("watermark_absent")
result = "unmarked"
elif claim is None:
result = "not-checked"
elif d["payload"] == payload_of(claim):
result = "agree"
elif inherits(claim, d["payload"], by_link):
result = "agree-inherited"
else:
no("layers_disagree")
st["detections"].append({"subject": d["subject"].hex(), "result": result, "rerun": rerun})
if case["policy"]["dual_layer"] == "required":
for i, c in enumerate(claims):
if c["class"] == "synthesized" and c["subject"][0] == "object" and not any(m["origin"] == c["link"] for m in markings):
raise Refuse("layer_missing", ["claim", i])
# Step 4.
for i, r in enumerate(reviews):
c = by_link.get(r["origin"])
if c is None:
raise Refuse("claim_unknown", ["review", i])
if c["subject"] != ("object", r["subject"]) or c["class"] != "synthesized":
raise Refuse("review_subject_mismatch", ["review", i])
# Step 5.
room = case["room"]
if room is not None:
chains = [[bytes.fromhex(e) for e in ch] for ch in room["chains"]]
grants = [bytes.fromhex(g) for g in room["grants"]]
out = LRV.verify_room(bytes.fromhex(room["descriptor"]), grants, room["key_log"], chains)
if out["outcome"] == "refuse":
raise Refuse(out["reason"], ["room"] + list(out["at"]))
st["room"] = out["outcome"]
d = LRV.read_descriptor(bytes.fromhex(room["descriptor"]))
held, _ = LRV.check_grants(d, grants)
by_digest = {g["digest"]: g for g in held}
current = {}
for c_i, r_i, under in out["order"]:
if under is not None:
continue
ev = LEV.read_record(chains[c_i][r_i])
if not 0x10 <= ev["kind"] <= 0x16:
continue
subject, x = ev["payload"][:32], LRV.read_detail(ev["kind"], ev["payload"][32:])
if ev["kind"] == 0x10:
current[subject] = x["grant"]
continue
if ev["kind"] == 0x14:
current[subject] = x["new"]
continue
if ev["kind"] == 0x11:
current.pop(subject, None)
continue
if ev["kind"] not in (0x12, 0x15):
continue
g = by_digest.get(current.get(subject))
model = None
for k, v in (LRV.read_attrs(g["attrs"]) or []) if g else []:
if k == "ai":
model = bytes.fromhex(v)
if model is None:
continue
owner = subject.hex() if not x["slot"] else "_" + x["slot"]
media = {1: "audio", 2: "video", 3: "screen"}[x["media"]]
for layer in range(x["layers"]):
t = track_digest(d["room"], owner, media, layer)
found = next((c for c in claims if c["subject"] == ("track", t) and c["class"] == "synthesized" and c["step"] == "generate"
and any(m["digest"] == model for m in c["models"])
and (c["signer"] == subject or LRV.is_room_key(d, c["signer"]))), None)
if found is None:
raise Refuse("ai_track_unlabelled", ["room", "record", c_i, r_i])
st["ai_tracks"].append({"track": t.hex(), "claim": found["link"].hex()})
# Step 6.
for j, p in enumerate(log):
ok = p["seq"] == j and p["session"] == log[0]["session"] and p["signer"] == log[0]["signer"]
ok = ok and (p["prev"] is None if j == 0 else (p["prev"] == log[j - 1]["link"] and p["at"] >= log[j - 1]["at"]))
if not ok:
raise Refuse("player_chain_broken", ["player", j])
for j, p in enumerate(log):
e = p["entry"]
if e["type"] == "present":
c = by_link.get(e["origin"]) if e["origin"] is not None else None
if c is not None and (c["subject"] != ("object", e["subject"]) or e["class"] != c["class"]):
raise Refuse("present_mismatch", ["player", j])
if e["output"] is not None and e["subject"] in decoded and sha(decoded[e["subject"]]) != e["output"]:
raise Refuse("present_output_irreproducible", ["player", j])
elif e["type"] == "review":
r = next((r for r in reviews if r["link"] == e["review"]), None)
if r is None:
raise Refuse("claim_unknown", ["player", j])
last = next((q["entry"] for q in reversed(log[:j]) if q["entry"]["type"] == "present" and q["entry"]["subject"] == e["subject"]), None)
if last is None or r["subject"] != e["subject"]:
raise Refuse("review_subject_mismatch", ["player", j])
if r["verdict"] == "rejected":
raise Refuse("review_rejected", ["player", j])
if last["tier"] != r["tier"]:
raise Refuse("review_path_not_exact", ["player", j])
if last["output"] != r["reconstruction"]:
raise Refuse("review_reconstruction_mismatch", ["player", j])
st["reviews_presented"].append({"entry": j, "review": e["review"].hex()})
if policy is not None:
st["visual_icon"] = ICONS.get(policy["visual"].hex())
for j, p in enumerate(log):
e = p["entry"]
if e["type"] == "shown" and (policy["audio"] if e["audio"] else policy["visual"]) != e["asset"]:
raise Refuse("label_asset_mismatch", ["player", j])
def shown_in(audio, lo, hi):
return next((k for k, q in enumerate(log) if q["entry"]["type"] == "shown" and q["entry"]["audio"] == audio and lo <= q["at"] <= hi), None)
last = {False: None, True: None}
for j, p in enumerate(log):
e = p["entry"]
if e["type"] != "present" or e["class"] not in policy["classes"]:
continue
audio = e["media"] == "audio"
hi = p["at"] + policy["grace_us"]
prev = last[audio]
if prev is None:
trigger, k = "first-exposure", shown_in(audio, 0, hi)
else:
resumes = [q["at"] for q in log[prev + 1:j] if q["entry"]["type"] == "resume"]
if resumes:
trigger, k = "resume", shown_in(audio, resumes[-1], hi)
elif policy["interval_us"] is not None:
trigger, k = "interval", shown_in(audio, max(p["at"] - policy["interval_us"], 0), hi)
else:
last[audio] = j
continue
if k is None:
raise Refuse("label_not_shown", ["player", j])
st["labels"].append({"entry": j, "trigger": trigger, "shown": k})
last[audio] = j
return st
# ---------------------------------------------------------------- the extension's tables
def section(text, head):
at = text.index("\n" + head)
nxt = re.search(r"\n#{2,3} ", text[at + 1:])
return text[at: at + 1 + nxt.start()] if nxt else text[at:]
with open(EXTENSION, encoding="utf-8") as _f:
_TEXT = _f.read()
ICONS = {}
for _line in section(_TEXT, "### 10.2 ").splitlines():
_m = re.fullmatch(r"\| `(LABEL_AI[^`]*)` \| `([0-9a-f]{64})` \|", _line.strip())
if _m:
ICONS[_m.group(2)] = _m.group(1)
REASONS = [l.split("`")[1] for l in section(_TEXT, "## 12. Reasons").splitlines() if l.startswith("| `")]
# §13: the codes only the scheme's corpus gives.
_m = re.search(r"The codes ((?:`[a-z_]+`(?:, | and )?)+) need frames", " ".join(section(_TEXT, "## 13. Conformance").split()))
MARK_ONLY = re.findall(r"`([a-z_]+)`", _m.group(1)) if _m else []
# §7.4: the registered scheme's digest.
_d = re.search(r"its digest is\s+`([0-9a-f]{64})`", section(_TEXT, "### 7.4 "))
SCHEME_DIGEST = bytes.fromhex(_d.group(1)) if _d else b""
# §7.9.1: the tiled scheme's digest.
_d = re.search(r"its digest is\s+`([0-9a-f]{64})`", section(_TEXT, "#### 7.9.1 "))
SCHEME_DIGEST_S = bytes.fromhex(_d.group(1)) if _d else b""
# §7.10.1: the audio scheme's digest.
_d = re.search(r"its digest is\s+`([0-9a-f]{64})`", section(_TEXT, "#### 7.10.1 "))
SCHEME_DIGEST_A = bytes.fromhex(_d.group(1)) if _d else b""
SCHEME_DIGESTS = {MARK.SCHEME: SCHEME_DIGEST, MARK.SCHEME_S: SCHEME_DIGEST_S, AMARK.SCHEME: SCHEME_DIGEST_A}
def outcome(v, classes):
try:
return {"outcome": "accept", "statements": verify(v, classes)}
except Refuse as e:
return {"outcome": "refuse", "reason": e.reason, "at": e.at}
def stated(v):
return {k: v[k] for k in ("outcome", "reason", "at", "statements") if k in v}
def check(doc, classes):
"""Failures of a corpus document: an outcome other than the one stated, or a table that
differs from the extension's."""
fails = []
pins = {d["sha256"]: d["file"] for d in doc["eu_icons"]}
if pins != ICONS or len(doc["eu_icons"]) != len(ICONS):
fails.append("the corpus's EU icons differ from §10.2's table")
for v in doc["vectors"]:
try:
got = outcome(v, classes)
except Exception as e: # noqa: BLE001 — a malformed copy fails, whatever it breaks
got = {"outcome": "unreadable: %r" % (e,)}
if got != stated(v):
fails.append("%s: %s" % (v["name"], json.dumps(got)[:300]))
return fails
def flip_hex_digit(s, at):
c = "0" if s[at] != "0" else "1"
return s[:at] + c + s[at + 1:]
def mutations(doc):
"""Copies of the corpus, each with one defect a conforming verifier must catch."""
vn = {v["name"]: k for k, v in enumerate(doc["vectors"])}
def mutate(label, f):
d = copy.deepcopy(doc)
f(d["vectors"])
return label, d
chain, gen, room, cert, sink = vn["chain"], vn["generated"], vn["ai_room"], vn["certified"], vn["sink_upscale"]
def retext(vs, i, key, k, f):
"""Rewrite record k of list `key` of vector i as JSON, applying f to its value."""
v = json.loads(vs[i][key][k])
f(v)
vs[i][key][k] = json.dumps(v)
def decoded_byte(vs):
d = vs[chain]["decoded"][0]
d["samples"] = flip_hex_digit(d["samples"], 0)
def upscale_digit(vs):
retext(vs, chain, "claims", 2, lambda v: v["evidence"].update(output_samples=flip_hex_digit(v["evidence"]["output_samples"], 5)))
def detection_class(vs):
retext(vs, gen, "detections", 0, lambda v: v.update(payload="00" + v["payload"][2:]))
def prior_byte(vs):
p = vs[sink]["priors"][0]
vs[sink]["priors"][0] = p[:40] + ("0" if p[40] != "0" else "1") + p[41:]
return [
mutate("a byte of the sink's prior: the prior its evidence names is not held", prior_byte),
mutate("a byte of the decoded picture the sink upscaled",
lambda vs: vs[sink]["decoded"][0].update(samples=flip_hex_digit(vs[sink]["decoded"][0]["samples"], 2))),
mutate("the sink's upscale stated as not recomputed",
lambda vs: vs[sink]["statements"]["claims"][2].update(assistive="not-recomputed")),
mutate("a digit of the upscale's signature", lambda vs: retext(vs, chain, "claims", 2, lambda v: v.update(sig=flip_hex_digit(v["sig"], 9)))),
mutate("the upscale's output digest changed", upscale_digit),
mutate("a byte of the reconstruction's decoded samples", decoded_byte),
mutate("the reconstruction's claim withheld from the chain", lambda vs: vs[chain]["claims"].pop(1)),
mutate("the generated video's detection reading class captured", detection_class),
mutate("the generated video's marking withheld", lambda vs: vs[gen]["markings"].pop(0)),
mutate("the composite's source stated compositeCapture",
lambda vs: vs[gen]["statements"]["claims"][4].update(source="compositeCapture")),
mutate("the transcode's statement stated as created",
lambda vs: vs[gen]["statements"]["claims"][5].update(c2pa=vs[gen]["statements"]["claims"][1]["c2pa"])),
mutate("the AI host's track claim withheld", lambda vs: vs[room].update(claims=[])),
mutate("the AI host's grant withheld from the room", lambda vs: vs[room]["room"]["grants"].pop(0)),
mutate("the certified log's first label withheld", lambda vs: vs[cert]["player"].pop(0)),
mutate("the label after the break stated as an interval",
lambda vs: vs[cert]["statements"]["labels"][1].update(trigger="interval")),
mutate("the review stated at generation-equivalence",
lambda vs: retext(vs, cert, "reviews", 0, lambda v: v.update(tier="generation-equivalence"))),
mutate("the presented video's decoded frame changed",
lambda vs: vs[cert]["decoded"][0].update(samples=flip_hex_digit(vs[cert]["decoded"][0]["samples"], 3))),
mutate("the policy's visual stated as another pinned icon",
lambda vs: vs[cert]["statements"].update(visual_icon="LABEL_AI_black.svg")),
mutate("the first refusal stated at another place", lambda vs: vs[next(k for k, v in enumerate(vs) if v["outcome"] == "refuse")].update(at=["claim", 1])),
("an EU icon's pin changed", dict(copy.deepcopy(doc), eu_icons=[dict(doc["eu_icons"][0], sha256="00" * 32)] + doc["eu_icons"][1:])),
]
def main(corpus_dir):
fails = []
st = ED._self_test()
print(("PASS" if not st else "FAIL") + " Ed25519 strict rule self-test " + ",".join(st))
if st:
fails.append("ed25519 self-test")
with open(REGISTRY, encoding="utf-8") as f:
classes = {c["capability"]: c["replicate"] for c in json.load(f)["capabilities"]}
with open(os.path.join(corpus_dir, "vectors.json"), encoding="utf-8") as f:
doc = json.load(f)
if len(ICONS) != 24:
fails.append("§10.2's table does not read as 24 pinned icons")
reasons_seen = set()
for v in doc["vectors"]:
try:
got = outcome(v, classes)
except Exception as e: # noqa: BLE001
got = {"outcome": "unreadable: %r" % (e,)}
ok = got == stated(v)
if v.get("reason"):
reasons_seen.add(v["reason"])
print(("PASS" if ok else "FAIL") + " vector %s: %s" % (v["name"], got["outcome"] + (" %s at %s" % (got["reason"], got["at"]) if "reason" in got else "")))
if not ok:
fails.append("%s: %s, the corpus states %s" % (v["name"], json.dumps(got)[:400], json.dumps(stated(v))[:400]))
table = check(doc, classes)
if any("EU icons" in t for t in table):
fails.append("the corpus's EU icons differ from §10.2's table")
missing = [r for r in REASONS if r not in reasons_seen and r not in MARK_ONLY]
if len(MARK_ONLY) != 3 or not set(MARK_ONLY) <= set(REASONS) or len(SCHEME_DIGEST) != 32 or len(SCHEME_DIGEST_S) != 32:
fails.append("§13's codes of the scheme's corpus, or §7.4's digest, do not read")
if missing:
fails.append("no vector refused with %s" % ", ".join(missing))
accepted = {v["name"] for v in doc["vectors"] if v["outcome"] == "accept"}
want = {"chain", "assistive_asserted", "sink_upscale", "generated", "ai_room", "certified"}
if not want <= accepted:
fails.append("vectors not accepted: %s" % ", ".join(sorted(want - accepted)))
print("%s every reason of §12 but %s refuses a vector, and every case of §13 is accepted"
% ("PASS" if not missing and want <= accepted else "FAIL", ", ".join(MARK_ONLY)))
for label, d in mutations(doc):
caught = check(d, classes)
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") + " synthesis corpus: %d vectors" % len(doc["vectors"]))
return 1 if fails else 0
def claim_main(path, samples=None):
"""One claim, as §11 verifies a case that holds it alone; with its samples, its subject held to
them."""
with open(REGISTRY, encoding="utf-8") as f:
classes = {c["capability"]: c["replicate"] for c in json.load(f)["capabilities"]}
with open(path, encoding="utf-8") as f:
text = f.read()
case = {"claims": [text], "markings": [], "detections": [], "reviews": [], "label_policy": None, "player": [],
"objects": [], "decoded": [], "unmarked": [], "priors": [], "room": None,
"policy": {"dual_layer": "when-given", "assistive": "accept-asserted", "second_layer": "accept-stated"}}
got = outcome(case, classes)
if got["outcome"] == "accept":
got = {"outcome": "accept", "statement": got["statements"]["claims"][0]}
if samples is not None:
with open(samples, "rb") as f:
digest = hashlib.sha256(f.read()).digest()
subject = read(text, "wai-origin", ORIGIN, origin_fields)["subject"]
if subject != ("samples", digest):
got = {"outcome": "refuse", "reason": "subject_not_these_samples"}
print(json.dumps(got, sort_keys=True))
return 0 if got["outcome"] == "accept" else 1
if __name__ == "__main__":
if len(sys.argv) in (3, 4) and sys.argv[1] == "claim":
sys.exit(claim_main(*sys.argv[2:]))
if len(sys.argv) != 2:
print("usage: synthesis_verify.py <live-conformance/synthesis> | claim <claim.json> [<samples>]", file=sys.stderr)
sys.exit(2)
sys.exit(main(sys.argv[1]))