open-standards/wai/http-conformance/http_receipts.py
Transaction Science 793ed1f2dc
All checks were successful
wai-app and Create / wai-app · does this change touch it (pull_request) Successful in 7s
wai-audit collector and player integration / does this change touch it (pull_request) Successful in 10s
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Successful in 12s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (pull_request) Successful in 14s
WAI operation catalogue / does this change touch it (pull_request) Successful in 13s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (pull_request) Successful in 13s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 14s
WAI examples / WAI examples · does this change touch them (pull_request) Successful in 7s
WAI HTTP delivery / WAI HTTP · does this change touch it (pull_request) Successful in 6s
wai-fleet service / does this change touch it (pull_request) Successful in 14s
WAI neural sink gates / does this change touch it (pull_request) Successful in 13s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 12s
wai-quantum for JavaScript / does this change touch it (pull_request) Successful in 9s
wai-ledger service / does this change touch it (pull_request) Successful in 14s
wai-ops-gpu integer operators on GPUs / does this change touch it (pull_request) Successful in 14s
wai-key-proxy / does this change touch it (pull_request) Successful in 16s
WAI conformance / does this change touch it (pull_request) Successful in 31s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 32s
wai-quantum studio and desktop / does this change touch it (pull_request) Successful in 6s
WAI energy receipts / does this change touch it (pull_request) Successful in 33s
wai-sink-android native sink library / does this change touch it (pull_request) Successful in 8s
wai-sink-swift native sink package / does this change touch it (pull_request) Successful in 7s
WAI Studio / does this change touch it (pull_request) Successful in 8s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 9s
wai-web wasm32 / does this change touch it (pull_request) Successful in 25s
WAI HTTP delivery / HTTP corpus · every patch and mutation in exactly one shard (pull_request) Successful in 5s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (pull_request) Successful in 55s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (pull_request) Successful in 1m11s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m58s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (pull_request) Successful in 1m22s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (pull_request) Successful in 1m51s
wai-audit collector and player integration / collector · linux (pull_request) Successful in 1m32s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (pull_request) Successful in 2m31s
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Successful in 1m51s
WAI neural sink gates / the exact JPEG-AI decoder reports its path (pinned inputs, required) (pull_request) Successful in 2m2s
WAI conformance / fast integer kernels, every x86_64 kernel set (AVX-512 silicon) (pull_request) Successful in 3m36s
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 4m32s
wai-fleet service / service · linux (pull_request) Successful in 2m42s
wai-key-proxy / proxy · linux (pull_request) Successful in 57s
wai-pack / wai-pack · linux (pull_request) Successful in 3m19s
WAI conformance / faces from keypoints under wasm32-wasip2 (wasmtime) (pull_request) Successful in 1m30s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 27s
WAI examples / WAI examples · android-app · linux-arm64 (pull_request) Successful in 5m59s
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Successful in 4m11s
WAI operation catalogue / the catalogue and every door · linux (pull_request) Successful in 5m56s
wai-ledger service / service · linux (pull_request) Successful in 1m50s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (pull_request) Successful in 7m18s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (pull_request) Successful in 1m7s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 3m48s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (pull_request) Successful in 7m43s
wai-quantum for JavaScript / javascript · linux (pull_request) Successful in 3m41s
wai-fleet service / service · macos (pull_request) Successful in 48s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 3m39s
wai-quantum studio and desktop / desktop app · linux (pull_request) Successful in 4m54s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 3m2s
wai-pack / wai-pack · macos (pull_request) Successful in 3m27s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 5m55s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (pull_request) Successful in 13m52s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 5m28s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 16m34s
WAI Studio / Studio in WebKit · linux (pull_request) Successful in 11m38s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 27s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 1m7s
wai-ledger service / service · macos (pull_request) Successful in 1m6s
wai-quantum studio and desktop / desktop app · windows (pull_request) Successful in 9m35s
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Successful in 10m56s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 17m20s
WAI HTTP delivery / HTTP corpus · falsify patches 0/3 (pull_request) Successful in 18m48s
WAI examples / WAI examples · portable · linux (pull_request) Successful in 24m50s
WAI HTTP delivery / HTTP corpus · falsify patches 1/3 (pull_request) Successful in 18m38s
WAI HTTP delivery / HTTP corpus · verified both ways, and the differential (pull_request) Successful in 2m31s
wai-quantum studio and desktop / desktop app · macos (pull_request) Successful in 7m3s
WAI HTTP delivery / cache rules · controls and labelled caches (pull_request) Successful in 1m46s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 8m4s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m47s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 17m16s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (pull_request) Successful in 7m47s
WAI HTTP delivery / HTTP corpus · falsify mutations 0/3 (pull_request) Successful in 34m23s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 21s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 16s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 7m5s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 18s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 37m20s
WAI Studio / Studio in Chromium and Firefox · macos (pull_request) Successful in 10m52s
WAI HTTP delivery / HTTP corpus · falsify patches 2/3 (pull_request) Successful in 18m17s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 11m18s
WAI HTTP delivery / HTTP corpus · falsify mutations 1/3 (pull_request) Successful in 39m59s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 15s
WAI HTTP delivery / HTTP corpus · falsify mutations 2/3 (pull_request) Successful in 40m12s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 50s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 2m42s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (pull_request) Successful in 3m47s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (pull_request) Successful in 17m30s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 18m34s
WAI conformance / each feature of the independent verifiers' steps compiles alone (pull_request) Successful in 14m28s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 14m15s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 30m4s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 1h18m5s
wai/one-way-delivery, converted to the layout of the fix: no shared append point
The change of wai/one-way-delivery (f1258784d..987625394, 28 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.

- 25240c4f0 wai: timeliness — when each object of a group is of use, and whether each holder held it in time
- c84992362 wai: timeliness_verify.py — the timeliness corpus's second implementation
- 44036fcdc wai: timeliness in the registry, SPEC §5, the changelog and CI; http_receipts.py checks its claims
- 27ce6754c wai: claim_verify.py lists not-held-by-deadline, and the claim corpus holds a session that switches for it
- 73d8d8eb9 wai: timeliness corpus — each holder's own line in key order, a holder's earliest fallback, and §7.2–§7.3's unreached plan rules
- 64c735f0c wai: the files timeliness changes are clippy-clean — a let chain, and the fetch callback's type allowed
- b72dc36bf wai: rung price — what delivering a rung costs, by bearer and direction, and which option a comparer takes
- 4e905e213 wai: rung_price_verify.py — the rung price corpus's second implementation
- 711fa6126 wai: rung price in the registry, SPEC §5, the changelog and CI
- 8ab70654e wai: rung price corpus — a refusal for every form rule 1 and §6.1 set, and the neighbour order a plan gives
- 1bc465959 wai: rung price ranks energy by checked evidence — a class counts only on the seller's own per-hop records, current and under a confirmed key
- 9e97997d9 wai: rung_price — the evidence text bound at its exact sizes, and a key confirmed for another class confirming nothing
- 8f1168683 wai: rung price corpus — a key confirmed for another class, the uncertainty product, each edge and order of the evidence verdict, and the evidence a plan read
- 7330e26ff wai: rung price — §6.1 links jwp-receipts at its first mention, and §10 says a record's age is its room's word
- 12c099457 wai: cache authority — who served what a node holds, and on whose authority
- f8f5d3e09 wai: cache_authority_verify.py — the cache authority corpus's second implementation
- 354146619 wai: cache authority in the registry, SPEC §5, the changelog and CI; its corpus holds what §12 states
- 55c7f3031 wai: timeliness §6 reads holdings records alone; cache-authority §9 derives the lines that read every kind
- b8fc6f02b wai: cache authority's lines read records as timeliness does, its deadline claim meets the envelopes held, and its corpus fixes each order and instant
- 91c5291cf wai: the files cache authority changes are clippy-clean — a boxed envelope, the base's names named once, slices borrowed
- 8e479daff wai: a held envelope boxes both its forms, so neither outweighs the other
- 3ec8da807 wai: impression aggregates — ad presentations counted over a bearer with no return path
- 0433a35c3 wai: impression_aggregate_verify.py — the impression aggregates corpus's second implementation
- 7d8e150e4 wai: impression aggregates in the registry, SPEC §5, the changelog and CI; its corpus states each receipt whole and a control for every malformed file
- d5e8ace02 wai: impression aggregates' size bounds are checked at their edge, and §4.3's decides no outcome
- 4bc8e13bd wai: impression aggregates' table totals are exact, and a receipt given twice is checked when either occurrence was
- 3c9f20524 wai: key_log's two result enums keep their public form, and say so to clippy
- 987625394 wai: Item::with_authority and Count::instance_received build only with the feature that calls each

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 01:45:32 -05:00

908 lines
39 KiB
Python

"""The receipt, key, grant and parameter-set side of http_verify.py (http-delivery §8.1-§8.5,
§10.5).
Written from the extension text. It shares no code with wai-rs. What jwp-receipts defines it
takes from the corpora's own second verifiers, each written from that text and sharing no
code with wai-rs either: group and object receipts, properties and receipt chains from
../jwp-conformance/jwp_verify.py, claims and the strict JSON reading from
../claim-conformance/claim_verify.py, and key logs, anchoring, confirmation and module
grants from ../signer-conformance/signer_verify.py. Those use the `cryptography` package for
Ed25519's equation, so these cases need it; http_verify.py loads this file only for them.
What http-delivery adds is written here: receipt files named by their link hash, the index
read item by item, the parent walk and its bound, what a client holds of the object (the
whole envelope, the cid a head proof established, or nothing), claims by subject and
parent, anchoring only from the origin's own location, logs through caches, grants files as
locators, and parameter sets held only by their pins and jwp-receipts §7.9.
"""
import hashlib, os, sys
HERE = os.path.dirname(os.path.abspath(__file__))
for _d in ("claim-conformance", "jwp-conformance", "signer-conformance", "live-conformance"):
sys.path.insert(0, os.path.join(HERE, "..", _d))
import claim_verify as CV # noqa: E402
import jwp_verify as JV # noqa: E402
import signer_verify as SV # noqa: E402
MUTATE = None # set by http_verify.py
_LISTED = []
ORIGIN = "https://origin.example"
KEY_LOG_PATH = "/.well-known/wai-keys"
RCPT = "objects/rcpt"
WALK_BOUND = 64 # parents fetched for one group, at most (§8.1)
MAX_RECEIPT_FILE_BYTES = 1 << 20 # a receipt file's bytes, at most (§8.1)
MAX_INDEX_BYTES = 4 << 20 # an index's bytes, at most (§8.2)
MAX_INDEX_ITEMS = 256 # delivery items an index lists, at most
MAX_INDEX_CLAIMS = 256 # claims an index lists, at most
FETCH_BUDGET = 1024 # receipt files fetched for one index, at most
MAX_INDEX_FETCH_BYTES = 64 << 20 # bytes of receipt files read for one index, at most
MAX_KEY_LOG_BYTES = 1 << 20 # a key log's bytes, at most (§8.4)
INDEX_KIND = "wai-receipt-index"
GRANTS_KIND = "wai-module-grants"
ACQUIRED = "2026-10-06T00:00:00Z"
ONE_FILE = {"wai.neural.int_mlicpp"} # the capabilities the corpus pins: each decodes against one file
class Refused(Exception):
def __init__(self, code, step=None, walked=None, stop=None):
super().__init__(code)
self.code, self.step, self.walked, self.stop = code, step, walked, stop
def json(self):
out = {"error": self.code, "step": self.step}
if self.code == "parent_not_held" and self.walked is not None:
out["walked"], out["stop"] = self.walked, self.stop
return out
def pnh(walked, stop):
return Refused("parent_not_held", None, walked, stop)
class Fault(Exception):
"""A fault of the case itself: no outcome."""
def edit_text(t, edits):
for e in edits or []:
if e[0] not in t:
raise Fault("an edit's text is not in the file")
t = t.replace(e[0], e[1], 1)
return t
def read_file(d, path):
return open(os.path.join(d, path), "rb").read()
def utf8(b):
try:
return b.decode("utf-8")
except UnicodeDecodeError:
return None
# ------------------------------------------------------------------ receipt files (§8.1)
def read_group_file(name, b):
"""A group receipt's file: JSON as §3.1 step 1 reads it, §3 step 1, then the name.
Returns the raw receipt and its reading."""
text = utf8(b)
if text is None:
raise Refused("malformed", 1)
try:
raw = CV.parse_strict(text)
except ValueError:
raise Refused("malformed", 1)
try:
g = JV.read_group(raw)
except JV.Refuse as r:
raise Refused(r.reason, r.step)
if not g["labelled"] and MUTATE != "legacy-admitted":
# The legacy link hash does not cover the parent: the name does not fix the bytes.
raise Refused("receipt_legacy_group")
if MUTATE != "rcpt-name-unchecked" and JV.link(g).hex() != name:
raise Refused("receipt_name_mismatch")
return raw, g
def read_claim_file(name, b):
"""A claim's file: §3.1 steps 1-2, then the name. The verdict of steps 3-4, run later
in order, is returned beside the claim (None: they pass)."""
text = utf8(b)
if text is None:
raise Refused("malformed", 1)
later = None
try:
CV.verify_claim(text)
except CV.Reject as r:
if r.step <= 2:
raise Refused(r.reason, r.step)
later = r
c = CV.parse_strict(text)
if CV.link_hash(c) != name:
raise Refused("receipt_name_mismatch")
return c, later
# ------------------------------------------------------------------ fetching (§8.1, §8.2)
class Fetcher:
"""One index check's receipt files: each name fetched once and what was served kept,
at most a budget of fetches (never above FETCH_BUDGET) and of bytes read
(MAX_INDEX_FETCH_BYTES), none over the file cap. Each file is read only as far as
what remains of the bytes and of its cap plus one byte; the file that reaches the end
of the bytes is refused, and once they are spent no further file is fetched."""
PAST = object()
def __init__(self, get, budget):
self.get_raw, self.memo, self.fetched, self.bytes = get, {}, 0, 0
self.budget = min(budget, FETCH_BUDGET)
def get(self, h):
if h not in self.memo or MUTATE == "memo-off":
if self.fetched >= self.budget and MUTATE != "budget-unchecked":
raise Refused("receipt_fetch_budget_exhausted")
remaining = MAX_INDEX_FETCH_BYTES - self.bytes
if remaining <= 0 and MUTATE != "bytes-unchecked":
raise Refused("receipt_fetch_bytes_exhausted")
self.fetched += 1
whole = MUTATE in ("bytes-unchecked", "bytes-read-whole")
limit = MAX_RECEIPT_FILE_BYTES + 1 if whole else min(remaining, MAX_RECEIPT_FILE_BYTES + 1)
b = self.get_raw(h)
if b is not None:
b = b[:limit]
self.bytes += len(b)
past = self.bytes > MAX_INDEX_FETCH_BYTES if MUTATE == "bytes-read-whole" else len(b) == remaining
if past and MUTATE != "bytes-unchecked":
b = Fetcher.PAST
self.memo[h] = b
b = self.memo[h]
if b is None:
raise Refused("receipt_not_served")
if b is Fetcher.PAST:
raise Refused("receipt_fetch_bytes_exhausted")
if len(b) > MAX_RECEIPT_FILE_BYTES and MUTATE != "caps-unchecked":
raise Refused("receipt_too_large")
return b
# ------------------------------------------------------------------ the index (§8.2)
def read_property(p):
"""moq-streaming-format §5's property, as it reads: exactly its five members."""
keys = {"content_hash", "joules_micro", "acquisition", "acquisition_evidence", "group_receipt"}
if not (isinstance(p, dict) and set(p) == keys and CV.is_hex(p["content_hash"], 32) and JV.u64(p["joules_micro"])
and isinstance(p["acquisition"], str) and CV.is_hex(p["acquisition_evidence"]) and CV.is_hex(p["group_receipt"], 32)):
raise Refused("property_invalid")
return p
def read_index(b):
if len(b) > MAX_INDEX_BYTES and MUTATE != "caps-unchecked":
raise Refused("receipt_index_too_large")
bad = Refused("receipt_index_malformed")
text = utf8(b)
if text is None:
raise bad
try:
v = CV.parse_strict(text)
except ValueError:
raise bad
if not isinstance(v, dict) or set(v) != {"kind", "object", "delivery", "claims"}:
raise bad
if v["kind"] != INDEX_KIND or not CV.is_hex(v["object"], 32):
raise bad
if not isinstance(v["delivery"], list) or not isinstance(v["claims"], list):
raise bad
if (len(v["delivery"]) > MAX_INDEX_ITEMS or len(v["claims"]) > MAX_INDEX_CLAIMS) and MUTATE != "caps-unchecked":
raise Refused("receipt_index_too_large")
items = []
for it in v["delivery"]:
try:
if not isinstance(it, dict) or set(it) != {"property", "object_receipt"}:
raise Refused("receipt_index_malformed")
p = read_property(it["property"])
try:
o = JV.read_object(it["object_receipt"])
except JV.Refuse as r:
raise Refused(r.reason, r.step)
items.append((p, o, it["object_receipt"]))
except Refused as r:
items.append(r)
claims = [c if CV.is_hex(c, 32) else Refused("receipt_index_malformed") for c in v["claims"]]
return v["object"], items, claims
# ------------------------------------------------------------------ chains and the walk (§8.1)
class Chains:
"""The client's chains (jwp-receipts §3 step 3): every group offered so far, in order,
run through jwp_verify.chain again for each offer; the last verdict is the offer's."""
def __init__(self, join):
self.seq, self.join = [], join
def admit(self, raw):
self.seq.append(raw)
out = JV.chain({"groups": self.seq, "guard": False, "recovery": {"join": True} if self.join else {}})
last = out[len("chain: "):].split(" / ")[-1]
if last.startswith("reject"):
f = dict(x.split("=", 1) for x in last.split()[1:])
raise Refused(f["reason"], int(f["step"]) if "step" in f else None)
return last
def admit_walking(ch, raw, g, fetcher, bound):
try:
return ch.admit(raw), 0
except Refused as r:
if r.code != "parent_not_held":
raise
if MUTATE == "walk-skipped":
return "accept", 0
if MUTATE != "walk-unclamped":
bound = min(bound, WALK_BOUND)
pending, link, fetched = [], g["parent_receipt_hash"], 0
while True:
if link is None:
raise pnh(fetched, "parent_not_held")
if fetched == bound:
raise pnh(fetched, "bound")
try:
b = fetcher.get(link)
except Refused as r:
raise pnh(fetched + 1 if r.code == "receipt_too_large" else fetched, r.code)
fetched += 1
try:
praw, p = read_group_file(link, b)
except Refused as r:
raise pnh(fetched, r.code)
if p["signer_pubkey"] != g["signer_pubkey"] or p["track_alias"] != g["track_alias"]:
raise pnh(fetched, "another_chain")
try:
ch.admit(praw)
break
except Refused as r:
if r.code != "parent_not_held":
raise pnh(fetched, r.code)
link = p["parent_receipt_hash"]
pending.append(praw)
for praw in reversed(pending):
try:
ch.admit(praw)
except Refused as r:
raise pnh(fetched, r.code)
return ch.admit(raw), fetched
# ------------------------------------------------------------------ delivery items (§8.2)
def reading(p, o, g):
"""The property against an object accepted under g (moq-streaming-format §5)."""
try:
c = JV.property_class(p)
except JV.Refuse as r:
raise Refused(r.reason)
if bytes.fromhex(p["content_hash"]) != o["content_hash"] or bytes.fromhex(p["group_receipt"]) != JV.link(g):
raise Refused("property_disagrees")
if not o["labelled"]:
return "unmetered" if c is None else "unsigned"
if o["class"] is None and c is None:
return "unmetered"
if o["class"] is not None and c is not None and o["class"] == c and o["joules_micro"] == p["joules_micro"]:
return f"signed class={c['label']} by={g['signer_pubkey']}"
raise Refused("property_disagrees")
def check_delivery(cid, item, held, fetcher, ch, bound):
p, o, raw_o = item
if p["content_hash"] != cid or o["content_hash"].hex() != cid:
raise Refused("receipt_not_about_object")
name = p["group_receipt"]
b = fetcher.get(name)
raw, g = read_group_file(name, b)
admission, walked = admit_walking(ch, raw, g, fetcher, bound)
if MUTATE == "nothing-as-delivered" and held is None:
held = (cid, "head-proof")
if held is not None and held[0] != o["content_hash"].hex():
raise Refused("content_hash_mismatch", 4)
try:
JV.inclusion(o, g)
except JV.Refuse as r:
raise Refused(r.reason, r.step)
if held is not None:
rd = reading(p, o, g)
else:
try:
JV.property_class(p)
except JV.Refuse as r:
raise Refused(r.reason)
rd = None
out = {"admission": admission, "walked": walked, "evidence": "delivered-verified" if held is not None else "inclusion-verified",
"established": held[1] if held is not None else None, "reading": rd}
return out, {"g": g, "o": o, "raw": raw_o, "held": held}
# ------------------------------------------------------------------ claims (§8.2)
def declaration_text(link, fetcher):
"""A declaration a claim names, fetched by its link and read as §8.1 reads every file
fetched by name (steps 1-2, then the name): not served is context_not_held; its steps
3-4 are step 6's, which reports a declaration that does not verify (context_invalid)."""
try:
b = fetcher.get(link)
except Refused as r:
if r.code == "receipt_not_served":
raise Refused("context_not_held", 6)
raise
if MUTATE != "declaration-unnamed":
read_claim_file(link, b)
t = utf8(b)
return t if t is not None else "\u0000" # not UTF-8: no claim reads from it (context_invalid)
class IndexGroup:
"""A group of the index as timeliness_verify.check_timeliness reads one: its receipt's link
hash, signer, leaf count and start (None where `group_start` is not a date-time), the
content hash of each object receipt listed under it here by position, each of which
passed inclusion, and no envelope."""
def __init__(self, g):
self.link = JV.link(g).hex()
self.signer = g["signer_pubkey"]
self.count = g["leaf_count"]
self.start = JV.seconds(g["group_start"])
self.included = {}
self.envelopes = {}
def step6(c, cid, passed, fetcher, exact_algs):
"""§3.1 step 6 over HTTP (§8.2): against the delivery items that passed and the
declarations the claim names."""
if MUTATE == "step6-skipped":
return
cls, b = c["class"], c["body"]
delivered = [v for v in passed if v["held"] is not None]
try:
if cls == "energy":
for v in delivered:
CV.check_context(c, {"object": v["raw"]})
if not delivered or b["oer_receipt"] is not None:
raise Refused("context_not_held", 6)
elif cls in ("measurement", "operating-point") and not (cls == "operating-point" and MUTATE == "operating-point-unchecked"):
v = next((v for v in delivered if JV.link(v["g"]).hex() == b["group"]), None)
if v is None:
raise Refused("figure_not_held" if cls == "measurement" else "context_not_held", 6)
fig = {k: v["raw"][k] for k in JV.CLASS_KEYS if k in v["raw"]}
fig.update(index=v["o"]["index"], joules_micro=v["o"]["joules_micro"])
view = {"profile": "moq-jwp:object", "identity": cid, "group": JV.link(v["g"]).hex(), "figures": [fig],
"signer": v["g"]["signer_pubkey"]}
CV.check_figure(c, CV.figure_view({"view": view}))
elif cls == "delivery-gap":
at = (b["track_alias"], b["group_id"], b["object_id"])
v = next((v for v in passed if (v["o"]["track_alias"], v["o"]["group_id"], v["o"]["object_id"]) == at), None)
if v is None:
raise Refused("context_not_held", 6)
CV.check_context(c, {"gap_object": {"track_alias": v["o"]["track_alias"], "group_id": v["o"]["group_id"],
"object_id": v["o"]["object_id"], "content_hash": v["o"]["content_hash"].hex()}})
elif cls == "lawful-basis" and b["record"] == "decision" and MUTATE != "decision-unchecked":
CV.check_context(c, {"declaration": declaration_text(bytes.fromhex(b["declaration"]).hex(), fetcher)})
elif cls == "binding-recovery" and MUTATE != "recovery-record-unchecked":
if not passed:
raise Refused("context_not_held", 6)
t = declaration_text(b["declaration"], fetcher)
signers = [v["g"]["signer_pubkey"] for v in passed]
try:
d = CV.parse_strict(t)
ds = d.get("signer_pubkey") if isinstance(d, dict) else None
except ValueError:
ds = None
pub = ds if ds in signers else signers[0]
CV.check_context(c, {"declaration": t, "publisher": pub, "exact_match_algs": exact_algs})
elif cls == "reconstruct-session" and MUTATE != "session-accepted":
raise Refused("context_not_held", 6)
elif cls in ("transmission", "reconciliation"):
# bearer-receipts §4.3, §5.3: against the group of an item its subject names, and a
# reconciliation also against the object receipts of the items listed under that
# group here; one naming an object this index does not list is context_not_held.
import bearer_verify as BV
v = next((v for v in passed if JV.link(v["g"]).hex() == c["subject"]), None)
if v is None:
raise Refused("context_not_held", 6)
if cls == "transmission":
BV.check_transmission(c, v["g"])
else:
objs = [w["o"] for w in passed if JV.link(w["g"]).hex() == c["subject"]]
try:
BV.check_reconciliation(c, v["g"], objs)
except CV.Reject as r:
if r.reason == "reconciliation_objects_not_held":
raise Refused("context_not_held", 6)
raise
elif cls == "schedule":
# carousel §4.3: against the group of an item its subject names and the object
# receipts of the items listed under it here, with no envelope bytes.
import carousel_verify as KV
v = next((v for v in passed if JV.link(v["g"]).hex() == c["subject"]), None)
if v is None:
raise Refused("context_not_held", 6)
KV.check_schedule(c, v["g"], [(w["o"], None) for w in passed if JV.link(w["g"]).hex() == c["subject"]])
elif cls == "timeliness":
# timeliness §4.3: against the group of an item its subject names, the object
# receipts of the items listed under it here, and a fallback's group and object
# receipt where an item lists them; an index carries no envelope bytes, so its
# steps 6 and 8 do not run here.
import timeliness_verify as TV
held = {}
for w in passed:
link = JV.link(w["g"]).hex()
if link not in held:
held[link] = IndexGroup(w["g"])
held[link].included[w["o"]["object_id"]] = w["o"]["content_hash"].hex()
if c["subject"] not in held:
raise Refused("context_not_held", 6)
TV.check_timeliness(c, held[c["subject"]], held)
elif cls == "cache-transmission":
# cache-authority §5.3: against the group of an item its subject names and the
# object receipts of the items listed under it here; one naming an object this index
# does not list is context_not_held, as a reconciliation's is.
import cache_authority_verify as CAV
v = next((v for v in passed if JV.link(v["g"]).hex() == c["subject"]), None)
if v is None:
raise Refused("context_not_held", 6)
objs = [w["o"] for w in passed if JV.link(w["g"]).hex() == c["subject"]]
try:
CAV.check_cache_transmission(c, v["g"], objs)
except CV.Reject as r:
if r.reason == "cache_transmission_objects_not_held":
raise Refused("context_not_held", 6)
raise
except CV.Reject as r:
raise Refused(r.reason, r.step)
def check_claim(cid, passed, held, name, fetcher, recovery):
b = fetcher.get(name)
c, later = read_claim_file(name, b)
groups = [JV.link(v["g"]).hex() for v in passed]
if MUTATE == "groups-from-listed":
groups += _LISTED
# §3.2's P: a recovery through a soft binding needs the object in hand; of the keys
# held for the subject, the declaration's own signer's where held, else the first.
pubs = (recovery or {}).get("publishers") or [] if held is not None else []
keys = [k for h, k in pubs if h == c["subject"]]
publisher = None
if keys:
publisher = keys[0] if MUTATE == "first-publisher" else next((k for k in keys if k == c["signer_pubkey"]), keys[0])
if c["subject_type"] == "object" and c["subject"] == cid:
attests = "object"
elif c["subject_type"] == "receipt" and c["subject"] in groups:
attests = "receipt"
elif c["subject_type"] == "object" and c["class"] == "soft-binding" and (publisher is not None or MUTATE == "ancestor-any"):
attests = "ancestor"
else:
raise Refused("receipt_not_about_object")
if later is not None:
raise Refused(later.reason, later.step)
parent = c["parent_receipt_hash"]
if parent is not None:
try:
pb = fetcher.get(parent)
except Refused as r:
raise pnh(1 if r.code == "receipt_too_large" else 0, r.code)
try:
prev, plater = read_claim_file(parent, pb)
except Refused as r:
raise pnh(1, r.code)
if plater is not None:
raise pnh(1, plater.reason)
try:
CV.check_chain(c, prev)
except CV.Reject as r:
raise Refused(r.reason, r.step)
algs = (recovery or {}).get("exact_match_algs") or []
if attests == "ancestor":
if MUTATE != "ancestor-any":
try:
CV.recover({"declaration": b.decode("utf-8"), "publisher": publisher, "observed_hash": cid,
"observed_value": recovery["observed_value"], "exact_match_algs": algs})
except CV.Reject as r:
raise Refused(r.reason, r.step)
else:
step6(c, cid, passed, fetcher, algs)
return {"class": c["class"], "attests": attests, "subject": c["subject"], "parent": parent}, c
# ------------------------------------------------------------------ keys (§8.4)
class KeyState:
def __init__(self):
self.anchor, self.held = None, []
def add(self, revs):
have = {SV.rev_hash(r) for r in self.held}
for r in revs:
if SV.rev_hash(r) not in have:
have.add(SV.rev_hash(r))
self.held.append(r)
def step(self, d, s):
"""What the step did, or its refusal ({"error": …}); a step refused changes nothing."""
origin = s.get("origin", ORIGIN)
f = s.get("anchor") or s.get("cached") or s.get("pin")
body = edit_text(read_file(d, f).decode("utf-8"), s.get("edit"))
if s.get("pad") is not None:
body = body + " " * max(0, s["pad"] - len(body.encode("utf-8")))
if s.get("pin") is not None:
out = SV.anchor_log(origin, body, "pinned", None)
if out[0] != "anchored":
raise Fault("a pinned log that does not anchor")
self.held = SV.read_log(origin, body)
self.anchor = {"origin": origin, "revision": out[1], "path": "pinned", "pinned_revision": out[3]}
return {"anchored": out[1], "path": "pinned"}
if s.get("cached") is not None:
if self.anchor is None:
raise Fault("a cached log before any anchor")
return self.cached(origin, body)
loc = origin + KEY_LOG_PATH
r = s.get("retrieval") or {}
requested = r.get("requested", loc)
answered = r.get("answered", requested)
status = r.get("status", 200)
if not origin.startswith("https://") or requested != loc or (answered != loc and MUTATE != "anchor-follows-redirect") or status != 200:
return {"error": "key_log_not_from_origin"}
if len(body.encode("utf-8")) > MAX_KEY_LOG_BYTES and MUTATE != "key-log-uncapped":
return {"error": "key_log_too_large"}
current = (self.anchor, self.held) if self.anchor is not None else None
out = SV.anchor_log(origin, body, "fetched", current)
if out[0] == "not_anchored":
return {"error": out[1]}
if out[0] == "replaced":
return {"error": "key_log_replaced"}
try:
self.add(SV.read_log(origin, body))
except SV.Stop:
pass
self.anchor = {"origin": origin, "revision": out[1], "path": out[2], "pinned_revision": out[3]}
return {"anchored": out[1], "path": out[2]}
def cached(self, origin, body):
if self.anchor["origin"] != origin:
return {"error": "origin_not_anchored"}
if len(body.encode("utf-8")) > MAX_KEY_LOG_BYTES and MUTATE != "key-log-uncapped":
return {"error": "key_log_too_large"}
try:
revs = SV.read_log(origin, body)
except SV.Stop as e:
return {"error": e.reason}
by = {SV.rev_hash(r): r for r in self.held if SV.is_held(r, origin)}
trusted = [self.anchor["revision"]]
while trusted[-1] in by and by[trusted[-1]]["prev"] in by and by[trusted[-1]]["prev"] not in trusted:
trusted.append(by[trusted[-1]]["prev"])
latest = SV.rev_hash(revs[-1])
if latest != self.anchor["revision"] and latest in trusted and MUTATE != "cached-rollback-unchecked":
return {"error": "key_log_rolled_back"}
before = len(self.held)
self.add(revs)
return {"added": len(self.held) - before}
def key_material(d, steps):
st = KeyState()
for s in steps or []:
r = st.step(d, s)
if "error" in r:
raise Fault("the case's key material does not anchor")
return st.held, ([st.anchor] if st.anchor is not None else [])
def read_grants_file(module, b):
bad = Refused("grants_file_malformed")
text = utf8(b)
if text is None:
raise bad
try:
v = CV.parse_strict(text)
except ValueError:
raise bad
if not isinstance(v, dict) or set(v) != {"kind", "module", "grants"} or v["kind"] != GRANTS_KIND:
raise bad
if not (CV.is_hex(v["module"], 32) and (v["module"] == module or MUTATE == "grants-module-unchecked")):
raise bad
if not isinstance(v["grants"], list):
raise bad
out = []
for g in v["grants"]:
try:
out.append(CV.jcs(g))
except ValueError:
out.append("null") # outside the canonical subset: no grant reads from it
return out
# ------------------------------------------------------------------ parameter sets (§8.5)
def module_check_digest(digest, grant_text, cap, origins, revisions, anchors, statuses, now):
"""jwp-receipts §7.9's checks with the pin's digest standing for the bytes' (check 2):
("run",) or ("not_held", step, reason, revoked_by)."""
def no(step, reason, by=None):
return ("not_held", step, reason, by)
g = SV.parse_grant(grant_text)
if g is None:
return no("1", "module_grant_invalid")
if digest != g["module"]:
return no("2", "module_digest_mismatch")
v = SV.confirm(bytes.fromhex(g["signer_pubkey"]), g["signer_id"], ["module/" + g["form"]], g["not_before"], revisions, anchors, origins)
if not v.startswith("confirmed"):
return no("3", v.split("reason=")[1])
fields = dict(x.split("=", 1) for x in v.split()[1:])
origin, hh = fields["origin"], fields["head"]
held = [r for r in revisions if SV.is_held(r, origin)]
hr = next(r for r in held if SV.rev_hash(r) == hh)
if now is None or not SV.is_log_time(now):
return no("4", "module_status_stale")
by = {SV.rev_hash(r): r for r in held}
line = [hh]
while by[line[-1]]["prev"] in by and by[line[-1]]["prev"] not in line:
line.append(by[line[-1]]["prev"])
valid = []
for t in statuses:
s = SV.parse_status(t)
if s is None or s["origin"] != origin or s["head"] not in line:
continue
for r in held:
if (SV.rev_hash(r) == s["head"] and r["seq"] == s["seq"]
and SV.verifies(bytes.fromhex(r["root"]), bytes.fromhex(s["sig"]), SV.D_STATUS + CV.jcs(SV.without_sig(s)).encode())):
valid.append(s)
break
current = [s["issued_at"] for s in valid if s["head"] == hh]
if not current:
return no("4", "module_status_not_descended" if valid else "module_status_stale")
age = SV.secs(now) - SV.secs(max(current))
if age < 0 or age > hr["status_max_age_s"]:
return no("4", "module_status_stale")
if g["module"] in hr["revoked_modules"]:
return no("5", "module_revoked")
o = SV.revoked_by(g["module"], revisions, anchors)
if o is not None:
return no("5", "module_revoked", o)
if now < g["not_before"]:
return no("6", "module_grant_not_yet_valid")
if now > g["not_after"]:
return no("6", "module_grant_expired")
if SV.secs(g["not_after"]) - SV.secs(g["not_before"]) > hr["grant_max_s"]:
return no("7", "module_grant_too_long")
if cap not in g["capabilities"]:
return no("8", "module_capability_not_granted")
return ("run",)
def check_set(cap, digest, revisions, anchors, statuses, grants, hold_to, now):
"""SPEC §3.1 check 5 at a sink that holds a key log: None where the set is held or
the check does not apply, else the refusal."""
revoked = SV.revoked_by(digest, revisions, anchors)
if hold_to is None:
return None if revoked is None else {"error": "prior_revoked", "revoked_by": revoked}
if MUTATE == "set-grants-ignored":
return None
origins = [{"origin": o, "source": "relying-party"} for o in hold_to]
def rank(o):
return 255 if o[0] == "run" or o[2] == "module_revoked" else int(o[1])
reported = None
for g in grants:
pg = SV.parse_grant(g)
if pg is not None and pg["form"] != "decoder":
continue
out = module_check_digest(digest, g, cap, origins, revisions, anchors, statuses, now)
if out[0] == "run":
return None
if reported is None or rank(out) > rank(reported):
reported = out
if reported is not None and reported[2] == "module_revoked":
return {"error": "prior_revoked", "revoked_by": reported[3]}
if revoked is not None:
return {"error": "prior_revoked", "revoked_by": revoked}
if reported is None:
return {"error": "prior_not_authorised", "check": None, "reason": None}
return {"error": "prior_not_authorised", "check": reported[1], "reason": reported[2]}
def param_set(d, inp):
cap, sha = inp["capability"], inp["sha256"]
if not CV.is_hex(sha, 32):
return {"error": "pin_malformed"}
if cap not in ONE_FILE:
raise Fault("a capability whose set shape this verifier does not read")
served = read_file(d, inp["served"])
revisions, anchors = key_material(d, inp.get("keys"))
statuses = [read_file(d, p).decode("utf-8") for p in inp.get("statuses") or []]
grants = []
if inp.get("grants") is not None:
try:
grants = read_grants_file(sha, read_file(d, inp["grants"]))
except Refused:
raise Fault("the case's grants file does not read")
r = check_set(cap, sha, revisions, anchors, statuses, grants, inp.get("hold_to"), inp.get("now"))
if r is not None:
return r
if hashlib.sha256(served).hexdigest() != sha and MUTATE != "set-by-name":
return {"error": "prior_not_held"}
return {"held": True}
# ------------------------------------------------------------------ confirmation (§7.4)
def confirm_group(g, o, revisions, anchors):
classes = []
if g["labelled"] and g["cls"] is not None:
classes.append(g["cls"]["label"])
if o["labelled"] and o["class"] is not None:
classes.append(o["class"]["label"])
return SV.confirm(bytes.fromhex(g["signer_pubkey"]), g["signer_id"], SV.item_authorities("group", classes), g["group_end"],
revisions, anchors, None)
def confirm_claim(c, revisions, anchors):
return SV.confirm(bytes.fromhex(c["signer_pubkey"]), c["signer_id"], SV.claim_authorities(c), c["issued_at"], revisions, anchors, None)
# ------------------------------------------------------------------ the ops
class Served:
"""A case's receipt files: the `rcpt` directories searched for <hash>.json, hashes
`omit`ted, hashes served `as` another file, and files `pad`ded with spaces to a
length, or every file to `pad_all`."""
def __init__(self, d, v):
self.d = d
self.dirs = v.get("rcpt") or [RCPT]
self.omit = set(v.get("omit") or [])
self.swap = v.get("as") or {}
self.pad = v.get("pad") or {}
self.pad_all = v.get("pad_all")
def get(self, h):
if h in self.omit:
return None
b = None
if h in self.swap:
p = os.path.join(self.d, self.swap[h])
b = open(p, "rb").read() if os.path.exists(p) else None
else:
for dd in self.dirs:
p = os.path.join(self.d, dd, h + ".json")
if os.path.exists(p):
b = open(p, "rb").read()
break
if b is not None and h in self.pad:
b = b + b" " * max(0, self.pad[h] - len(b))
elif b is not None and self.pad_all is not None:
b = b + b" " * max(0, self.pad_all - len(b))
return b
def receipt_file(d, inp):
path = inp["file"]
t = edit_text(read_file(d, path).decode("utf-8"), inp.get("edit"))
name = inp.get("name") or path.rsplit("/", 1)[-1][:-len(".json")]
try:
if inp["kind"] == "group":
_, g = read_group_file(name, t.encode("utf-8"))
return {"link": JV.link(g).hex()}
c, _ = read_claim_file(name, t.encode("utf-8"))
return {"link": CV.link_hash(c)}
except Refused as r:
return r.json()
def receipts(d, objs, inp, fetch_object, content_hash):
served = Served(d, inp.get("serve") or {})
ch = Chains(inp.get("join") is True)
bound = WALK_BOUND if inp.get("max_walk") is None else inp["max_walk"]
budget = FETCH_BUDGET if inp.get("fetch_budget") is None else inp["fetch_budget"]
keys = key_material(d, inp["keys"]) if inp.get("keys") is not None else None
rec_in = inp.get("recovery")
publishers = []
out = []
for chk in inp["checks"]:
if "object_cid" in chk:
b, cid = None, chk["object_cid"]
else:
b = read_file(d, chk["object_file"]) if "object_file" in chk else objs[chk["object"]]
cid = content_hash(b).hex()
ixb = edit_text(read_file(d, chk.get("index") or f"objects/{cid}.wai.receipts").decode("utf-8"), chk.get("edit_index")).encode("utf-8")
if "pad_index" in chk:
ixb = ixb + b" " * max(0, chk["pad_index"] - len(ixb))
fetched = None
h = chk["held"]
if h == "none":
held = None
elif h == "whole":
hb = objs[chk["held_object"]] if "held_object" in chk else b
held = (content_hash(hb).hex(), "envelope")
else:
held, fetched = fetch_object(b, h["fetch"])
try:
obj, items, claims = read_index(ixb)
if obj != cid:
raise Refused("receipt_not_about_object")
except Refused as r:
entry = {"index": r.code}
if fetched is not None:
entry["fetched"] = fetched
out.append(entry)
continue
fetcher = Fetcher(served.get, budget)
dl, passed = [], []
global _LISTED # what the mutation groups-from-listed attaches claims through
_LISTED = [it[0]["group_receipt"] for it in items if not isinstance(it, Refused)]
for it in items:
if isinstance(it, Refused):
dl.append(it.json())
continue
try:
v, ctx = check_delivery(cid, it, held, fetcher, ch, bound)
if keys is not None:
v["confirm"] = confirm_group(ctx["g"], it[1], *keys)
dl.append(v)
passed.append(ctx)
except Refused as r:
dl.append(r.json())
# A recovery through a soft binding runs only for an object the client holds
# (check_claim gates it); the matchers serve a recovery record's step 6 either way.
recovery = None
if rec_in is not None and (held is not None or MUTATE != "matchers-need-object"):
recovery = {"publishers": publishers, "observed_value": rec_in["observed_value"],
"exact_match_algs": rec_in.get("exact_match_algs") or []}
cl = []
for c in claims:
if isinstance(c, Refused):
cl.append(c.json())
continue
try:
v, claim = check_claim(cid, passed, held, c, fetcher, recovery)
if keys is not None:
v["confirm"] = confirm_claim(claim, *keys)
cl.append(v)
except Refused as r:
cl.append(r.json())
entry = {"index": "ok", "delivery": dl, "claims": cl, "files_fetched": fetcher.fetched}
if fetched is not None:
entry["fetched"] = fetched
out.append(entry)
# The delivery receipts this check verified over the object's bytes: publisher keys
# a later check may recover through.
publishers += [(cid, v["g"]["signer_pubkey"]) for v in passed if v["held"] is not None]
return {"checks": out}
def key_log(d, inp):
st = KeyState()
return {"steps": [st.step(d, s) for s in inp["steps"]]}
def grants_file(d, inp):
path = inp["file"]
t = edit_text(read_file(d, path).decode("utf-8"), inp.get("edit"))
module = inp.get("module") or path.rsplit("/", 1)[-1][:-len(".json")]
try:
gs = read_grants_file(module, t.encode("utf-8"))
except Refused as r:
return {"error": r.code}
return {"grants": ["read" if SV.parse_grant(g) is not None else "module_grant_invalid" for g in gs]}
def evaluate(d, objs, op, inp, fetch_object, content_hash):
if op == "receipt_file":
return receipt_file(d, inp)
if op == "receipts":
return receipts(d, objs, inp, fetch_object, content_hash)
if op == "key_log":
return key_log(d, inp)
if op == "grants_file":
return grants_file(d, inp)
if op == "param_set":
return param_set(d, inp)
raise Fault("unknown op " + op)