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
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]>
908 lines
39 KiB
Python
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)
|