diff --git a/.github/workflows/wai-conformance.yml b/.github/workflows/wai-conformance.yml index 2cc9cfe29..dc3912bc3 100644 --- a/.github/workflows/wai-conformance.yml +++ b/.github/workflows/wai-conformance.yml @@ -283,13 +283,22 @@ jobs: # does: objects named by their content hash with their head proofs, a sequence's union # and floor, the origin's headers, ranges and If-Range on loopback, and a fetch by # the byte-range procedure whose record states energy as not measured; and `hls`, - # `mpd`, `seal-playlist` and `check-playlist`, whose receipts need video_manifest. + # `mpd`, `seal-playlist` and `check-playlist`, whose receipts need video_manifest; + # and `receipts`, an object's receipt index checked over HTTP with its signers + # confirmed under a key log, which needs key_discovery: the log anchored over TLS + # (cli_tls; the binary's own tests run its TLS client against servers under a test + # CA, tests/fixtures/tls/), never over plaintext, and a plaintext test anchor + # labelled as one. The second run builds without cli_tls: the CLI then refuses to + # anchor and connects to nothing. # Cargo.toml declares it with required-features = ["http"]. tests/dash_read_bounds.rs # holds Appendix I's reader to the document's size under a counting allocator, and an # element's attributes to linear CPU time; a binary of its own, one test. - name: The wai http commands working-directory: wai/wai-rs - run: cargo test --no-default-features --features http,video_manifest --test cli_http --test dash_read_bounds + shell: bash + run: | + cargo test --no-default-features --features http,video_manifest,key_discovery,cli_tls --bin wai --test cli_http --test dash_read_bounds + cargo test --no-default-features --features http,video_manifest,key_discovery --test cli_http -- receipts_never_anchors_over_plaintext # tests/staged_moq.rs runs the MoQ trace of staged-conformance/moq/ (moq-streaming-format # §4.1) with signed receipts: a chain for each track, a receipt held for its parent and # admitted when it arrives, the relay's per-hop delivery timeout, dropped objects checked @@ -338,17 +347,19 @@ jobs: working-directory: wai/wai-rs run: cargo test --no-default-features --features key_discovery --test wai_keys_cli # key_log has no I/O, clock or threads, so a browser sink could run it before it - # instantiates a module (§7.9); no browser sink does yet. This step checks that it - # builds for wasm32 on the unix legs (the target is added to the pinned toolchain - # here; the Windows host runner is not set up for it). - - name: key_log builds for wasm32 (unix) + # instantiates a module (§7.9); no browser sink does yet. Nor do the HTTP binding's + # receipts and keys (http-delivery §8.1–§8.5), which a browser client checks what it + # fetched with. This step checks that both build for wasm32 on the unix legs (the + # target is added to the pinned toolchain here; the Windows host runner is not set up + # for it). + - name: key_log and the HTTP receipts build for wasm32 (unix) if: matrix.os != 'windows' shell: bash working-directory: wai/wai-rs run: | set -euo pipefail rustup target add --toolchain 1.98.0 wasm32-unknown-unknown - cargo check --lib --target wasm32-unknown-unknown --no-default-features --features key_discovery + cargo check --lib --target wasm32-unknown-unknown --no-default-features --features key_discovery,http # The corpus-verify loop is genuinely shell-specific (a helper that groups # log output and propagates the first non-zero exit), so it ships once per @@ -546,9 +557,13 @@ jobs: # the head proof (every committed proof rebuilt from the text), response checks, plans, # selection, the byte-range procedure and its fallback, the codecs union and floor, the # playlists (read, rendered, held adjacent, the classical master unchanged), playlist - # receipts (Ed25519 from RFC 8032) and the live edge, and the adaptation policies, - # checked first against published BLAKE3 vectors. Standard library only. Its - # mutations and the differential against wai-rs run in wai-http.yml's falsify job. + # receipts (WAI's strict Ed25519, _ed25519.py) and the live edge, and the adaptation policies, + # checked first against published BLAKE3 vectors, with the standard library alone; + # and the receipts, key logs, grants files and parameter sets of §8.1–§8.5, through + # http_receipts.py and the corpora's own verifiers of jwp-receipts, which need the + # `cryptography` package installed above (without it those cases fail, never skip). + # Its mutations and the differential against wai-rs run in wai-http.yml's falsify + # job. - name: http_verify.py working-directory: wai shell: bash @@ -558,7 +573,9 @@ jobs: # refines, layers declared against their bases, layers over WAI2 components, objects # past the first request through a tampering origin; playlists, adjacent pairs, # published masters, renderings, adaptation states, the integer log and changed - # receipt files) with wai-rs's outcomes, and + # receipt files; receipt files, indexes, grants files and key logs of §8.1–§8.5, + # whose cases need the `cryptography` package installed above) with wai-rs's + # outcomes, and # http_verify.py must give every one, a refusal by the same code. It prints each # family's count of cases and of refusals. Two fixed seeds, so a run is reproducible. - name: The HTTP differential outside the corpus (wai-rs against http_verify.py) @@ -566,7 +583,7 @@ jobs: shell: bash run: | set -euo pipefail - (cd wai-rs && cargo build --no-default-features --features http,video_manifest --bin wai_http_vectors) + (cd wai-rs && cargo build --no-default-features --features http,video_manifest,key_discovery --bin wai_http_vectors) for seed in 11 12; do wai-rs/target/debug/wai_http_vectors fuzz http-conformance --seed "$seed" --count 400 --out "$RUNNER_TEMP/http-fuzz-$seed.json" python3 http-conformance/http_verify.py cases "$RUNNER_TEMP/http-fuzz-$seed.json" http-conformance diff --git a/.github/workflows/wai-http.yml b/.github/workflows/wai-http.yml index 322fe9f20..29cddddae 100644 --- a/.github/workflows/wai-http.yml +++ b/.github/workflows/wai-http.yml @@ -1,6 +1,7 @@ name: WAI HTTP delivery -# extensions/http-delivery.md: two jobs beside the corpus checks in wai-conformance.yml. +# extensions/http-delivery.md: the falsification, the differential and the cache rules, +# beside the corpus checks in wai-conformance.yml. # # falsify — the HTTP corpus tool and its independent verifier must catch defects. Each # patch in wai/http-conformance/falsify/ breaks one function of wai-rs (the content @@ -12,12 +13,27 @@ name: WAI HTTP delivery # bytes after a mismatch, an unanswered base, the selection digest test, the component # conformance that withholds an envelope's refinements; and on the # playlist side adjacency, rendering, bandwidths, durations, the classical master, the -# playlist receipt and its chain, listed cids, and the adaptation policies); it -# is applied to a fresh worktree, the build must succeed, and `wai_http_vectors verify` -# must fail naming the cases on the patch's first line. Then each `http_verify.py -# --mutate` must fail, and the differential: wai-rs writes random envelopes' covered -# prefixes and head proofs, the Python verifier accepts every one, and refuses exactly -# the ten it changes one chaining-value byte in. +# playlist receipt and its chain, listed cids, and the adaptation policies; and on the +# receipt side the receipt file's name, legacy groups, the parent walk and its bound, +# the caps on files, indexes and their items, fetching each name once within budgets +# of files and bytes, what a client holds of an object, the items a claim attaches +# through, §3.1 step 6 for every class and the declarations it reads by name, the +# ancestor rule, its recovery and the publisher key it uses, a claim's parent, +# anchoring from the origin only and the request that anchors, cached logs, grants +# files and sets held by their pins); it +# is applied to one worktree of the head, built once and rebuilt incrementally per +# patch under the `falsify` profile (overflow checks and debug assertions on, as in the +# tests), and `wai_http_vectors verify` must fail naming the cases on the patch's first +# line; the patch is then reversed and the tree must be clean. Each `http_verify.py +# --mutate` must exit non-zero naming the cases falsify/mutations.txt lists for it; a +# crash, or a non-zero exit that names none, is a failure. Both run in shards (falsify/run.sh --patches I/N, --mutations +# I/N), each printing what it ran and failing unless that is exactly what was assigned +# to it; `shards` checks the shards the matrix declares cover every patch and every +# mutation once, so none is dropped silently. +# +# differential — the corpus verified both ways, and the differential: wai-rs writes +# random envelopes' covered prefixes and head proofs, the Python verifier accepts every +# one, and refuses exactly the ten it changes one chaining-value byte in. # # cache-conformance — the cache rules (§9). wai/cache-conformance/run.sh stages the # corpus's objects behind the reference origin (`wai http origin`) and runs driver.py @@ -45,7 +61,29 @@ on: - 'wai/wai-rs/src/bin/wai_cli/http.rs' - 'wai/wai-rs/src/bin/wai_cli/http_vectors.rs' - 'wai/wai-rs/src/bin/wai_cli/http_vectors_playlists.rs' + - 'wai/wai-rs/src/bin/wai_cli/http_vectors_receipts.rs' - 'wai/wai-rs/src/bin/wai_cli/http_playlists.rs' + # Receipts, keys, grants and sets over HTTP (§8.1–§8.5) are checked by the receipt, + # claim, key-log and pin code, and on the Python side by their own verifiers. + - 'wai/wai-rs/src/jwp_delivery.rs' + - 'wai/wai-rs/src/claim_receipt.rs' + - 'wai/wai-rs/src/claim_receipt/**' + - 'wai/wai-rs/src/key_log.rs' + - 'wai/wai-rs/src/pin.rs' + # ...and by what those read with: JCS, signer identities, step 6's figures and + # classes, and the OER receipts an energy claim names. + - 'wai/wai-rs/src/jcs.rs' + - 'wai/wai-rs/src/signer_id.rs' + - 'wai/wai-rs/src/energy_class.rs' + - 'wai/wai-rs/src/energy_figure.rs' + - 'wai/wai-rs/src/energy_binding.rs' + - 'wai/wai-rs/src/energy_report.rs' + - 'wai/wai-rs/src/claim_text.rs' + - 'wai/wai-rs/src/oer.rs' + - 'wai/jwp-conformance/jwp_verify.py' + - 'wai/claim-conformance/claim_verify.py' + - 'wai/signer-conformance/signer_verify.py' + - 'wai/oer-conformance/oer2_verify.py' # The playlists render through hls_render and are sealed by wai_video_manifest. - 'wai/wai-rs/src/hls_render.rs' - 'wai/wai-rs/src/wai_video_manifest.rs' @@ -75,7 +113,29 @@ on: - 'wai/wai-rs/src/bin/wai_cli/http.rs' - 'wai/wai-rs/src/bin/wai_cli/http_vectors.rs' - 'wai/wai-rs/src/bin/wai_cli/http_vectors_playlists.rs' + - 'wai/wai-rs/src/bin/wai_cli/http_vectors_receipts.rs' - 'wai/wai-rs/src/bin/wai_cli/http_playlists.rs' + # Receipts, keys, grants and sets over HTTP (§8.1–§8.5) are checked by the receipt, + # claim, key-log and pin code, and on the Python side by their own verifiers. + - 'wai/wai-rs/src/jwp_delivery.rs' + - 'wai/wai-rs/src/claim_receipt.rs' + - 'wai/wai-rs/src/claim_receipt/**' + - 'wai/wai-rs/src/key_log.rs' + - 'wai/wai-rs/src/pin.rs' + # ...and by what those read with: JCS, signer identities, step 6's figures and + # classes, and the OER receipts an energy claim names. + - 'wai/wai-rs/src/jcs.rs' + - 'wai/wai-rs/src/signer_id.rs' + - 'wai/wai-rs/src/energy_class.rs' + - 'wai/wai-rs/src/energy_figure.rs' + - 'wai/wai-rs/src/energy_binding.rs' + - 'wai/wai-rs/src/energy_report.rs' + - 'wai/wai-rs/src/claim_text.rs' + - 'wai/wai-rs/src/oer.rs' + - 'wai/jwp-conformance/jwp_verify.py' + - 'wai/claim-conformance/claim_verify.py' + - 'wai/signer-conformance/signer_verify.py' + - 'wai/oer-conformance/oer2_verify.py' # The playlists render through hls_render and are sealed by wai_video_manifest. - 'wai/wai-rs/src/hls_render.rs' - 'wai/wai-rs/src/wai_video_manifest.rs' @@ -99,8 +159,94 @@ on: - cron: '17 4 * * 1' jobs: + # The path filters above gate the trigger, which on this forge is the push delta: a + # rebase fires them all. This gates the work, from the pull request's own diff, reading + # the path list out of this file (scripts/pr-touches-paths.py); a push to main or the + # schedule has no pull-request context and runs everything. + relevant: + name: WAI HTTP · does this change touch it + runs-on: linux + outputs: + run: ${{ steps.gate.outputs.run }} + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - id: gate + run: | + set -euo pipefail + r=$(python3 scripts/pr-touches-paths.py .github/workflows/wai-http.yml \ + "${{ github.event.pull_request.base.sha }}" \ + "${{ github.event.pull_request.head.sha }}") + echo "this change touches WAI HTTP: $r" + echo "run=$r" >> "$GITHUB_OUTPUT" + + # Every patch and every mutation runs in exactly one shard: the one whose index mod N + # it is, in the sorted list. The shards job reads the list below; keep it on one line. falsify: - name: HTTP corpus · falsification and differential + name: HTTP corpus · falsify ${{ matrix.shard }} + needs: relevant + if: needs.relevant.outputs.run == 'true' + runs-on: linux + strategy: + fail-fast: false + matrix: + shard: ['patches 0/3', 'patches 1/3', 'patches 2/3', 'mutations 0/3', 'mutations 1/3', 'mutations 2/3'] + steps: + - uses: actions/checkout@v5 + + - name: Install Rust 1.98.0 + if: startsWith(matrix.shard, 'patches') + shell: bash + run: | + export PATH="$HOME/.cargo/bin:$PATH" + command -v rustup >/dev/null 2>&1 || \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0 + rustup toolchain install 1.98.0 --profile minimal + rustup default 1.98.0 + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + + # http_verify.py checks the receipt and key cases through the corpora's receipt, + # claim and key-discovery verifiers, which check Ed25519's equation with this + # package; its mutations reach those cases. + - name: Install the Ed25519 library + shell: bash + run: python -m pip install cryptography + + - name: Every patch or mutation of this shard is caught + shell: bash + env: + CARGO_TARGET_DIR: ${{ runner.temp }}/falsify-target + SHARD: ${{ matrix.shard }} + run: | + git config --global --add safe.directory "$GITHUB_WORKSPACE" + set -- $SHARD + bash wai/http-conformance/falsify/run.sh "$RUNNER_TEMP/falsify" "--$1" "$2" + + # The shards the matrix above declares cover every patch and every mutation exactly + # once: a shard dropped from the list, or a total that disagrees, fails here. + shards: + name: HTTP corpus · every patch and mutation in exactly one shard + needs: relevant + if: needs.relevant.outputs.run == 'true' + runs-on: linux + steps: + - uses: actions/checkout@v5 + - shell: bash + run: | + set -euo pipefail + line=$(grep -E "^ +shard: \[" .github/workflows/wai-http.yml) + mapfile -t shards < <(printf '%s\n' "$line" | grep -oE "'(patches|mutations) [0-9]+/[0-9]+'" | tr -d "'") + bash wai/http-conformance/falsify/run.sh "$RUNNER_TEMP/falsify" --check-shards "${shards[@]}" + + differential: + name: HTTP corpus · verified both ways, and the differential + needs: relevant + if: needs.relevant.outputs.run == 'true' runs-on: linux steps: - uses: actions/checkout@v5 @@ -119,17 +265,26 @@ jobs: with: python-version: '3.11' - # One target directory for every patched build, so only the wai crate rebuilds. - - name: Every patch is caught; every mutation is caught; the differential holds + - name: Install the Ed25519 library + shell: bash + run: python -m pip install cryptography + + - name: The corpus, both ways; the differential shell: bash env: CARGO_TARGET_DIR: ${{ runner.temp }}/falsify-target run: | + set -euo pipefail git config --global --add safe.directory "$GITHUB_WORKSPACE" - bash wai/http-conformance/falsify/run.sh "$RUNNER_TEMP/falsify" + (cd wai/wai-rs && cargo build --profile falsify --no-default-features --features http,video_manifest,key_discovery --bin wai_http_vectors) + "$CARGO_TARGET_DIR/falsify/wai_http_vectors" verify wai/http-conformance + (cd wai/http-conformance && python3 http_verify.py .) + bash wai/http-conformance/falsify/run.sh "$RUNNER_TEMP/falsify" --differential cache-conformance: name: cache rules · controls and labelled caches + needs: relevant + if: needs.relevant.outputs.run == 'true' runs-on: linux steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/wai-pack.yml b/.github/workflows/wai-pack.yml index 6fcd6c678..83075ff3e 100644 --- a/.github/workflows/wai-pack.yml +++ b/.github/workflows/wai-pack.yml @@ -275,3 +275,9 @@ jobs: fi cargo tree --locked -e normal,build --target all -i cbindgen | grep -q '^cbindgen v' || { echo "::error::cbindgen not found as wai's build dependency"; exit 1; } echo "cbindgen is a build dependency only" + # The `wai` binary's HTTP commands with their TLS client (the cli_tls feature: + # rustls and the operating system's trust store), under wai-rs's own deny.toml: the + # same allow-list, over the binary's features on the hosts it is built for. + - name: cargo deny check licenses (the wai binary's TLS client) + working-directory: wai/wai-rs + run: cargo deny --locked check licenses diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 112c348c5..a6d990be2 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -682,6 +682,64 @@ wire format may change and such changes are recorded here. The corpus gains a case for each, and `http_verify.py` agrees with wai-rs on them and on a seeded differential outside the corpus, in which a refusal must match by its code and each family reports how many cases it generated and how many were refused. +- **Receipts, keys, grants and parameter sets** (http-delivery §8.1–§8.5, §10.5): + - a receipt file `rcpt/.json` holds one group receipt (in the labelled payload) or + claim in JCS, at most 1 MiB, read as jwp-receipts reads one and refused unless its + link hash is its name (`receipt_name_mismatch`, `receipt_not_served`, + `receipt_too_large`); a legacy group, whose name does not cover its parent, is + refused (`receipt_legacy_group`); + - an index, at most 4 MiB with at most 256 delivery items and 256 claims + (`receipt_index_too_large`), is read item by item (`receipt_index_malformed`), each + receipt file fetched once per index, at most 1024 in all + (`receipt_fetch_budget_exhausted`) and 64 MiB read in all, each file read no further + than what remains of that (`receipt_fetch_bytes_exhausted`), each cap with its reason + stated: each delivery item's group goes through + jwp-receipts §3 against the client's chains, a missing parent fetched by its link up + to 64 of them, never more, and never skipped (`parent_not_held`, with where the walk + stopped), a joining client anchoring instead; its object receipt through steps 4 + and 5 against what the client holds — the hash recomputed over the envelope, the cid + a head proof established (§8.3), or nothing, which is `inclusion-verified` and gives + no property reading (§10.5); each claim by its subject against the items that + passed, its parent fetched by its link, then §3.1 step 6 against what those items + deliver and the receipts it names (`figure_not_held`, `context_not_held` where the + client does not hold them; an energy claim naming an OER receipt, which this revision + does not carry over HTTP, is `context_not_held`), a declaration it names read as + §8.1 reads every file fetched by name before step 6 compares it, a recovery record + checked with the client's matchers whether or not it holds the object, and a soft + binding of another object attesting the ancestor only through §3.2's recovery, + under the held publisher key that signed it; + - a key log anchors only from the origin's own location over HTTPS, answered `200` + with no redirect (`key_log_not_from_origin`), from a retrieval the verifier made + itself, asking with `Cache-Control: no-cache`; a key log is read to 1 MiB and a longer + one refused, anchored or cached (`key_log_too_large`); a log through any cache adds revisions + and never moves the anchor, an older copy reported (`key_log_rolled_back`); a grants + file is a locator (`grants_file_malformed`); a key status is served for a quarter of + the head's `status_max_age_s` at most; a module grant is §7.9's form, and a JCP grant + is carried only by reference in the receipt that names it; + - a parameter set served by its digest is held only by its pin and §7.9 + (`pin::verify_bytes_with`, `int_payload::resolve`), never by its name. + - `wai-rs`: `http_binding::receipts` and `http_binding::keys`; + `jwp_hash::EstablishedHash`, built only from bytes or by a fetch session whose head + proof verified (`FetchSession::established`); `ObjectReceipt::accept_hash` and + `verify_against_hash`, `WaiReceiptProperty::check_against_object_hash` and + `ReceiptChains::verify_admitted_object`, which the existing checks call; + `energy_figure::HeldObject`, an object receipt's figure for step 6; `wai http + receipts`, which fetches under a deadline and the caps above, follows no redirect, + and with `--key-origin` retrieves the key log from the origin itself over TLS + (the `cli_tls` feature: a TLS client validating against the operating system's + trust store; SNI and the certificate check name the origin's host, port 443 by + default, each address the host resolves to tried in turn), every step within one + deadline however slowly a peer answers; a build + without it refuses to anchor rather than fetch the log over plaintext, and the + test-only `--test-plaintext-connect` anchors with its own path, `test-plaintext`, + wherever the anchor or a confirmation is reported, as anchoring nothing a verifier + may rely on. A group or + object receipt whose `kind` is not a string is `wrong_kind` + (jwp-receipts §3 step 1), as the independent verifier already read it. + - The corpus serves receipts, indexes, a key log, a status, a grants file and a set; + `http_receipts.py` checks them for `http_verify.py` through the jwp-receipts, claim + and key-discovery verifiers, and the differential adds receipt files, indexes, + grants files and key logs. - No envelope byte or receipt byte changes for existing material. ### Packaging: objects, the stage index, the floor, catalogs and the package receipt diff --git a/wai/extensions/http-delivery.md b/wai/extensions/http-delivery.md index 016177264..ce80c3b22 100644 --- a/wai/extensions/http-delivery.md +++ b/wai/extensions/http-delivery.md @@ -7,9 +7,10 @@ > live. No envelope byte and no manifest member changes. The reference implementation > is in `wai-rs`: `http_binding` (feature `http`) for §3–§7, the origin side of §5 > and §9, the playlists of §10.4 and §11 and the adaptation policies of Appendix F; -> `wai_video_manifest` (feature `video_manifest`) for the playlist receipts of §8.6; -> and the `wai http` commands of the `wai` tool. Keywords **MUST**, **MUST NOT**, -> **SHOULD**, **MAY** are RFC 2119 / RFC 8174. +> `http_binding::receipts` (with `receipt_claims`) and `http_binding::keys` (with +> `key_discovery`) for §8.1–§8.5; `wai_video_manifest` (feature `video_manifest`) for +> the playlist receipts of §8.6; and the `wai http` commands of the `wai` tool. +> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119 / RFC 8174. ## 1. Scope @@ -389,19 +390,48 @@ pin was refused may also fail its digest, which only its bytes would show. ### 8.1 Receipt files `rcpt/.json`, resolved against the object's URI, holds exactly one receipt: a group -receipt whose link hash ([jwp-receipts](jwp-receipts.md) §3 step 3) is `h`, in its -canonical form, or a claim whose `link_hash` (§2.4.3) is `h`, in its JCS form. It is -immutable and served under §5.3's cache headers with `Content-Type: application/json`. -A client refuses a receipt file whose computed hash is not its name -(`receipt_name_mismatch`). A group receipt's `parent_receipt_hash` is the name of its -parent's file, so a client resolves [jwp-receipts](jwp-receipts.md) §3 step 3 by -fetching `rcpt/.json`, and reports `parent_not_held` when it -cannot, never skipping the step. +receipt in the labelled payload whose link hash ([jwp-receipts](jwp-receipts.md) §3 step +3) is `h`, in its JSON form (§2.4.1 there), or a claim whose `link_hash` (§2.4.3) is `h`, +each written in JCS (RFC 8785), in at most 1 MiB (1 048 576 bytes). It is immutable and +served under §5.3's cache headers with `Content-Type: application/json`. A client reads at +most that many bytes of a receipt file and refuses a longer one (`receipt_too_large`); it +reads the file as [jwp-receipts](jwp-receipts.md) §3.1 step 1 reads JSON, then as the +receipt it fetched it for — a group receipt by §3 step 1, a claim by §3.1 steps 1–2, with +their reasons — and refuses it unless its link hash, computed from what it read, is its +name (`receipt_name_mismatch`). A group in the legacy payload is refused +(`receipt_legacy_group`): its link hash does not cover its parent, so a server could +serve other bytes, naming another parent, under the one name. A name at which nothing is +served is `receipt_not_served`. Every file a client fetches by name — a group, a parent, +a claim, or a declaration a claim names (§8.2) — is read so first, and refused with these +reasons before any check of what it holds. A receipt is a few kilobytes; the cap leaves +room for claim bodies that list many objects, and bounds what one name costs. + +A group receipt's `parent_receipt_hash` is the name of its parent's file. A client +resolves [jwp-receipts](jwp-receipts.md) §3 step 3's `parent_not_held` by fetching +`rcpt/.json`, never by skipping the step: it reads the file as a +group receipt of that name, requires it on the group's key and track (a group of another +chain is never a predecessor), and offers it to its chains before the group; while that +parent is itself `parent_not_held`, it fetches the parent's parent the same way. Once one +is admitted, the parents fetched after it are offered again, nearest it first, then the +group. A client fetches at most 64 parents for one group, whatever bound it is asked to +use, and MAY hold a lower one. A parent that is not served, that does not read or is +refused, that is of another chain, a walk past the bound, and a walk that would pass the +index's fetch budget (§8.2), each leave the group `parent_not_held`, and the client says +which. A client that declares it joins (jwp-receipts §3 step 3, *Joining*) anchors a +chain it holds nothing of at the first group instead, walks nothing, and reports the +chain anchored. A claim's `parent_receipt_hash` names its parent's file the same way: +for §3.1 step 5 the client fetches it, reads it as a claim of that name and verifies it +under §3.1 steps 1–4, and a parent not served, or refused, leaves the claim +`parent_not_held`. ### 8.2 Receipt index `.wai.receipts` is the sidecar carriage of the `WAI_RECEIPT` property -([moq-streaming-format](moq-streaming-format.md) §5): +([moq-streaming-format](moq-streaming-format.md) §5), and the place a client finds an +object's receipts by their hashes: each delivery item's group receipt by the property's +`group_receipt`, each claim about the object by its link hash — a `stage` claim about a +stage set's base ([jwp-receipts](jwp-receipts.md) §2.4.10) among them — and each of their +parents by the link its child names (§8.1). ```json { "kind": "wai-receipt-index", "object": "", @@ -410,25 +440,106 @@ cannot, never skipping the step. ``` It is mutable and is served with `Cache-Control: no-cache, no-transform` and an `ETag` -of its own. It is a locator, never evidence. A client checks each item on its own and -ignores, item by item, one that fails: +of its own. It is a locator, never evidence. A client reads at most 4 MiB (4 194 304 +bytes) of it and refuses a longer one (`receipt_index_too_large`): room for the most +items and claims it may list. It reads it as +jwp-receipts §3.1 step 1 reads JSON, with exactly the members above, `kind` that string, +`object` 64 lowercase hexadecimal digits and the other two arrays, and refuses it whole +otherwise (`receipt_index_malformed`), when it lists more than 256 delivery items or more +than 256 claims (`receipt_index_too_large`), or when `object` is not the cid +(`receipt_not_about_object`). An index lists the groups an object was delivered under, +one per track and publisher, and each signer's latest claims, which chain to their +parents, so these bound a listing, not a history. Each item it lists is read and checked on its own, and one +that fails is ignored, with its reason, item by item: a delivery item is an object with +exactly `property` and `object_receipt` (`receipt_index_malformed`), whose property reads +as moq-streaming-format §5's (`property_invalid`) and whose object receipt reads as +jwp-receipts §2.4.2's; a claim entry is a link hash (`receipt_index_malformed`). -- a delivery item: the property's `content_hash` and the object receipt's - `content_hash` equal the cid (`receipt_not_about_object`); the group receipt is - fetched as `rcpt/.json` and verified under - [jwp-receipts](jwp-receipts.md) §3 steps 1–5, step 4 as §8.3 states; the property - is checked against the object receipt as moq-streaming-format §5 states; -- a claim: verified under [jwp-receipts](jwp-receipts.md) §3.1; its `subject` is the - cid or the link hash of a group listed in the index. A soft-binding claim - ([jwp-receipts](jwp-receipts.md) §2.6.2) is reported under the ancestor rule (§3.2): - as attesting the ancestor, never the object in hand. +To check one index, a client fetches each receipt file name once, keeping what was served +for the rest of the check, and fetches at most 1024 files in all, enough for every item +and claim an index may list, a declaration for each claim and full parent walks for a +few groups; a fetch past that is `receipt_fetch_budget_exhausted`. It reads at most 64 MiB +(67 108 864 bytes) of receipt files in all: it reads each file no further than what +remains of that budget, or than one byte past the file's cap where less remains of +neither; a file that reaches the end of the budget is refused, since whether it ends +there is not read, and once the budget is spent no further file is fetched +(`receipt_fetch_bytes_exhausted`). Without it, 1024 files at their cap would let one +index make a client read and hold a gibibyte. A client MAY hold lower budgets. + +A delivery item is checked in this order: + +1. the property's `content_hash` and the object receipt's are the cid + (`receipt_not_about_object`); +2. the group receipt is fetched as `rcpt/.json` and read as + §8.1 states; +3. [jwp-receipts](jwp-receipts.md) §3 steps 1–3, against the chains the client holds, + missing parents recovered as §8.1 states; +4. §3 steps 4 and 5 for the object receipt, step 4 against what the client holds of the + object (§8.3): the object is then `delivered-verified`; or, for an object the client + did not receive, step 4 not run: it is `inclusion-verified`, never delivered; +5. the property: for a delivered object, against its object receipt as + moq-streaming-format §5 states (`property_invalid`, `property_disagrees`), which + gives the property's reading; for one not delivered, alone, giving no reading. + +A claim is fetched as `rcpt/.json` and read as §8.1 states, then checked +against the delivery items that passed the checks above for this cid, in the index's +order. Its `subject` is the cid, or, for a claim about a receipt, the link hash of the +group of one of those items: a group named only by an item that was refused, or by +nothing, attaches nothing, and the claim is `receipt_not_about_object`. Then +[jwp-receipts](jwp-receipts.md) §3.1 steps 1–4; step 5, its parent fetched as §8.1 +states; and step 6, against the receipts it names that the client holds here: + +- an `energy` claim, against the object receipt of each item delivered (step 4 run over + what the client holds of the object, §8.3), and against the OER receipt it names. This + revision carries no OER receipt over HTTP, so an energy claim that names one is + `context_not_held`; +- a `measurement` or `operating-point` claim, against the receipt that carries its + figure: for the `moq-jwp:object` profile, the object receipt of an item delivered under + the group the claim names; +- a `delivery-gap` claim, against the object receipt of an item at its coordinates, + verified by inclusion at least; +- a lawful-basis decision, and a recovery record, against the declaration it names, + fetched as `rcpt/.json` and read as §8.1 states, whose reasons refuse it + first (another claim served under the declaration's name is `receipt_name_mismatch`); + a declaration that reads under its name and does not match is refused with step 6's + reason. A recovery record is also checked against the publisher key of the group of an + item that passed, the one that signed the declaration where there is one, and with + the client's matchers (§2.6.3 step 5), whether or not the client holds the object; +- a `reconstruct-session` claim, against its outputs, the objects it received and the + envelopes it received, none of which the index carries. + +A claim is accepted only when step 6 ran against what it needs. A `measurement` claim +whose figure the client does not hold is `figure_not_held`, as §3.1 states; any other +claim whose step 6 needs a receipt the client does not hold is `context_not_held`. Neither +is accepted. A claim of a class step 6 checks against nothing is accepted after step 5. + +A soft-binding claim ([jwp-receipts](jwp-receipts.md) §2.6.2) whose subject is another +object attests that object, the ancestor, never the object in hand, and only through +§3.2's recovery: the client holds the object, it holds the publisher key of a delivery +receipt for the ancestor that it verified under §3 over that object's bytes, and §3.2's +steps 2–5 pass with a matcher for the claim's algorithm, the claim then reported as +attesting the ancestor. Of the publisher keys it holds for the ancestor, it uses the one +that signed the claim, whichever delivery it verified first, so a delivery under another +key cannot block a recovery; holding none that did, it uses the first, and the recovery +fails with §3.2's reason. A failed recovery is reported with that reason. Without such a +key or the object, no recovery runs, and the claim is `receipt_not_about_object`. + +A client MAY confirm the signer of each item it accepts under +[jwp-receipts](jwp-receipts.md) §7.4, against key logs it holds as §8.4 states, and +reports the confirmation beside the item's verdict, never folded into it. ### 8.3 A range-fetched object A client that holds only the head and some components cannot recompute the content hash over the envelope. Under [jwp-receipts](jwp-receipts.md) §3 step 4 it takes the cid its head proof verified, and the receipt then attests the object the cid names; the -components it holds are attested through the verified head's `sha256` values. +components it holds are attested through the verified head's `sha256` values. The +content hash step 4 compares with comes only from bytes the client hashed, or from a head +proof it verified with every component it uses checked against its entry's digest (§7): +never from a URI, an index, a playlist or a receipt, which only state one. A fetch that +was refused establishes nothing, and an object the client did not receive — a +refinement it abandoned (§10.5), or one it never requested — has its receipts checked +by inclusion alone. A component's exploded envelope ([SPEC](../SPEC.md) §7.1, *Exploding a component*) is a function of the verified head and the component's bytes, so a client that holds them @@ -445,20 +556,47 @@ only on a receipt checked over its envelope bytes. | key status (§7.9 there) | `/.well-known/wai-key-status` | `max-age` at most a quarter of the head's `status_max_age_s`, `no-transform` | | module grants (§7.9 there) | `/.well-known/wai-grants/.json`: `{"kind":"wai-module-grants","module":"","grants":[…]}` | `max-age=300, no-transform` | -All are `application/json`. A verifier that **anchors** an origin fetches its key log -from the origin over HTTPS without following a redirect, as [jwp-receipts](jwp-receipts.md) -§7.3 states, and with `Cache-Control: no-cache`. Later revisions MAY come through any -cache: each is checked against its parent, and a rolled-back copy is reported -(`key_log_rolled_back`). The grants file is a locator: each grant is checked on its -own. +All are `application/json`. A verifier **anchors** an origin ([jwp-receipts](jwp-receipts.md) +§7.3) only from a key log retrieved from the origin's key-log location — an `https` +origin followed by `/.well-known/wai-keys` — answered `200` by that same URL: it does +not follow a redirect, asks with `Cache-Control: no-cache`, and a log that reached it any +other way anchors nothing (`key_log_not_from_origin`). A retrieval is an `https` +retrieval the verifier made itself: TLS to the origin's host and port (443 when the +origin names none), the server's certificate validated for that host against the trust +anchors the verifier holds, the request and the `200` exchanged on that connection, and +the response's end authenticated by TLS or by its own framing. These are facts of the +request it sent and the response it received, so a log retrieved over plaintext, or one +another party states was so retrieved, anchors nothing. A verifier reads at most 1 MiB +(1 048 576 bytes) of a key log, however it reaches it, and refuses a longer one whole +(`key_log_too_large`): a log is a few revisions of a few keys each, and the cap bounds +what one retrieval costs. Later revisions MAY come through +any cache, a mirror, or the origin: such a log is read as §7.3 reads one, its revisions +are held and each is checked against its parent by §7.4's head walk, and it never moves +the anchor. A copy whose latest revision is a trusted revision older than the anchor is +reported (`key_log_rolled_back`) and adds nothing. + +The grants file is read as jwp-receipts §3.1 step 1 reads JSON, with exactly the members +above, `module` the digest it is fetched for, or not at all (`grants_file_malformed`). It +is a locator: each grant in it goes to §7.9's checks on its own, and one that fails them +is passed over. + +A module grant is §7.9's form. WAI carries a JCP grant only as a reference by its content +hash inside the receipt that names it, as a playlist receipt carries its publisher's +grant (§8.6); this binding serves no JCP grant, and a client checks such a reference only +as the receipt carrying it binds it. ### 8.5 Parameter sets -A parameter set MAY be served as an immutable resource named by its pin's digest -(`/`), under §5.3's cache headers with `application/octet-stream`. A -sink verifies it by its pin ([SPEC](../SPEC.md) §3.1) and decodes against it only if -the checks of [jwp-receipts](jwp-receipts.md) §7.9 pass. A set they refuse is not -held, and dispatch continues as [SPEC](../SPEC.md) §4 and §7.1 state. +A parameter set MAY be served as an immutable resource named by the digest its pin +states (`/`), under §5.3's cache headers with `application/octet-stream`; +a set in the form `wai.prior.set` is served as its listing under that digest and each +file the listing names under its own digest. The name locates the bytes and proves +nothing. A sink holds a fetched set only as [SPEC](../SPEC.md) §3.1's checks hold it over +the bytes it received, the digest among them, check 5 being +[jwp-receipts](jwp-receipts.md) §7.9 with the statuses and grants it fetched as §8.4 +states. Bytes served under a set's name that are not the set are not held +(`prior_not_held`), a set §7.9 refuses is not held (`prior_revoked`, +`prior_not_authorised`), and dispatch continues as [SPEC](../SPEC.md) §4 and §7.1 state. ### 8.6 Playlist receipts @@ -896,6 +1034,9 @@ is a share of sessions, not of devices or people. | `component_digest_mismatch` | §7 step 6 ([SPEC](../SPEC.md) §7.1's reason) | | `object_digest_mismatch` | §7 step 7, or any whole fetch | | `receipt_name_mismatch`, `receipt_not_about_object` | §8.1, §8.2, §8.6 | +| `receipt_not_served`, `receipt_index_malformed`, `receipt_index_too_large`, `receipt_too_large`, `receipt_legacy_group`, `receipt_fetch_budget_exhausted`, `receipt_fetch_bytes_exhausted` | §8.1, §8.2 | +| `context_not_held` | §8.2 (jwp-receipts §3.1 step 6) | +| `key_log_not_from_origin`, `key_log_too_large`, `grants_file_malformed` | §8.4 | | `codecs_malformed`, `codecs_not_media`, `codecs_candidate`, `codecs_no_common_capability`, `codecs_undeclared`, `codecs_layer_unbound` | §4 | | `base_unanswered` | §10.1 | | `not_a_layer`, `layer_tag_malformed` | §10.4 | @@ -926,7 +1067,20 @@ step 5 states. skipped or fetched again), a published classical master and a playable classical presentation for players, and the policies' choices; the differential adds playlists, adjacent pairs, published masters, renderings, receipt - files, adaptation states and MPD pairs. + files, adaptation states and MPD pairs. For §8.1–§8.5 it holds group receipts and + claims served as receipt files, an index beside each object that has receipts, an + origin's key log, key status and grants file, and a parameter set served by its + digest, with cases for receipt files read under their names, indexes checked against + the whole object, a head proof's cid and nothing, parents walked and not recovered, + walks at and past the bound, legacy groups, files and indexes at and past their caps, + each name fetched once within budgets of files and bytes, claims by subject, parent + and the receipts §3.1 step 6 checks them against for every class (declarations read + by their names first), ancestors by recovery under each publisher key held, + anchoring from the origin only, logs through caches, + grants files and sets held by their pins and §7.9; the differential adds receipt files, + indexes, grants files and key logs each changed one way. `http_verify.py` checks these + cases with the `cryptography` package, through the corpora's own second verifiers of + [jwp-receipts](jwp-receipts.md) §3, §3.1 and §7. - [`cache-conformance/`](../cache-conformance/) checks §9 against HTTP caches and reports, per cache label and version, the configuration that meets it. Products are not named in published reports. @@ -948,7 +1102,13 @@ step 5 states. of its capabilities, to every cache and the origin. A sink that does not want this fetches whole objects. - **Unsigned files.** Receipt indexes, grants files and aliases are locators, never - evidence. + evidence. A receipt file is named by its link hash and a parameter set by its digest, + so a cache or mirror that serves other bytes under the name is refused; a group whose + name does not fix its bytes is not read (§8.1); a key log anchors only from its origin + (§8.4). +- **What a server can make a client do.** Every receipt file, index and item count is + capped, each name is fetched once, and the files and bytes one index costs are bounded + (§8.1, §8.2), so an index cannot make a client fetch or hold without limit. ## Appendix A — CDNI configuration diff --git a/wai/extensions/jwp-receipts.md b/wai/extensions/jwp-receipts.md index 7a114fa2f..a2688520a 100644 --- a/wai/extensions/jwp-receipts.md +++ b/wai/extensions/jwp-receipts.md @@ -1585,6 +1585,8 @@ match the algorithm style of WAI SPEC.md §4 (numbered, terminating). `parent_not_held` is recoverable: the verifier MAY request the missing receipt and offer it before the group again; it MUST NOT silently accept. + Over HTTP the missing receipt is fetched by the link hash the group names + ([http-delivery](http-delivery.md) §8.1). A verifier that keeps only the head cannot tell a fork from a gap and reports both `parent_not_held`; one that keeps the link hashes of earlier groups of the chain tells them apart, and refuses both either diff --git a/wai/http-conformance/.well-known/wai-grants/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4.json b/wai/http-conformance/.well-known/wai-grants/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4.json new file mode 100644 index 000000000..f27160979 --- /dev/null +++ b/wai/http-conformance/.well-known/wai-grants/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4.json @@ -0,0 +1 @@ +{"grants":[{"capabilities":["wai.neural.int_mlicpp"],"form":"decoder","kind":"wai-module-grant","module":"7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4","not_after":"2026-10-06T12:00:00Z","not_before":"2026-10-05T12:00:00Z","origin":"https://origin.example","sig":"85283724547d6c4b72a44d4cb3b1f65aaf1e24c2ee0b5227fa8248e4ad7d7a38513354fadac9c7adc62b23725fa7156455ccd401d17d5ecc341a76b563b55504","signer_id":"did:web:origin.example#x9","signer_pubkey":"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8"},{"capabilities":["wai.neural.int_mlicpp"],"form":"decoder","kind":"wai-module-grant","module":"7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4","not_after":"2026-10-06T12:00:00Z","not_before":"2026-10-05T12:00:00Z","origin":"https://origin.example","sig":"91958c6751f8d2a7fd0206a3f66e02e7b33ffca1c00b872a2f6951bb3c25dd4402054a772675b73a10c44b81d760a87932d9d5dbb805b5d4c38ac942f610b90c","signer_id":"did:web:origin.example#g1","signer_pubkey":"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367"}],"kind":"wai-module-grants","module":"7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4"} \ No newline at end of file diff --git a/wai/http-conformance/.well-known/wai-key-status b/wai/http-conformance/.well-known/wai-key-status new file mode 100644 index 000000000..83139e856 --- /dev/null +++ b/wai/http-conformance/.well-known/wai-key-status @@ -0,0 +1 @@ +{"head":"fab4c950188cdacaee2b290bda245bf8c7bbe5ba5f0549451a72a9241cf4da33","issued_at":"2026-10-06T00:00:00Z","kind":"wai-key-status","origin":"https://origin.example","seq":1,"sig":"1c72d7f3c146edbf6f068e03bf444d66defb78dff1065593d17f82a388b1301511a0bbd45fb08b715f7551039dd555cb3baf794e39c4fabe8719b316aaa4a703"} \ No newline at end of file diff --git a/wai/http-conformance/.well-known/wai-keys b/wai/http-conformance/.well-known/wai-keys new file mode 100644 index 000000000..46bb7c375 --- /dev/null +++ b/wai/http-conformance/.well-known/wai-keys @@ -0,0 +1 @@ +{"kind":"wai-key-log","revisions":[{"grant_max_s":86400,"issued_at":"2026-10-01T00:00:00Z","keys":[{"compromised_since":null,"id":"d1","key":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","may_sign":["delivery","energy/Estimator","log-inclusion","soft-binding"],"status":"active","until":null},{"compromised_since":null,"id":"g1","key":"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367","may_sign":["module/decoder"],"status":"active","until":null}],"kind":"wai-key-set","next_root":"d481afb3d00a3168a515f522aec12623ae3ee77b919ad5db74353a9f55972d8c","origin":"https://origin.example","prev":null,"revoked_modules":[],"root":"4a72e4035bb435d0aa42d43b8873d9f07cb9bf476fe2ade0aba49983eebd425e","seq":0,"sig":"92de01efa058176b0dc77a5784d401427c6266eec4b5583b4a4ac3eea1f2e52c5cc30809c27fb6b3c1908bd29f6658c85452702677f88c392319249df6a9680f","status_max_age_s":3600},{"grant_max_s":86400,"issued_at":"2026-10-05T00:00:00Z","keys":[{"compromised_since":null,"id":"d1","key":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","may_sign":["delivery","energy/Estimator","log-inclusion","soft-binding"],"status":"active","until":null},{"compromised_since":null,"id":"g1","key":"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367","may_sign":["module/decoder"],"status":"active","until":null}],"kind":"wai-key-set","next_root":"d481afb3d00a3168a515f522aec12623ae3ee77b919ad5db74353a9f55972d8c","origin":"https://origin.example","prev":"7f06fd9425a3583d2007074cb32c75514d34b11d064c88304c6e4d4d4cb5ab01","revoked_modules":[],"root":"4a72e4035bb435d0aa42d43b8873d9f07cb9bf476fe2ade0aba49983eebd425e","seq":1,"sig":"abbca51a46e83d57166db431990bb3dc1340fc3effdff4537d3de2f1dbc53f36f73f8190bc59b5c718a00e03294c0bed5133f6806915af3eb1109b0de4dcf409","status_max_age_s":3600}]} \ No newline at end of file diff --git a/wai/http-conformance/README.md b/wai/http-conformance/README.md index d5f2cbae0..57d01d49d 100644 --- a/wai/http-conformance/README.md +++ b/wai/http-conformance/README.md @@ -6,16 +6,25 @@ for §4–§7 — the head, the head proof, the response checks, the plans, sele head and on the whole envelope, the byte-range procedure with its fallback, the `codecs` grammar, union, floor and support; and for §8.6, §10.4, §11 and Appendix F the WAI playlists beside their classical ones, their receipts, the live edge, and the -reference adaptation policies. Two implementations check every case and must agree: +reference adaptation policies; and for §8.1–§8.5 and §10.5 the receipts, keys, grants and +parameter sets served beside the objects. Two implementations check every case and must +agree: - `wai_http_vectors verify .` (also `wai http vectors verify .`), in `wai-rs`, built - with the features `http` and `video_manifest` (the playlist receipts); + with the features `http`, `video_manifest` (the playlist receipts) and + `key_discovery` (the receipts, keys, grants and sets); - `python3 http_verify.py .`, written from the extension text and SPEC §7.1, sharing no - code with `wai-rs`. It needs only the Python standard library, and reads - `../claim-conformance/_blake3.py` (BLAKE3 from its specification), - `../claim-conformance/_ed25519.py` (WAI's strict Ed25519 rule, SPEC §7 *Signatures*, - written from RFC 8032; it checks the playlist receipts) and - `../registry/capabilities.json` (the published registry export). + code with `wai-rs`. It reads `../claim-conformance/_blake3.py` (BLAKE3 from its + specification), `../claim-conformance/_ed25519.py` (WAI's strict Ed25519 rule, SPEC §7 + *Signatures*, written from RFC 8032; it checks the playlist receipts) and + `../registry/capabilities.json` (the published registry export), and needs only the + Python standard library for every case but the receipt and key cases. Those + (`receipt_file`, `receipts`, `key_log`, `grants_file`, `param_set`) are checked by + `http_receipts.py`, which takes what jwp-receipts defines from the corpora's own + second verifiers — `../jwp-conformance/jwp_verify.py`, + `../claim-conformance/claim_verify.py` and `../signer-conformance/signer_verify.py`, + none of which shares code with `wai-rs` — and so needs the `cryptography` package + they use. Without it those cases fail; they are never skipped. **Provenance.** Generated here by `wai_http_vectors`, from a staged-delivery case and from the frozen sources `src/SOURCES.json` lists, each encoded once by an encoder of its format: pictures of the `int_mlicpp` vector's generated 64×64 pattern, AV1 colour-bar test patterns, and a clip of a square moving over a gradient. The test patterns and the clip are generated content whose generators are not in this tree. None is a recording or a photograph. @@ -34,6 +43,7 @@ reference adaptation policies. Two implementations check every case and must agr | `blake3_vectors.json` | a subset of the published BLAKE3 test vectors (CC0), which `http_verify.py` checks itself against first | | `http_verify.py` | the independent verifier | | `falsify/*.patch` | defects that `wai_http_vectors verify` must catch (below) | +| `falsify/mutations.txt` | the `http_verify.py --mutate` defects, each with the cases it must fail (below) | | `src/live/`, `src/players/`, `src/playlists/` | frozen sources of the playlist side: the integer video payloads of O12 and O13, the classical playlists the WAI ones sit beside, and the player fixture's classical master as authored | | `playlists/o9/`, `playlists/o10/`, `playlists/o11/` | WAI media and multivariant playlists (and MPDs) over O9 (and over O9 with its second segment a gap, which `wai-gap.mpd` leaves out), O10 with its layers, and O11, each beside its classical playlist in `src/playlists/`; O9's MPDs are on the timeline of `src/playlists/o9-classical.mpd` (90 kHz, written with `@r`, beside an audio set on its own timescale) | | `live/` | four revisions of a low-latency WAI playlist over O12 beside `src/live/classical-r*.m3u8`, and its multivariant playlist; the parts are listed under aliases `720/..wai` with their cids (the alias files are not in the corpus: the cases serve the objects under them, and `wai http origin --root . --live-edge live/wai-720.m3u8=live/wai-r0.m3u8,live/wai-r1.m3u8,live/wai-r2.m3u8,live/wai-r3.m3u8` replays the edge over HTTP, blocking reloads and hinted parts included) | @@ -44,6 +54,14 @@ reference adaptation policies. Two implementations check every case and must agr | `cases/other-key/` | the third live revision's receipt chained to the second by hash and signed by another key (`r2.json`, and in `mr/` under its receipt hash), and the fourth revision's receipt chained to it (`r3.json`) | | `cases/relist/` | the third live revision listing the alias of part 0 of segment 1201 with part 1's object (`wai-r2.m3u8`), sealed and chained to the second (`r2.json`, and in `mr/`); the fourth revision's receipt chained to it (`r3-after.json`); the fourth revision listing segment 1201's object at sequence 1200 (`wai-r3.m3u8`, `r3.json`); the third revision listing that alias as `./720/1201.0.wai`, with part 1's object and with its own (`wai-r2-dot*.m3u8`, `r2-dot*.json`); and the fourth revision withdrawing segment 1200 as a gap (`wai-r3-gap.m3u8`, `r3-gap.json`) | | `cases/walk-substituted/`, `cases/walk-invalid/` | parents a walk fetches by their links: the second revision's receipt served under the third's receipt hash; and the third revision changed after it was signed, served under its receipt hash, with the fourth revision's receipt chained to it (`r3.json`) | +| `objects/.wai.receipts` | the receipt index of each object that has receipts (§8.2): O9a–O9c (one group per segment, chained), O3, O5 and O7 (one group each, figures of a stated class), O10, O10L1 and O10L2 (one group for the stage set) | +| `objects/rcpt/.json` | each group receipt and claim (§8.1), in JCS: the five groups; a sink's two `stage` claims about O10, chained, the second listing layer 1 applied and layer 2 dropped (§10.5); the publisher's soft binding of O7, which O3's index lists as its ancestor's; a `log-inclusion` claim about O3's group, chained to it; the publisher's `measurement` of O5's figure, chained to that claim; two `energy` claims about O3, one agreeing with its delivery and one not; and a relay's two `delivery-gap` claims about O10L2, one at its coordinates and one not. The publisher signs as `did:web:origin.example#d1` (seed 32 bytes of `0x61`), the sink as `did:example:http-sink` (`0x64`): published test keys, attesting nothing | +| `.well-known/wai-keys`, `.well-known/wai-key-status`, `.well-known/wai-grants/.json` | the key log of `https://origin.example` (two revisions, root `0x60`), a status for its head frozen at `2026-10-06T00:00:00Z`, and the grants file for O2's pinned set: a grant by `g1` (`0x63`) and one by a key the log does not list (§8.4) | +| `sets/` | O2's pinned parameter set, served by its digest (§8.5) | +| `cases/keys/` | the log's first revision alone (an older copy), and a log whose second revision revokes the set's digest | +| `cases/rcpt/`, `cases/receipts/` | O9a's group with its signature changed, served under the link hash of what it then is, and an index naming it; O1's group in the legacy payload, under its legacy link hash, and an index naming it (§8.1); O9a's index with 256 and 257 delivery items and with 257 claims; O9c's index listing its item fifty times (§8.2) | +| `cases/rcpt/`, `cases/receipts/O3-*`, `O5-*`, `O7-*` | claims of the classes the published indexes carry none of, their bodies claim-conformance's accepted vectors retargeted to the corpus's objects and signed with its keys, and indexes listing them (§8.2 step 6): the publisher's lawful-basis declarations about O3 and O7 and a sink's decisions on a declared purpose, an undeclared one and O7's declaration; soft bindings of O3 by the publisher and by the relay and a sink's recovery records of a value found in O7, matching, not matching, and naming the relay's; operating points over O5's leaf, agreeing and not; and a session claim. And O7 delivered again under a second publisher's key (seed `0x67`), with indexes listing that delivery before the publisher's, after it, and alone (jwp-receipts §3.2's *P*) | +| `cases/rcpt-deep/`, `cases/receipts/deep-*.wai.receipts` | a chain of 66 groups over O1, each the parent of the next, and indexes naming the groups 64 and 65 parents from its root (§8.1's bound) | | `cases/sig-*.json` | the O9 playlist's receipt re-signed at the edges of Ed25519 (Appendix H): the identity key, canonical and respelled `y` = p + 1, under `(R, S) = (B, 1)`; under the key B (scalar 1), `R` the identity, canonical and respelled, with `S` = k, so the cofactorless equation holds point for point; and under the key B a signature the strict rule accepts (`sig-key-base-point.json`) | ## Objects @@ -116,6 +134,11 @@ outside this repository, and the AV1 streams were checked to decode. | `playlist_receipt` | a `playlist`, its `receipt` file and the `name` it is fetched as (the file's own by default), each optionally edited | the receipt hash and its parent, or `error` | | `live` | `revisions` in the order a client fetched them (each a WAI playlist, its receipt, its classical playlist, `edit_wai`), `fetches` (an `alias` and the `object` served under it, optionally patched), the playlist's `url` (against which its lines resolve; `https://origin.example/live/wai-720.m3u8` by default), where parents are fetched from (`parents`, a directory holding `.json`; the first revision's `mr/` by default) and the walk's bound (`walk_bound`, 64 by default) | per revision its adjacency, its listing against what accepted revisions bound, its receipt, what it did to the chain (`true`, `unchanged`, `reanchored`, or the reason) and the parents walked; per fetch the accepted revision that first lists the alias, the outcome and what held (`listed`, or `signed` when a receipt in the chain binds the alias's URL to that cid) | | `uri_resolve` | a `base` and a `reference` | the `url` (RFC 3986 §5.2 and §6.2.2, without the fragment) | +| `receipt_file` | a `file` read as a receipt of `kind` (`group` or `claim`) under its `name` (its own by default), optionally edited | its `link`, or `error` with the jwp-receipts `step` | +| `receipts` | `checks` in order, each an `object` (or `object_file`), what the client holds of it (`held`: `none`; `whole`, its bytes or `held_object`'s; or `{"fetch": {…}}`, a fetch as the `fetch` op runs one), its `index` (beside it by default) optionally edited, or another object's index (`object_cid`), optionally padded (`pad_index`, bytes); one set of chains (`join`), a parent bound (`max_walk`, 64 by default, never more) and a fetch budget (`fetch_budget`, 1024 by default), the receipt files `serve`d (`rcpt` directories searched, `omit`ted hashes, hashes served `as` another file, files `pad`ded past their cap, or every file to `pad_all`), `recovery` (the soft-binding algorithms a matcher is held for; the publishers are those of the items delivered in earlier checks), and `keys` (key-log steps, as `key_log` runs them) | per check the index's verdict and, when it reads, each delivery item's admission, parents `walked`, `evidence`, how the hash was `established` and the property's `reading`, or its `error` (with where a walk stopped); each claim's `class`, what it `attests`, its `subject` and `parent`, or its `error`; the receipt files the check fetched (`files_fetched`); with `keys`, each signer's confirmation; the fetch's verdict | +| `key_log` | `steps`: `anchor` (a log retrieved as `retrieval` states: `requested`, `answered`, `status`), `pin`, or `cached` (a log through a cache), each for an `origin` (the corpus's by default), optionally edited and `pad`ded with spaces to a length | what each step did (`anchored`, `added`) or its `error` | +| `grants_file` | a grants `file` for a `module` (the digest in its name by default), optionally edited | each grant's reading by §7.9's check 1, or `error` | +| `param_set` | a set `served` for a `capability` under a pin of `sha256`, the sink's `keys`, `statuses` and `grants` file, the origins it holds the capability's sets to §7.9 for (`hold_to`; absent, not held to it), and `now` (null: no trusted clock) | `held`, or `error` with §7.9's `check` and `reason`, or the origin a revocation is by | | `abr` | a `policy` and a `state` (Appendix F) | the `choice` and what the `budget` did, or `error` beyond Appendix F's bounds | | `abr_order` | an object, what the sink `advertises`, a `policy`, a `state` | the entry `order` | | `depth` | layer `objects` (or `layers_n`), a `policy`, a `state` | the `depth` and what the `budget` did, or `error` | @@ -157,9 +180,19 @@ to the reason it exists for, and OD to Appendix D's cid, length and proof size. ## Falsification Each patch in `falsify/` names the function it breaks and, on its first line, the -cases that must fail. The `falsify` job of `.github/workflows/wai-http.yml` applies -each to a fresh worktree, requires the build to succeed, and requires -`wai_http_vectors verify` to exit non-zero naming those cases. `http_verify.py +cases that must fail. `falsify/run.sh` applies each to one worktree of the head, built +once and rebuilt incrementally per patch under the `falsify` profile (overflow checks +and debug assertions on, as in the tests), requires the build to succeed and +`wai_http_vectors verify` to exit non-zero naming those cases, then reverses the patch +and requires the tree to be clean; the unpatched tree must build and pass first. The +`falsify` job of `.github/workflows/wai-http.yml` runs the patches and the mutations in +shards (`run.sh --patches I/N`, `--mutations I/N`, each taking the ones whose index mod +N is I), each printing what it ran and failing unless that is exactly what was assigned +to it, and its `shards` job checks the shards the matrix declares cover every patch and +every mutation once (`run.sh --check-shards`). Each mutation in `falsify/mutations.txt` +lists the cases it must fail, as each patch does: it passes only when `http_verify.py +--mutate NAME` exits non-zero and names every one in a `FAIL` line, and a crash, or a +non-zero exit that names no case, is a failure of the harness. `http_verify.py --mutate NAME` does the same on the Python side (`left-half`, `no-root`, `counter-zero` fail the proof cases; `floor-as-codecs` fails `union_O9`; `blake3-iv` fails the self-test; `base-at-done`, `tick-noop` and `refinement-unclipped` fail the @@ -167,8 +200,17 @@ fails the self-test; `base-at-done`, `tick-noop` and `refinement-unclipped` fail `render-digest-off`, `name-unchecked`, `chain-unchecked`, `ilog2-no-interp`, `bola-waits`, `unlabelled-as-zero` and `round-half-down` fail the playlist, receipt and adaptation cases they reach; `nonconforming-offered`, `conforming-withheld` and -`refines-read-whole` fail the ORN, ONC, OCF, ONM and OR fetches; `falsify/run.sh` runs -every one), and the job checks a differential: `wai_http_vectors diff` +`refines-read-whole` fail the ORN, ONC, OCF, ONM and OR fetches; `rcpt-name-unchecked`, +`legacy-admitted`, `walk-skipped`, `walk-unclamped`, `memo-off`, `budget-unchecked`, +`bytes-unchecked`, `caps-unchecked`, `nothing-as-delivered`, `groups-from-listed`, +`step6-skipped`, `decision-unchecked`, `recovery-record-unchecked`, +`declaration-unnamed`, `first-publisher`, `matchers-need-object`, +`operating-point-unchecked`, `session-accepted`, `bytes-read-whole`, +`key-log-uncapped`, `ancestor-any`, +`anchor-follows-redirect`, `cached-rollback-unchecked`, `grants-module-unchecked`, +`set-grants-ignored` and `set-by-name` fail the receipt, key, grant and set cases of +§8.1–§8.5; `falsify/run.sh` runs every one), and the job checks a differential: +`wai_http_vectors diff` writes random envelopes' covered prefixes and proofs, `http_verify.py diff` verifies every one, and, with one chaining-value byte changed in ten of them, refuses exactly those ten. @@ -210,7 +252,13 @@ inputs where two readers most easily part, in families: receipt file with one member changed; - `fuzz_abr`: states and parameters of the four policies, sizes out of order, samples of 0 ms, empty options and budgets; `fuzz_ilog2`: the integer log at random points - and the ends of `u64`. + and the ends of `u64`; +- `fuzz_receipt_file`: a committed group receipt or claim file with one member changed, + read as its kind or the other; `fuzz_receipt_index`: an index with one member changed, + checked with its object held whole or not at all; `fuzz_grants_file`: the grants file + with one member changed; `fuzz_key_log`: the key log anchored from a retrieval whose + URLs, status or origin may be off, the log sometimes changed one way. A refusal inside + an outcome of steps or checks counts as one. A refusal is an outcome like any other: `http_verify.py cases` must give the same code. A case wai-rs gives no outcome for stops the run, and `fuzz` prints, per family, diff --git a/wai/http-conformance/cases.json b/wai/http-conformance/cases.json index 148adc9ce..218aa2ebf 100644 --- a/wai/http-conformance/cases.json +++ b/wai/http-conformance/cases.json @@ -941,5 +941,107 @@ {"name":"same_pick_abr_O7_low","op":"same_pick","input":{"object":"O7","advertises":["wai.video.av1","wai.meta.hdr10plus"],"abr":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,12500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}}},"expect":{"pick":1}}, {"name":"fetch_abr_O3_high","op":"fetch","input":{"object":"O3","advertises":["wai.video.av1","wai.meta.hdr10plus"],"applies":["wai.meta.hdr10plus"],"abr":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,1250000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"fragment":"#h=1009"},"expect":{"role":"base","pick":1,"verified_by":"head-proof","why_whole":null,"body_bytes":78526,"requests":3,"companions":[2],"refinements":[],"abandoned":[],"dropped":[],"skipped":[],"component_sha256":"68a51be787c439226eb7bb1cf434e913fc1f0e221870da87f70615d667a7ab12"}}, {"name":"same_pick_abr_O3_high","op":"same_pick","input":{"object":"O3","advertises":["wai.video.av1","wai.meta.hdr10plus"],"applies":["wai.meta.hdr10plus"],"abr":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,1250000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"fragment":"#h=1009"},"expect":{"pick":1}}, -{"name":"same_pick_abr_O7_high","op":"same_pick","input":{"object":"O7","advertises":["wai.video.av1","wai.meta.hdr10plus"],"abr":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,1250000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}}},"expect":{"pick":1}} +{"name":"same_pick_abr_O7_high","op":"same_pick","input":{"object":"O7","advertises":["wai.video.av1","wai.meta.hdr10plus"],"abr":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,1250000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}}},"expect":{"pick":1}}, +{"name":"receipt_file_group","op":"receipt_file","input":{"file":"objects/rcpt/3fba8315347da348bbb2f7a4b3f5488ff18a2be9bfb95efc7921142969ec3807.json","kind":"group"},"expect":{"link":"3fba8315347da348bbb2f7a4b3f5488ff18a2be9bfb95efc7921142969ec3807"}}, +{"name":"receipt_file_claim","op":"receipt_file","input":{"file":"objects/rcpt/0c0b87f288cc459e99b914920284bf633eab89bf035078137b89936002072768.json","kind":"claim"},"expect":{"link":"0c0b87f288cc459e99b914920284bf633eab89bf035078137b89936002072768"}}, +{"name":"receipt_file_name_mismatch","op":"receipt_file","input":{"file":"objects/rcpt/3fba8315347da348bbb2f7a4b3f5488ff18a2be9bfb95efc7921142969ec3807.json","name":"e0e1d579fff00461f48124d31edac54e44cf157fd26307fca69de8e1c7cd9d30","kind":"group"},"expect":{"error":"receipt_name_mismatch","step":null}}, +{"name":"receipt_file_group_as_claim","op":"receipt_file","input":{"file":"objects/rcpt/3fba8315347da348bbb2f7a4b3f5488ff18a2be9bfb95efc7921142969ec3807.json","kind":"claim"},"expect":{"error":"wrong_kind","step":1}}, +{"name":"receipt_file_claim_as_group","op":"receipt_file","input":{"file":"objects/rcpt/14c0d31e65ae842627c606581c492bad7a73ee9190fc7db33d15de262d535491.json","kind":"group"},"expect":{"error":"wrong_kind","step":1}}, +{"name":"receipt_file_kind_null","op":"receipt_file","input":{"file":"objects/rcpt/3fba8315347da348bbb2f7a4b3f5488ff18a2be9bfb95efc7921142969ec3807.json","kind":"group","edit":[["\"kind\":\"group\"","\"kind\":null"]]},"expect":{"error":"wrong_kind","step":1}}, +{"name":"receipt_file_member_twice","op":"receipt_file","input":{"file":"objects/rcpt/3fba8315347da348bbb2f7a4b3f5488ff18a2be9bfb95efc7921142969ec3807.json","kind":"group","edit":[["{","{\"group_id\":0,"]]},"expect":{"error":"malformed","step":1}}, +{"name":"receipt_file_claim_member_twice","op":"receipt_file","input":{"file":"objects/rcpt/0c0b87f288cc459e99b914920284bf633eab89bf035078137b89936002072768.json","kind":"claim","edit":[["{","{\"seq\":1,"]]},"expect":{"error":"malformed","step":1}}, +{"name":"receipts_walk_two","op":"receipts","input":{"checks":[{"object":"O9c","held":"whole"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":2,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"}],"claims":[],"files_fetched":3}]}}, +{"name":"receipts_walk_one","op":"receipts","input":{"checks":[{"object":"O9b","held":"whole"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":1,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"}],"claims":[],"files_fetched":2}]}}, +{"name":"receipts_in_order","op":"receipts","input":{"checks":[{"object":"O9a","held":"whole"},{"object":"O9b","held":"whole"},{"object":"O9c","held":"whole"},{"object":"O9c","held":"whole"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"}],"claims":[],"files_fetched":1},{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"}],"claims":[],"files_fetched":1},{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"}],"claims":[],"files_fetched":1},{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"}],"claims":[],"files_fetched":1}]}}, +{"name":"receipts_back_after_head","op":"receipts","input":{"checks":[{"object":"O9c","held":"whole"},{"object":"O9a","held":"none"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":2,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"}],"claims":[],"files_fetched":3},{"index":"ok","delivery":[{"admission":"accept again","walked":0,"evidence":"inclusion-verified","established":null,"reading":null}],"claims":[],"files_fetched":1}]}}, +{"name":"receipts_join_anchors","op":"receipts","input":{"join":true,"checks":[{"object":"O9c","held":"whole"}],"serve":{"omit":["3fba8315347da348bbb2f7a4b3f5488ff18a2be9bfb95efc7921142969ec3807","e0e1d579fff00461f48124d31edac54e44cf157fd26307fca69de8e1c7cd9d30"]}},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept anchor","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"}],"claims":[],"files_fetched":1}]}}, +{"name":"receipts_parent_not_served","op":"receipts","input":{"checks":[{"object":"O9c","held":"whole"}],"serve":{"omit":["3fba8315347da348bbb2f7a4b3f5488ff18a2be9bfb95efc7921142969ec3807"]}},"expect":{"checks":[{"index":"ok","delivery":[{"error":"parent_not_held","step":null,"walked":1,"stop":"receipt_not_served"}],"claims":[],"files_fetched":3}]}}, +{"name":"receipts_walk_bound","op":"receipts","input":{"max_walk":1,"checks":[{"object":"O9c","held":"whole"}]},"expect":{"checks":[{"index":"ok","delivery":[{"error":"parent_not_held","step":null,"walked":1,"stop":"bound"}],"claims":[],"files_fetched":2}]}}, +{"name":"receipts_parent_renamed","op":"receipts","input":{"checks":[{"object":"O9c","held":"whole"}],"serve":{"as":{"3fba8315347da348bbb2f7a4b3f5488ff18a2be9bfb95efc7921142969ec3807":"objects/rcpt/e0e1d579fff00461f48124d31edac54e44cf157fd26307fca69de8e1c7cd9d30.json"}}},"expect":{"checks":[{"index":"ok","delivery":[{"error":"parent_not_held","step":null,"walked":2,"stop":"receipt_name_mismatch"}],"claims":[],"files_fetched":3}]}}, +{"name":"receipts_group_not_served","op":"receipts","input":{"checks":[{"object":"O9a","held":"whole"}],"serve":{"omit":["3fba8315347da348bbb2f7a4b3f5488ff18a2be9bfb95efc7921142969ec3807"]}},"expect":{"checks":[{"index":"ok","delivery":[{"error":"receipt_not_served","step":null}],"claims":[],"files_fetched":1}]}}, +{"name":"receipts_group_renamed","op":"receipts","input":{"checks":[{"object":"O9a","held":"whole"}],"serve":{"as":{"3fba8315347da348bbb2f7a4b3f5488ff18a2be9bfb95efc7921142969ec3807":"objects/rcpt/8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765.json"}}},"expect":{"checks":[{"index":"ok","delivery":[{"error":"receipt_name_mismatch","step":null}],"claims":[],"files_fetched":1}]}}, +{"name":"receipts_group_bad_signature","op":"receipts","input":{"serve":{"rcpt":["objects/rcpt","cases/rcpt"]},"checks":[{"object":"O9a","held":"whole","index":"cases/receipts/O9a-bad-signature.wai.receipts"}]},"expect":{"checks":[{"index":"ok","delivery":[{"error":"signature_verify_failed","step":2}],"claims":[],"files_fetched":1}]}}, +{"name":"receipts_head_proof","op":"receipts","input":{"checks":[{"object":"O3","held":{"fetch":{"advertises":["wai.video.av1"],"applies":["wai.meta.hdr10plus"]}}}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"head-proof","reading":"signed class=Estimator by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d"}],"claims":[{"error":"receipt_not_about_object","step":null},{"class":"log-inclusion","attests":"receipt","subject":"ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018","parent":"14c0d31e65ae842627c606581c492bad7a73ee9190fc7db33d15de262d535491"},{"class":"energy","attests":"object","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","parent":"b766a9838403aa0b33528ac30947c8f1e6fd993ba6434f112392f11b482464f0"},{"error":"energy_disagrees_with_delivery","step":6}],"files_fetched":6,"fetched":"head-proof"}]}}, +{"name":"receipts_fetch_refused_holds_nothing","op":"receipts","input":{"checks":[{"object":"O3","held":{"fetch":{"advertises":["wai.video.av1"],"applies":["wai.meta.hdr10plus"],"tamper":{"patch_object":[[200,"23"]]}}}}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"inclusion-verified","established":null,"reading":null}],"claims":[{"error":"receipt_not_about_object","step":null},{"class":"log-inclusion","attests":"receipt","subject":"ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018","parent":"14c0d31e65ae842627c606581c492bad7a73ee9190fc7db33d15de262d535491"},{"error":"context_not_held","step":6},{"error":"context_not_held","step":6}],"files_fetched":6,"fetched":"head_proof_mismatch"}]}}, +{"name":"receipts_other_object_in_hand","op":"receipts","input":{"checks":[{"object":"O9a","held":"whole","held_object":"O9b"}]},"expect":{"checks":[{"index":"ok","delivery":[{"error":"content_hash_mismatch","step":4}],"claims":[],"files_fetched":1}]}}, +{"name":"receipts_stage_set","op":"receipts","input":{"checks":[{"object":"O10","held":"whole"},{"object":"O10L1","held":"whole"},{"object":"O10L2","held":"none"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"}],"claims":[{"class":"stage","attests":"object","subject":"1eb764beb4a8a739054341df8b5b7d065e490032b187a93787be15f1478080a8","parent":"8754b1d2e028880a1d21fe29dca565c0588125ea25979e37e1d90723b674654c"}],"files_fetched":3},{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"}],"claims":[],"files_fetched":1},{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"inclusion-verified","established":null,"reading":null}],"claims":[{"class":"delivery-gap","attests":"object","subject":"753e8ac465d347547c688690eb51615f19118c0a4c17490499fbb31aefd5cd0c","parent":null},{"error":"context_not_held","step":6}],"files_fetched":3}]}}, +{"name":"receipts_layer_fetched","op":"receipts","input":{"checks":[{"object":"O10L1","held":{"fetch":{"listed_as_layer":true}}}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"}],"claims":[],"files_fetched":1,"fetched":"whole-object"}]}}, +{"name":"receipts_claims_O3","op":"receipts","input":{"checks":[{"object":"O3","held":"whole"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d"}],"claims":[{"error":"receipt_not_about_object","step":null},{"class":"log-inclusion","attests":"receipt","subject":"ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018","parent":"14c0d31e65ae842627c606581c492bad7a73ee9190fc7db33d15de262d535491"},{"class":"energy","attests":"object","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","parent":"b766a9838403aa0b33528ac30947c8f1e6fd993ba6434f112392f11b482464f0"},{"error":"energy_disagrees_with_delivery","step":6}],"files_fetched":6}]}}, +{"name":"receipts_claims_O7","op":"receipts","input":{"checks":[{"object":"O7","held":"whole"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d"}],"claims":[{"class":"soft-binding","attests":"object","subject":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f","parent":null}],"files_fetched":2}]}}, +{"name":"receipts_claim_parent_not_served","op":"receipts","input":{"checks":[{"object":"O10","held":"whole"}],"serve":{"omit":["8754b1d2e028880a1d21fe29dca565c0588125ea25979e37e1d90723b674654c"]}},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"}],"claims":[{"error":"parent_not_held","step":null,"walked":0,"stop":"receipt_not_served"}],"files_fetched":3}]}}, +{"name":"receipts_index_other_object","op":"receipts","input":{"checks":[{"object":"O9b","held":"whole","index":"objects/31abc1da2cdf927191f3f447f68508ead30d078690cb7bea2435ae3d1df5f4f3.wai.receipts"}]},"expect":{"checks":[{"index":"receipt_not_about_object"}]}}, +{"name":"receipts_property_disagrees","op":"receipts","input":{"checks":[{"object":"O3","held":"whole","edit_index":[["\"group_receipt\":\"ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018\",\"joules_micro\":1500}","\"group_receipt\":\"ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018\",\"joules_micro\":1501}"]]}]},"expect":{"checks":[{"index":"ok","delivery":[{"error":"property_disagrees","step":null}],"claims":[{"error":"receipt_not_about_object","step":null},{"error":"receipt_not_about_object","step":null},{"error":"context_not_held","step":6},{"error":"context_not_held","step":6}],"files_fetched":6}]}}, +{"name":"receipts_item_not_about_object","op":"receipts","input":{"checks":[{"object":"O9a","held":"whole","edit_index":[["\"content_hash\":\"31abc1da2cdf927191f3f447f68508ead30d078690cb7bea2435ae3d1df5f4f3\",\"group_receipt\"","\"content_hash\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"group_receipt\""]]}]},"expect":{"checks":[{"index":"ok","delivery":[{"error":"receipt_not_about_object","step":null}],"claims":[],"files_fetched":0}]}}, +{"name":"receipts_index_member_added","op":"receipts","input":{"checks":[{"object":"O9a","held":"whole","edit_index":[["{","{\"note\":1,"]]}]},"expect":{"checks":[{"index":"receipt_index_malformed"}]}}, +{"name":"receipts_item_member_added","op":"receipts","input":{"checks":[{"object":"O3","held":"whole","edit_index":[["{\"object_receipt\"","{\"note\":1,\"object_receipt\""]]}]},"expect":{"checks":[{"index":"ok","delivery":[{"error":"receipt_index_malformed","step":null}],"claims":[{"error":"receipt_not_about_object","step":null},{"error":"receipt_not_about_object","step":null},{"error":"context_not_held","step":6},{"error":"context_not_held","step":6}],"files_fetched":5}]}}, +{"name":"receipts_claim_entry_not_a_hash","op":"receipts","input":{"checks":[{"object":"O7","held":"whole","edit_index":[["14c0d31e65ae842627c606581c492bad7a73ee9190fc7db33d15de262d535491","s0"]]}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d"}],"claims":[{"error":"receipt_index_malformed","step":null}],"files_fetched":1}]}}, +{"name":"receipts_measurement_figure","op":"receipts","input":{"checks":[{"object":"O5","held":"whole"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=OnChipCounter by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d"}],"claims":[{"class":"measurement","attests":"object","subject":"e43e038c10c0494d9647444fd4daf54b19fc3a946a3c7980431bc6e0a236093b","parent":"dae83c921710311cbd34d8d4a2bedab53cf56a9be5afea2dc3d264ee978a03c5"}],"files_fetched":3}]}}, +{"name":"receipts_measurement_figure_not_held","op":"receipts","input":{"checks":[{"object":"O5","held":"none"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"inclusion-verified","established":null,"reading":null}],"claims":[{"error":"figure_not_held","step":6}],"files_fetched":3}]}}, +{"name":"receipts_energy_not_delivered","op":"receipts","input":{"checks":[{"object":"O3","held":"none"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"inclusion-verified","established":null,"reading":null}],"claims":[{"error":"receipt_not_about_object","step":null},{"class":"log-inclusion","attests":"receipt","subject":"ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018","parent":"14c0d31e65ae842627c606581c492bad7a73ee9190fc7db33d15de262d535491"},{"error":"context_not_held","step":6},{"error":"context_not_held","step":6}],"files_fetched":6}]}}, +{"name":"receipts_delivery_gap","op":"receipts","input":{"checks":[{"object":"O10L2","held":"none"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"inclusion-verified","established":null,"reading":null}],"claims":[{"class":"delivery-gap","attests":"object","subject":"753e8ac465d347547c688690eb51615f19118c0a4c17490499fbb31aefd5cd0c","parent":null},{"error":"context_not_held","step":6}],"files_fetched":3}]}}, +{"name":"receipts_claim_through_foreign_item","op":"receipts","input":{"checks":[{"object":"O9a","held":"whole","edit_index":[["\"claims\":[]","\"claims\":[\"dae83c921710311cbd34d8d4a2bedab53cf56a9be5afea2dc3d264ee978a03c5\"]"],["\"delivery\":[","\"delivery\":[{\"object_receipt\":{\"content_hash\":\"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9\",\"energy_provenance\":\"Estimator\",\"group_id\":0,\"joules_micro\":1500,\"kind\":\"object\",\"leaf_version\":2,\"merkle_proof\":{\"direction_bits\":0,\"leaf_index\":0,\"siblings\":[\"368b22537cfa0d3d0bfcfe1f6256302af54c54711dfa411c193beda171c97edb\"]},\"object_id\":0,\"origin\":\"origin\",\"track_alias\":2},\"property\":{\"acquisition\":\"Estimator\",\"acquisition_evidence\":\"\",\"content_hash\":\"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9\",\"group_receipt\":\"ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018\",\"joules_micro\":1500}},"]]}]},"expect":{"checks":[{"index":"ok","delivery":[{"error":"receipt_not_about_object","step":null},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"}],"claims":[{"error":"receipt_not_about_object","step":null}],"files_fetched":2}]}}, +{"name":"receipts_ancestor_recovered","op":"receipts","input":{"recovery":{"observed_value":"0ff0","exact_match_algs":["org.example.fingerprint"]},"checks":[{"object":"O7","held":"whole"},{"object":"O3","held":"whole"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d"}],"claims":[{"class":"soft-binding","attests":"object","subject":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f","parent":null}],"files_fetched":2},{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d"}],"claims":[{"class":"soft-binding","attests":"ancestor","subject":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f","parent":null},{"class":"log-inclusion","attests":"receipt","subject":"ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018","parent":"14c0d31e65ae842627c606581c492bad7a73ee9190fc7db33d15de262d535491"},{"class":"energy","attests":"object","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","parent":"b766a9838403aa0b33528ac30947c8f1e6fd993ba6434f112392f11b482464f0"},{"error":"energy_disagrees_with_delivery","step":6}],"files_fetched":6}]}}, +{"name":"receipts_ancestor_no_publisher","op":"receipts","input":{"recovery":{"observed_value":"0ff0","exact_match_algs":["org.example.fingerprint"]},"checks":[{"object":"O3","held":"whole"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d"}],"claims":[{"error":"receipt_not_about_object","step":null},{"class":"log-inclusion","attests":"receipt","subject":"ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018","parent":"14c0d31e65ae842627c606581c492bad7a73ee9190fc7db33d15de262d535491"},{"class":"energy","attests":"object","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","parent":"b766a9838403aa0b33528ac30947c8f1e6fd993ba6434f112392f11b482464f0"},{"error":"energy_disagrees_with_delivery","step":6}],"files_fetched":6}]}}, +{"name":"receipts_ancestor_value_mismatch","op":"receipts","input":{"recovery":{"observed_value":"0000","exact_match_algs":["org.example.fingerprint"]},"checks":[{"object":"O7","held":"whole"},{"object":"O3","held":"whole"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d"}],"claims":[{"class":"soft-binding","attests":"object","subject":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f","parent":null}],"files_fetched":2},{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d"}],"claims":[{"error":"recovery_value_mismatch","step":6},{"class":"log-inclusion","attests":"receipt","subject":"ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018","parent":"14c0d31e65ae842627c606581c492bad7a73ee9190fc7db33d15de262d535491"},{"class":"energy","attests":"object","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","parent":"b766a9838403aa0b33528ac30947c8f1e6fd993ba6434f112392f11b482464f0"},{"error":"energy_disagrees_with_delivery","step":6}],"files_fetched":6}]}}, +{"name":"receipts_ancestor_no_matcher","op":"receipts","input":{"recovery":{"observed_value":"0ff0","exact_match_algs":[]},"checks":[{"object":"O7","held":"whole"},{"object":"O3","held":"whole"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d"}],"claims":[{"class":"soft-binding","attests":"object","subject":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f","parent":null}],"files_fetched":2},{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d"}],"claims":[{"error":"recovery_no_matcher","step":6},{"class":"log-inclusion","attests":"receipt","subject":"ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018","parent":"14c0d31e65ae842627c606581c492bad7a73ee9190fc7db33d15de262d535491"},{"class":"energy","attests":"object","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","parent":"b766a9838403aa0b33528ac30947c8f1e6fd993ba6434f112392f11b482464f0"},{"error":"energy_disagrees_with_delivery","step":6}],"files_fetched":6}]}}, +{"name":"receipts_ancestor_any_object","op":"receipts","input":{"checks":[{"object":"O9a","held":"whole","edit_index":[["\"claims\":[]","\"claims\":[\"14c0d31e65ae842627c606581c492bad7a73ee9190fc7db33d15de262d535491\"]"]]}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"}],"claims":[{"error":"receipt_not_about_object","step":null}],"files_fetched":2}]}}, +{"name":"receipts_legacy_group","op":"receipts","input":{"serve":{"rcpt":["objects/rcpt","cases/rcpt"]},"checks":[{"object":"O1","held":"whole","index":"cases/receipts/O1-legacy.wai.receipts"}]},"expect":{"checks":[{"index":"ok","delivery":[{"error":"receipt_legacy_group","step":null}],"claims":[],"files_fetched":1}]}}, +{"name":"receipts_walk_64","op":"receipts","input":{"serve":{"rcpt":["cases/rcpt-deep"]},"max_walk":null,"checks":[{"object_cid":"f6fa1ca70e69bee70aa2bd36620b3e93a50ca2e110e1fbb91f3c6747f5deb860","held":"none","index":"cases/receipts/deep-64.wai.receipts"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":64,"evidence":"inclusion-verified","established":null,"reading":null}],"claims":[],"files_fetched":65}]}}, +{"name":"receipts_walk_65","op":"receipts","input":{"serve":{"rcpt":["cases/rcpt-deep"]},"max_walk":null,"checks":[{"object_cid":"002ebec0ca68dc089695fcc8b1375c6964fd8f25d07f79c1d3e568a684770b3b","held":"none","index":"cases/receipts/deep-65.wai.receipts"}]},"expect":{"checks":[{"index":"ok","delivery":[{"error":"parent_not_held","step":null,"walked":64,"stop":"bound"}],"claims":[],"files_fetched":65}]}}, +{"name":"receipts_walk_65_asks_1000","op":"receipts","input":{"serve":{"rcpt":["cases/rcpt-deep"]},"max_walk":1000,"checks":[{"object_cid":"002ebec0ca68dc089695fcc8b1375c6964fd8f25d07f79c1d3e568a684770b3b","held":"none","index":"cases/receipts/deep-65.wai.receipts"}]},"expect":{"checks":[{"index":"ok","delivery":[{"error":"parent_not_held","step":null,"walked":64,"stop":"bound"}],"claims":[],"files_fetched":65}]}}, +{"name":"receipts_items_fetch_each_name_once","op":"receipts","input":{"checks":[{"object":"O9c","held":"whole","index":"cases/receipts/O9c-x50.wai.receipts"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":2,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"}],"claims":[],"files_fetched":3}]}}, +{"name":"receipts_fetch_budget","op":"receipts","input":{"fetch_budget":2,"checks":[{"object":"O9c","held":"whole"}]},"expect":{"checks":[{"index":"ok","delivery":[{"error":"parent_not_held","step":null,"walked":1,"stop":"receipt_fetch_budget_exhausted"}],"claims":[],"files_fetched":2}]}}, +{"name":"receipts_file_too_large","op":"receipts","input":{"serve":{"pad":{"3fba8315347da348bbb2f7a4b3f5488ff18a2be9bfb95efc7921142969ec3807":1048577}},"checks":[{"object":"O9a","held":"whole"}]},"expect":{"checks":[{"index":"ok","delivery":[{"error":"receipt_too_large","step":null}],"claims":[],"files_fetched":1}]}}, +{"name":"receipts_parent_too_large","op":"receipts","input":{"serve":{"pad":{"3fba8315347da348bbb2f7a4b3f5488ff18a2be9bfb95efc7921142969ec3807":1048577}},"checks":[{"object":"O9b","held":"whole"}]},"expect":{"checks":[{"index":"ok","delivery":[{"error":"parent_not_held","step":null,"walked":1,"stop":"receipt_too_large"}],"claims":[],"files_fetched":2}]}}, +{"name":"receipts_file_at_its_cap","op":"receipts","input":{"serve":{"pad":{"3fba8315347da348bbb2f7a4b3f5488ff18a2be9bfb95efc7921142969ec3807":1048576}},"checks":[{"object":"O9a","held":"whole"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"}],"claims":[],"files_fetched":1}]}}, +{"name":"receipts_index_too_large","op":"receipts","input":{"checks":[{"object":"O9a","held":"whole","pad_index":4194305}]},"expect":{"checks":[{"index":"receipt_index_too_large"}]}}, +{"name":"receipts_index_at_its_cap","op":"receipts","input":{"checks":[{"object":"O9a","held":"whole","pad_index":4194304}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered"}],"claims":[],"files_fetched":1}]}}, +{"name":"receipts_index_256_items","op":"receipts","input":{"checks":[{"object":"O9a","held":"whole","index":"cases/receipts/O9a-256-items.wai.receipts"}]},"expect":{"checks":[{"index":"ok","delivery":[{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null}],"claims":[{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null},{"error":"receipt_index_malformed","step":null}],"files_fetched":0}]}}, +{"name":"receipts_index_257_items","op":"receipts","input":{"checks":[{"object":"O9a","held":"whole","index":"cases/receipts/O9a-257-items.wai.receipts"}]},"expect":{"checks":[{"index":"receipt_index_too_large"}]}}, +{"name":"receipts_index_257_claims","op":"receipts","input":{"checks":[{"object":"O9a","held":"whole","index":"cases/receipts/O9a-257-claims.wai.receipts"}]},"expect":{"checks":[{"index":"receipt_index_too_large"}]}}, +{"name":"receipt_file_legacy_group","op":"receipt_file","input":{"file":"cases/rcpt/6a29eb0066513cad092217b4f905f3decfc703c081c832fb35e3e42f3d1c7043.json","kind":"group"},"expect":{"error":"receipt_legacy_group","step":null}}, +{"name":"receipts_fetch_bytes_exhausted","op":"receipts","input":{"serve":{"rcpt":["cases/rcpt-deep"],"pad_all":1048576},"checks":[{"object_cid":"f6fa1ca70e69bee70aa2bd36620b3e93a50ca2e110e1fbb91f3c6747f5deb860","held":"none","index":"cases/receipts/deep-64.wai.receipts"}]},"expect":{"checks":[{"index":"ok","delivery":[{"error":"parent_not_held","step":null,"walked":62,"stop":"receipt_fetch_bytes_exhausted"}],"claims":[],"files_fetched":64}]}}, +{"name":"receipts_fetch_bytes_within","op":"receipts","input":{"serve":{"rcpt":["cases/rcpt-deep"],"pad_all":1000000},"checks":[{"object_cid":"f6fa1ca70e69bee70aa2bd36620b3e93a50ca2e110e1fbb91f3c6747f5deb860","held":"none","index":"cases/receipts/deep-64.wai.receipts"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":64,"evidence":"inclusion-verified","established":null,"reading":null}],"claims":[],"files_fetched":65}]}}, +{"name":"receipts_lawful_basis","op":"receipts","input":{"serve":{"rcpt":["objects/rcpt","cases/rcpt"]},"checks":[{"object":"O3","held":"whole","index":"cases/receipts/O3-lawful-basis.wai.receipts"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d"}],"claims":[{"class":"lawful-basis","attests":"object","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","parent":null},{"error":"decision_purpose_not_declared","step":6},{"error":"decision_declaration_mismatch","step":6}],"files_fetched":6}]}}, +{"name":"receipts_lawful_basis_declaration_not_served","op":"receipts","input":{"serve":{"rcpt":["objects/rcpt","cases/rcpt"],"omit":["13e1857af31da433ed26408d5cfdb8a486dfdef79026836dda43b4b7a790ac1e"]},"checks":[{"object":"O3","held":"whole","index":"cases/receipts/O3-lawful-basis.wai.receipts"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d"}],"claims":[{"error":"context_not_held","step":6},{"error":"context_not_held","step":6},{"error":"decision_declaration_mismatch","step":6}],"files_fetched":6}]}}, +{"name":"receipts_lawful_basis_declaration_renamed","op":"receipts","input":{"serve":{"rcpt":["objects/rcpt","cases/rcpt"],"as":{"13e1857af31da433ed26408d5cfdb8a486dfdef79026836dda43b4b7a790ac1e":"cases/rcpt/aedd5190e7eb2e5e61854f1009402caf242d51df0d76506a60481fd79a7ca2ca.json"}},"checks":[{"object":"O3","held":"whole","index":"cases/receipts/O3-lawful-basis.wai.receipts"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d"}],"claims":[{"error":"receipt_name_mismatch","step":null},{"error":"receipt_name_mismatch","step":null},{"error":"decision_declaration_mismatch","step":6}],"files_fetched":6}]}}, +{"name":"receipts_binding_recovery","op":"receipts","input":{"serve":{"rcpt":["objects/rcpt","cases/rcpt"]},"recovery":{"observed_value":"00","exact_match_algs":["org.example.watermark.1"]},"checks":[{"object":"O3","held":"whole","index":"cases/receipts/O3-binding-recovery.wai.receipts"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d"}],"claims":[{"class":"binding-recovery","attests":"object","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","parent":null},{"error":"recovery_value_mismatch","step":6},{"error":"recovery_signer_mismatch","step":6}],"files_fetched":6}]}}, +{"name":"receipts_binding_recovery_object_not_held","op":"receipts","input":{"serve":{"rcpt":["objects/rcpt","cases/rcpt"]},"recovery":{"observed_value":"00","exact_match_algs":["org.example.watermark.1"]},"checks":[{"object":"O3","held":"none","index":"cases/receipts/O3-binding-recovery.wai.receipts"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"inclusion-verified","established":null,"reading":null}],"claims":[{"class":"binding-recovery","attests":"object","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","parent":null},{"error":"recovery_value_mismatch","step":6},{"error":"recovery_signer_mismatch","step":6}],"files_fetched":6}]}}, +{"name":"receipts_binding_recovery_no_matcher","op":"receipts","input":{"serve":{"rcpt":["objects/rcpt","cases/rcpt"]},"checks":[{"object":"O3","held":"whole","index":"cases/receipts/O3-binding-recovery.wai.receipts"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d"}],"claims":[{"error":"recovery_no_matcher","step":6},{"error":"recovery_no_matcher","step":6},{"error":"recovery_signer_mismatch","step":6}],"files_fetched":6}]}}, +{"name":"receipts_operating_point","op":"receipts","input":{"serve":{"rcpt":["objects/rcpt","cases/rcpt"]},"checks":[{"object":"O5","held":"whole","index":"cases/receipts/O5-operating-point.wai.receipts"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=OnChipCounter by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d"}],"claims":[{"class":"operating-point","attests":"object","subject":"e43e038c10c0494d9647444fd4daf54b19fc3a946a3c7980431bc6e0a236093b","parent":null},{"error":"figure_disagrees","step":6}],"files_fetched":3}]}}, +{"name":"receipts_operating_point_not_held","op":"receipts","input":{"serve":{"rcpt":["objects/rcpt","cases/rcpt"]},"checks":[{"object":"O5","held":"none","index":"cases/receipts/O5-operating-point.wai.receipts"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"inclusion-verified","established":null,"reading":null}],"claims":[{"error":"context_not_held","step":6},{"error":"context_not_held","step":6}],"files_fetched":3}]}}, +{"name":"receipts_reconstruct_session","op":"receipts","input":{"serve":{"rcpt":["objects/rcpt","cases/rcpt"]},"checks":[{"object":"O3","held":"whole","index":"cases/receipts/O3-session.wai.receipts"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d"}],"claims":[{"error":"context_not_held","step":6}],"files_fetched":2}]}}, +{"name":"receipts_ancestor_other_publisher_first","op":"receipts","input":{"serve":{"rcpt":["objects/rcpt","cases/rcpt"]},"recovery":{"observed_value":"0ff0","exact_match_algs":["org.example.fingerprint"]},"checks":[{"object":"O7","held":"whole","index":"cases/receipts/O7-other-first.wai.receipts"},{"object":"O3","held":"whole"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=12a41592c8b7c17d4059e7b29b61e8ff96c7415f2f803348f2f017e05b9ea1da"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d"}],"claims":[],"files_fetched":2},{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d"}],"claims":[{"class":"soft-binding","attests":"ancestor","subject":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f","parent":null},{"class":"log-inclusion","attests":"receipt","subject":"ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018","parent":"14c0d31e65ae842627c606581c492bad7a73ee9190fc7db33d15de262d535491"},{"class":"energy","attests":"object","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","parent":"b766a9838403aa0b33528ac30947c8f1e6fd993ba6434f112392f11b482464f0"},{"error":"energy_disagrees_with_delivery","step":6}],"files_fetched":6}]}}, +{"name":"receipts_ancestor_publisher_first","op":"receipts","input":{"serve":{"rcpt":["objects/rcpt","cases/rcpt"]},"recovery":{"observed_value":"0ff0","exact_match_algs":["org.example.fingerprint"]},"checks":[{"object":"O7","held":"whole","index":"cases/receipts/O7-publisher-first.wai.receipts"},{"object":"O3","held":"whole"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d"},{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=12a41592c8b7c17d4059e7b29b61e8ff96c7415f2f803348f2f017e05b9ea1da"}],"claims":[],"files_fetched":2},{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d"}],"claims":[{"class":"soft-binding","attests":"ancestor","subject":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f","parent":null},{"class":"log-inclusion","attests":"receipt","subject":"ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018","parent":"14c0d31e65ae842627c606581c492bad7a73ee9190fc7db33d15de262d535491"},{"class":"energy","attests":"object","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","parent":"b766a9838403aa0b33528ac30947c8f1e6fd993ba6434f112392f11b482464f0"},{"error":"energy_disagrees_with_delivery","step":6}],"files_fetched":6}]}}, +{"name":"receipts_ancestor_other_publisher_only","op":"receipts","input":{"serve":{"rcpt":["objects/rcpt","cases/rcpt"]},"recovery":{"observed_value":"0ff0","exact_match_algs":["org.example.fingerprint"]},"checks":[{"object":"O7","held":"whole","index":"cases/receipts/O7-other-only.wai.receipts"},{"object":"O3","held":"whole"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=12a41592c8b7c17d4059e7b29b61e8ff96c7415f2f803348f2f017e05b9ea1da"}],"claims":[],"files_fetched":1},{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d"}],"claims":[{"error":"recovery_signer_mismatch","step":6},{"class":"log-inclusion","attests":"receipt","subject":"ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018","parent":"14c0d31e65ae842627c606581c492bad7a73ee9190fc7db33d15de262d535491"},{"class":"energy","attests":"object","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","parent":"b766a9838403aa0b33528ac30947c8f1e6fd993ba6434f112392f11b482464f0"},{"error":"energy_disagrees_with_delivery","step":6}],"files_fetched":6}]}}, +{"name":"receipts_confirmed","op":"receipts","input":{"keys":[{"anchor":".well-known/wai-keys"}],"checks":[{"object":"O3","held":"whole"},{"object":"O10","held":"whole"}]},"expect":{"checks":[{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"signed class=Estimator by=af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","confirm":"confirmed origin=https://origin.example key_id=d1 head=fab4c950188cdacaee2b290bda245bf8c7bbe5ba5f0549451a72a9241cf4da33 anchor=fetched"}],"claims":[{"error":"receipt_not_about_object","step":null},{"class":"log-inclusion","attests":"receipt","subject":"ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018","parent":"14c0d31e65ae842627c606581c492bad7a73ee9190fc7db33d15de262d535491","confirm":"confirmed origin=https://origin.example key_id=d1 head=fab4c950188cdacaee2b290bda245bf8c7bbe5ba5f0549451a72a9241cf4da33 anchor=fetched"},{"class":"energy","attests":"object","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","parent":"b766a9838403aa0b33528ac30947c8f1e6fd993ba6434f112392f11b482464f0","confirm":"confirmed origin=https://origin.example key_id=d1 head=fab4c950188cdacaee2b290bda245bf8c7bbe5ba5f0549451a72a9241cf4da33 anchor=fetched"},{"error":"energy_disagrees_with_delivery","step":6}],"files_fetched":6},{"index":"ok","delivery":[{"admission":"accept","walked":0,"evidence":"delivered-verified","established":"envelope","reading":"unmetered","confirm":"confirmed origin=https://origin.example key_id=d1 head=fab4c950188cdacaee2b290bda245bf8c7bbe5ba5f0549451a72a9241cf4da33 anchor=fetched"}],"claims":[{"class":"stage","attests":"object","subject":"1eb764beb4a8a739054341df8b5b7d065e490032b187a93787be15f1478080a8","parent":"8754b1d2e028880a1d21fe29dca565c0588125ea25979e37e1d90723b674654c","confirm":"unconfirmed step=1 reason=signer_id_not_origin_bound"}],"files_fetched":3}]}}, +{"name":"key_log_anchor","op":"key_log","input":{"steps":[{"anchor":".well-known/wai-keys"}]},"expect":{"steps":[{"anchored":"fab4c950188cdacaee2b290bda245bf8c7bbe5ba5f0549451a72a9241cf4da33","path":"fetched"}]}}, +{"name":"key_log_anchor_redirect","op":"key_log","input":{"steps":[{"anchor":".well-known/wai-keys","retrieval":{"requested":"https://origin.example/.well-known/wai-keys","answered":"https://mirror.example/.well-known/wai-keys"}}]},"expect":{"steps":[{"error":"key_log_not_from_origin"}]}}, +{"name":"key_log_anchor_elsewhere","op":"key_log","input":{"steps":[{"anchor":".well-known/wai-keys","retrieval":{"requested":"https://mirror.example/.well-known/wai-keys"}}]},"expect":{"steps":[{"error":"key_log_not_from_origin"}]}}, +{"name":"key_log_anchor_not_200","op":"key_log","input":{"steps":[{"anchor":".well-known/wai-keys","retrieval":{"status":304}}]},"expect":{"steps":[{"error":"key_log_not_from_origin"}]}}, +{"name":"key_log_anchor_plain_http","op":"key_log","input":{"steps":[{"anchor":".well-known/wai-keys","origin":"http://origin.example"}]},"expect":{"steps":[{"error":"key_log_not_from_origin"}]}}, +{"name":"key_log_anchor_other_origin","op":"key_log","input":{"steps":[{"anchor":".well-known/wai-keys","origin":"https://other.example"}]},"expect":{"steps":[{"error":"origin_mismatch"}]}}, +{"name":"key_log_anchor_malformed","op":"key_log","input":{"steps":[{"anchor":".well-known/wai-keys","edit":[["{\"kind\":\"wai-key-log\"","{\"kind\":\"wai-key-log\",\"note\":1"]]}]},"expect":{"steps":[{"error":"key_log_malformed"}]}}, +{"name":"key_log_anchor_forward","op":"key_log","input":{"steps":[{"anchor":"cases/keys/wai-keys-r0"},{"anchor":".well-known/wai-keys"}]},"expect":{"steps":[{"anchored":"7f06fd9425a3583d2007074cb32c75514d34b11d064c88304c6e4d4d4cb5ab01","path":"fetched"},{"anchored":"fab4c950188cdacaee2b290bda245bf8c7bbe5ba5f0549451a72a9241cf4da33","path":"fetched"}]}}, +{"name":"key_log_anchor_rolled_back","op":"key_log","input":{"steps":[{"anchor":".well-known/wai-keys"},{"anchor":"cases/keys/wai-keys-r0"}]},"expect":{"steps":[{"anchored":"fab4c950188cdacaee2b290bda245bf8c7bbe5ba5f0549451a72a9241cf4da33","path":"fetched"},{"error":"key_log_rolled_back"}]}}, +{"name":"key_log_cached_newer","op":"key_log","input":{"steps":[{"anchor":"cases/keys/wai-keys-r0"},{"cached":".well-known/wai-keys"}]},"expect":{"steps":[{"anchored":"7f06fd9425a3583d2007074cb32c75514d34b11d064c88304c6e4d4d4cb5ab01","path":"fetched"},{"added":1}]}}, +{"name":"key_log_cached_rolled_back","op":"key_log","input":{"steps":[{"anchor":".well-known/wai-keys"},{"cached":"cases/keys/wai-keys-r0"}]},"expect":{"steps":[{"anchored":"fab4c950188cdacaee2b290bda245bf8c7bbe5ba5f0549451a72a9241cf4da33","path":"fetched"},{"error":"key_log_rolled_back"}]}}, +{"name":"key_log_cached_other_origin","op":"key_log","input":{"steps":[{"anchor":".well-known/wai-keys"},{"cached":".well-known/wai-keys","origin":"https://other.example"}]},"expect":{"steps":[{"anchored":"fab4c950188cdacaee2b290bda245bf8c7bbe5ba5f0549451a72a9241cf4da33","path":"fetched"},{"error":"origin_not_anchored"}]}}, +{"name":"key_log_anchor_at_its_cap","op":"key_log","input":{"steps":[{"anchor":".well-known/wai-keys","pad":1048576}]},"expect":{"steps":[{"anchored":"fab4c950188cdacaee2b290bda245bf8c7bbe5ba5f0549451a72a9241cf4da33","path":"fetched"}]}}, +{"name":"key_log_anchor_too_large","op":"key_log","input":{"steps":[{"anchor":".well-known/wai-keys","pad":1048577}]},"expect":{"steps":[{"error":"key_log_too_large"}]}}, +{"name":"key_log_cached_too_large","op":"key_log","input":{"steps":[{"anchor":"cases/keys/wai-keys-r0"},{"cached":".well-known/wai-keys","pad":1048577}]},"expect":{"steps":[{"anchored":"7f06fd9425a3583d2007074cb32c75514d34b11d064c88304c6e4d4d4cb5ab01","path":"fetched"},{"error":"key_log_too_large"}]}}, +{"name":"grants_file","op":"grants_file","input":{"file":".well-known/wai-grants/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4.json"},"expect":{"grants":["read","read"]}}, +{"name":"grants_file_other_module","op":"grants_file","input":{"file":".well-known/wai-grants/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4.json","module":"0000000000000000000000000000000000000000000000000000000000000000"},"expect":{"error":"grants_file_malformed"}}, +{"name":"grants_file_member_added","op":"grants_file","input":{"file":".well-known/wai-grants/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4.json","edit":[["{\"grants\"","{\"note\":1,\"grants\""]]},"expect":{"error":"grants_file_malformed"}}, +{"name":"grants_file_wrong_kind","op":"grants_file","input":{"file":".well-known/wai-grants/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4.json","edit":[["wai-module-grants","wai-module-grant"]]},"expect":{"error":"grants_file_malformed"}}, +{"name":"param_set_held","op":"param_set","input":{"capability":"wai.neural.int_mlicpp","sha256":"7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4","served":"sets/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4","keys":[{"anchor":".well-known/wai-keys"}],"statuses":[".well-known/wai-key-status"],"grants":".well-known/wai-grants/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4.json","hold_to":["https://origin.example"],"now":"2026-10-06T00:30:00Z"},"expect":{"held":true}}, +{"name":"param_set_status_stale","op":"param_set","input":{"capability":"wai.neural.int_mlicpp","sha256":"7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4","served":"sets/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4","keys":[{"anchor":".well-known/wai-keys"}],"statuses":[".well-known/wai-key-status"],"grants":".well-known/wai-grants/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4.json","hold_to":["https://origin.example"],"now":"2026-10-06T02:00:00Z"},"expect":{"error":"prior_not_authorised","check":"4","reason":"module_status_stale"}}, +{"name":"param_set_no_clock","op":"param_set","input":{"capability":"wai.neural.int_mlicpp","sha256":"7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4","served":"sets/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4","keys":[{"anchor":".well-known/wai-keys"}],"statuses":[".well-known/wai-key-status"],"grants":".well-known/wai-grants/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4.json","hold_to":["https://origin.example"],"now":null},"expect":{"error":"prior_not_authorised","check":"4","reason":"module_status_stale"}}, +{"name":"param_set_no_grant","op":"param_set","input":{"capability":"wai.neural.int_mlicpp","sha256":"7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4","served":"sets/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4","keys":[{"anchor":".well-known/wai-keys"}],"statuses":[".well-known/wai-key-status"],"grants":null,"hold_to":["https://origin.example"],"now":"2026-10-06T00:30:00Z"},"expect":{"error":"prior_not_authorised","check":null,"reason":null}}, +{"name":"param_set_other_bytes","op":"param_set","input":{"capability":"wai.neural.int_mlicpp","sha256":"7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4","served":"../wai-rs/tests/vectors/int_mlicpp/stream.wai","keys":[{"anchor":".well-known/wai-keys"}],"statuses":[".well-known/wai-key-status"],"grants":".well-known/wai-grants/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4.json","hold_to":["https://origin.example"],"now":"2026-10-06T00:30:00Z"},"expect":{"error":"prior_not_held"}}, +{"name":"param_set_revoked","op":"param_set","input":{"capability":"wai.neural.int_mlicpp","sha256":"7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4","served":"sets/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4","keys":[{"anchor":"cases/keys/wai-keys-revoked"}],"statuses":[".well-known/wai-key-status"],"grants":".well-known/wai-grants/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4.json","hold_to":["https://origin.example"],"now":"2026-10-06T00:30:00Z"},"expect":{"error":"prior_revoked","revoked_by":"https://origin.example"}}, +{"name":"param_set_not_held_to","op":"param_set","input":{"capability":"wai.neural.int_mlicpp","sha256":"7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4","served":"sets/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4","keys":[{"anchor":".well-known/wai-keys"}],"statuses":[".well-known/wai-key-status"],"grants":".well-known/wai-grants/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4.json","now":"2026-10-06T00:30:00Z"},"expect":{"held":true}}, +{"name":"param_set_revoked_not_held_to","op":"param_set","input":{"capability":"wai.neural.int_mlicpp","sha256":"7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4","served":"sets/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4","keys":[{"anchor":"cases/keys/wai-keys-revoked"}],"statuses":[".well-known/wai-key-status"],"grants":".well-known/wai-grants/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4.json","now":"2026-10-06T00:30:00Z"},"expect":{"error":"prior_revoked","revoked_by":"https://origin.example"}}, +{"name":"param_set_pinned_anchor","op":"param_set","input":{"capability":"wai.neural.int_mlicpp","sha256":"7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4","served":"sets/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4","keys":[{"pin":".well-known/wai-keys"}],"statuses":[".well-known/wai-key-status"],"grants":".well-known/wai-grants/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4.json","hold_to":["https://origin.example"],"now":"2026-10-06T00:30:00Z"},"expect":{"held":true}}, +{"name":"param_set_sha_uppercase","op":"param_set","input":{"capability":"wai.neural.int_mlicpp","sha256":"7EF44F3B1FC4A2DF6C3E3B68460F595CAE3D31CB7F9018699E29E738340272A4","served":"sets/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4","keys":[{"anchor":".well-known/wai-keys"}],"statuses":[".well-known/wai-key-status"],"grants":".well-known/wai-grants/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4.json","hold_to":["https://origin.example"],"now":"2026-10-06T00:30:00Z"},"expect":{"error":"pin_malformed"}} ] diff --git a/wai/http-conformance/cases/keys/wai-keys-r0 b/wai/http-conformance/cases/keys/wai-keys-r0 new file mode 100644 index 000000000..ffef74ad8 --- /dev/null +++ b/wai/http-conformance/cases/keys/wai-keys-r0 @@ -0,0 +1 @@ +{"kind":"wai-key-log","revisions":[{"grant_max_s":86400,"issued_at":"2026-10-01T00:00:00Z","keys":[{"compromised_since":null,"id":"d1","key":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","may_sign":["delivery","energy/Estimator","log-inclusion","soft-binding"],"status":"active","until":null},{"compromised_since":null,"id":"g1","key":"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367","may_sign":["module/decoder"],"status":"active","until":null}],"kind":"wai-key-set","next_root":"d481afb3d00a3168a515f522aec12623ae3ee77b919ad5db74353a9f55972d8c","origin":"https://origin.example","prev":null,"revoked_modules":[],"root":"4a72e4035bb435d0aa42d43b8873d9f07cb9bf476fe2ade0aba49983eebd425e","seq":0,"sig":"92de01efa058176b0dc77a5784d401427c6266eec4b5583b4a4ac3eea1f2e52c5cc30809c27fb6b3c1908bd29f6658c85452702677f88c392319249df6a9680f","status_max_age_s":3600}]} \ No newline at end of file diff --git a/wai/http-conformance/cases/keys/wai-keys-revoked b/wai/http-conformance/cases/keys/wai-keys-revoked new file mode 100644 index 000000000..f9516f614 --- /dev/null +++ b/wai/http-conformance/cases/keys/wai-keys-revoked @@ -0,0 +1 @@ +{"kind":"wai-key-log","revisions":[{"grant_max_s":86400,"issued_at":"2026-10-01T00:00:00Z","keys":[{"compromised_since":null,"id":"d1","key":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","may_sign":["delivery","energy/Estimator","log-inclusion","soft-binding"],"status":"active","until":null},{"compromised_since":null,"id":"g1","key":"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367","may_sign":["module/decoder"],"status":"active","until":null}],"kind":"wai-key-set","next_root":"d481afb3d00a3168a515f522aec12623ae3ee77b919ad5db74353a9f55972d8c","origin":"https://origin.example","prev":null,"revoked_modules":[],"root":"4a72e4035bb435d0aa42d43b8873d9f07cb9bf476fe2ade0aba49983eebd425e","seq":0,"sig":"92de01efa058176b0dc77a5784d401427c6266eec4b5583b4a4ac3eea1f2e52c5cc30809c27fb6b3c1908bd29f6658c85452702677f88c392319249df6a9680f","status_max_age_s":3600},{"grant_max_s":86400,"issued_at":"2026-10-05T00:00:00Z","keys":[{"compromised_since":null,"id":"d1","key":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","may_sign":["delivery","energy/Estimator","log-inclusion","soft-binding"],"status":"active","until":null},{"compromised_since":null,"id":"g1","key":"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367","may_sign":["module/decoder"],"status":"active","until":null}],"kind":"wai-key-set","next_root":"d481afb3d00a3168a515f522aec12623ae3ee77b919ad5db74353a9f55972d8c","origin":"https://origin.example","prev":"7f06fd9425a3583d2007074cb32c75514d34b11d064c88304c6e4d4d4cb5ab01","revoked_modules":["7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4"],"root":"4a72e4035bb435d0aa42d43b8873d9f07cb9bf476fe2ade0aba49983eebd425e","seq":1,"sig":"6d9dd2f33367dba777cca49da470d294e206d7a28a22a09348759e6bd974b93a8118c2f8573e536f9c36790e2ca808ed408322a267041bb7c0daf2dcc3b94905","status_max_age_s":3600}]} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/003ce009852d8c0c3e808699db87f862368967295eaad10087ee86cd356a65dc.json b/wai/http-conformance/cases/rcpt-deep/003ce009852d8c0c3e808699db87f862368967295eaad10087ee86cd356a65dc.json new file mode 100644 index 000000000..289c700c9 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/003ce009852d8c0c3e808699db87f862368967295eaad10087ee86cd356a65dc.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":25,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"82a9fe0631aaaa7a86edc18e09dad8f9080ae1faa6089a2632a9ce028562740a","root_hash":"1211874e6b3ecacd036dc0b1af531f5720cc5cf390bff94efba962c3bbf298aa","sig":"4ec5139386e521d1bde6cefb11ee020d0698331ca24379b7a8ba4c933b4c5bc0a5457d181a709d8ded89a0ff593bfd9887c6f3a69f3c38684b46de66af36810f","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/009454fd76395b07a8db1c205e1eceb2fc39b70d6f19ce28f029f0aa25489eac.json b/wai/http-conformance/cases/rcpt-deep/009454fd76395b07a8db1c205e1eceb2fc39b70d6f19ce28f029f0aa25489eac.json new file mode 100644 index 000000000..2719ddf3f --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/009454fd76395b07a8db1c205e1eceb2fc39b70d6f19ce28f029f0aa25489eac.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":43,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"ae03f97d23795d99c4db5ddf04668bac928bd9e90969a20221cd7326e6a0cd1e","root_hash":"eadf6410e5cfb8a63d64905d19009d4c618d1a86a420d53881d0f6763b2b57ea","sig":"d684427786c93ae9e782871c25ee11bfda7c53cc9949a7d2c98c616f78abac68bc26cb207e92a656aff836dc540ca942667d8c4c0349e0675e7627f10d5da007","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/01a07d8b4af7e85c22c5ecbba976073b5e043ce831e96f8c411121fde6bdfa24.json b/wai/http-conformance/cases/rcpt-deep/01a07d8b4af7e85c22c5ecbba976073b5e043ce831e96f8c411121fde6bdfa24.json new file mode 100644 index 000000000..bde518dac --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/01a07d8b4af7e85c22c5ecbba976073b5e043ce831e96f8c411121fde6bdfa24.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":32,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"296b25a0475ca8ba860b3db33ec1c42f9c67a3dcda5fee8b7ab924c14d05d02c","root_hash":"a7a29aa447946ff2ca37266cbbf4cd58c3d3f1b12f21931eee1adff1ad8aceb9","sig":"8d35a40f4e6751b32ae5453b87d4c388c3b032345fc936fe6c035778e1dfa7d13db666ef5f3a7d344fcbcf2de92d5bbcb0adedc7c6730f4b62e0fd6d8459f80a","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/17ef1654e9710960f846a1453e40c927d3d526151006f5d01dcd0a50d8fe5cc0.json b/wai/http-conformance/cases/rcpt-deep/17ef1654e9710960f846a1453e40c927d3d526151006f5d01dcd0a50d8fe5cc0.json new file mode 100644 index 000000000..06e72d73a --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/17ef1654e9710960f846a1453e40c927d3d526151006f5d01dcd0a50d8fe5cc0.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":39,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"c018e85a69702e3966349a8b1a5a6cb91925f53377612947760bcabee6325e46","root_hash":"e43da1d3bca87b9a4be2d64da5bcedd1880c31a2a0c88551fd6dadfd9844858c","sig":"deb4d975d400f50fca515599a5d4fea0e8942e37d6172429b0313f74da8002cedc80b5d4e971af494976578f23b88799a11dabdca50bc2fc97343caa56f6ba05","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/186babdb6cd3cfbc6d5ea3420bd0b9583d3a3328544f90d1f6ec337e833da309.json b/wai/http-conformance/cases/rcpt-deep/186babdb6cd3cfbc6d5ea3420bd0b9583d3a3328544f90d1f6ec337e833da309.json new file mode 100644 index 000000000..8ff33a160 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/186babdb6cd3cfbc6d5ea3420bd0b9583d3a3328544f90d1f6ec337e833da309.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":1,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"1a0a5b813a9829960fc55d40e8db40c94f9e8fc49c65d76e8fddd9dc4e879f4e","root_hash":"509c5b9572d70f7baea01627f6c0dc0ab69214872f6c99279e3302742c1dfcbb","sig":"5764f7cc86de56d32d0cf4dabd15ae61450c7686deaaf144035d60cbf7b7c129155c639967ad4e941c6eb65ba9fc68647db0e973de1fc06bf418842e3dc28601","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/1a0a5b813a9829960fc55d40e8db40c94f9e8fc49c65d76e8fddd9dc4e879f4e.json b/wai/http-conformance/cases/rcpt-deep/1a0a5b813a9829960fc55d40e8db40c94f9e8fc49c65d76e8fddd9dc4e879f4e.json new file mode 100644 index 000000000..1043e73bf --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/1a0a5b813a9829960fc55d40e8db40c94f9e8fc49c65d76e8fddd9dc4e879f4e.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":0,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":null,"root_hash":"da5a5c13d1a01cb3c5f05e29993e9808516d27d7b3e2608c7ec3ca13577a7337","sig":"8674712a2f4598ad9b981b4df6864300b8df77315ba4e10c8b2430314cf43e92ec73b4e01cd15f35857a3201f24f4e7945f070fed1507a9ccc0706079f45a20b","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/21dcc9e852e59acff6ae0749f4a6afb81621f6a9226825ef597c860f306ff755.json b/wai/http-conformance/cases/rcpt-deep/21dcc9e852e59acff6ae0749f4a6afb81621f6a9226825ef597c860f306ff755.json new file mode 100644 index 000000000..3f3f8b860 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/21dcc9e852e59acff6ae0749f4a6afb81621f6a9226825ef597c860f306ff755.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":33,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"01a07d8b4af7e85c22c5ecbba976073b5e043ce831e96f8c411121fde6bdfa24","root_hash":"d09a3b36b4ba20ba294c4c50b89832742d99989e8685ac6abc2aa47b5f96796c","sig":"87128fda6bb5637d562380237d9c56de409a8bf61c1d4d84255e346fa28d2157564eaed727194bb390fa6112e41516d81c6c1ecadd53140f7d31d79bc768d80d","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/25632b6926e484b967f63df5a868d252c15d8fbba68d6bd6beb7c5db2bb8b284.json b/wai/http-conformance/cases/rcpt-deep/25632b6926e484b967f63df5a868d252c15d8fbba68d6bd6beb7c5db2bb8b284.json new file mode 100644 index 000000000..f4cbb27a9 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/25632b6926e484b967f63df5a868d252c15d8fbba68d6bd6beb7c5db2bb8b284.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":28,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"6fe102acdfb722a4ac1e9708bd2524df96612c2b8572fb36187b36aaef695d19","root_hash":"aec3dcc41f2d3c2109d555ef3870523e174a11170ef4616fb3ff742627f07325","sig":"4d17b95bc95a55eedb3850f00ee49addcaaaea1cff30fc2201bf101658b6162f19cdcda205fe223b376684471491b70b519a07604de46a3ab44f19bc44209408","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/258947cefedab4a8c44cf3d02bc66e33c4c94b20baefdee5924100ac13ad2631.json b/wai/http-conformance/cases/rcpt-deep/258947cefedab4a8c44cf3d02bc66e33c4c94b20baefdee5924100ac13ad2631.json new file mode 100644 index 000000000..eb912f616 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/258947cefedab4a8c44cf3d02bc66e33c4c94b20baefdee5924100ac13ad2631.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":55,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"7209b5f3d838d80969f2bedb44b36c62b9cb980de376565d29be1a36290673db","root_hash":"14feae56e11597a25b41680f39a198e9e230ef9876bcaf76b822b55e0f5647ff","sig":"6203742512be52ba7593f542a98e536ba07c641838dee5c62cb0711006544fa94708df3dbf2bddc5fcc24bd17a09b9d0d1a78365e035195d92ce9c9eb0357a01","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/296b25a0475ca8ba860b3db33ec1c42f9c67a3dcda5fee8b7ab924c14d05d02c.json b/wai/http-conformance/cases/rcpt-deep/296b25a0475ca8ba860b3db33ec1c42f9c67a3dcda5fee8b7ab924c14d05d02c.json new file mode 100644 index 000000000..7ea7b2709 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/296b25a0475ca8ba860b3db33ec1c42f9c67a3dcda5fee8b7ab924c14d05d02c.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":31,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"64100b9ff95fe8517af6d53ffae89420bcc4855b7a9fbfa9c0c2fcfd4c8f6e02","root_hash":"11af67f8b4a51b1cfc3fd45d6a45d139143aad28d77ca24f830db80fa49af063","sig":"f23dfefaf26e6eb1bb59dd186489da7e06c05c38611b1ac7a7467d40e45a24921038d51f9269978c01690f98de13ac58d4defa4382376a3fa9b883acc18f4c04","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/2d31d0bd821ee2578210482b192d2cb47d1d9a9846fc9f032311c4b156b3170a.json b/wai/http-conformance/cases/rcpt-deep/2d31d0bd821ee2578210482b192d2cb47d1d9a9846fc9f032311c4b156b3170a.json new file mode 100644 index 000000000..910feaf15 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/2d31d0bd821ee2578210482b192d2cb47d1d9a9846fc9f032311c4b156b3170a.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":13,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"89f6acee1ffd1ab9abcee3f448f2a7d1265f5de70c40ecdbd514fc2412a9581d","root_hash":"a35f7025866f344f636c09c22631c7f4c1a7e694834739af9e18e0703c35eb90","sig":"0646d08d5c8cdc8f0bcd604fb7ea0cd8c9363e08adada9a0668ed3c9b669ef77f93543950bc9f67d89a14d6256eee1886fa25d5919c430e8f78263ddde5ddb0f","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/316ef8d8a425cee1a88181a83f46698d36124879649ebdef8104c66398a37580.json b/wai/http-conformance/cases/rcpt-deep/316ef8d8a425cee1a88181a83f46698d36124879649ebdef8104c66398a37580.json new file mode 100644 index 000000000..1e7d8b5c4 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/316ef8d8a425cee1a88181a83f46698d36124879649ebdef8104c66398a37580.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":40,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"17ef1654e9710960f846a1453e40c927d3d526151006f5d01dcd0a50d8fe5cc0","root_hash":"7dbbdd8640eba8cfccbff0a54849a101f0be8399a9b299e9b300018a7bb007cd","sig":"0506f65ac8538cdcc58a5a8fde8e9e81140966149d1cd32702beb0608330db9896a5a488577a8ba0bd9df61b82caad940b0ab252d118a0fb0de07c4fb9d08707","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/319e3bd23ecc66f714d2226d160457fdface7e18022d071f2332adf1dc42f60a.json b/wai/http-conformance/cases/rcpt-deep/319e3bd23ecc66f714d2226d160457fdface7e18022d071f2332adf1dc42f60a.json new file mode 100644 index 000000000..3e009b717 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/319e3bd23ecc66f714d2226d160457fdface7e18022d071f2332adf1dc42f60a.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":46,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"3d8bcf2693699485bf4560e6db247cfba2ed713171f434a76a8f90c55facae9a","root_hash":"883c6c212087e54d9956d6274d23d065bb85dd7dec7ac19a4a067b84d5bc3b72","sig":"22c7c0f913a779057176eb965937637a58e58a99eaa0d19d5bd10196708e6d27968d869353206c676138e5e19040393ac0df3bfbd3cfc03c7447075cfc00bd0a","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/36f03021b405cf486db202f776174b6c81098add4784395a59327037c411aa05.json b/wai/http-conformance/cases/rcpt-deep/36f03021b405cf486db202f776174b6c81098add4784395a59327037c411aa05.json new file mode 100644 index 000000000..a0a082b1d --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/36f03021b405cf486db202f776174b6c81098add4784395a59327037c411aa05.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":8,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"dde8580c86bd5c8b984f26ecf19b9f93f2dfef67c08fa99138c2bbcbfc5fb387","root_hash":"c23f6de9c2788364cf6fa7623dbd2dee3914c113adf5d38ed19c3ce4b339f7af","sig":"1eb2d4017bb2cc724997b2d288f4a16776a27a23ac159c4ca917cc3e6d18a40d2ef79830bb903c9d833dd878c3ce73afe5fc7cdb2ec8d4a007d008c7c14d960b","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/3aa91d8e5f296a997f8473822a5558478a3302b02c06c3192986bbcb2d5d27b1.json b/wai/http-conformance/cases/rcpt-deep/3aa91d8e5f296a997f8473822a5558478a3302b02c06c3192986bbcb2d5d27b1.json new file mode 100644 index 000000000..1d30dce83 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/3aa91d8e5f296a997f8473822a5558478a3302b02c06c3192986bbcb2d5d27b1.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":52,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"9add9e068b526da6bd0d7dd6bac6f0bd53630f0cff4e303b0e2b5fa7032ab7b8","root_hash":"9ecd4d472e6335329ea651a6198528363b3d5117f12d2987b2b29b31ad45d3fd","sig":"b88194e4fecc524c457cb14c895e2f6b85b2b0a240de04c328bdc33799a36cf357f1c076f614104d5138c0987bd872b8a3a3f6a5aa9dc0e91e6ff77a10737804","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/3c8ead78e4ccaaa1b49790f87ea593a38e586e45cf8d2d7c1c575e64d3322893.json b/wai/http-conformance/cases/rcpt-deep/3c8ead78e4ccaaa1b49790f87ea593a38e586e45cf8d2d7c1c575e64d3322893.json new file mode 100644 index 000000000..dcc0af325 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/3c8ead78e4ccaaa1b49790f87ea593a38e586e45cf8d2d7c1c575e64d3322893.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":56,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"258947cefedab4a8c44cf3d02bc66e33c4c94b20baefdee5924100ac13ad2631","root_hash":"600aca31e4294ce457830489261698ed98559673f1dac64b2a2cda891a30fba9","sig":"505d1f31276fe9f6fcba73a0789d5560809752f7dd1706c3134b7bdb955ee593379779498625ac002f5bc95de8c94a702d199785d8baa92a52165cad21e7e30d","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/3d8bcf2693699485bf4560e6db247cfba2ed713171f434a76a8f90c55facae9a.json b/wai/http-conformance/cases/rcpt-deep/3d8bcf2693699485bf4560e6db247cfba2ed713171f434a76a8f90c55facae9a.json new file mode 100644 index 000000000..517dc00db --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/3d8bcf2693699485bf4560e6db247cfba2ed713171f434a76a8f90c55facae9a.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":45,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"cb54de1a2fb520d4b1b98a38732eeb0ae9260679c0ef38ee0c2db625d4690f08","root_hash":"b11ff8e897523699345a87ae17abe8f6de5d0ce1263e67acbd4514949e250c35","sig":"d9e95b6ec5b60023e3c1ae73dc1174e9f05c8ddbbb443df11a0ce422cacf89fa0283ff3dc4727180cb205e75b41340ce89e278f94c545a8f77b1d64a3e55eb07","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/45f8fa1e36cb9988a8ffecc639e92feb3c2cce92e70eeaebea7ce2bd75c5e581.json b/wai/http-conformance/cases/rcpt-deep/45f8fa1e36cb9988a8ffecc639e92feb3c2cce92e70eeaebea7ce2bd75c5e581.json new file mode 100644 index 000000000..3626eb1d7 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/45f8fa1e36cb9988a8ffecc639e92feb3c2cce92e70eeaebea7ce2bd75c5e581.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":11,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"ce8cdb9ba42eae1916d6543cc0df37a0e9e3cf6eb9f23dace8cadbcc68a1ce6e","root_hash":"c7698fb31901c6d88bece812a242874c83afa5cc6f1cd3b1a6fc9b18b07b01db","sig":"2429af876ef9db23ee01d0b2d02532e1cb96ac35bd34e766195ed59c560d51d2b1af42ffda916676787d2b23f1493b4ff07dffed5aae3a7ceb5e4c554bc9f40c","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/4a929990963ba614aaac8bb2896cf2f0b9f1bb4724dec179d2457576e6de493b.json b/wai/http-conformance/cases/rcpt-deep/4a929990963ba614aaac8bb2896cf2f0b9f1bb4724dec179d2457576e6de493b.json new file mode 100644 index 000000000..8c1d82e30 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/4a929990963ba614aaac8bb2896cf2f0b9f1bb4724dec179d2457576e6de493b.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":49,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"9437c7584edf8061151cc407cdc2cf5875138daa33def243f8e7958095159d43","root_hash":"2c40459481075ebd4a519f164bc9e314bc1dc4883b7a31e1d005bd801c7cf3a6","sig":"4e4cf7d43629fc7b207ae6e5590c8bf08d0a6bf390d4c7e9e4fe5909f2e763068450ec5032d10d2f557eaccd562abcb31df7a76727c7f4165f97f5d33447de0c","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/4fddb12f5ebd443ad749945c70453e80223fa9f89c642fbc14bb736a4fcc0c19.json b/wai/http-conformance/cases/rcpt-deep/4fddb12f5ebd443ad749945c70453e80223fa9f89c642fbc14bb736a4fcc0c19.json new file mode 100644 index 000000000..9a209e0ff --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/4fddb12f5ebd443ad749945c70453e80223fa9f89c642fbc14bb736a4fcc0c19.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":65,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"63379f1324fcb7c7d65bec7ea0d81f585fad641c0881076dae2384c0dab77350","root_hash":"e95d9348d502540b2ec55b9ffbfcca3f28ddf098b621f2b3c7e8af71b312d066","sig":"acb1b2461b193ecb38f1e4f7d7621134140fba0c5f91bb40b74c9bc5d3652f34fa8b3f782fbb6e2b9eddd99b3d4c58991a08a16cd278817f89981a5fe1e1e701","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/5cf23a6733332727a616961d36300beb3ab39a637d985efa02f51f9ad9a788b1.json b/wai/http-conformance/cases/rcpt-deep/5cf23a6733332727a616961d36300beb3ab39a637d985efa02f51f9ad9a788b1.json new file mode 100644 index 000000000..12d0f52de --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/5cf23a6733332727a616961d36300beb3ab39a637d985efa02f51f9ad9a788b1.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":6,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"628e127eff3ff5f3fc41008cd2aba447b2e6a5c6b6f7a7c355aeee5131614845","root_hash":"febbe500a3ec7f780b74ed90e380bc6787f2aa58de33e1da2aa48cd020ff6d39","sig":"7cbe7c6fb1d8c8255f54aefd054f33ab447a93d8426eb0558ed2fbf1d5b08a24702830ccdb4cf8d3302eb3eb1a86ea080dfa1ad8dfdb62b385ba9e15f7b8d908","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/628e127eff3ff5f3fc41008cd2aba447b2e6a5c6b6f7a7c355aeee5131614845.json b/wai/http-conformance/cases/rcpt-deep/628e127eff3ff5f3fc41008cd2aba447b2e6a5c6b6f7a7c355aeee5131614845.json new file mode 100644 index 000000000..674d4c118 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/628e127eff3ff5f3fc41008cd2aba447b2e6a5c6b6f7a7c355aeee5131614845.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":5,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"8642ba98b3ef5233e6fe4c2b2f8bf0511b48b25ad11139781874ca585dad39db","root_hash":"78bcdb3b5b191cc6a2b18db12339bd6563b560115cdddd48b2c9d4ddc2db9c15","sig":"dd2e0d84a9f6f923d914f91e574050bca6c6eca0802f3004892694f88ddedd6f72a58f4f82332ccc3d8b504af5d4f4b51243f56cdf75bdbfa7d34387df4f4c0a","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/63379f1324fcb7c7d65bec7ea0d81f585fad641c0881076dae2384c0dab77350.json b/wai/http-conformance/cases/rcpt-deep/63379f1324fcb7c7d65bec7ea0d81f585fad641c0881076dae2384c0dab77350.json new file mode 100644 index 000000000..2001dd325 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/63379f1324fcb7c7d65bec7ea0d81f585fad641c0881076dae2384c0dab77350.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":64,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"dddbdf295d8efc9890ea00b8cd09355b8bc12aa8c1b8a0abfa2736dc8e52beca","root_hash":"b3616793d8b3218474f284c7e2785c7fe6776638f1aa57adc24fe8c55a376d54","sig":"38a1a257897e535520b610f77202f0ec8ab56223277b5b8dd227cfc7a70e31a49662687efdb95ac88354258dd47fce5ee39af7b0a99b2abe5afb40b708a69606","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/64100b9ff95fe8517af6d53ffae89420bcc4855b7a9fbfa9c0c2fcfd4c8f6e02.json b/wai/http-conformance/cases/rcpt-deep/64100b9ff95fe8517af6d53ffae89420bcc4855b7a9fbfa9c0c2fcfd4c8f6e02.json new file mode 100644 index 000000000..294d682d2 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/64100b9ff95fe8517af6d53ffae89420bcc4855b7a9fbfa9c0c2fcfd4c8f6e02.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":30,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"8499e3ae0889231bb4cf44be763227155c737d80c3119d6ef49ee37ac384f0f4","root_hash":"6f2a0d99a41838ea16e8036d802a883aacbe3032b534be0f58ad3fd13089d0ff","sig":"e1bb86183a28a3e66a840dc2791c504b50a3ba68c7ca9dbb65a41131d9108d3bf39c58946fc8f14c2eb95bfb68d84cd2833a5fb502a5286c53f47ffaa0048c09","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/6fe102acdfb722a4ac1e9708bd2524df96612c2b8572fb36187b36aaef695d19.json b/wai/http-conformance/cases/rcpt-deep/6fe102acdfb722a4ac1e9708bd2524df96612c2b8572fb36187b36aaef695d19.json new file mode 100644 index 000000000..39c9080f6 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/6fe102acdfb722a4ac1e9708bd2524df96612c2b8572fb36187b36aaef695d19.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":27,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"9f4e6f8de572ae37b854c3f356c117a451713ae32d05176e8aae7d3cb928da3c","root_hash":"4cc83a55bfc90d849b0634547ff5c539cbcb35ef73b638bf3a13c3e1d698e8d0","sig":"97719491e9c5079101f6196518b7479b436f3afa5251fe9c1cb5c4068c4a83411e8d7c4e8eedcc7b80c7af44aa1b8f04586eb38219a5408e17d1b4e9f48bb203","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/702d3f032ec6e859379cc73da1b5e2af6d8522ec13b20b3cb80579065cf3cb8f.json b/wai/http-conformance/cases/rcpt-deep/702d3f032ec6e859379cc73da1b5e2af6d8522ec13b20b3cb80579065cf3cb8f.json new file mode 100644 index 000000000..efd7c19f2 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/702d3f032ec6e859379cc73da1b5e2af6d8522ec13b20b3cb80579065cf3cb8f.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":9,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"36f03021b405cf486db202f776174b6c81098add4784395a59327037c411aa05","root_hash":"2bf3fcf47f10adda38d9e84e6daa104b7ad428c8a7569f2f1baebd0c86116c0c","sig":"a998bb7ee846b7842ce19937c1934b7e1cd3c0dff3032abb8de8b1dd240bc1a89075ca8f91ca1348cf916c1032cb68b28244541b483d083bda4a367f90e2fa08","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/7209b5f3d838d80969f2bedb44b36c62b9cb980de376565d29be1a36290673db.json b/wai/http-conformance/cases/rcpt-deep/7209b5f3d838d80969f2bedb44b36c62b9cb980de376565d29be1a36290673db.json new file mode 100644 index 000000000..262731688 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/7209b5f3d838d80969f2bedb44b36c62b9cb980de376565d29be1a36290673db.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":54,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"f424f181500d06d27e5344e5ef28ff6a68eed7386ead966dd4c5f11baf14d41b","root_hash":"3f0ce8fdce0974ea8a459ef05a50f49892ff45763b93d77091dd366437386b34","sig":"98186af124ee0d0870b0298ade1ec95b11319e7f0ba0c7855f345607e2bd05d9286e9c6a02496dd85a3f6c466f2626317ab896a3e4b801e25afde366ba6a6b05","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/727ec6e7702c42e445c28f7db5ca526c4cfcf72207a793b8a75b457b0004cbb7.json b/wai/http-conformance/cases/rcpt-deep/727ec6e7702c42e445c28f7db5ca526c4cfcf72207a793b8a75b457b0004cbb7.json new file mode 100644 index 000000000..1f4541d5c --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/727ec6e7702c42e445c28f7db5ca526c4cfcf72207a793b8a75b457b0004cbb7.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":16,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"a14dd2629372b10738a35dcb3e71db5b60c2e6aac29776766f60df21debaa7a6","root_hash":"1b0bc2320fc07c2dfb31afd5442e8960e7e057666dbf0e29ba010a090460696d","sig":"c0093f3d85b441b68875aeb16dc44a05faaeab9530ca88dc6ac734494af20acbc0e5e2eb9b038c8b26bf941990896ec19a5964d05f1c811e027b953f9a2b8b08","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/73c6117b02857292a6b459ed1f433b42d7455a91a2941aab05c31a54a0ddf30a.json b/wai/http-conformance/cases/rcpt-deep/73c6117b02857292a6b459ed1f433b42d7455a91a2941aab05c31a54a0ddf30a.json new file mode 100644 index 000000000..ff140a639 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/73c6117b02857292a6b459ed1f433b42d7455a91a2941aab05c31a54a0ddf30a.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":57,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"3c8ead78e4ccaaa1b49790f87ea593a38e586e45cf8d2d7c1c575e64d3322893","root_hash":"fd1fa378fc47672fac5acfbf23211009991aeeec45f905812b514cd1f4bf80c1","sig":"9cd1d0aa49c8a2eb7600853365fdfad741880273de4f35b008b500aacdb28a3ddf0a7b36f130e78d018a641a9692d39c63621864a7f4ea30ce67f2f72fb2fb02","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/82a9fe0631aaaa7a86edc18e09dad8f9080ae1faa6089a2632a9ce028562740a.json b/wai/http-conformance/cases/rcpt-deep/82a9fe0631aaaa7a86edc18e09dad8f9080ae1faa6089a2632a9ce028562740a.json new file mode 100644 index 000000000..2198a2f32 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/82a9fe0631aaaa7a86edc18e09dad8f9080ae1faa6089a2632a9ce028562740a.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":24,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"9db4594f2e453f9cff014f3028d4c02dd5862b28855783f836891ec26e911616","root_hash":"c239061f02af70e8a96990905ecf2a23e8fcedf71fa2b92c3dd16b42f5a8d0d1","sig":"fb2d679c452a49ae81e95487d8c3b30225bab6446162cd679c9de333b33d84995dd46c9e36385692605b0052a776dd51f705f8cf97fdbbc373a5ad3d26f35d01","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/8499e3ae0889231bb4cf44be763227155c737d80c3119d6ef49ee37ac384f0f4.json b/wai/http-conformance/cases/rcpt-deep/8499e3ae0889231bb4cf44be763227155c737d80c3119d6ef49ee37ac384f0f4.json new file mode 100644 index 000000000..3e1980e4c --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/8499e3ae0889231bb4cf44be763227155c737d80c3119d6ef49ee37ac384f0f4.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":29,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"25632b6926e484b967f63df5a868d252c15d8fbba68d6bd6beb7c5db2bb8b284","root_hash":"3f131d500a428e452804bf5fcd2671f9ce41c80164860772587eca6a9783b087","sig":"ed1f5dedc6610c846c4a3d8b66b6ed46db626b1aefe1fd637a19d723ae54f9917837acde9fa5cef0df93b9fbadc31eea66c6d0ad43b60af35ece73dae39a5d00","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/8642ba98b3ef5233e6fe4c2b2f8bf0511b48b25ad11139781874ca585dad39db.json b/wai/http-conformance/cases/rcpt-deep/8642ba98b3ef5233e6fe4c2b2f8bf0511b48b25ad11139781874ca585dad39db.json new file mode 100644 index 000000000..1081ac36b --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/8642ba98b3ef5233e6fe4c2b2f8bf0511b48b25ad11139781874ca585dad39db.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":4,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"9bc7eb5b3a07d2c3bfa344a7f32612128c54efa856c8d8feab4c2d768559a153","root_hash":"f367d35de324aa1583e8b7979bb5362be2ccaacd417a47dfd225402d9ef8fbdd","sig":"6a2d7f5928d55ea90a3c125781d5ee6f149c751d969576a52c88fe71ae10d9ca0005aaec0d1df2412554c155959f2ee54df146d2fbbc934ad75de4529440e301","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/89f6acee1ffd1ab9abcee3f448f2a7d1265f5de70c40ecdbd514fc2412a9581d.json b/wai/http-conformance/cases/rcpt-deep/89f6acee1ffd1ab9abcee3f448f2a7d1265f5de70c40ecdbd514fc2412a9581d.json new file mode 100644 index 000000000..609c41132 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/89f6acee1ffd1ab9abcee3f448f2a7d1265f5de70c40ecdbd514fc2412a9581d.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":12,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"45f8fa1e36cb9988a8ffecc639e92feb3c2cce92e70eeaebea7ce2bd75c5e581","root_hash":"ceb79bda8cc7d0af2e400d611198cbda13698f5ea38a99b14012c74ac7d13d66","sig":"79a9ba6333dfb5c79a7f75337b36fb01e6cb3be6ea86931f6beae44847a5103698562f92f352c9eb239f1ff21a764f14a8c28ba05c817cebeb1225906739910b","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/8ce05b04d49062f191ae072bf970ba23863c7cc88b654d178acf61361fc81efa.json b/wai/http-conformance/cases/rcpt-deep/8ce05b04d49062f191ae072bf970ba23863c7cc88b654d178acf61361fc81efa.json new file mode 100644 index 000000000..78a305c61 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/8ce05b04d49062f191ae072bf970ba23863c7cc88b654d178acf61361fc81efa.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":20,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"d5c54a0582e4aae21a61b315843a4a3bc50af0b97a75f9fc6fa213f438839b59","root_hash":"3293bd5b8d34c63bc98d733b5a46009650fe642a668ad1daa659b577b2a90fbf","sig":"2cf0713a80b268e8747273c8dd2b96e28ab1bc5302f12f44edd7468468489b99c99648c60c80f8eb0cb35ad9495d7548585c5fd74384c3517d219a3bab448608","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/9437c7584edf8061151cc407cdc2cf5875138daa33def243f8e7958095159d43.json b/wai/http-conformance/cases/rcpt-deep/9437c7584edf8061151cc407cdc2cf5875138daa33def243f8e7958095159d43.json new file mode 100644 index 000000000..6e15b9e5d --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/9437c7584edf8061151cc407cdc2cf5875138daa33def243f8e7958095159d43.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":48,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"c40b1d684d5696efc5f2cc1f39cee4d99b4dc4fe5aa6d43992a8c8d5be5bcbe9","root_hash":"2d38a57b8206d3359111802e9878315d9ad5b40ed728cc05f7ccfe739bd541eb","sig":"ba9bffe0bc4a80837120bbce5317ec44ce11adec2c8c0eb0e5e4d4e77538bd2ed29176ef508409789af8eb6e85cb0f35cb712dc78fc3fdb697aa8d748ae8c204","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/9add9e068b526da6bd0d7dd6bac6f0bd53630f0cff4e303b0e2b5fa7032ab7b8.json b/wai/http-conformance/cases/rcpt-deep/9add9e068b526da6bd0d7dd6bac6f0bd53630f0cff4e303b0e2b5fa7032ab7b8.json new file mode 100644 index 000000000..bacdeea7a --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/9add9e068b526da6bd0d7dd6bac6f0bd53630f0cff4e303b0e2b5fa7032ab7b8.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":51,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"9be1a59aa2577c7431548c5d117413044a37fdc1a05cb72c58d0ec16770bcfdf","root_hash":"58352e524be52f2772d3cad18651fd35ca9784258515a9b0c252deb56e2aa76e","sig":"0c579d3b232dd622ffc5eb886acbfb6d614f20d0768a1ad33ee59fb0327f87199fdb06a03a4cc8a716f4756d9bcd3129f9d11ffec2df984362471f436c68e60c","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/9bc7eb5b3a07d2c3bfa344a7f32612128c54efa856c8d8feab4c2d768559a153.json b/wai/http-conformance/cases/rcpt-deep/9bc7eb5b3a07d2c3bfa344a7f32612128c54efa856c8d8feab4c2d768559a153.json new file mode 100644 index 000000000..f9090e48f --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/9bc7eb5b3a07d2c3bfa344a7f32612128c54efa856c8d8feab4c2d768559a153.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":3,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"dce4681c9a487a24a4b20f216f79d8319dfb7048ca2d965f1d34e5724e33d802","root_hash":"608ab68252a3cba5e1af70044e718d4c195e38e1b302505bd13011eb96f98b7a","sig":"7b368cb784f34dcb41fac59d9050d465e2133bfe811d5a3929aac7e2401825e3b6733c9ac5e7b0cfc856bf3719aea19150eb71a43fa285bcee9f840ff86c2301","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/9be1a59aa2577c7431548c5d117413044a37fdc1a05cb72c58d0ec16770bcfdf.json b/wai/http-conformance/cases/rcpt-deep/9be1a59aa2577c7431548c5d117413044a37fdc1a05cb72c58d0ec16770bcfdf.json new file mode 100644 index 000000000..f042b898e --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/9be1a59aa2577c7431548c5d117413044a37fdc1a05cb72c58d0ec16770bcfdf.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":50,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"4a929990963ba614aaac8bb2896cf2f0b9f1bb4724dec179d2457576e6de493b","root_hash":"422d01d518378dd434dcabd52a104902f82008fbec8481f829c4461ad8662e1c","sig":"e0f1375e470ce6804c8e46c050c7bc9d8b8b2b5db1c1c4f70842e113e48e64922ae5a6bbe8bca2f95b08a4148b179988a28ff002625322b83a84b32f526a4008","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/9db4594f2e453f9cff014f3028d4c02dd5862b28855783f836891ec26e911616.json b/wai/http-conformance/cases/rcpt-deep/9db4594f2e453f9cff014f3028d4c02dd5862b28855783f836891ec26e911616.json new file mode 100644 index 000000000..af6201607 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/9db4594f2e453f9cff014f3028d4c02dd5862b28855783f836891ec26e911616.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":23,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"e13cff8676af896dc1c0a7b82178a8dea0f39e2c1ffe5fe5fd18cbfbd38fcf13","root_hash":"10fc6a3be74e8ade3e56dfc46a1ffd4626f1aa907b6ef691f50abfc9c946d00a","sig":"4c2a1559b57b581cfe3d8a7a9e8fae9da97681a9f094cf31b4704ea6bdafaa6f5d11f361f592762df4143920c13399731241ffb33136f01b266719801a8b3602","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/9f4e6f8de572ae37b854c3f356c117a451713ae32d05176e8aae7d3cb928da3c.json b/wai/http-conformance/cases/rcpt-deep/9f4e6f8de572ae37b854c3f356c117a451713ae32d05176e8aae7d3cb928da3c.json new file mode 100644 index 000000000..a6e2d529d --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/9f4e6f8de572ae37b854c3f356c117a451713ae32d05176e8aae7d3cb928da3c.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":26,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"003ce009852d8c0c3e808699db87f862368967295eaad10087ee86cd356a65dc","root_hash":"c6ed74cf5f60fdc95368574db8113aaf0113a56bd6e7d8a159e08047b5a84b93","sig":"6075493ab96746b061cb99dbdd3f583898abdf4b504f6345e11b3a0aad37a351af1e5fd756f905b6c9cba48729c23f55355479d567255c07f562b3fe5fdc6303","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/a14dd2629372b10738a35dcb3e71db5b60c2e6aac29776766f60df21debaa7a6.json b/wai/http-conformance/cases/rcpt-deep/a14dd2629372b10738a35dcb3e71db5b60c2e6aac29776766f60df21debaa7a6.json new file mode 100644 index 000000000..bdab9fa74 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/a14dd2629372b10738a35dcb3e71db5b60c2e6aac29776766f60df21debaa7a6.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":15,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"fcdf29a5c1f6567155dd0df394306fd179f9fdd04b252535d937c6537004735c","root_hash":"442c93c524f9de9be026d1b9a7bea2c1a94602ab1950e19d12494bb2e1ec7d68","sig":"5682bb506edee5371405e0eed42e4a1310115d70cbd3077fc3fe811eb3d290f81ebc0910053e32cf46ca4952326cc786ad80353575a78e38d214a0343c948d06","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/a577ff30a365f5af48f5ea6f2b874ea9520ec024639f33f9fcc8545d0535ea64.json b/wai/http-conformance/cases/rcpt-deep/a577ff30a365f5af48f5ea6f2b874ea9520ec024639f33f9fcc8545d0535ea64.json new file mode 100644 index 000000000..d36dfb92b --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/a577ff30a365f5af48f5ea6f2b874ea9520ec024639f33f9fcc8545d0535ea64.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":58,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"73c6117b02857292a6b459ed1f433b42d7455a91a2941aab05c31a54a0ddf30a","root_hash":"d38f2afa7a1fbea16f5176bb0ea463588572e7a2204db26818e97a0c276a4328","sig":"189319842f856bb56cd3fcb95e826f64b8a118ecbf37453d862bb385691830a13c9a15ab4a7439e2406d519ba8595809add1060ccf7bc111f58e97447a268a0f","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/ae03f97d23795d99c4db5ddf04668bac928bd9e90969a20221cd7326e6a0cd1e.json b/wai/http-conformance/cases/rcpt-deep/ae03f97d23795d99c4db5ddf04668bac928bd9e90969a20221cd7326e6a0cd1e.json new file mode 100644 index 000000000..48bfd2759 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/ae03f97d23795d99c4db5ddf04668bac928bd9e90969a20221cd7326e6a0cd1e.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":42,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"c5c6797e71d82c249afdfd5fa3e45c607168f5f46caab52de55ade65a7dca6db","root_hash":"1de3128d0455a29c3eea4460d02878a4335e9b467ffc2c115cb7e690081786cd","sig":"eb0d5bb76ff3e014f33b1eeae1847190f538bc6c2cbafa69c0d27741de80c6c9341e5f163955945029aa2f22b7ac48227d526747b77d59e33f9ae6f3b2880505","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/ba386a976d3bb07999fa2b48a0505aad51f5bcf1ad926f30e17475143abdaef7.json b/wai/http-conformance/cases/rcpt-deep/ba386a976d3bb07999fa2b48a0505aad51f5bcf1ad926f30e17475143abdaef7.json new file mode 100644 index 000000000..b8b9509e5 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/ba386a976d3bb07999fa2b48a0505aad51f5bcf1ad926f30e17475143abdaef7.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":61,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"fbe11c778ba08a3178591ad782e1f2392419aad7d8b53be5e4e8432d7100c7fe","root_hash":"5d33516bd5bfa72f37aaf04c6a4848df4eed06bd04b84dac8e4f5b28e406f6d1","sig":"8dd7b67f160bf89f5596ca913ba1bb8d9cbe908d3fdfdd75f1741ea2f7f4dff8b9e97b9c68fd3aad9c8e5b568b068a39b95fc982eb0e6b8ecfa1706c9f46810b","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/bc54f7a82afbf23e1a81249785409dbf27d11f1c7871850d11f37afd637b0d8a.json b/wai/http-conformance/cases/rcpt-deep/bc54f7a82afbf23e1a81249785409dbf27d11f1c7871850d11f37afd637b0d8a.json new file mode 100644 index 000000000..6c94f3e3e --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/bc54f7a82afbf23e1a81249785409dbf27d11f1c7871850d11f37afd637b0d8a.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":34,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"21dcc9e852e59acff6ae0749f4a6afb81621f6a9226825ef597c860f306ff755","root_hash":"3a89c7e674b975bdd0a4e191ee3d2cecf468e9ff9b7c362a212229caf3488be0","sig":"fcf5e28a000a1cf6800cf4221fe18630eee0c9917e8ced467b6c4be2cdf0980fc0575930a54a64176ac835557de187ee3d829e40296a88a094b74ac67b6bd10e","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/c018e85a69702e3966349a8b1a5a6cb91925f53377612947760bcabee6325e46.json b/wai/http-conformance/cases/rcpt-deep/c018e85a69702e3966349a8b1a5a6cb91925f53377612947760bcabee6325e46.json new file mode 100644 index 000000000..6d3dd1c98 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/c018e85a69702e3966349a8b1a5a6cb91925f53377612947760bcabee6325e46.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":38,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"c24a80173b436fddaec2e9efe8a243eb58334442ebbc0f5170d219fc833c1b56","root_hash":"10afe37e8a34ea9ef5f03c97bc61435ca36a16a3956885ffa6a1567b342b7337","sig":"9dfa4a3270750f942197fc8e52658cb4eeae00af4f7b483c83cc335df0834cc00d04e35c95b1ade49bc104ea853d85c2f9d12e98a8cf55cd4e6c5b4aced0970e","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/c24a80173b436fddaec2e9efe8a243eb58334442ebbc0f5170d219fc833c1b56.json b/wai/http-conformance/cases/rcpt-deep/c24a80173b436fddaec2e9efe8a243eb58334442ebbc0f5170d219fc833c1b56.json new file mode 100644 index 000000000..317025713 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/c24a80173b436fddaec2e9efe8a243eb58334442ebbc0f5170d219fc833c1b56.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":37,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"ff696c1fecfcfdf1b1fe0d3a3bc7124039209dc2b8dfbb2efd9ee7dcbafc955a","root_hash":"eb80ab8c660d2217484ea3dbcbb05d103de5d1b78dfbbfc97792b73285a8182f","sig":"a73fa90c1e7fc446ecad0ccc6eb5536c76e33e92f0215996fbbbae15ce7c4d6a13655c4eea4f0225becb2c48b834e67458e07e970c0ccb89ab3b5e473136260e","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/c40b1d684d5696efc5f2cc1f39cee4d99b4dc4fe5aa6d43992a8c8d5be5bcbe9.json b/wai/http-conformance/cases/rcpt-deep/c40b1d684d5696efc5f2cc1f39cee4d99b4dc4fe5aa6d43992a8c8d5be5bcbe9.json new file mode 100644 index 000000000..335789df8 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/c40b1d684d5696efc5f2cc1f39cee4d99b4dc4fe5aa6d43992a8c8d5be5bcbe9.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":47,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"319e3bd23ecc66f714d2226d160457fdface7e18022d071f2332adf1dc42f60a","root_hash":"a308d60a2fa2a48a80462991469a36c2d7be9ac69ed56ed4323d8e11357d39ce","sig":"e65c1a6f25cedb3675f846e44c388e314b2d3630c896b4dc753a430d5bc83fa203d5e566af69463558faecc4e3070567da8798294b380e8489bb4fb37f71eb00","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/c5c6797e71d82c249afdfd5fa3e45c607168f5f46caab52de55ade65a7dca6db.json b/wai/http-conformance/cases/rcpt-deep/c5c6797e71d82c249afdfd5fa3e45c607168f5f46caab52de55ade65a7dca6db.json new file mode 100644 index 000000000..733a713d2 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/c5c6797e71d82c249afdfd5fa3e45c607168f5f46caab52de55ade65a7dca6db.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":41,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"316ef8d8a425cee1a88181a83f46698d36124879649ebdef8104c66398a37580","root_hash":"199ec3d5e8002144a82aaf27b9be13c608de8a67795ce89d46ccf62f788a37c0","sig":"890c40427a1911a011d59dd35719931819e84803a9b327fc99d053540df9919a171e28f7ea314f6d18ffa7cd6e710422d45accde765845ee9379b8ee130d570c","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/cb54de1a2fb520d4b1b98a38732eeb0ae9260679c0ef38ee0c2db625d4690f08.json b/wai/http-conformance/cases/rcpt-deep/cb54de1a2fb520d4b1b98a38732eeb0ae9260679c0ef38ee0c2db625d4690f08.json new file mode 100644 index 000000000..35c920d26 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/cb54de1a2fb520d4b1b98a38732eeb0ae9260679c0ef38ee0c2db625d4690f08.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":44,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"009454fd76395b07a8db1c205e1eceb2fc39b70d6f19ce28f029f0aa25489eac","root_hash":"3e8cbc691a02138642542f384c7afe3c10c9c3433021a1c8390fef1f5bee3b91","sig":"97f6d9efc72c57715853a296748a13e6fde0193dbcb4bac184fb28d50a30f7f7da0d0268cc0fd735f18da5c1364ee0bf2508e3ea77c3808a7e45be4b43733603","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/ce8cdb9ba42eae1916d6543cc0df37a0e9e3cf6eb9f23dace8cadbcc68a1ce6e.json b/wai/http-conformance/cases/rcpt-deep/ce8cdb9ba42eae1916d6543cc0df37a0e9e3cf6eb9f23dace8cadbcc68a1ce6e.json new file mode 100644 index 000000000..62175cbc7 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/ce8cdb9ba42eae1916d6543cc0df37a0e9e3cf6eb9f23dace8cadbcc68a1ce6e.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":10,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"702d3f032ec6e859379cc73da1b5e2af6d8522ec13b20b3cb80579065cf3cb8f","root_hash":"5ef7f6d313365e53428317272b802b3deadb8a8f9e9e89625f0b27e4f9b40a38","sig":"24b032ca9c18e09ebbf5a302ba180bea90b25052e150d7b013a0ff6cdc094f138be69bfa8597ea4148455112bbea23afb1fa1c85031622dd62bdf023ccaf6d0e","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/d5c54a0582e4aae21a61b315843a4a3bc50af0b97a75f9fc6fa213f438839b59.json b/wai/http-conformance/cases/rcpt-deep/d5c54a0582e4aae21a61b315843a4a3bc50af0b97a75f9fc6fa213f438839b59.json new file mode 100644 index 000000000..957edb253 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/d5c54a0582e4aae21a61b315843a4a3bc50af0b97a75f9fc6fa213f438839b59.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":19,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"e2dfcd5e2cf232b3a0077e4a2c93598bd49a5408497bea9048e2dc1519655d19","root_hash":"5ee8619e187bbdecab6c6ee7cddfeff22f1bb8b8efd07a0e19671e0e858635fb","sig":"5320ce3f025426c22f9ff13c272746a33d9d5a6dddfe3f5c2ee1171213559c62fce2fa6470fefb5edeb39d06c20be30519deebc63e73490d424d432c22d9510c","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/dce4681c9a487a24a4b20f216f79d8319dfb7048ca2d965f1d34e5724e33d802.json b/wai/http-conformance/cases/rcpt-deep/dce4681c9a487a24a4b20f216f79d8319dfb7048ca2d965f1d34e5724e33d802.json new file mode 100644 index 000000000..810965117 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/dce4681c9a487a24a4b20f216f79d8319dfb7048ca2d965f1d34e5724e33d802.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":2,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"186babdb6cd3cfbc6d5ea3420bd0b9583d3a3328544f90d1f6ec337e833da309","root_hash":"76bc8ac36e4074bf211ba9c943ac7802c06c2feaab17d2adab88305e65149726","sig":"6ed03c784041ec9af4b1ec277881b1e8ff727cad2d6d0229335263bcf43e6b084ea2dc8e7386488f2112c980b755017ca2c5fca7e9b7fc83cfebcc6d0731030e","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/dddbdf295d8efc9890ea00b8cd09355b8bc12aa8c1b8a0abfa2736dc8e52beca.json b/wai/http-conformance/cases/rcpt-deep/dddbdf295d8efc9890ea00b8cd09355b8bc12aa8c1b8a0abfa2736dc8e52beca.json new file mode 100644 index 000000000..1a2d64b76 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/dddbdf295d8efc9890ea00b8cd09355b8bc12aa8c1b8a0abfa2736dc8e52beca.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":63,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"eff8231145c90bca502126ff8268464a9c30a11436d3efd3a8aff1e9cc065d3f","root_hash":"19d01e1ed00ed77f58f2c3e6cd3c5f8499636f23642f18daf1ed2ab30cf19c26","sig":"3cfc67a581a88b227f85f20a360e36ffd353864b0964bc3ea2fabbc9a3dc6835b6151a5561514209cab83a17cc0a797ca8650c7fce1272a84304720d08d6c107","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/dde8580c86bd5c8b984f26ecf19b9f93f2dfef67c08fa99138c2bbcbfc5fb387.json b/wai/http-conformance/cases/rcpt-deep/dde8580c86bd5c8b984f26ecf19b9f93f2dfef67c08fa99138c2bbcbfc5fb387.json new file mode 100644 index 000000000..0df87c2b9 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/dde8580c86bd5c8b984f26ecf19b9f93f2dfef67c08fa99138c2bbcbfc5fb387.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":7,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"5cf23a6733332727a616961d36300beb3ab39a637d985efa02f51f9ad9a788b1","root_hash":"820ca9b68f7cdee5aececf2c88e88eb93a26c75f68b6477434b49b1df4b1f8fe","sig":"7bacfbeedd9e6ace35b673b71ddf58045a68463b7bcc9d128d39f806832e34eadd1ca2aba7c69382c88def7611c2a1334f418601c255c03f474cc465e0fbb204","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/e13cff8676af896dc1c0a7b82178a8dea0f39e2c1ffe5fe5fd18cbfbd38fcf13.json b/wai/http-conformance/cases/rcpt-deep/e13cff8676af896dc1c0a7b82178a8dea0f39e2c1ffe5fe5fd18cbfbd38fcf13.json new file mode 100644 index 000000000..234ab2aa0 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/e13cff8676af896dc1c0a7b82178a8dea0f39e2c1ffe5fe5fd18cbfbd38fcf13.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":22,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"f8d4ee129c6d180112f714e02ac68eed633ebf2a1bfcee7f1fe8bc91d57b4b62","root_hash":"8b16dd1913dc61fdc34e54095e2823ea6b9af830a59d65798463cefaa6271af2","sig":"c252f417457c7b39dde25c68c33edc56ddb2543153dbdf95a5a560596a060b474b9e60645d05daf75d6fddfaf5eac18be979db128e8d6d2820c667e36c7f540c","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/e2dfcd5e2cf232b3a0077e4a2c93598bd49a5408497bea9048e2dc1519655d19.json b/wai/http-conformance/cases/rcpt-deep/e2dfcd5e2cf232b3a0077e4a2c93598bd49a5408497bea9048e2dc1519655d19.json new file mode 100644 index 000000000..2ea110fa9 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/e2dfcd5e2cf232b3a0077e4a2c93598bd49a5408497bea9048e2dc1519655d19.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":18,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"ecd54bf262ce60576be4a2fb0aa1c9f5885e366c534ec7cda62e6bbca91c1e6c","root_hash":"163b98a19991ffecd5abdc61168270c9417bd291af4e0713499225ab24fa30de","sig":"34538f032f22a9a0977a209dfad825ccf2f7d165aab1d58a5e6d770bb8f2671c74a579ccde74af2d4c447406fe80295df1afb0466d09d787103ccc29b5543d08","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/ecd54bf262ce60576be4a2fb0aa1c9f5885e366c534ec7cda62e6bbca91c1e6c.json b/wai/http-conformance/cases/rcpt-deep/ecd54bf262ce60576be4a2fb0aa1c9f5885e366c534ec7cda62e6bbca91c1e6c.json new file mode 100644 index 000000000..434acf6c8 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/ecd54bf262ce60576be4a2fb0aa1c9f5885e366c534ec7cda62e6bbca91c1e6c.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":17,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"727ec6e7702c42e445c28f7db5ca526c4cfcf72207a793b8a75b457b0004cbb7","root_hash":"e881b379ccc2bdd32f7e60cf192ced65e61f1f819db76637ce9d3c5e271ab553","sig":"6f00e7c85aa2b45cecb93bbf6b0c5877010d198217d84038f30d5a33da71a597e006966b851f7f0629f5dbe77d76bba63ba08fe4a0649ab1647d8e895cffe801","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/eff8231145c90bca502126ff8268464a9c30a11436d3efd3a8aff1e9cc065d3f.json b/wai/http-conformance/cases/rcpt-deep/eff8231145c90bca502126ff8268464a9c30a11436d3efd3a8aff1e9cc065d3f.json new file mode 100644 index 000000000..dac89fabf --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/eff8231145c90bca502126ff8268464a9c30a11436d3efd3a8aff1e9cc065d3f.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":62,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"ba386a976d3bb07999fa2b48a0505aad51f5bcf1ad926f30e17475143abdaef7","root_hash":"dc77c06bdb81ba441d0b64be464a4ee9aed5afe41f526b08179a950510f4a3eb","sig":"ffa6b894fa42c107454b811fc64f0d507f3c0a3bc9135469f6e0fe0dfca2b36c2b90ac63f3996c6c0c2c8b1cfc9b3ba71400eb7f1b002cd9c293bd866117200f","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/f1d86cc4dc42d0dde7a760f09076dde6454c304a2b95c1150d244ab8d9868480.json b/wai/http-conformance/cases/rcpt-deep/f1d86cc4dc42d0dde7a760f09076dde6454c304a2b95c1150d244ab8d9868480.json new file mode 100644 index 000000000..e4037ebfa --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/f1d86cc4dc42d0dde7a760f09076dde6454c304a2b95c1150d244ab8d9868480.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":59,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"a577ff30a365f5af48f5ea6f2b874ea9520ec024639f33f9fcc8545d0535ea64","root_hash":"c682354abc6bc3647d119e22ab284e863f451d1c42e3d60a3a2f3c5cca9c442a","sig":"75630b78b4d6ba8a12eb8949d371831fd2859c4ce0dfc0517ffbba54531e20e556be0e2a494abf89b7a9f5101eb0506240c9f2117eb2e778768160a1d4a68406","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/f2fed1d8ecfa414530d8bbc5114c80946a313a0c46a0d624c5083cfc4f9ab61b.json b/wai/http-conformance/cases/rcpt-deep/f2fed1d8ecfa414530d8bbc5114c80946a313a0c46a0d624c5083cfc4f9ab61b.json new file mode 100644 index 000000000..3dcc9ee91 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/f2fed1d8ecfa414530d8bbc5114c80946a313a0c46a0d624c5083cfc4f9ab61b.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":35,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"bc54f7a82afbf23e1a81249785409dbf27d11f1c7871850d11f37afd637b0d8a","root_hash":"82d65bab931e358299e96004ecf3751116988069c6aad5036de8334ca38af376","sig":"5433e450ffa6dc21a5efe41e9a6a348e5fb54bc1a2fb39bac198732b82196a5a85fb02008d631fd62d508b966861a78fb05b77dd85f7a0ffa44de3eed8bee608","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/f424f181500d06d27e5344e5ef28ff6a68eed7386ead966dd4c5f11baf14d41b.json b/wai/http-conformance/cases/rcpt-deep/f424f181500d06d27e5344e5ef28ff6a68eed7386ead966dd4c5f11baf14d41b.json new file mode 100644 index 000000000..a43ce8e1f --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/f424f181500d06d27e5344e5ef28ff6a68eed7386ead966dd4c5f11baf14d41b.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":53,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"3aa91d8e5f296a997f8473822a5558478a3302b02c06c3192986bbcb2d5d27b1","root_hash":"123c39a71f5af2019b8b6cc43e1368a1584df015d3c4a67c4ed879b40c18b820","sig":"20754edbbc0143a5d390b0705bdb9cf3ba79cbc89d15900a5b33a0a0e0ecea3e98ccadf39359f357ffb9c981cba2c81df06824ceaf3fe769e8a60ee1852b4b0c","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/f8d4ee129c6d180112f714e02ac68eed633ebf2a1bfcee7f1fe8bc91d57b4b62.json b/wai/http-conformance/cases/rcpt-deep/f8d4ee129c6d180112f714e02ac68eed633ebf2a1bfcee7f1fe8bc91d57b4b62.json new file mode 100644 index 000000000..71c6daa7f --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/f8d4ee129c6d180112f714e02ac68eed633ebf2a1bfcee7f1fe8bc91d57b4b62.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":21,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"8ce05b04d49062f191ae072bf970ba23863c7cc88b654d178acf61361fc81efa","root_hash":"b824ab92e657202ce8e9f4b978899246192028d4467fc6cc2f8f438c4f056e31","sig":"dd2945bb23417dbbf2c654c251c2e6b70c30f03ed17d3c1ea4429cb6a8bd5e0d3ca6c23f1808230bea909f02ec71bd17aaee0dd21f95271b8a2360953010af0a","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/fbe11c778ba08a3178591ad782e1f2392419aad7d8b53be5e4e8432d7100c7fe.json b/wai/http-conformance/cases/rcpt-deep/fbe11c778ba08a3178591ad782e1f2392419aad7d8b53be5e4e8432d7100c7fe.json new file mode 100644 index 000000000..aeb5d25be --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/fbe11c778ba08a3178591ad782e1f2392419aad7d8b53be5e4e8432d7100c7fe.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":60,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"f1d86cc4dc42d0dde7a760f09076dde6454c304a2b95c1150d244ab8d9868480","root_hash":"6458b7019b5695981f86d818372e4a8c2dcb3977d5a566fa29c38e461d13386a","sig":"869a6f1107e0780deeafacb92e6db68b5de4d513627c28dc796b1a774f939b098b7b1e55681469910e4ec56c3c325e9213605ff21a6956ade3f33ef96a30a301","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/fcdf29a5c1f6567155dd0df394306fd179f9fdd04b252535d937c6537004735c.json b/wai/http-conformance/cases/rcpt-deep/fcdf29a5c1f6567155dd0df394306fd179f9fdd04b252535d937c6537004735c.json new file mode 100644 index 000000000..4644d94e4 --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/fcdf29a5c1f6567155dd0df394306fd179f9fdd04b252535d937c6537004735c.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":14,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"2d31d0bd821ee2578210482b192d2cb47d1d9a9846fc9f032311c4b156b3170a","root_hash":"ced251a071645d86ee1c9230908d643ca77f95f31116470bc260652a3a7adfd0","sig":"26c6118f157aee8eabea938c20668b9de3299dc27b8eb87f08f411dd26ae3812e64a6bc830cbd13058cdf6898e535e4e0bf1feb0bc03259d246031e805e01906","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt-deep/ff696c1fecfcfdf1b1fe0d3a3bc7124039209dc2b8dfbb2efd9ee7dcbafc955a.json b/wai/http-conformance/cases/rcpt-deep/ff696c1fecfcfdf1b1fe0d3a3bc7124039209dc2b8dfbb2efd9ee7dcbafc955a.json new file mode 100644 index 000000000..0769782ea --- /dev/null +++ b/wai/http-conformance/cases/rcpt-deep/ff696c1fecfcfdf1b1fe0d3a3bc7124039209dc2b8dfbb2efd9ee7dcbafc955a.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":36,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"f2fed1d8ecfa414530d8bbc5114c80946a313a0c46a0d624c5083cfc4f9ab61b","root_hash":"dee97708247df3b4f351eb02434662b265f6e0b6ecf4b031cb9ce9ea528e34bc","sig":"93237e15424ba686f8ce91fd859d64b75a106f51ffbb05bcc8854f562f7801a0c95c4ba317184e1b8d8e67b011744773bd9bdc96972a332173b26b56697f750f","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":5} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt/02154a175d9519491b6321797e67e5e20db3346ffc4e14bd4e30c0c6a9e9f358.json b/wai/http-conformance/cases/rcpt/02154a175d9519491b6321797e67e5e20db3346ffc4e14bd4e30c0c6a9e9f358.json new file mode 100644 index 000000000..65b6c453c --- /dev/null +++ b/wai/http-conformance/cases/rcpt/02154a175d9519491b6321797e67e5e20db3346ffc4e14bd4e30c0c6a9e9f358.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":0,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":null,"root_hash":"43c3a4e8ff2e9cb84fa4529dd16c18008f7ac1b324117853c0f45871e46f85c7","sig":"7f0a32e960222763ba31d025cb4fefc0f9b210310c66b319baaeefe6f5845864e5338dc1ddd89a49a694e4e0342b23fb5bf4813dd546e9518b8f79d93b1c3c04","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":1} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt/11c2d4a654057161f6450907c69bb1136f5228fdfcf05e599fb92ab318eba935.json b/wai/http-conformance/cases/rcpt/11c2d4a654057161f6450907c69bb1136f5228fdfcf05e599fb92ab318eba935.json new file mode 100644 index 000000000..a586cd43b --- /dev/null +++ b/wai/http-conformance/cases/rcpt/11c2d4a654057161f6450907c69bb1136f5228fdfcf05e599fb92ab318eba935.json @@ -0,0 +1 @@ +{"body":{"declaration":"13e1857af31da433ed26408d5cfdb8a486dfdef79026836dda43b4b7a790ac1e","outcome":"dpv:ConsentGiven","purpose":"dpv:PersonalisedAdvertising","record":"decision","vocabulary":"https://w3id.org/dpv"},"class":"lawful-basis","issued_at":"2026-10-06T00:03:31Z","kind":"claim","parent_receipt_hash":null,"seq":0,"sig":"3092b7395f839e770892be7f8a054fb9003829d91961dea0e3e92f5befc01831c22df5b984aea10dd38e0394954f1a2e9c6c30c9eb901f9ac8bea02cc009af07","signer_id":"did:example:http-sink","signer_pubkey":"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","subject_type":"object"} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt/133ffb1355f912e7ea23db3373cf3dd497a1e9f309741d3fa071b4fd71cac9a0.json b/wai/http-conformance/cases/rcpt/133ffb1355f912e7ea23db3373cf3dd497a1e9f309741d3fa071b4fd71cac9a0.json new file mode 100644 index 000000000..a4eb01c1b --- /dev/null +++ b/wai/http-conformance/cases/rcpt/133ffb1355f912e7ea23db3373cf3dd497a1e9f309741d3fa071b4fd71cac9a0.json @@ -0,0 +1 @@ +{"body":{"alg":"org.example.watermark.1","attests":"ancestor","binding_type":"watermark","declaration":"f95a4c6858d22e963130429b04b26653117ea3615b889975f0693168fe5b217c","observed":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f","resolution_endpoint":"https://resolver.example.org/matches","value":"636c61696d2d636f6e666f726d616e63652077617465726d61726b207061796c6f6164"},"class":"binding-recovery","issued_at":"2026-10-06T00:03:52Z","kind":"claim","parent_receipt_hash":null,"seq":0,"sig":"cbf00b096d3ffa22a49c3344101f7036ae786fbbc567af4c7b3031cb8b7850cc9e32f65fbd5be4c761f049401ada8cf65d5e1f8ea65e895b37226bd9225bdb06","signer_id":"did:example:http-sink","signer_pubkey":"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","subject_type":"object"} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt/13e1857af31da433ed26408d5cfdb8a486dfdef79026836dda43b4b7a790ac1e.json b/wai/http-conformance/cases/rcpt/13e1857af31da433ed26408d5cfdb8a486dfdef79026836dda43b4b7a790ac1e.json new file mode 100644 index 000000000..e30679e40 --- /dev/null +++ b/wai/http-conformance/cases/rcpt/13e1857af31da433ed26408d5cfdb8a486dfdef79026836dda43b4b7a790ac1e.json @@ -0,0 +1 @@ +{"body":{"fragment":"9109aa1bee9f220a0cfb9bf4c5794f4c11027b8f20f6a804462b8b0b86463376","purposes":[{"basis":"dpv:Contract","data":[],"purpose":"dpv:ServiceProvision"},{"basis":"dpv:Consent","data":["pd:Location"],"purpose":"dpv:ServiceOptimisation"}],"record":"declaration","vocabulary":"https://w3id.org/dpv"},"class":"lawful-basis","issued_at":"2026-10-06T00:03:20Z","kind":"claim","parent_receipt_hash":null,"seq":0,"sig":"74aa5c2f020964aa6a3c784fac8e7c1166768c70fe16fba000887506c545e9f4b7b51e1a274df8ca3483442e7e4d2943d0c0b6b67c960de1ea991e4c91b8600d","signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","subject_type":"object"} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt/21bc2df99c8f61e4b736e9df0ef64cd9a3c76f6a4f91d1f806895b7425280b28.json b/wai/http-conformance/cases/rcpt/21bc2df99c8f61e4b736e9df0ef64cd9a3c76f6a4f91d1f806895b7425280b28.json new file mode 100644 index 000000000..0c4ac15ed --- /dev/null +++ b/wai/http-conformance/cases/rcpt/21bc2df99c8f61e4b736e9df0ef64cd9a3c76f6a4f91d1f806895b7425280b28.json @@ -0,0 +1 @@ +{"body":{"declaration":"13e1857af31da433ed26408d5cfdb8a486dfdef79026836dda43b4b7a790ac1e","outcome":"dpv:ConsentGiven","purpose":"dpv:ServiceOptimisation","record":"decision","vocabulary":"https://w3id.org/dpv"},"class":"lawful-basis","issued_at":"2026-10-06T00:03:30Z","kind":"claim","parent_receipt_hash":null,"seq":0,"sig":"840c67fce73952a3191772125e54ec2a92deb7bbcb872e1a4eba5f4e54dbac103121613196473c65aa120a02c155bc889426135a4cb2b3a82a2a84b62bd2e607","signer_id":"did:example:http-sink","signer_pubkey":"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","subject_type":"object"} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt/23e93ea54cbdbe5a203dc6d346752c77c396e1758466b8eb785b46d31f54760f.json b/wai/http-conformance/cases/rcpt/23e93ea54cbdbe5a203dc6d346752c77c396e1758466b8eb785b46d31f54760f.json new file mode 100644 index 000000000..7eb07e184 --- /dev/null +++ b/wai/http-conformance/cases/rcpt/23e93ea54cbdbe5a203dc6d346752c77c396e1758466b8eb785b46d31f54760f.json @@ -0,0 +1 @@ +{"body":{"alg":"org.example.watermark.1","attests":"ancestor","binding_type":"watermark","declaration":"3f4c9dcae5e9654eec359360cf03a0881b61a53ff1b8e4ecaf1cef696a917650","observed":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f","resolution_endpoint":"https://resolver.example.org/matches","value":"636c61696d2d636f6e666f726d616e63652077617465726d61726b207061796c6f6164"},"class":"binding-recovery","issued_at":"2026-10-06T00:03:50Z","kind":"claim","parent_receipt_hash":null,"seq":0,"sig":"3ad58aef5854137d6c88d3108a0a1db204432b85c983e5392097b494a88cc2fbedc48d13b437e4e00d8e9ff3ff87c5c6ce94cc91d68ef14a7e5bf3e5a2f22800","signer_id":"did:example:http-sink","signer_pubkey":"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","subject_type":"object"} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt/3f4c9dcae5e9654eec359360cf03a0881b61a53ff1b8e4ecaf1cef696a917650.json b/wai/http-conformance/cases/rcpt/3f4c9dcae5e9654eec359360cf03a0881b61a53ff1b8e4ecaf1cef696a917650.json new file mode 100644 index 000000000..42e2a67b1 --- /dev/null +++ b/wai/http-conformance/cases/rcpt/3f4c9dcae5e9654eec359360cf03a0881b61a53ff1b8e4ecaf1cef696a917650.json @@ -0,0 +1 @@ +{"body":{"alg":"org.example.watermark.1","binding_type":"watermark","registry":"https://registry.example.org/soft-binding-algorithms","resolution_endpoint":"https://resolver.example.org/matches","value":"636c61696d2d636f6e666f726d616e63652077617465726d61726b207061796c6f6164"},"class":"soft-binding","issued_at":"2026-10-06T00:03:40Z","kind":"claim","parent_receipt_hash":null,"seq":0,"sig":"53af8446142c32089cee5030e9ce1278fb119f90869857671f45a9dfdaf01e81904a755f2b4b72ddfd68503f6a1aa30ede051b003d1af18222222353c7a00905","signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","subject_type":"object"} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt/3fa148d4b79df7eb77f3d813122ab6463c339212111830f251e7c61b0e3fe784.json b/wai/http-conformance/cases/rcpt/3fa148d4b79df7eb77f3d813122ab6463c339212111830f251e7c61b0e3fe784.json new file mode 100644 index 000000000..f9cdb08f8 --- /dev/null +++ b/wai/http-conformance/cases/rcpt/3fa148d4b79df7eb77f3d813122ab6463c339212111830f251e7c61b0e3fe784.json @@ -0,0 +1 @@ +{"body":{"energy_calibration_ref":null,"energy_counter_filtering":"Unknown","energy_provenance":"OnChipCounter","energy_uncertainty":{"relative_ppm":44669,"window_us":3000000},"figure_index":0,"figure_profile":"moq-jwp:object","group":"95477f223c6bee89a57c7171c831352fd43f4a348f9d56f90e32e922c2c2b16f","joules_micro":461538,"op_committed":{"frame_rate_milli":30000,"height":1080},"op_delivered":{"frame_rate_milli":30000,"height":1080},"op_requested":{"frame_rate_milli":30000,"height":1080},"prediction":{"descriptor_hash":"1122437cacd17ba56225e487fcb4f5350858024b1fa7051cdd09dd526cba2f32","predicted_joules_micro":450000,"predictor":"https://models.example.org/complexity-to-energy"},"vocabulary":"https://vocabulary.example.org/operating-points"},"class":"operating-point","issued_at":"2026-10-06T00:04:00Z","kind":"claim","parent_receipt_hash":null,"seq":0,"sig":"afba2a669086a83c6e7bbd3133ae6f419c524667ff8e5d1ccc28b742c13ef10541e2450a976ecaf4b08f1fb3460eb26f3ca3f5ba31ff3a67bdf58e72a0828b06","signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","subject":"e43e038c10c0494d9647444fd4daf54b19fc3a946a3c7980431bc6e0a236093b","subject_type":"object"} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt/67b2612eb6b1e6ff7eeae5a6004682cd67fba92f8474c1a4d604fbf5934f7045.json b/wai/http-conformance/cases/rcpt/67b2612eb6b1e6ff7eeae5a6004682cd67fba92f8474c1a4d604fbf5934f7045.json new file mode 100644 index 000000000..4862fcf01 --- /dev/null +++ b/wai/http-conformance/cases/rcpt/67b2612eb6b1e6ff7eeae5a6004682cd67fba92f8474c1a4d604fbf5934f7045.json @@ -0,0 +1 @@ +{"body":{"alg":"org.example.watermark.1","attests":"ancestor","binding_type":"watermark","declaration":"3f4c9dcae5e9654eec359360cf03a0881b61a53ff1b8e4ecaf1cef696a917650","observed":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f","resolution_endpoint":"https://resolver.example.org/matches","value":"616e6f74686572207061796c6f6164"},"class":"binding-recovery","issued_at":"2026-10-06T00:03:51Z","kind":"claim","parent_receipt_hash":null,"seq":0,"sig":"6f30b641a7fdb2480d462dd418dc1ead09b749beb442f6ffcdc3ebfdbfa48d28234b9d53198aa8d530a4737f0eedb578853747148eabe49dc6c152face821307","signer_id":"did:example:http-sink","signer_pubkey":"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","subject_type":"object"} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt/6a29eb0066513cad092217b4f905f3decfc703c081c832fb35e3e42f3d1c7043.json b/wai/http-conformance/cases/rcpt/6a29eb0066513cad092217b4f905f3decfc703c081c832fb35e3e42f3d1c7043.json new file mode 100644 index 000000000..d2576b3a4 --- /dev/null +++ b/wai/http-conformance/cases/rcpt/6a29eb0066513cad092217b4f905f3decfc703c081c832fb35e3e42f3d1c7043.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":1,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"3fba8315347da348bbb2f7a4b3f5488ff18a2be9bfb95efc7921142969ec3807","root_hash":"a5d07c9c94c353d57371a2f316d0cf3acb17baa6980ad6ba8b82b0bda83dda9a","sig":"681691b46bd8de7541ec316d8bb144a9bd5e77a077f168e022ee262aefcecb1eba237f8a0226450188eb7ad072981886102f4fcaa64dadad4fbbf3066d5dab08","signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":6} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt/8bc21df138b97b63f5f4730ca6dd4ad911ce2194843a671678afe1a97a4a92f8.json b/wai/http-conformance/cases/rcpt/8bc21df138b97b63f5f4730ca6dd4ad911ce2194843a671678afe1a97a4a92f8.json new file mode 100644 index 000000000..497c42571 --- /dev/null +++ b/wai/http-conformance/cases/rcpt/8bc21df138b97b63f5f4730ca6dd4ad911ce2194843a671678afe1a97a4a92f8.json @@ -0,0 +1 @@ +{"body":{"declaration":"aedd5190e7eb2e5e61854f1009402caf242d51df0d76506a60481fd79a7ca2ca","outcome":"dpv:ConsentGiven","purpose":"dpv:ServiceOptimisation","record":"decision","vocabulary":"https://w3id.org/dpv"},"class":"lawful-basis","issued_at":"2026-10-06T00:03:32Z","kind":"claim","parent_receipt_hash":null,"seq":0,"sig":"febeeeb47fe8f0b4ce01185a1e422a708171b2819e17ed6f1984091e13ff2229b8e3b431807137757aed2d9a02d3bead9cec7fb5f216d1badb673aeaa43e8c09","signer_id":"did:example:http-sink","signer_pubkey":"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","subject_type":"object"} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt/9f8d38818a696727645324c3b7da8b1e1c4989f472138bf3dbb50e22227ae93e.json b/wai/http-conformance/cases/rcpt/9f8d38818a696727645324c3b7da8b1e1c4989f472138bf3dbb50e22227ae93e.json new file mode 100644 index 000000000..63e97e335 --- /dev/null +++ b/wai/http-conformance/cases/rcpt/9f8d38818a696727645324c3b7da8b1e1c4989f472138bf3dbb50e22227ae93e.json @@ -0,0 +1 @@ +{"body":{"classical_declared":700,"classical_source":"wai2-table","counted_discarded":1904,"counted_fallback":0,"counted_primary":5935,"deadline_us":40000,"decoders":[{"capability":"wai.video.int_hyper","decoder":"builtin:wai@0.4.0","first_unit":0,"leaves":4,"module_status":null,"object":"d09526cec3961c21e36cc08f0b393323eb82dcc4203cb08df57fee36784b1113","path":"primary","pin":"7b4371854df0aa1645ac6525b373387c0617b873d5a57748932ee0f386ea164b","rendition":null,"tier":"decode-equivalence","variant":null},{"capability":"wai.video.int_hyper","decoder":"builtin:wai@0.4.0","first_unit":4,"leaves":1,"module_status":null,"object":"f18f33c4eebadf2caf00fb22a38cf394e19d31a62ebe68f3043c31c84ecbcf44","path":"primary","pin":"7b4371854df0aa1645ac6525b373387c0617b873d5a57748932ee0f386ea164b","rendition":0,"tier":"decode-equivalence","variant":null},{"capability":"wai.video.av1","decoder":"builtin:wai@0.4.0","first_unit":5,"leaves":1,"module_status":null,"object":"13b04249eca2b1a9061dba8ca2846169d3f5fa3548d9e138a0cf2cb553798cf4","path":"fallback","pin":null,"rendition":null,"tier":null,"variant":null},{"capability":"wai.video.int_refine","decoder":"builtin:wai@0.4.0","first_unit":0,"leaves":1,"module_status":null,"object":"47a2c23727d2eb9d2beef0931c3e9b2675408d372660420cce427dc27fd21fe1","path":"primary","pin":null,"rendition":null,"tier":null,"variant":null},{"capability":"wai.video.int_refine","decoder":"builtin:wai@0.4.0","first_unit":0,"leaves":1,"module_status":null,"object":"6b74be11e64461c7d972fc5cb6ba15e98427e9d6e60622b0a753222138de28cf","path":"primary","pin":null,"rendition":null,"tier":null,"variant":null},{"capability":"wai.video.int_refine","decoder":"builtin:wai@0.4.0","first_unit":0,"leaves":1,"module_status":null,"object":"5e7a38ca5c04442cbe00b040d58e4494f5aa36fb81e8bbe0d304fd2d1ce722b2","path":"primary","pin":null,"rendition":null,"tier":null,"variant":null},{"capability":"wai.video.int_refine","decoder":"builtin:wai@0.4.0","first_unit":0,"leaves":1,"module_status":null,"object":"be5055af8e32359265852c512667b10ab6a07455e5ab06603e278763f6633a85","path":"primary","pin":null,"rendition":null,"tier":null,"variant":null}],"energy":null,"fallback_units":1,"framing":"registered","host":{"build":{"profile":"release","target":"aarch64-unknown-linux-gnu","toolchain":"rustc 1.95.0"},"profile":"native-ffi","target":"aarch64-unknown-linux-gnu","threads":4},"leaf_count":10,"media":"video","missed":1,"missed_units":[{"late_us":1200,"layers":1,"unit":5}],"output_root":"b62a8425b7fe3a085a14f2f2156ea22ae3b4349884d799b5afe66dccc7d4dd7f","primary_units":5,"receipt_bytes":640,"session_nonce":"57c5462dfdabd1b7bdd204d34ba33649c057eb535349f29f45eb8f8c7d4a7e7f","stage_sets":[{"claim":"7ff63c64ae3826c013fd8c2387f51307c4bf715842b0a803528349e200d95551","first_unit":0}],"switches":[{"from":"primary","reason":"set-incomplete","to":"fallback","unit":5}],"switches_total":1,"transferred":9995,"uncounted":2,"units":6},"class":"reconstruct-session","issued_at":"2026-10-06T00:04:10Z","kind":"claim","parent_receipt_hash":null,"seq":0,"sig":"2456c62add11b3c56cd8a161274b57f8de1eac6ef579484d619cfbb33b90f8c348b4ce48b17f7be57cc48951192b386f8f642831f362242235d4813996584d06","signer_id":"did:example:http-sink","signer_pubkey":"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","subject_type":"object"} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt/a0410e703b803a08ec1b8ec0618f17b68c9d307f82e22fa301083aa90c8def29.json b/wai/http-conformance/cases/rcpt/a0410e703b803a08ec1b8ec0618f17b68c9d307f82e22fa301083aa90c8def29.json new file mode 100644 index 000000000..5c22e7a1d --- /dev/null +++ b/wai/http-conformance/cases/rcpt/a0410e703b803a08ec1b8ec0618f17b68c9d307f82e22fa301083aa90c8def29.json @@ -0,0 +1 @@ +{"body":{"energy_calibration_ref":null,"energy_counter_filtering":"Unknown","energy_provenance":"OnChipCounter","energy_uncertainty":{"relative_ppm":44669,"window_us":3000000},"figure_index":0,"figure_profile":"moq-jwp:object","group":"95477f223c6bee89a57c7171c831352fd43f4a348f9d56f90e32e922c2c2b16f","joules_micro":461539,"op_committed":{"frame_rate_milli":30000,"height":1080},"op_delivered":{"frame_rate_milli":30000,"height":1080},"op_requested":{"frame_rate_milli":30000,"height":1080},"prediction":{"descriptor_hash":"1122437cacd17ba56225e487fcb4f5350858024b1fa7051cdd09dd526cba2f32","predicted_joules_micro":450000,"predictor":"https://models.example.org/complexity-to-energy"},"vocabulary":"https://vocabulary.example.org/operating-points"},"class":"operating-point","issued_at":"2026-10-06T00:04:01Z","kind":"claim","parent_receipt_hash":null,"seq":0,"sig":"661a0293f236e45d55742c1d2cb8736e052cf885abb3416e42f583e1c79ad008d6bb761f0db2bee07da182657d4805360378f7df8b53217739ad995be6ffcf01","signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","subject":"e43e038c10c0494d9647444fd4daf54b19fc3a946a3c7980431bc6e0a236093b","subject_type":"object"} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt/aedd5190e7eb2e5e61854f1009402caf242d51df0d76506a60481fd79a7ca2ca.json b/wai/http-conformance/cases/rcpt/aedd5190e7eb2e5e61854f1009402caf242d51df0d76506a60481fd79a7ca2ca.json new file mode 100644 index 000000000..3bcdb174e --- /dev/null +++ b/wai/http-conformance/cases/rcpt/aedd5190e7eb2e5e61854f1009402caf242d51df0d76506a60481fd79a7ca2ca.json @@ -0,0 +1 @@ +{"body":{"fragment":"9109aa1bee9f220a0cfb9bf4c5794f4c11027b8f20f6a804462b8b0b86463376","purposes":[{"basis":"dpv:Contract","data":[],"purpose":"dpv:ServiceProvision"},{"basis":"dpv:Consent","data":["pd:Location"],"purpose":"dpv:ServiceOptimisation"}],"record":"declaration","vocabulary":"https://w3id.org/dpv"},"class":"lawful-basis","issued_at":"2026-10-06T00:03:21Z","kind":"claim","parent_receipt_hash":null,"seq":0,"sig":"4cdfff3a6a343148099eccb79ef2fcbe1439e847985027a409319c1e469b15513bf4efa298372fb233474a0c53370bb5a4731b0fc8dad787c6d48c6523e60407","signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","subject":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f","subject_type":"object"} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt/c608ce5df043fb58021d9360c131668c0896cf0d3e4010520c4482eeb33a821e.json b/wai/http-conformance/cases/rcpt/c608ce5df043fb58021d9360c131668c0896cf0d3e4010520c4482eeb33a821e.json new file mode 100644 index 000000000..64fa5f5ee --- /dev/null +++ b/wai/http-conformance/cases/rcpt/c608ce5df043fb58021d9360c131668c0896cf0d3e4010520c4482eeb33a821e.json @@ -0,0 +1 @@ +{"energy_coverage":"Whole","energy_provenance":"Estimator","group_end":"2026-10-06T00:00:04Z","group_id":0,"group_start":"2026-10-06T00:00:00Z","joules_total":300,"kind":"group","leaf_count":1,"parent_receipt_hash":null,"root_hash":"368b22537cfa0d3d0bfcfe1f6256302af54c54711dfa411c193beda171c97edb","sig":"2042a90c80338d11fa12ce385bafb946f836ef8f1a2f3bbe55168ee53b79885e3bf7fac8b895015d75e98102ea0d22427c6c477dd17a6bd8dd78e8c6e028550f","sign_version":2,"signer_id":"did:example:other-publisher","signer_pubkey":"12a41592c8b7c17d4059e7b29b61e8ff96c7415f2f803348f2f017e05b9ea1da","track_alias":7} \ No newline at end of file diff --git a/wai/http-conformance/cases/rcpt/f95a4c6858d22e963130429b04b26653117ea3615b889975f0693168fe5b217c.json b/wai/http-conformance/cases/rcpt/f95a4c6858d22e963130429b04b26653117ea3615b889975f0693168fe5b217c.json new file mode 100644 index 000000000..803fee251 --- /dev/null +++ b/wai/http-conformance/cases/rcpt/f95a4c6858d22e963130429b04b26653117ea3615b889975f0693168fe5b217c.json @@ -0,0 +1 @@ +{"body":{"alg":"org.example.watermark.1","binding_type":"watermark","registry":"https://registry.example.org/soft-binding-algorithms","resolution_endpoint":"https://resolver.example.org/matches","value":"636c61696d2d636f6e666f726d616e63652077617465726d61726b207061796c6f6164"},"class":"soft-binding","issued_at":"2026-10-06T00:03:41Z","kind":"claim","parent_receipt_hash":null,"seq":0,"sig":"86d7f3d6263653d55cba2135507780fddef561f5692000362c57be7c1204d9302109263d6e5ec0e9ff7aeca0a2dfc47119bfbb226d655902cb219b28b699c502","signer_id":"did:example:http-relay","signer_pubkey":"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","subject_type":"object"} \ No newline at end of file diff --git a/wai/http-conformance/cases/receipts/O1-legacy.wai.receipts b/wai/http-conformance/cases/receipts/O1-legacy.wai.receipts new file mode 100644 index 000000000..bd45072f4 --- /dev/null +++ b/wai/http-conformance/cases/receipts/O1-legacy.wai.receipts @@ -0,0 +1 @@ +{"claims":[],"delivery":[{"object_receipt":{"content_hash":"df5337ea38c4d61fd15f006c08df937f480788fd669e7337b0f8105a5816a9d2","group_id":1,"joules_micro":0,"kind":"object","merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":6},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"df5337ea38c4d61fd15f006c08df937f480788fd669e7337b0f8105a5816a9d2","group_receipt":"6a29eb0066513cad092217b4f905f3decfc703c081c832fb35e3e42f3d1c7043","joules_micro":0}}],"kind":"wai-receipt-index","object":"df5337ea38c4d61fd15f006c08df937f480788fd669e7337b0f8105a5816a9d2"} \ No newline at end of file diff --git a/wai/http-conformance/cases/receipts/O3-binding-recovery.wai.receipts b/wai/http-conformance/cases/receipts/O3-binding-recovery.wai.receipts new file mode 100644 index 000000000..4e94c142c --- /dev/null +++ b/wai/http-conformance/cases/receipts/O3-binding-recovery.wai.receipts @@ -0,0 +1 @@ +{"claims":["23e93ea54cbdbe5a203dc6d346752c77c396e1758466b8eb785b46d31f54760f","67b2612eb6b1e6ff7eeae5a6004682cd67fba92f8474c1a4d604fbf5934f7045","133ffb1355f912e7ea23db3373cf3dd497a1e9f309741d3fa071b4fd71cac9a0"],"delivery":[{"object_receipt":{"content_hash":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","energy_provenance":"Estimator","group_id":0,"joules_micro":1500,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":["368b22537cfa0d3d0bfcfe1f6256302af54c54711dfa411c193beda171c97edb"]},"object_id":0,"origin":"origin","track_alias":2},"property":{"acquisition":"Estimator","acquisition_evidence":"","content_hash":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","group_receipt":"ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018","joules_micro":1500}}],"kind":"wai-receipt-index","object":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9"} \ No newline at end of file diff --git a/wai/http-conformance/cases/receipts/O3-lawful-basis.wai.receipts b/wai/http-conformance/cases/receipts/O3-lawful-basis.wai.receipts new file mode 100644 index 000000000..9f01ffbcf --- /dev/null +++ b/wai/http-conformance/cases/receipts/O3-lawful-basis.wai.receipts @@ -0,0 +1 @@ +{"claims":["21bc2df99c8f61e4b736e9df0ef64cd9a3c76f6a4f91d1f806895b7425280b28","11c2d4a654057161f6450907c69bb1136f5228fdfcf05e599fb92ab318eba935","8bc21df138b97b63f5f4730ca6dd4ad911ce2194843a671678afe1a97a4a92f8"],"delivery":[{"object_receipt":{"content_hash":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","energy_provenance":"Estimator","group_id":0,"joules_micro":1500,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":["368b22537cfa0d3d0bfcfe1f6256302af54c54711dfa411c193beda171c97edb"]},"object_id":0,"origin":"origin","track_alias":2},"property":{"acquisition":"Estimator","acquisition_evidence":"","content_hash":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","group_receipt":"ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018","joules_micro":1500}}],"kind":"wai-receipt-index","object":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9"} \ No newline at end of file diff --git a/wai/http-conformance/cases/receipts/O3-session.wai.receipts b/wai/http-conformance/cases/receipts/O3-session.wai.receipts new file mode 100644 index 000000000..6388cc82e --- /dev/null +++ b/wai/http-conformance/cases/receipts/O3-session.wai.receipts @@ -0,0 +1 @@ +{"claims":["9f8d38818a696727645324c3b7da8b1e1c4989f472138bf3dbb50e22227ae93e"],"delivery":[{"object_receipt":{"content_hash":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","energy_provenance":"Estimator","group_id":0,"joules_micro":1500,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":["368b22537cfa0d3d0bfcfe1f6256302af54c54711dfa411c193beda171c97edb"]},"object_id":0,"origin":"origin","track_alias":2},"property":{"acquisition":"Estimator","acquisition_evidence":"","content_hash":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","group_receipt":"ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018","joules_micro":1500}}],"kind":"wai-receipt-index","object":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9"} \ No newline at end of file diff --git a/wai/http-conformance/cases/receipts/O5-operating-point.wai.receipts b/wai/http-conformance/cases/receipts/O5-operating-point.wai.receipts new file mode 100644 index 000000000..f8ce11eea --- /dev/null +++ b/wai/http-conformance/cases/receipts/O5-operating-point.wai.receipts @@ -0,0 +1 @@ +{"claims":["3fa148d4b79df7eb77f3d813122ab6463c339212111830f251e7c61b0e3fe784","a0410e703b803a08ec1b8ec0618f17b68c9d307f82e22fa301083aa90c8def29"],"delivery":[{"object_receipt":{"content_hash":"e43e038c10c0494d9647444fd4daf54b19fc3a946a3c7980431bc6e0a236093b","energy_counter_filtering":"Unknown","energy_provenance":"OnChipCounter","energy_uncertainty":{"relative_ppm":44669,"window_us":3000000},"group_id":0,"joules_micro":461538,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":4},"property":{"acquisition":"OnChipCounter","acquisition_evidence":"0000ae7d00000000002dc6c000","content_hash":"e43e038c10c0494d9647444fd4daf54b19fc3a946a3c7980431bc6e0a236093b","group_receipt":"95477f223c6bee89a57c7171c831352fd43f4a348f9d56f90e32e922c2c2b16f","joules_micro":461538}}],"kind":"wai-receipt-index","object":"e43e038c10c0494d9647444fd4daf54b19fc3a946a3c7980431bc6e0a236093b"} \ No newline at end of file diff --git a/wai/http-conformance/cases/receipts/O7-other-first.wai.receipts b/wai/http-conformance/cases/receipts/O7-other-first.wai.receipts new file mode 100644 index 000000000..50dbac1ec --- /dev/null +++ b/wai/http-conformance/cases/receipts/O7-other-first.wai.receipts @@ -0,0 +1 @@ +{"claims":[],"delivery":[{"object_receipt":{"content_hash":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f","energy_provenance":"Estimator","group_id":0,"joules_micro":300,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":7},"property":{"acquisition":"Estimator","acquisition_evidence":"","content_hash":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f","group_receipt":"c608ce5df043fb58021d9360c131668c0896cf0d3e4010520c4482eeb33a821e","joules_micro":300}},{"object_receipt":{"content_hash":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f","energy_provenance":"Estimator","group_id":0,"joules_micro":300,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":1,"leaf_index":1,"siblings":["6ecdf4781b24002780c2fa637e034759ebd9b88ec7a341bc7a250cc36972f5bc"]},"object_id":1,"origin":"origin","track_alias":2},"property":{"acquisition":"Estimator","acquisition_evidence":"","content_hash":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f","group_receipt":"ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018","joules_micro":300}}],"kind":"wai-receipt-index","object":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f"} \ No newline at end of file diff --git a/wai/http-conformance/cases/receipts/O7-other-only.wai.receipts b/wai/http-conformance/cases/receipts/O7-other-only.wai.receipts new file mode 100644 index 000000000..f7ae91786 --- /dev/null +++ b/wai/http-conformance/cases/receipts/O7-other-only.wai.receipts @@ -0,0 +1 @@ +{"claims":[],"delivery":[{"object_receipt":{"content_hash":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f","energy_provenance":"Estimator","group_id":0,"joules_micro":300,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":7},"property":{"acquisition":"Estimator","acquisition_evidence":"","content_hash":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f","group_receipt":"c608ce5df043fb58021d9360c131668c0896cf0d3e4010520c4482eeb33a821e","joules_micro":300}}],"kind":"wai-receipt-index","object":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f"} \ No newline at end of file diff --git a/wai/http-conformance/cases/receipts/O7-publisher-first.wai.receipts b/wai/http-conformance/cases/receipts/O7-publisher-first.wai.receipts new file mode 100644 index 000000000..a6d761c54 --- /dev/null +++ b/wai/http-conformance/cases/receipts/O7-publisher-first.wai.receipts @@ -0,0 +1 @@ +{"claims":[],"delivery":[{"object_receipt":{"content_hash":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f","energy_provenance":"Estimator","group_id":0,"joules_micro":300,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":1,"leaf_index":1,"siblings":["6ecdf4781b24002780c2fa637e034759ebd9b88ec7a341bc7a250cc36972f5bc"]},"object_id":1,"origin":"origin","track_alias":2},"property":{"acquisition":"Estimator","acquisition_evidence":"","content_hash":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f","group_receipt":"ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018","joules_micro":300}},{"object_receipt":{"content_hash":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f","energy_provenance":"Estimator","group_id":0,"joules_micro":300,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":7},"property":{"acquisition":"Estimator","acquisition_evidence":"","content_hash":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f","group_receipt":"c608ce5df043fb58021d9360c131668c0896cf0d3e4010520c4482eeb33a821e","joules_micro":300}}],"kind":"wai-receipt-index","object":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f"} \ No newline at end of file diff --git a/wai/http-conformance/cases/receipts/O9a-256-items.wai.receipts b/wai/http-conformance/cases/receipts/O9a-256-items.wai.receipts new file mode 100644 index 000000000..f733faaf3 --- /dev/null +++ b/wai/http-conformance/cases/receipts/O9a-256-items.wai.receipts @@ -0,0 +1 @@ +{"claims":["x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x"],"delivery":[{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}],"kind":"wai-receipt-index","object":"31abc1da2cdf927191f3f447f68508ead30d078690cb7bea2435ae3d1df5f4f3"} \ No newline at end of file diff --git a/wai/http-conformance/cases/receipts/O9a-257-claims.wai.receipts b/wai/http-conformance/cases/receipts/O9a-257-claims.wai.receipts new file mode 100644 index 000000000..50f79ebb8 --- /dev/null +++ b/wai/http-conformance/cases/receipts/O9a-257-claims.wai.receipts @@ -0,0 +1 @@ +{"claims":["x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x"],"delivery":[],"kind":"wai-receipt-index","object":"31abc1da2cdf927191f3f447f68508ead30d078690cb7bea2435ae3d1df5f4f3"} \ No newline at end of file diff --git a/wai/http-conformance/cases/receipts/O9a-257-items.wai.receipts b/wai/http-conformance/cases/receipts/O9a-257-items.wai.receipts new file mode 100644 index 000000000..e6c60b0b0 --- /dev/null +++ b/wai/http-conformance/cases/receipts/O9a-257-items.wai.receipts @@ -0,0 +1 @@ +{"claims":[],"delivery":[{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}],"kind":"wai-receipt-index","object":"31abc1da2cdf927191f3f447f68508ead30d078690cb7bea2435ae3d1df5f4f3"} \ No newline at end of file diff --git a/wai/http-conformance/cases/receipts/O9a-bad-signature.wai.receipts b/wai/http-conformance/cases/receipts/O9a-bad-signature.wai.receipts new file mode 100644 index 000000000..16318f69c --- /dev/null +++ b/wai/http-conformance/cases/receipts/O9a-bad-signature.wai.receipts @@ -0,0 +1 @@ +{"claims":[],"delivery":[{"object_receipt":{"content_hash":"31abc1da2cdf927191f3f447f68508ead30d078690cb7bea2435ae3d1df5f4f3","group_id":0,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"31abc1da2cdf927191f3f447f68508ead30d078690cb7bea2435ae3d1df5f4f3","group_receipt":"02154a175d9519491b6321797e67e5e20db3346ffc4e14bd4e30c0c6a9e9f358","joules_micro":0}}],"kind":"wai-receipt-index","object":"31abc1da2cdf927191f3f447f68508ead30d078690cb7bea2435ae3d1df5f4f3"} \ No newline at end of file diff --git a/wai/http-conformance/cases/receipts/O9c-x50.wai.receipts b/wai/http-conformance/cases/receipts/O9c-x50.wai.receipts new file mode 100644 index 000000000..3f1b2d6e6 --- /dev/null +++ b/wai/http-conformance/cases/receipts/O9c-x50.wai.receipts @@ -0,0 +1 @@ +{"claims":[],"delivery":[{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}},{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}}],"kind":"wai-receipt-index","object":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133"} \ No newline at end of file diff --git a/wai/http-conformance/cases/receipts/deep-64.wai.receipts b/wai/http-conformance/cases/receipts/deep-64.wai.receipts new file mode 100644 index 000000000..b42ee26dd --- /dev/null +++ b/wai/http-conformance/cases/receipts/deep-64.wai.receipts @@ -0,0 +1 @@ +{"claims":[],"delivery":[{"object_receipt":{"content_hash":"f6fa1ca70e69bee70aa2bd36620b3e93a50ca2e110e1fbb91f3c6747f5deb860","group_id":64,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":5},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"f6fa1ca70e69bee70aa2bd36620b3e93a50ca2e110e1fbb91f3c6747f5deb860","group_receipt":"63379f1324fcb7c7d65bec7ea0d81f585fad641c0881076dae2384c0dab77350","joules_micro":0}}],"kind":"wai-receipt-index","object":"f6fa1ca70e69bee70aa2bd36620b3e93a50ca2e110e1fbb91f3c6747f5deb860"} \ No newline at end of file diff --git a/wai/http-conformance/cases/receipts/deep-65.wai.receipts b/wai/http-conformance/cases/receipts/deep-65.wai.receipts new file mode 100644 index 000000000..e81d4918e --- /dev/null +++ b/wai/http-conformance/cases/receipts/deep-65.wai.receipts @@ -0,0 +1 @@ +{"claims":[],"delivery":[{"object_receipt":{"content_hash":"002ebec0ca68dc089695fcc8b1375c6964fd8f25d07f79c1d3e568a684770b3b","group_id":65,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":5},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"002ebec0ca68dc089695fcc8b1375c6964fd8f25d07f79c1d3e568a684770b3b","group_receipt":"4fddb12f5ebd443ad749945c70453e80223fa9f89c642fbc14bb736a4fcc0c19","joules_micro":0}}],"kind":"wai-receipt-index","object":"002ebec0ca68dc089695fcc8b1375c6964fd8f25d07f79c1d3e568a684770b3b"} \ No newline at end of file diff --git a/wai/http-conformance/falsify/ancestor-as-object.patch b/wai/http-conformance/falsify/ancestor-as-object.patch new file mode 100644 index 000000000..5661739a6 --- /dev/null +++ b/wai/http-conformance/falsify/ancestor-as-object.patch @@ -0,0 +1,15 @@ +# must fail: receipts_claims_O3 receipts_ancestor_no_publisher receipts_ancestor_any_object +# a soft binding of another object reported as attesting the object in hand (receipts::check_claim) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index f2bdba57..023cf830 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -674,7 +674,7 @@ pub fn check_claim( + let attests = match claim.subject_type { + SubjectType::Object if claim.subject == *cid => Attests::Object, + SubjectType::Receipt if groups.contains(&claim.subject) => Attests::Receipt, +- SubjectType::Object if claim.class() == ClaimClass::SoftBinding && publisher(&claim.subject).is_some() => Attests::Ancestor, ++ SubjectType::Object if claim.class() == ClaimClass::SoftBinding => Attests::Object, + _ => return Err(ReceiptRefusal::NotAboutObject), + }; + claim.verify().map_err(ReceiptRefusal::Claim)?; diff --git a/wai/http-conformance/falsify/ancestor-unrecovered.patch b/wai/http-conformance/falsify/ancestor-unrecovered.patch new file mode 100644 index 000000000..7ae753c23 --- /dev/null +++ b/wai/http-conformance/falsify/ancestor-unrecovered.patch @@ -0,0 +1,26 @@ +# must fail: receipts_ancestor_value_mismatch receipts_ancestor_no_matcher receipts_ancestor_no_publisher +# a soft binding of another object reported as attesting the ancestor without §3.2's recovery (receipts::check_claim) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index f2bdba57..d0e7543b 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -674,7 +674,7 @@ pub fn check_claim( + let attests = match claim.subject_type { + SubjectType::Object if claim.subject == *cid => Attests::Object, + SubjectType::Receipt if groups.contains(&claim.subject) => Attests::Receipt, +- SubjectType::Object if claim.class() == ClaimClass::SoftBinding && publisher(&claim.subject).is_some() => Attests::Ancestor, ++ SubjectType::Object if claim.class() == ClaimClass::SoftBinding => Attests::Ancestor, + _ => return Err(ReceiptRefusal::NotAboutObject), + }; + claim.verify().map_err(ReceiptRefusal::Claim)?; +@@ -696,9 +696,7 @@ pub fn check_claim( + let no_matchers: [&dyn SoftBindingMatcher; 0] = []; + let matchers = recovery.map_or(&no_matchers[..], |r| r.matchers); + if attests == Attests::Ancestor { +- let r = recovery.expect("a publisher key came from the recovery"); +- let key = publisher(&claim.subject).expect("checked above"); +- crate::claim_receipt::recover(&claim, &key, r.observed_value, Some(*cid), matchers).map_err(ReceiptRefusal::Recovery)?; ++ let _ = (recovery, publisher, cid); + } else { + step6(&claim, passed, fetch, matchers)?; + } diff --git a/wai/http-conformance/falsify/anchor-follows-redirect.patch b/wai/http-conformance/falsify/anchor-follows-redirect.patch new file mode 100644 index 000000000..62e4f78b5 --- /dev/null +++ b/wai/http-conformance/falsify/anchor-follows-redirect.patch @@ -0,0 +1,15 @@ +# must fail: key_log_anchor_redirect +# a key log anchored from the URL a redirect led to (keys::anchor_retrieved) +diff --git a/wai/wai-rs/src/http_binding/keys.rs b/wai/wai-rs/src/http_binding/keys.rs +index ad1572bc..042208df 100644 +--- a/wai/wai-rs/src/http_binding/keys.rs ++++ b/wai/wai-rs/src/http_binding/keys.rs +@@ -140,7 +140,7 @@ pub fn anchor_retrieved( + if retrieval.requested != location { + return Err(KeysRefusal::NotFromOrigin("the request was not for the origin's key-log location")); + } +- if retrieval.answered != location { ++ if retrieval.answered.is_empty() { + return Err(KeysRefusal::NotFromOrigin("the answer came from another URL: a redirect was followed")); + } + if retrieval.status != 200 { diff --git a/wai/http-conformance/falsify/bytes-read-whole.patch b/wai/http-conformance/falsify/bytes-read-whole.patch new file mode 100644 index 000000000..eb343f5ce --- /dev/null +++ b/wai/http-conformance/falsify/bytes-read-whole.patch @@ -0,0 +1,22 @@ +# must fail: receipts_fetch_bytes_exhausted +# each receipt file read to its cap plus one byte whatever remains of the index's bytes, the budget counted after (receipts::ReceiptFetcher::get) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index bdec9436..9d68b900 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -300,13 +300,13 @@ impl<'f> ReceiptFetcher<'f> { + return Err(ReceiptRefusal::BytesExhausted); + } + self.fetched += 1; +- let limit = remaining.min(MAX_RECEIPT_FILE_BYTES + 1); ++ let limit = MAX_RECEIPT_FILE_BYTES + 1; + let served = match (self.fetch)(h, limit) { + None => Served::Nothing, + Some(mut b) => { + b.truncate(limit); + self.bytes += b.len(); +- if b.len() == remaining { Served::PastBytes } else { Served::Bytes(b) } ++ if b.len() > remaining { Served::PastBytes } else { Served::Bytes(b) } + } + }; + self.memo.insert(*h, served); diff --git a/wai/http-conformance/falsify/bytes-unchecked.patch b/wai/http-conformance/falsify/bytes-unchecked.patch new file mode 100644 index 000000000..4bc18a293 --- /dev/null +++ b/wai/http-conformance/falsify/bytes-unchecked.patch @@ -0,0 +1,17 @@ +# must fail: receipts_fetch_bytes_exhausted +# the bytes read for one index unbounded (receipts::ReceiptFetcher::get) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index bdec9436..6df45459 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -296,9 +296,7 @@ impl<'f> ReceiptFetcher<'f> { + return Err(ReceiptRefusal::BudgetExhausted); + } + let remaining = MAX_INDEX_FETCH_BYTES.saturating_sub(self.bytes); +- if remaining == 0 { +- return Err(ReceiptRefusal::BytesExhausted); +- } ++ let remaining = remaining + usize::MAX / 2; + self.fetched += 1; + let limit = remaining.min(MAX_RECEIPT_FILE_BYTES + 1); + let served = match (self.fetch)(h, limit) { diff --git a/wai/http-conformance/falsify/cached-rollback-unchecked.patch b/wai/http-conformance/falsify/cached-rollback-unchecked.patch new file mode 100644 index 000000000..fe07b0e91 --- /dev/null +++ b/wai/http-conformance/falsify/cached-rollback-unchecked.patch @@ -0,0 +1,15 @@ +# must fail: key_log_cached_rolled_back +# an older copy of a key log through a cache taken without report (keys::hold_cached_revisions) +diff --git a/wai/wai-rs/src/http_binding/keys.rs b/wai/wai-rs/src/http_binding/keys.rs +index ad1572bc..5a1555c2 100644 +--- a/wai/wai-rs/src/http_binding/keys.rs ++++ b/wai/wai-rs/src/http_binding/keys.rs +@@ -179,7 +179,7 @@ pub fn hold_cached_revisions(origin: &str, body: &[u8], anchor: &Anchor, held: & + trusted.push(p); + } + let latest = revs.last().expect("a log is non-empty").revision_hash(); +- if latest != anchor.revision && trusted.contains(&latest) { ++ if latest != anchor.revision && trusted.is_empty() { + return Err(KeysRefusal::Log(Reason::KeyLogRolledBack)); + } + let mut have: Vec<[u8; 32]> = held.iter().map(Revision::revision_hash).collect(); diff --git a/wai/http-conformance/falsify/claim-parent-unfetched.patch b/wai/http-conformance/falsify/claim-parent-unfetched.patch new file mode 100644 index 000000000..361a5c5df --- /dev/null +++ b/wai/http-conformance/falsify/claim-parent-unfetched.patch @@ -0,0 +1,15 @@ +# must fail: receipts_claim_parent_not_served +# a claim's parent not served taken as linked (receipts::check_claim) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index f2bdba57..be265488 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -685,7 +685,7 @@ pub fn check_claim( + let pb = match fetch.get(&p) { + Ok(b) => b, + Err(ReceiptRefusal::TooLarge) => return Err(stop(1, "receipt_too_large")), +- Err(r) => return Err(stop(0, r.code())), ++ Err(_) => return Ok(ClaimVerdict { claim, attests, parent: Some(p) }), + }; + let prev = read_claim_file(&p, &pb).map_err(|r| stop(1, r.code()))?; + prev.verify().map_err(|r| stop(1, r.reason.label()))?; diff --git a/wai/http-conformance/falsify/claim-through-any-receipt.patch b/wai/http-conformance/falsify/claim-through-any-receipt.patch new file mode 100644 index 000000000..7906326b4 --- /dev/null +++ b/wai/http-conformance/falsify/claim-through-any-receipt.patch @@ -0,0 +1,15 @@ +# must fail: receipts_claim_through_foreign_item +# a claim about a receipt attached through a group no item that passed names (receipts::check_claim) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index 83cd7ccb..42574240 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -732,7 +732,7 @@ pub fn check_claim( + }; + let attests = match claim.subject_type { + SubjectType::Object if claim.subject == *cid => Attests::Object, +- SubjectType::Receipt if groups.contains(&claim.subject) => Attests::Receipt, ++ SubjectType::Receipt if groups.contains(&claim.subject) || !groups.is_empty() => Attests::Receipt, + SubjectType::Object if claim.class() == ClaimClass::SoftBinding && publisher(&claim.subject).is_some() => Attests::Ancestor, + _ => return Err(ReceiptRefusal::NotAboutObject), + }; diff --git a/wai/http-conformance/falsify/decision-unchecked.patch b/wai/http-conformance/falsify/decision-unchecked.patch new file mode 100644 index 000000000..d664d4122 --- /dev/null +++ b/wai/http-conformance/falsify/decision-unchecked.patch @@ -0,0 +1,17 @@ +# must fail: receipts_lawful_basis receipts_lawful_basis_declaration_not_served receipts_lawful_basis_declaration_renamed +# a lawful-basis decision accepted without its declaration (receipts::step6) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index 83cd7ccb..d8cdcc81 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -654,8 +654,8 @@ fn step6( + } + // §2.4.6: against the declaration it names, fetched by its link. + ClaimBody::LawfulBasis(LawfulBasisClaim::Decision { declaration, .. }) => { +- let d = declaration_of(declaration, fetch)?; +- claim.check_decision_against(&d).map_err(c) ++ let _ = (declaration, &fetch); ++ Ok(()) + } + // §2.6.3: against its declaration together with the publisher key of a verified + // delivery receipt for the declaration's subject. diff --git a/wai/http-conformance/falsify/declaration-unnamed.patch b/wai/http-conformance/falsify/declaration-unnamed.patch new file mode 100644 index 000000000..6b0bf1515 --- /dev/null +++ b/wai/http-conformance/falsify/declaration-unnamed.patch @@ -0,0 +1,16 @@ +# must fail: receipts_lawful_basis_declaration_renamed +# a declaration fetched by its link read without §8.1's name check (receipts::declaration_of) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index 83cd7ccb..a9187306 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -688,7 +688,8 @@ fn declaration_of(link: &[u8; 32], fetch: &mut ReceiptFetcher<'_>) -> Result return Err(ReceiptRefusal::ContextNotHeld), + Err(r) => return Err(r), + }; +- read_claim_file(link, &bytes) ++ let _ = link; ++ ClaimReceipt::from_json(std::str::from_utf8(&bytes).unwrap_or("")).map_err(ReceiptRefusal::Claim) + } + + /// §8.2's check of one claim the index of the object `cid` lists by its link hash diff --git a/wai/http-conformance/falsify/energy-context-assumed.patch b/wai/http-conformance/falsify/energy-context-assumed.patch new file mode 100644 index 000000000..e9cd3a60c --- /dev/null +++ b/wai/http-conformance/falsify/energy-context-assumed.patch @@ -0,0 +1,15 @@ +# must fail: receipts_energy_not_delivered +# an energy claim accepted with no object delivered to check it against (receipts::step6) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index f2bdba57..33b2bc57 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -572,7 +572,7 @@ fn step6( + let class = v.object.label.as_ref().and_then(|l| l.class()); + claim.check_energy_against_labelled_object(&v.object.content_hash, v.object.joules_micro, class).map_err(c)?; + } +- if !any || e.oer_receipt.is_some() { ++ if (!any && any) || e.oer_receipt.is_some() { + return Err(ReceiptRefusal::ContextNotHeld); + } + Ok(()) diff --git a/wai/http-conformance/falsify/established-on-refusal.patch b/wai/http-conformance/falsify/established-on-refusal.patch new file mode 100644 index 000000000..5a3b61491 --- /dev/null +++ b/wai/http-conformance/falsify/established-on-refusal.patch @@ -0,0 +1,15 @@ +# must fail: receipts_fetch_refused_holds_nothing +# a fetch that was refused establishing its cid (session::FetchSession::established) +diff --git a/wai/wai-rs/src/http_binding/session.rs b/wai/wai-rs/src/http_binding/session.rs +index 2381b3fb..549fe94c 100644 +--- a/wai/wai-rs/src/http_binding/session.rs ++++ b/wai/wai-rs/src/http_binding/session.rs +@@ -383,7 +383,7 @@ impl FetchSession { + /// for a layer whose request was abandoned, of which nothing was received. Only a + /// session builds the second kind, so a cid nothing verified never stands in for one. + pub fn established(&self) -> Option { +- let f = self.done()?; ++ let Some(f) = self.done() else { return Some(crate::jwp_hash::EstablishedHash::by_head_proof(self.cid)) }; + match f.verified_by { + VerifiedBy::WholeObject => f.envelope.as_deref().map(crate::jwp_hash::EstablishedHash::of_envelope), + VerifiedBy::HeadProof => Some(crate::jwp_hash::EstablishedHash::by_head_proof(self.cid)), diff --git a/wai/http-conformance/falsify/fetch-budget-unchecked.patch b/wai/http-conformance/falsify/fetch-budget-unchecked.patch new file mode 100644 index 000000000..c2baedcf9 --- /dev/null +++ b/wai/http-conformance/falsify/fetch-budget-unchecked.patch @@ -0,0 +1,15 @@ +# must fail: receipts_fetch_budget +# an index's fetches unbounded (receipts::ReceiptFetcher::get) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index bdec9436..3cb67b43 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -292,7 +292,7 @@ impl<'f> ReceiptFetcher<'f> { + /// there is not read, and once they are spent no further file is fetched. + pub fn get(&mut self, h: &[u8; 32]) -> Result, ReceiptRefusal> { + if !self.memo.contains_key(h) { +- if self.fetched >= self.budget { ++ if self.fetched >= usize::MAX - self.budget { + return Err(ReceiptRefusal::BudgetExhausted); + } + let remaining = MAX_INDEX_FETCH_BYTES.saturating_sub(self.bytes); diff --git a/wai/http-conformance/falsify/fetch-unmemoised.patch b/wai/http-conformance/falsify/fetch-unmemoised.patch new file mode 100644 index 000000000..a175d532d --- /dev/null +++ b/wai/http-conformance/falsify/fetch-unmemoised.patch @@ -0,0 +1,15 @@ +# must fail: receipts_items_fetch_each_name_once +# each name fetched again for every item and walk that names it (receipts::ReceiptFetcher::get) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index bdec9436..f8e7ba27 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -291,7 +291,7 @@ impl<'f> ReceiptFetcher<'f> { + /// the bytes is refused (`receipt_fetch_bytes_exhausted`), since whether it ends + /// there is not read, and once they are spent no further file is fetched. + pub fn get(&mut self, h: &[u8; 32]) -> Result, ReceiptRefusal> { +- if !self.memo.contains_key(h) { ++ if !self.memo.contains_key(h) || self.fetched < usize::MAX { + if self.fetched >= self.budget { + return Err(ReceiptRefusal::BudgetExhausted); + } diff --git a/wai/http-conformance/falsify/figure-not-held-accepted.patch b/wai/http-conformance/falsify/figure-not-held-accepted.patch new file mode 100644 index 000000000..798e21be4 --- /dev/null +++ b/wai/http-conformance/falsify/figure-not-held-accepted.patch @@ -0,0 +1,15 @@ +# must fail: receipts_measurement_figure_not_held +# a measurement claim whose figure the client does not hold accepted (receipts::step6) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index f2bdba57..8b6dcbb0 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -590,7 +590,7 @@ fn step6( + }); + match view { + Some(view) => claim.check_figure_against(Some(&view)).map_err(c), +- None if claim.needs_figure() => Err(c(Rejection { step: 6, reason: ClaimReason::FigureNotHeld })), ++ None if claim.needs_figure() => Ok(()), + None => Err(ReceiptRefusal::ContextNotHeld), + } + } diff --git a/wai/http-conformance/falsify/file-cap-unchecked.patch b/wai/http-conformance/falsify/file-cap-unchecked.patch new file mode 100644 index 000000000..12991ddeb --- /dev/null +++ b/wai/http-conformance/falsify/file-cap-unchecked.patch @@ -0,0 +1,15 @@ +# must fail: receipts_file_too_large receipts_parent_too_large +# a receipt file over 1 MiB read (receipts::ReceiptFetcher::get) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index 83cd7ccb..822b9975 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -307,7 +307,7 @@ impl<'f> ReceiptFetcher<'f> { + match &self.memo[h] { + Served::Nothing => Err(ReceiptRefusal::NotServed), + Served::PastBytes => Err(ReceiptRefusal::BytesExhausted), +- Served::Bytes(b) if b.len() > MAX_RECEIPT_FILE_BYTES => Err(ReceiptRefusal::TooLarge), ++ Served::Bytes(b) if b.len() > usize::MAX - MAX_RECEIPT_FILE_BYTES => Err(ReceiptRefusal::TooLarge), + Served::Bytes(b) => Ok(b.clone()), + } + } diff --git a/wai/http-conformance/falsify/first-publisher.patch b/wai/http-conformance/falsify/first-publisher.patch new file mode 100644 index 000000000..673d9351f --- /dev/null +++ b/wai/http-conformance/falsify/first-publisher.patch @@ -0,0 +1,15 @@ +# must fail: receipts_ancestor_other_publisher_first +# a soft binding's recovery under the first publisher key held, not the one that signed it (receipts::check_claim) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index 83cd7ccb..c7f1b0d5 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -728,7 +728,7 @@ pub fn check_claim( + let r = recovery.filter(|_| matches!(held, ObjectHeld::Object(_)))?; + let mut keys = r.publishers.iter().filter(|(h, _)| h == subject).map(|(_, k)| *k); + let first = keys.next()?; +- Some(if first == claim.signer_pubkey { first } else { keys.find(|k| *k == claim.signer_pubkey).unwrap_or(first) }) ++ Some(if first == claim.signer_pubkey { first } else { keys.find(|k| *k == [0; 32]).unwrap_or(first) }) + }; + let attests = match claim.subject_type { + SubjectType::Object if claim.subject == *cid => Attests::Object, diff --git a/wai/http-conformance/falsify/gap-coordinates-unchecked.patch b/wai/http-conformance/falsify/gap-coordinates-unchecked.patch new file mode 100644 index 000000000..48b976ff0 --- /dev/null +++ b/wai/http-conformance/falsify/gap-coordinates-unchecked.patch @@ -0,0 +1,15 @@ +# must fail: receipts_delivery_gap +# a delivery-gap claim checked against an item at other coordinates (receipts::step6) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index f2bdba57..d7049807 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -597,7 +597,7 @@ fn step6( + // §2.4.11: against the object receipt at its coordinates, verified by inclusion + // at least. + ClaimBody::DeliveryGap(g) => { +- let at = passed.iter().find(|v| { ++ let at = passed.iter().rev().find(|v| v.object.object_id < u64::MAX || { + (v.object.track_alias, v.object.group_id, v.object.object_id) == (g.track_alias, g.group_id, g.object_id) + }); + let v = at.ok_or(ReceiptRefusal::ContextNotHeld)?; diff --git a/wai/http-conformance/falsify/grants-module-unchecked.patch b/wai/http-conformance/falsify/grants-module-unchecked.patch new file mode 100644 index 000000000..b04b81e98 --- /dev/null +++ b/wai/http-conformance/falsify/grants-module-unchecked.patch @@ -0,0 +1,15 @@ +# must fail: grants_file_other_module +# a grants file for another module read (keys::read_grants_file) +diff --git a/wai/wai-rs/src/http_binding/keys.rs b/wai/wai-rs/src/http_binding/keys.rs +index ad1572bc..1466070b 100644 +--- a/wai/wai-rs/src/http_binding/keys.rs ++++ b/wai/wai-rs/src/http_binding/keys.rs +@@ -213,7 +213,7 @@ pub fn read_grants_file(module: &[u8; 32], bytes: &[u8]) -> Result, + if m["kind"].as_str() != Some(GRANTS_KIND) { + return Err(bad("kind is not wai-module-grants")); + } +- if m["module"].as_str().and_then(super::unhex32) != Some(*module) { ++ if m["module"].as_str().and_then(super::unhex32).is_none() { + return Err(bad("module is not the digest the file is fetched for")); + } + let grants = m["grants"].as_array().ok_or(bad("grants is not an array"))?; diff --git a/wai/http-conformance/falsify/index-claims-uncapped.patch b/wai/http-conformance/falsify/index-claims-uncapped.patch new file mode 100644 index 000000000..d3108c31f --- /dev/null +++ b/wai/http-conformance/falsify/index-claims-uncapped.patch @@ -0,0 +1,15 @@ +# must fail: receipts_index_257_claims +# an index listing more than 256 claims read (receipts::ReceiptIndex::from_bytes) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index f2bdba57..9a891749 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -318,7 +318,7 @@ impl ReceiptIndex { + if delivery.len() > MAX_INDEX_ITEMS { + return Err(ReceiptRefusal::IndexTooLarge("more delivery items than a client reads")); + } +- if claims.len() > MAX_INDEX_CLAIMS { ++ if claims.len() > usize::MAX - MAX_INDEX_CLAIMS { + return Err(ReceiptRefusal::IndexTooLarge("more claims than a client reads")); + } + Ok(ReceiptIndex { diff --git a/wai/http-conformance/falsify/index-items-uncapped.patch b/wai/http-conformance/falsify/index-items-uncapped.patch new file mode 100644 index 000000000..7577701c7 --- /dev/null +++ b/wai/http-conformance/falsify/index-items-uncapped.patch @@ -0,0 +1,15 @@ +# must fail: receipts_index_257_items +# an index listing more than 256 delivery items read (receipts::ReceiptIndex::from_bytes) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index f2bdba57..13b89881 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -315,7 +315,7 @@ impl ReceiptIndex { + let object = m["object"].as_str().and_then(unhex32).ok_or(bad("object is not a content hash"))?; + let delivery = m["delivery"].as_array().ok_or(bad("delivery is not an array"))?; + let claims = m["claims"].as_array().ok_or(bad("claims is not an array"))?; +- if delivery.len() > MAX_INDEX_ITEMS { ++ if delivery.len() > usize::MAX - MAX_INDEX_ITEMS { + return Err(ReceiptRefusal::IndexTooLarge("more delivery items than a client reads")); + } + if claims.len() > MAX_INDEX_CLAIMS { diff --git a/wai/http-conformance/falsify/index-member-added.patch b/wai/http-conformance/falsify/index-member-added.patch new file mode 100644 index 000000000..441d266a7 --- /dev/null +++ b/wai/http-conformance/falsify/index-member-added.patch @@ -0,0 +1,15 @@ +# must fail: receipts_index_member_added +# an index with a member it does not define read (receipts::ReceiptIndex::from_bytes) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index 95b77f2f..26bf86c7 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -189,7 +189,7 @@ impl ReceiptIndex { + let v = crate::claim_receipt::parse_strict(text).map_err(|_| bad("not JSON as a receipt is read"))?; + let m = v.as_object().ok_or(bad("not an object"))?; + const MEMBERS: [&str; 4] = ["kind", "object", "delivery", "claims"]; +- if m.len() != MEMBERS.len() || MEMBERS.iter().any(|k| !m.contains_key(*k)) { ++ if MEMBERS.iter().any(|k| !m.contains_key(*k)) { + return Err(bad("the members are not kind, object, delivery and claims")); + } + if m["kind"].as_str() != Some(INDEX_KIND) { diff --git a/wai/http-conformance/falsify/index-size-unchecked.patch b/wai/http-conformance/falsify/index-size-unchecked.patch new file mode 100644 index 000000000..3f2ed1fbb --- /dev/null +++ b/wai/http-conformance/falsify/index-size-unchecked.patch @@ -0,0 +1,15 @@ +# must fail: receipts_index_too_large +# an index over 4 MiB read (receipts::ReceiptIndex::from_bytes) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index f2bdba57..0562284b 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -298,7 +298,7 @@ impl ReceiptIndex { + /// reading's reason). Each claim entry is a link hash (`receipt_index_malformed`). An + /// item that does not read is kept as its refusal. + pub fn from_bytes(bytes: &[u8]) -> Result { +- if bytes.len() > MAX_INDEX_BYTES { ++ if bytes.len() > usize::MAX - MAX_INDEX_BYTES { + return Err(ReceiptRefusal::IndexTooLarge("more bytes than a client reads")); + } + let bad = ReceiptRefusal::IndexMalformed; diff --git a/wai/http-conformance/falsify/key-log-uncapped.patch b/wai/http-conformance/falsify/key-log-uncapped.patch new file mode 100644 index 000000000..0e38ed3eb --- /dev/null +++ b/wai/http-conformance/falsify/key-log-uncapped.patch @@ -0,0 +1,26 @@ +# must fail: key_log_anchor_too_large key_log_cached_too_large +# a key log past its cap read, anchored or cached (keys::anchor_retrieved, keys::hold_cached_revisions) +diff --git a/wai/wai-rs/src/http_binding/keys.rs b/wai/wai-rs/src/http_binding/keys.rs +index 68832c79..79700d2c 100644 +--- a/wai/wai-rs/src/http_binding/keys.rs ++++ b/wai/wai-rs/src/http_binding/keys.rs +@@ -158,9 +158,6 @@ pub fn anchor_retrieved( + if retrieval.status != 200 { + return Err(KeysRefusal::NotFromOrigin("the answer is not a 200")); + } +- if body.len() > MAX_KEY_LOG_BYTES { +- return Err(KeysRefusal::TooLarge); +- } + let text = std::str::from_utf8(body).map_err(|_| KeysRefusal::Log(Reason::KeyLogMalformed))?; + match anchor_log(origin, text, AnchorPath::Fetched { acquired_at: acquired_at.into() }, current) { + Anchoring::Anchored(a) => Ok(a), +@@ -182,9 +179,6 @@ pub fn hold_cached_revisions(origin: &str, body: &[u8], anchor: &Anchor, held: & + if anchor.origin != origin { + return Err(KeysRefusal::Log(Reason::OriginNotAnchored)); + } +- if body.len() > MAX_KEY_LOG_BYTES { +- return Err(KeysRefusal::TooLarge); +- } + let text = std::str::from_utf8(body).map_err(|_| KeysRefusal::Log(Reason::KeyLogMalformed))?; + let revs = read_log(origin, text).map_err(KeysRefusal::Log)?; + let by: BTreeMap<[u8; 32], &Revision> = diff --git a/wai/http-conformance/falsify/kind-type-malformed.patch b/wai/http-conformance/falsify/kind-type-malformed.patch new file mode 100644 index 000000000..f9b5ce1e9 --- /dev/null +++ b/wai/http-conformance/falsify/kind-type-malformed.patch @@ -0,0 +1,14 @@ +# must fail: receipt_file_kind_null +# a group receipt whose kind is not a string read as malformed (jwp_delivery::GroupReceipt::from_json) +diff --git a/wai/wai-rs/src/jwp_delivery.rs b/wai/wai-rs/src/jwp_delivery.rs +index 956f7b8b..db8d5373 100644 +--- a/wai/wai-rs/src/jwp_delivery.rs ++++ b/wai/wai-rs/src/jwp_delivery.rs +@@ -527,6 +527,7 @@ impl GroupReceipt { + match m.get("kind") { + None => return Err(bad), + Some(k) if k.as_str() == Some("group") => {} ++ Some(k) if !k.is_string() => return Err(bad), + Some(_) => return Err(DeliveryError::at(1, Reason::WrongKind)), + } + if GROUP_MEMBERS.iter().any(|k| !m.contains_key(*k)) diff --git a/wai/http-conformance/falsify/legacy-group-read.patch b/wai/http-conformance/falsify/legacy-group-read.patch new file mode 100644 index 000000000..9038000e8 --- /dev/null +++ b/wai/http-conformance/falsify/legacy-group-read.patch @@ -0,0 +1,15 @@ +# must fail: receipts_legacy_group receipt_file_legacy_group +# a legacy group file read, its name not covering its parent (receipts::read_group_file) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index f2bdba57..263dc3c9 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -193,7 +193,7 @@ pub fn read_group_file(name: &[u8; 32], bytes: &[u8]) -> Result Some(*e), +- ObjectHeld::Nothing => None, ++ ObjectHeld::Nothing => Some(EstablishedHash::by_head_proof(*cid)), + }; + let evidence = chains + .verify_admitted_object(&group, &item.object_receipt, established.as_ref()) diff --git a/wai/http-conformance/falsify/operating-point-unchecked.patch b/wai/http-conformance/falsify/operating-point-unchecked.patch new file mode 100644 index 000000000..c52b00d53 --- /dev/null +++ b/wai/http-conformance/falsify/operating-point-unchecked.patch @@ -0,0 +1,16 @@ +# must fail: receipts_operating_point receipts_operating_point_not_held +# an operating-point claim accepted without its figure (receipts::step6) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index 83cd7ccb..865e7bb4 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -628,7 +628,8 @@ fn step6( + } + // §2.4.8, §2.4.9: against the receipt carrying the figure — for `moq-jwp:object`, + // an object receipt accepted under the group the claim names. +- ClaimBody::Measurement(_) | ClaimBody::OperatingPoint(_) => { ++ ClaimBody::OperatingPoint(_) => Ok(()), ++ ClaimBody::Measurement(_) => { + let group = match &claim.body { + ClaimBody::Measurement(m) => m.figure.group, + ClaimBody::OperatingPoint(o) => o.figure.group, diff --git a/wai/http-conformance/falsify/rcpt-name-unchecked.patch b/wai/http-conformance/falsify/rcpt-name-unchecked.patch new file mode 100644 index 000000000..8b45d5744 --- /dev/null +++ b/wai/http-conformance/falsify/rcpt-name-unchecked.patch @@ -0,0 +1,15 @@ +# must fail: receipt_file_name_mismatch receipts_group_renamed +# a group receipt file read under another name (receipts::read_group_file) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index f2bdba57..062637ec 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -196,7 +196,7 @@ pub fn read_group_file(name: &[u8; 32], bytes: &[u8]) -> Result { +- if passed.is_empty() { +- return Err(ReceiptRefusal::ContextNotHeld); ++ if passed.len() < usize::MAX { ++ return Ok(()); + } + let d = declaration_of(&r.declaration, fetch)?; + let publisher = passed diff --git a/wai/http-conformance/falsify/run.sh b/wai/http-conformance/falsify/run.sh index df63f4073..ff86604f9 100755 --- a/wai/http-conformance/falsify/run.sh +++ b/wai/http-conformance/falsify/run.sh @@ -1,74 +1,257 @@ #!/usr/bin/env bash -# Falsify the HTTP corpus tool: each patch here breaks one function of wai-rs. For each, -# in a fresh worktree of HEAD: apply it, require the build to succeed, and require -# `wai_http_vectors verify` to exit non-zero and to name every case the patch's first -# line lists. Then the Python side: each `http_verify.py --mutate` must fail, and the -# differential must verify every proof and refuse exactly the flipped ones. +# Falsify the HTTP corpus tool and its independent verifier. # -# bash wai/http-conformance/falsify/run.sh [work-dir [patch-name…]] +# Patches: each patch here breaks one function of wai-rs. In one worktree of HEAD, +# built once, each patch is applied, the tree rebuilt incrementally, and +# `wai_http_vectors verify` must exit non-zero naming every case the patch's first line +# lists; then the patch is reversed and the tree must be clean again. The builds use +# the `falsify` profile (wai-rs/Cargo.toml): overflow checks and debug assertions on, as +# in the tests, so a patch is judged by the code's own checks, and incremental, so each +# patch rebuilds only what it touched. The unpatched tree must pass first. # -# With patch names, only those patches run (and nothing else). -# Run from the repository root. CARGO_TARGET_DIR, if set, is shared by the builds, which -# need the features http and video_manifest (the playlist receipts). +# Mutations: each `http_verify.py --mutate NAME` must exit non-zero naming, as FAIL lines, +# every case mutations.txt lists for it; a crash, or a non-zero exit that names none, is +# a failure of the harness. +# +# The differential: wai-rs writes random envelopes' covered prefixes and head proofs, +# http_verify.py verifies every one, and refuses exactly the ten it changes one +# chaining-value byte in. +# +# bash wai/http-conformance/falsify/run.sh [work-dir] [--patches I/N | --mutations I/N | +# --differential | --check-shards 'patches I/N …' 'mutations I/N …' | patch-name…] +# +# With no selector, all three run. `--patches I/N` runs the patches whose index in the +# sorted list, mod N, is I; `--mutations I/N` the same over the mutations. Each shard +# prints every patch or mutation it ran, and fails unless what it ran is exactly what was +# assigned to it. `--check-shards` takes the shards a workflow declares and fails unless +# their totals agree, their indices are 0..N-1, and their assignments cover every patch +# and every mutation exactly once. With patch names, only those patches run. +# +# Run from the repository root. CARGO_TARGET_DIR, if set, holds the builds, which need +# the features http, video_manifest (the playlist receipts) and key_discovery (the +# receipts, keys, grants and sets of §8.1–§8.5). The receipt and key cases of +# http_verify.py need the `cryptography` package. set -uo pipefail ROOT="$(git rev-parse --show-toplevel)" -WORK="${1:-${RUNNER_TEMP:-/tmp}/wai-http-falsify}" -shift || true -ONLY="$*" +FALSIFY="$ROOT/wai/http-conformance/falsify" +WORK="${RUNNER_TEMP:-/tmp}/wai-http-falsify" +if [ $# -gt 0 ] && [ "${1#--}" = "$1" ] && [ ! -f "$FALSIFY/$1.patch" ]; then + WORK="$1" + shift +fi mkdir -p "$WORK" +FEATURES=http,video_manifest,key_discovery fail=0 -for patch in "$ROOT"/wai/http-conformance/falsify/*.patch; do - name="$(basename "$patch" .patch)" - if [ -n "$ONLY" ] && ! printf ' %s ' $ONLY | grep -q " $name "; then continue; fi - wt="$WORK/f-$name" + +# The mutations and the cases each must fail, from mutations.txt (one a line: the name, +# then the cases). +MUTATIONS=() +while read -r m _; do + case "$m" in ""|"#"*) continue ;; esac + MUTATIONS+=("$m") +done < "$FALSIFY/mutations.txt" + +# The cases the mutation $1 must fail, as mutations.txt lists them. +mutation_cases() { + awk -v m="$1" '$1 == m { $1 = ""; print; exit }' "$FALSIFY/mutations.txt" +} + +# Every patch, by name, in sorted order: the list shards index into. +all_patches() { + local p + for p in "$FALSIFY"/*.patch; do basename "$p" .patch; done | LC_ALL=C sort +} + +# The members of a list (one per line on stdin) whose index mod N is I. +shard_of() { + awk -v i="$1" -v n="$2" '(NR - 1) % n == i' +} + +# Parse I/N into the globals SI and SN, refusing anything else. +parse_shard() { + if ! [[ "$1" =~ ^([0-9]+)/([0-9]+)$ ]] || [ "${BASH_REMATCH[2]}" -eq 0 ] || [ "${BASH_REMATCH[1]}" -ge "${BASH_REMATCH[2]}" ]; then + echo "::error::a shard is I/N with 0 <= I < N, not '$1'" + exit 2 + fi + SI="${BASH_REMATCH[1]}" + SN="${BASH_REMATCH[2]}" +} + +# The lines of stdin into the array NAMES (bash 3.2 has no mapfile). +read_names() { + NAMES=() + local line + while IFS= read -r line; do [ -n "$line" ] && NAMES+=("$line"); done +} + +# The patches stage over the names on stdin, in one worktree of HEAD. +run_patches() { + local names wt bin name patch must rc missing ran=() t0 t1 + read_names + names=("${NAMES[@]}") + wt="$WORK/tree" + git -C "$ROOT" worktree remove --force "$wt" > /dev/null 2>&1 rm -rf "$wt" git -C "$ROOT" worktree add --detach --quiet "$wt" HEAD - must="$(head -1 "$patch" | sed 's/^# must fail: //')" - if ! git -C "$wt" apply "$patch"; then - echo "::error::$name: the patch does not apply"; fail=1 - elif ! (cd "$wt/wai/wai-rs" && cargo build --release --no-default-features --features http,video_manifest --bin wai_http_vectors) > "$WORK/$name.build.log" 2>&1; then - echo "::error::$name: the patched tree does not build"; tail -20 "$WORK/$name.build.log"; fail=1 - else - bin="${CARGO_TARGET_DIR:-$wt/wai/wai-rs/target}/release/wai_http_vectors" - "$bin" verify "$wt/wai/http-conformance" > "$WORK/$name.verify.log" 2>&1 + bin="${CARGO_TARGET_DIR:-$wt/wai/wai-rs/target}/falsify/wai_http_vectors" + build() { (cd "$wt/wai/wai-rs" && cargo build --profile falsify --no-default-features --features "$FEATURES" --bin wai_http_vectors); } + # The unpatched tree builds and passes, or no patch's failure means anything. + t0=$(date +%s) + if ! build > "$WORK/unpatched.build.log" 2>&1; then + echo "::error::the unpatched tree does not build"; tail -20 "$WORK/unpatched.build.log" + git -C "$ROOT" worktree remove --force "$wt"; return 1 + fi + if ! "$bin" verify "$wt/wai/http-conformance" > "$WORK/unpatched.verify.log" 2>&1; then + echo "::error::the unpatched tree does not pass"; tail -20 "$WORK/unpatched.verify.log" + git -C "$ROOT" worktree remove --force "$wt"; return 1 + fi + t1=$(date +%s) + echo "the unpatched tree builds and passes ($((t1 - t0)) s)" + for name in "${names[@]}"; do + patch="$FALSIFY/$name.patch" + t0=$(date +%s) + must="$(head -1 "$patch" | sed 's/^# must fail: //')" + if ! git -C "$wt" apply "$patch"; then + echo "::error::$name: the patch does not apply"; fail=1; continue + fi + if ! build > "$WORK/$name.build.log" 2>&1; then + echo "::error::$name: the patched tree does not build"; tail -20 "$WORK/$name.build.log"; fail=1 + else + "$bin" verify "$wt/wai/http-conformance" > "$WORK/$name.verify.log" 2>&1 + rc=$? + missing="" + for c in $must; do grep -q "^FAIL $c:" "$WORK/$name.verify.log" || missing="$missing $c"; done + t1=$(date +%s) + if [ $rc -eq 0 ] || [ -n "$missing" ]; then + echo "::error::$name: verify exited $rc; cases not failed:$missing"; fail=1 + else + echo "ok $name: verify exited $rc and failed$(printf ' %s' $must) ($(grep -c '^FAIL' "$WORK/$name.verify.log") failures, $((t1 - t0)) s)" + fi + fi + ran+=("$name") + # Back to HEAD exactly, or every later patch is judged on a changed tree. + if ! git -C "$wt" apply -R "$patch" || [ -n "$(git -C "$wt" status --porcelain --untracked-files=no)" ]; then + echo "::error::$name: the tree is not clean after reversing the patch"; git -C "$ROOT" worktree remove --force "$wt"; return 1 + fi + done + git -C "$ROOT" worktree remove --force "$wt" + check_ran patches "$(printf '%s\n' "${names[@]}")" "$(printf '%s\n' "${ran[@]}")" + return $fail +} + +# The mutations stage over the names on stdin. +run_mutations() { + local names m ran=() + read_names + names=("${NAMES[@]}") + for m in "${names[@]}"; do + local must log rc missing c + must="$(mutation_cases "$m")" + log="$WORK/py-$m.log" + (cd "$ROOT/wai/http-conformance" && python3 http_verify.py . --mutate "$m") > "$log" 2>&1 rc=$? missing="" - for c in $must; do grep -q "^FAIL $c:" "$WORK/$name.verify.log" || missing="$missing $c"; done - if [ $rc -eq 0 ] || [ -n "$missing" ]; then - echo "::error::$name: verify exited $rc; cases not failed:$missing"; fail=1 + for c in $must; do grep -q "^FAIL $c:" "$log" || missing="$missing $c"; done + if [ -z "$must" ]; then + echo "::error::$m: mutations.txt declares no case it must fail"; fail=1 + elif [ $rc -eq 0 ]; then + echo "::error::http_verify.py --mutate $m passed"; fail=1 + elif grep -q '^Traceback' "$log" || ! grep -q '^FAIL ' "$log"; then + # A crash, or a refusal to run, is not a catch: the harness failed. + echo "::error::$m: a harness failure: http_verify.py exited $rc without naming the cases it failed"; tail -5 "$log"; fail=1 + elif [ -n "$missing" ]; then + echo "::error::$m: http_verify.py exited $rc; cases not failed:$missing"; fail=1 else - echo "ok $name: verify exited $rc and failed$(printf ' %s' $must) ($(grep -c '^FAIL' "$WORK/$name.verify.log") failures)" + echo "ok http_verify.py --mutate $m: failed$(printf ' %s' $must) ($(grep -c '^FAIL' "$log") failures)" fi - fi - git -C "$ROOT" worktree remove --force "$wt" -done + ran+=("$m") + done + check_ran mutations "$(printf '%s\n' "${names[@]}")" "$(printf '%s\n' "${ran[@]}")" + return $fail +} -[ -n "$ONLY" ] && exit $fail -cd "$ROOT/wai/http-conformance" -for m in left-half no-root counter-zero floor-as-codecs blake3-iv base-at-done tick-noop refinement-unclipped \ - pdt-ignored dseq-ignored parts-after-extinf average-floor render-digest-off name-unchecked chain-unchecked \ - ilog2-no-interp bola-waits unlabelled-as-zero round-half-down small-order-key small-order-r \ - rebind-uri rebind-sequence key-unchecked walk-unbounded reload-walked \ - xml-char-unchecked xml-attribute-twice dash-start-ignored dash-timescale-ignored \ - unlabelled-escapes abr-unbounded gap-carries receipt-duplicates receipt-surrogates session-data-anywhere \ - dash-r-expanded xml-attributes-scanned dash-timelines-unbounded budget-raises \ - uri-unresolved gap-withdraws reanchor-backwards walk-exceeded-wedges walk-name-unchecked walk-verify-unchecked bindings-unbounded \ - file-numbers-unbounded nonconforming-offered conforming-withheld refines-read-whole; do - if python3 http_verify.py . --mutate "$m" > "$WORK/py-$m.log" 2>&1; then - echo "::error::http_verify.py --mutate $m passed"; fail=1 - else - echo "ok http_verify.py --mutate $m: $(grep -c '^FAIL' "$WORK/py-$m.log") failures" +# A shard ran exactly what it was assigned (one name a line in each): printed, and +# compared. +check_ran() { + local kind="$1" want got + want="$(printf '%s\n' "$2" | grep -v '^$' | LC_ALL=C sort)" + got="$(printf '%s\n' "$3" | grep -v '^$' | LC_ALL=C sort)" + echo "$kind run in this shard ($(printf '%s\n' "$got" | grep -c .)): $(echo $got)" + if [ "$want" != "$got" ]; then + echo "::error::this shard did not run exactly the $kind assigned to it" + diff <(printf '%s\n' "$want") <(printf '%s\n' "$got") + fail=1 fi -done -# The differential runs the unpatched tool: build it again (the patched builds shared -# its output path). -(cd "$ROOT/wai/wai-rs" && cargo build --release --no-default-features --features http,video_manifest --bin wai_http_vectors) > "$WORK/unpatched.build.log" 2>&1 || fail=1 -bin="${CARGO_TARGET_DIR:-$ROOT/wai/wai-rs/target}/release/wai_http_vectors" -if [ -x "$bin" ]; then +} + +# The shards a workflow declares cover every patch and every mutation exactly once. +check_shards() { + local kind list declared n total covered expected + for kind in patches mutations; do + if [ "$kind" = patches ]; then list="$(all_patches)"; else list="$(printf '%s\n' "${MUTATIONS[@]}")"; fi + declared="$(printf '%s\n' "$@" | awk -v k="$kind" '$1 == k { print $2 }')" + if [ -z "$declared" ]; then echo "::error::no $kind shard is declared"; fail=1; continue; fi + total="$(printf '%s\n' "$declared" | cut -d/ -f2 | sort -u)" + if [ "$(printf '%s\n' "$total" | wc -l)" -ne 1 ]; then echo "::error::$kind shards disagree on their total: $(echo $total)"; fail=1; continue; fi + n="$total" + if [ "$(printf '%s\n' "$declared" | cut -d/ -f1 | LC_ALL=C sort -n | tr '\n' ' ')" != "$(seq 0 $((n - 1)) | tr '\n' ' ')" ]; then + echo "::error::$kind shards are not 0..$((n - 1)) of $n, each once: $(echo $declared)"; fail=1; continue + fi + covered="$(for s in $declared; do parse_shard "$s"; printf '%s\n' "$list" | shard_of "$SI" "$SN"; done | LC_ALL=C sort)" + expected="$(printf '%s\n' "$list" | LC_ALL=C sort)" + if [ "$covered" != "$expected" ]; then + echo "::error::the $kind shards do not cover every one exactly once" + diff <(printf '%s\n' "$expected") <(printf '%s\n' "$covered") + fail=1 + else + echo "the $n $kind shards cover all $(printf '%s\n' "$expected" | wc -l | tr -d ' ') $kind, each once" + fi + done +} + +# The differential, with the unpatched tool. +run_differential() { + local bin + if ! (cd "$ROOT/wai/wai-rs" && cargo build --profile falsify --no-default-features --features "$FEATURES" --bin wai_http_vectors) > "$WORK/differential.build.log" 2>&1; then + echo "::error::the unpatched tool does not build"; tail -20 "$WORK/differential.build.log"; fail=1; return + fi + bin="${CARGO_TARGET_DIR:-$ROOT/wai/wai-rs/target}/falsify/wai_http_vectors" + cd "$ROOT/wai/http-conformance" || return 1 "$bin" diff . --seed 7 --count 500 --out "$WORK/diff.jsonl" > /dev/null || fail=1 python3 http_verify.py diff "$WORK/diff.jsonl" || fail=1 python3 http_verify.py diff "$WORK/diff.jsonl" --flip 10 --seed 3 || fail=1 -else - echo "::error::no release wai_http_vectors at $bin for the differential"; fail=1 -fi + cd "$ROOT" || return 1 +} + +case "${1:-}" in + --patches) + parse_shard "${2:?--patches I/N}" + all_patches | shard_of "$SI" "$SN" | run_patches || fail=1 + ;; + --mutations) + parse_shard "${2:?--mutations I/N}" + printf '%s\n' "${MUTATIONS[@]}" | shard_of "$SI" "$SN" | run_mutations || fail=1 + ;; + --differential) + run_differential + ;; + --check-shards) + shift + check_shards "$@" + ;; + "") + all_patches | run_patches || fail=1 + printf '%s\n' "${MUTATIONS[@]}" | run_mutations || fail=1 + run_differential + ;; + --*) + echo "::error::unknown option $1"; exit 2 + ;; + *) + for name in "$@"; do + [ -f "$FALSIFY/$name.patch" ] || { echo "::error::no patch $name"; exit 2; } + done + printf '%s\n' "$@" | run_patches || fail=1 + ;; +esac exit $fail diff --git a/wai/http-conformance/falsify/session-accepted.patch b/wai/http-conformance/falsify/session-accepted.patch new file mode 100644 index 000000000..e0a583108 --- /dev/null +++ b/wai/http-conformance/falsify/session-accepted.patch @@ -0,0 +1,15 @@ +# must fail: receipts_reconstruct_session +# a reconstruct-session claim accepted with none of its context (receipts::step6) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index 83cd7ccb..ce2f2a04 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -673,7 +673,7 @@ fn step6( + } + // §2.4.12: its outputs, received objects and envelopes are the sink's, not + // served beside an object. +- ClaimBody::ReconstructSession(_) => Err(ReceiptRefusal::ContextNotHeld), ++ ClaimBody::ReconstructSession(_) => Ok(()), + _ => Ok(()), + } + } diff --git a/wai/http-conformance/falsify/set-sha-faults.patch b/wai/http-conformance/falsify/set-sha-faults.patch new file mode 100644 index 000000000..25fde0ce5 --- /dev/null +++ b/wai/http-conformance/falsify/set-sha-faults.patch @@ -0,0 +1,15 @@ +# must fail: param_set_sha_uppercase +# a pin whose digest is not lowercase hexadecimal read as a fault, not pin_malformed (the corpus tool's param_set) +diff --git a/wai/wai-rs/src/bin/wai_cli/http_vectors_receipts.rs b/wai/wai-rs/src/bin/wai_cli/http_vectors_receipts.rs +index 12a55982..34d16fd9 100644 +--- a/wai/wai-rs/src/bin/wai_cli/http_vectors_receipts.rs ++++ b/wai/wai-rs/src/bin/wai_cli/http_vectors_receipts.rs +@@ -925,7 +925,7 @@ mod built { + // A pin whose digest is not 64 lowercase hexadecimal digits locates no grants + // file: the pin's own check refuses it (pin_malformed). + let grants = match (input["grants"].as_str(), unhex32(sha)) { +- (Some(p), Some(d)) => read_grants_file(&d, &c.file(p)?).map_err(|e| Stop::Fault(format!("the grants file: {e}")))?, ++ (Some(p), d) => read_grants_file(&d.ok_or(Stop::Fault("sha256 is not a hash".into()))?, &c.file(p)?).map_err(|e| Stop::Fault(format!("the grants file: {e}")))?, + _ => Vec::new(), + }; + let mut sink = SinkKeyLog::new(revs, anchors).with_statuses(statuses).with_grants(grants); diff --git a/wai/http-conformance/falsify/set-sink-dropped.patch b/wai/http-conformance/falsify/set-sink-dropped.patch new file mode 100644 index 000000000..3b61e0c06 --- /dev/null +++ b/wai/http-conformance/falsify/set-sink-dropped.patch @@ -0,0 +1,15 @@ +# must fail: param_set_status_stale param_set_revoked +# a fetched set held without the sink's key log (keys::check_fetched_set) +diff --git a/wai/wai-rs/src/http_binding/keys.rs b/wai/wai-rs/src/http_binding/keys.rs +index ad1572bc..de63a97b 100644 +--- a/wai/wai-rs/src/http_binding/keys.rs ++++ b/wai/wai-rs/src/http_binding/keys.rs +@@ -252,7 +252,7 @@ pub fn check_fetched_set( + prior, + std::slice::from_ref(served), + crate::pin::READABLE_FORMS, +- sink.map(|s| s as &dyn PinAuthority), ++ sink.map(|s| s as &dyn PinAuthority).filter(|_| false), + ) + } + diff --git a/wai/http-conformance/falsify/step6-skipped.patch b/wai/http-conformance/falsify/step6-skipped.patch new file mode 100644 index 000000000..7350f976e --- /dev/null +++ b/wai/http-conformance/falsify/step6-skipped.patch @@ -0,0 +1,15 @@ +# must fail: receipts_measurement_figure_not_held receipts_energy_not_delivered receipts_delivery_gap receipts_claims_O3 +# a claim accepted without §3.1 step 6 (receipts::check_claim) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index f2bdba57..972a6e39 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -700,7 +700,7 @@ pub fn check_claim( + let key = publisher(&claim.subject).expect("checked above"); + crate::claim_receipt::recover(&claim, &key, r.observed_value, Some(*cid), matchers).map_err(ReceiptRefusal::Recovery)?; + } else { +- step6(&claim, passed, fetch, matchers)?; ++ let _ = (step6, passed, matchers); + } + Ok(ClaimVerdict { claim, attests, parent }) + } diff --git a/wai/http-conformance/falsify/walk-skipped.patch b/wai/http-conformance/falsify/walk-skipped.patch new file mode 100644 index 000000000..da6e1e794 --- /dev/null +++ b/wai/http-conformance/falsify/walk-skipped.patch @@ -0,0 +1,15 @@ +# must fail: receipts_walk_two receipts_parent_not_served +# a group whose parent is not held accepted without its walk (receipts::admit_walking) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index f2bdba57..5bf15818 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -408,7 +408,7 @@ fn admit_walking( + match chains.admit(group, now_ms) { + Ok(a) => return Ok((a, 0)), + Err(e) if e.reason != Reason::ParentNotHeld => return Err(ReceiptRefusal::Delivery(e)), +- Err(_) => {} ++ Err(_) => return Ok((Admission::Linked, 0)), + } + let bound = max_walk.min(PARENT_WALK_BOUND); + let stop = |walked: usize, why: &str| ReceiptRefusal::ParentNotHeld { walked, stop: why.to_string() }; diff --git a/wai/http-conformance/falsify/walk-unbounded.patch b/wai/http-conformance/falsify/walk-unbounded.patch new file mode 100644 index 000000000..2e38ff057 --- /dev/null +++ b/wai/http-conformance/falsify/walk-unbounded.patch @@ -0,0 +1,15 @@ +# must fail: receipts_walk_bound receipts_walk_65 +# the parent walk past its bound (receipts::admit_walking) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index f2bdba57..7d4eb0a9 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -418,7 +418,7 @@ fn admit_walking( + loop { + // §3 step 3 says `parent_not_held` only of a group that names a parent. + let Some(l) = link else { return Err(stop(fetched, "parent_not_held")) }; +- if fetched == bound { ++ if fetched == usize::MAX - bound { + return Err(stop(fetched, "bound")); + } + let bytes = match fetch.get(&l) { diff --git a/wai/http-conformance/falsify/walk-unclamped.patch b/wai/http-conformance/falsify/walk-unclamped.patch new file mode 100644 index 000000000..5b9ffd245 --- /dev/null +++ b/wai/http-conformance/falsify/walk-unclamped.patch @@ -0,0 +1,15 @@ +# must fail: receipts_walk_65_asks_1000 +# the parent walk bounded by what a client asks, not by 64 (receipts::admit_walking) +diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs +index f2bdba57..a4f67ec1 100644 +--- a/wai/wai-rs/src/http_binding/receipts.rs ++++ b/wai/wai-rs/src/http_binding/receipts.rs +@@ -410,7 +410,7 @@ fn admit_walking( + Err(e) if e.reason != Reason::ParentNotHeld => return Err(ReceiptRefusal::Delivery(e)), + Err(_) => {} + } +- let bound = max_walk.min(PARENT_WALK_BOUND); ++ let bound = max_walk; + let stop = |walked: usize, why: &str| ReceiptRefusal::ParentNotHeld { walked, stop: why.to_string() }; + let mut pending: Vec = Vec::new(); + let mut link = group.parent_receipt_hash; diff --git a/wai/http-conformance/http_receipts.py b/wai/http-conformance/http_receipts.py new file mode 100644 index 000000000..812521973 --- /dev/null +++ b/wai/http-conformance/http_receipts.py @@ -0,0 +1,837 @@ +"""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"): + 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) + + +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) + 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 .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) diff --git a/wai/http-conformance/http_verify.py b/wai/http-conformance/http_verify.py index 947bd8ed0..a8bf62d9c 100644 --- a/wai/http-conformance/http_verify.py +++ b/wai/http-conformance/http_verify.py @@ -10,7 +10,9 @@ companions and refinements, the codecs grammar, union, floor and support (§4), written here. BLAKE3 is ../claim-conformance/_blake3.py (written from its specification, shared with the other Python verifiers) and SHA-256 is hashlib. The registry's kinds, roles and classes are read from ../registry/capabilities.json, the published export. -Only the standard library is needed. +Only the standard library is needed, except for the receipt and key cases of §8.1-§8.5, +which http_receipts.py checks through the corpora's own receipt, claim and key-discovery +verifiers and so with the `cryptography` package; without it those cases fail. Pins (SPEC §3.1) are read as far as the corpus needs: a pin applies to a capability that decodes against a parameter set (registry class neural-int-exact, neural-entropy-exact, @@ -23,7 +25,15 @@ stops the verifier, never passes. python3 http_verify.py [dir] --mutate NAME the same with a deliberate defect: left-half | no-root | counter-zero | floor-as-codecs | blake3-iv | base-at-done | tick-noop | refinement-unclipped | nonconforming-offered | conforming-withheld | - refines-read-whole | ... (falsify/run.sh runs every one) + refines-read-whole | … | rcpt-name-unchecked | legacy-admitted | + walk-skipped | walk-unclamped | memo-off | budget-unchecked | bytes-unchecked | + caps-unchecked | nothing-as-delivered | groups-from-listed | step6-skipped | + decision-unchecked | recovery-record-unchecked | declaration-unnamed | + first-publisher | matchers-need-object | operating-point-unchecked | + session-accepted | bytes-read-whole | key-log-uncapped | ancestor-any | + anchor-follows-redirect | + cached-rollback-unchecked | grants-module-unchecked | set-grants-ignored | + set-by-name (falsify/run.sh runs every one) python3 http_verify.py cases FILE [dir] give every case of a case file (wai_http_vectors fuzz) its stated outcome python3 http_verify.py diff FILE [--flip N --seed S] @@ -2761,8 +2771,17 @@ def be(n, width): return n.to_bytes(width, "big") +def text_bytes(s): + """A receipt's text as the bytes hashed and compared. The reader has already refused + an unpaired surrogate (RFC 8259 §8.2) with its reason, so valid text is plain UTF-8; + encoding never raises, so a reader that admitted one (the receipt-surrogates + mutation) reaches a verdict over the bytes a lenient reader would hash, never a + crash.""" + return s.encode("utf-8", "surrogatepass") + + def lp(s): - b = s.encode("utf-8") + b = text_bytes(s) return be(len(b), 4) + b @@ -2783,7 +2802,7 @@ def merkle_root(hashes): def class_hash(c): - bs = sorted(set(c["blackouts"]), key=lambda x: x.encode("utf-8")) + bs = sorted(set(c["blackouts"]), key=text_bytes) out = [b"wai:video-manifest-class\x01"] + [lp(c[k]) for k in ("content_id", "entitlement", "geo", "ad_pod_id")] out.append(be(len(bs), 4)) out += [lp(b) for b in bs] @@ -2800,7 +2819,7 @@ def render_digest(r): c = jcs({"render": r["render_json"][0], "extras": r["render_json"][1]}) except ValueError: return None - return b3(b"wai:video-manifest-render\x01", c.encode("utf-8")) + return b3(b"wai:video-manifest-render\x01", text_bytes(c)) def signing_payload(r): @@ -2836,7 +2855,7 @@ def receipt_verifies(r): return False if render_digest(r) is None: return False - if blake3(rendered(r).encode("utf-8")) != r["manifest_hash"]: + if blake3(text_bytes(rendered(r))) != r["manifest_hash"]: return False if merkle_root([segment_leaf(sg) for sg in r["segments"]]) != r["merkle_root"] or class_hash(r["class"]) != r["class_hash"]: return False @@ -2857,7 +2876,7 @@ def check_playlist(playlist, receipt_bytes, name): raise Refusal("playlist_receipt_malformed") if name is not None and name != r["manifest_hash"].hex() + ".json" and MUTATE != "name-unchecked": raise Refusal("receipt_name_mismatch") - if blake3(playlist) != r["manifest_hash"] or rendered(r).encode("utf-8") != playlist: + if blake3(playlist) != r["manifest_hash"] or text_bytes(rendered(r)) != playlist: raise Refusal("playlist_receipt_mismatch") if not receipt_verifies(r): raise Refusal("playlist_receipt_invalid") @@ -3227,6 +3246,35 @@ def refusal(e): return out +RECEIPT_OPS = ("receipt_file", "receipts", "key_log", "grants_file", "param_set") + + +def receipt_evaluate(d, objs, op, inp): + """§8.1-§8.5's cases, which http_receipts.py checks. They need the `cryptography` + package, through the receipt and key verifiers it reads; nothing else here does.""" + import http_receipts as RC + RC.MUTATE = MUTATE + + def fetch_object(env, f): + """What a client holds of `env` after fetching it as the `fetch` op does: the cid + and how it was established (§8.3), or nothing; and the fetch's verdict.""" + t = f.get("tamper") or {} + served = apply_patch(env, t.get("patch_object")) + try: + r = fetch(env, served, frag_of(f.get("fragment")), sink_of(d, f, objs), f.get("threshold_permille", 900), t, + f.get("listed_as_layer", False)) + except Refusal as e: + return None, e.code + if r["role"] == "layer" and 0 in r["abandoned"]: + return None, r["verified_by"] + return (content_hash(env).hex(), "head-proof" if r["verified_by"] == "head-proof" else "envelope"), r["verified_by"] + + try: + return RC.evaluate(d, objs, op, inp, fetch_object, content_hash) + except RC.Fault as e: + return {"fault": str(e)} + + def evaluate(d, objs, op, inp): def obj(i, patch=True): if "envelope_hex" in i: @@ -3303,6 +3351,8 @@ def evaluate(d, objs, op, inp): return {"supports": supports(parse_element(inp["floor"]), inp["containers"], inp["caps"])} if op == "layer_tag": return parse_layer_tag(inp["line"]) + if op in RECEIPT_OPS: + return receipt_evaluate(d, objs, op, inp) r = playlist_evaluate(d, objs, op, inp, obj) if r is not None: return r diff --git a/wai/http-conformance/objects/1eb764beb4a8a739054341df8b5b7d065e490032b187a93787be15f1478080a8.wai.receipts b/wai/http-conformance/objects/1eb764beb4a8a739054341df8b5b7d065e490032b187a93787be15f1478080a8.wai.receipts new file mode 100644 index 000000000..eb7044404 --- /dev/null +++ b/wai/http-conformance/objects/1eb764beb4a8a739054341df8b5b7d065e490032b187a93787be15f1478080a8.wai.receipts @@ -0,0 +1 @@ +{"claims":["0c0b87f288cc459e99b914920284bf633eab89bf035078137b89936002072768"],"delivery":[{"object_receipt":{"content_hash":"1eb764beb4a8a739054341df8b5b7d065e490032b187a93787be15f1478080a8","group_id":0,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":["5a13748ae5901ba489bda77a5e4411b146ca3ad5d9ea1413165d3ad83b1583e4","a9823344677a8b16a55310735bf7278c19d83a70919eaa501bd02cc43b36a11b"]},"object_id":0,"origin":"origin","track_alias":3},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"1eb764beb4a8a739054341df8b5b7d065e490032b187a93787be15f1478080a8","group_receipt":"0755611daa44a9474511562d9ac7e429d4be353fb387c54edd9a24217853ae75","joules_micro":0}}],"kind":"wai-receipt-index","object":"1eb764beb4a8a739054341df8b5b7d065e490032b187a93787be15f1478080a8"} \ No newline at end of file diff --git a/wai/http-conformance/objects/31abc1da2cdf927191f3f447f68508ead30d078690cb7bea2435ae3d1df5f4f3.wai.receipts b/wai/http-conformance/objects/31abc1da2cdf927191f3f447f68508ead30d078690cb7bea2435ae3d1df5f4f3.wai.receipts new file mode 100644 index 000000000..1b315f699 --- /dev/null +++ b/wai/http-conformance/objects/31abc1da2cdf927191f3f447f68508ead30d078690cb7bea2435ae3d1df5f4f3.wai.receipts @@ -0,0 +1 @@ +{"claims":[],"delivery":[{"object_receipt":{"content_hash":"31abc1da2cdf927191f3f447f68508ead30d078690cb7bea2435ae3d1df5f4f3","group_id":0,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"31abc1da2cdf927191f3f447f68508ead30d078690cb7bea2435ae3d1df5f4f3","group_receipt":"3fba8315347da348bbb2f7a4b3f5488ff18a2be9bfb95efc7921142969ec3807","joules_micro":0}}],"kind":"wai-receipt-index","object":"31abc1da2cdf927191f3f447f68508ead30d078690cb7bea2435ae3d1df5f4f3"} \ No newline at end of file diff --git a/wai/http-conformance/objects/53a48057cb99b88fd235b9165799122df2d1b55a4b84245112a5a5ea948458ab.wai.receipts b/wai/http-conformance/objects/53a48057cb99b88fd235b9165799122df2d1b55a4b84245112a5a5ea948458ab.wai.receipts new file mode 100644 index 000000000..2d629a73d --- /dev/null +++ b/wai/http-conformance/objects/53a48057cb99b88fd235b9165799122df2d1b55a4b84245112a5a5ea948458ab.wai.receipts @@ -0,0 +1 @@ +{"claims":[],"delivery":[{"object_receipt":{"content_hash":"53a48057cb99b88fd235b9165799122df2d1b55a4b84245112a5a5ea948458ab","group_id":0,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":1,"leaf_index":1,"siblings":["5b7309ebd6e33a6f412a182d37c2c07587f79777ae71aaa57c15a8ae606b5add","a9823344677a8b16a55310735bf7278c19d83a70919eaa501bd02cc43b36a11b"]},"object_id":1,"origin":"origin","track_alias":3},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"53a48057cb99b88fd235b9165799122df2d1b55a4b84245112a5a5ea948458ab","group_receipt":"0755611daa44a9474511562d9ac7e429d4be353fb387c54edd9a24217853ae75","joules_micro":0}}],"kind":"wai-receipt-index","object":"53a48057cb99b88fd235b9165799122df2d1b55a4b84245112a5a5ea948458ab"} \ No newline at end of file diff --git a/wai/http-conformance/objects/6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9.wai.receipts b/wai/http-conformance/objects/6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9.wai.receipts new file mode 100644 index 000000000..3504a1509 --- /dev/null +++ b/wai/http-conformance/objects/6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9.wai.receipts @@ -0,0 +1 @@ +{"claims":["14c0d31e65ae842627c606581c492bad7a73ee9190fc7db33d15de262d535491","dae83c921710311cbd34d8d4a2bedab53cf56a9be5afea2dc3d264ee978a03c5","8dc0815e30c004abc33175080c733c3aec1e0187ebd6f8374333f22373d9fe11","144f86074d065d7f6f8bb30b3505870241b439eec6341f7b5dafa55eb5c5e936"],"delivery":[{"object_receipt":{"content_hash":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","energy_provenance":"Estimator","group_id":0,"joules_micro":1500,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":["368b22537cfa0d3d0bfcfe1f6256302af54c54711dfa411c193beda171c97edb"]},"object_id":0,"origin":"origin","track_alias":2},"property":{"acquisition":"Estimator","acquisition_evidence":"","content_hash":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","group_receipt":"ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018","joules_micro":1500}}],"kind":"wai-receipt-index","object":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9"} \ No newline at end of file diff --git a/wai/http-conformance/objects/753e8ac465d347547c688690eb51615f19118c0a4c17490499fbb31aefd5cd0c.wai.receipts b/wai/http-conformance/objects/753e8ac465d347547c688690eb51615f19118c0a4c17490499fbb31aefd5cd0c.wai.receipts new file mode 100644 index 000000000..bdc355abe --- /dev/null +++ b/wai/http-conformance/objects/753e8ac465d347547c688690eb51615f19118c0a4c17490499fbb31aefd5cd0c.wai.receipts @@ -0,0 +1 @@ +{"claims":["0c0de1e8d6eca027482086221a3022c5f130d46a45f27aedbf0377b62a0675c0","40ef9986d24aaf108f7d9ba9995c6add781cf5733e6d639d9a783ca0c0ddd61c"],"delivery":[{"object_receipt":{"content_hash":"753e8ac465d347547c688690eb51615f19118c0a4c17490499fbb31aefd5cd0c","group_id":0,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":2,"leaf_index":2,"siblings":["d6cbe6c9d7da6adbcd392979eef608ca9831a20c65ef141478a0e71613d2c5e2","cf3ca3f2f8980312874592a512e91aa868b4bd835805a06e5e3b777540683581"]},"object_id":2,"origin":"origin","track_alias":3},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"753e8ac465d347547c688690eb51615f19118c0a4c17490499fbb31aefd5cd0c","group_receipt":"0755611daa44a9474511562d9ac7e429d4be353fb387c54edd9a24217853ae75","joules_micro":0}}],"kind":"wai-receipt-index","object":"753e8ac465d347547c688690eb51615f19118c0a4c17490499fbb31aefd5cd0c"} \ No newline at end of file diff --git a/wai/http-conformance/objects/922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f.wai.receipts b/wai/http-conformance/objects/922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f.wai.receipts new file mode 100644 index 000000000..1fe196772 --- /dev/null +++ b/wai/http-conformance/objects/922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f.wai.receipts @@ -0,0 +1 @@ +{"claims":["14c0d31e65ae842627c606581c492bad7a73ee9190fc7db33d15de262d535491"],"delivery":[{"object_receipt":{"content_hash":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f","energy_provenance":"Estimator","group_id":0,"joules_micro":300,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":1,"leaf_index":1,"siblings":["6ecdf4781b24002780c2fa637e034759ebd9b88ec7a341bc7a250cc36972f5bc"]},"object_id":1,"origin":"origin","track_alias":2},"property":{"acquisition":"Estimator","acquisition_evidence":"","content_hash":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f","group_receipt":"ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018","joules_micro":300}}],"kind":"wai-receipt-index","object":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f"} \ No newline at end of file diff --git a/wai/http-conformance/objects/a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133.wai.receipts b/wai/http-conformance/objects/a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133.wai.receipts new file mode 100644 index 000000000..2a1fd40bb --- /dev/null +++ b/wai/http-conformance/objects/a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133.wai.receipts @@ -0,0 +1 @@ +{"claims":[],"delivery":[{"object_receipt":{"content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_id":2,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133","group_receipt":"8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765","joules_micro":0}}],"kind":"wai-receipt-index","object":"a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133"} \ No newline at end of file diff --git a/wai/http-conformance/objects/b7a711ab4b0a0939a561a16b3167039e7fe65c640d73a040ebc5d5cf86e4ecbb.wai.receipts b/wai/http-conformance/objects/b7a711ab4b0a0939a561a16b3167039e7fe65c640d73a040ebc5d5cf86e4ecbb.wai.receipts new file mode 100644 index 000000000..89af76871 --- /dev/null +++ b/wai/http-conformance/objects/b7a711ab4b0a0939a561a16b3167039e7fe65c640d73a040ebc5d5cf86e4ecbb.wai.receipts @@ -0,0 +1 @@ +{"claims":[],"delivery":[{"object_receipt":{"content_hash":"b7a711ab4b0a0939a561a16b3167039e7fe65c640d73a040ebc5d5cf86e4ecbb","group_id":1,"joules_micro":0,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":1},"property":{"acquisition":"","acquisition_evidence":"","content_hash":"b7a711ab4b0a0939a561a16b3167039e7fe65c640d73a040ebc5d5cf86e4ecbb","group_receipt":"e0e1d579fff00461f48124d31edac54e44cf157fd26307fca69de8e1c7cd9d30","joules_micro":0}}],"kind":"wai-receipt-index","object":"b7a711ab4b0a0939a561a16b3167039e7fe65c640d73a040ebc5d5cf86e4ecbb"} \ No newline at end of file diff --git a/wai/http-conformance/objects/e43e038c10c0494d9647444fd4daf54b19fc3a946a3c7980431bc6e0a236093b.wai.receipts b/wai/http-conformance/objects/e43e038c10c0494d9647444fd4daf54b19fc3a946a3c7980431bc6e0a236093b.wai.receipts new file mode 100644 index 000000000..639172222 --- /dev/null +++ b/wai/http-conformance/objects/e43e038c10c0494d9647444fd4daf54b19fc3a946a3c7980431bc6e0a236093b.wai.receipts @@ -0,0 +1 @@ +{"claims":["b766a9838403aa0b33528ac30947c8f1e6fd993ba6434f112392f11b482464f0"],"delivery":[{"object_receipt":{"content_hash":"e43e038c10c0494d9647444fd4daf54b19fc3a946a3c7980431bc6e0a236093b","energy_counter_filtering":"Unknown","energy_provenance":"OnChipCounter","energy_uncertainty":{"relative_ppm":44669,"window_us":3000000},"group_id":0,"joules_micro":461538,"kind":"object","leaf_version":2,"merkle_proof":{"direction_bits":0,"leaf_index":0,"siblings":[]},"object_id":0,"origin":"origin","track_alias":4},"property":{"acquisition":"OnChipCounter","acquisition_evidence":"0000ae7d00000000002dc6c000","content_hash":"e43e038c10c0494d9647444fd4daf54b19fc3a946a3c7980431bc6e0a236093b","group_receipt":"95477f223c6bee89a57c7171c831352fd43f4a348f9d56f90e32e922c2c2b16f","joules_micro":461538}}],"kind":"wai-receipt-index","object":"e43e038c10c0494d9647444fd4daf54b19fc3a946a3c7980431bc6e0a236093b"} \ No newline at end of file diff --git a/wai/http-conformance/objects/rcpt/0755611daa44a9474511562d9ac7e429d4be353fb387c54edd9a24217853ae75.json b/wai/http-conformance/objects/rcpt/0755611daa44a9474511562d9ac7e429d4be353fb387c54edd9a24217853ae75.json new file mode 100644 index 000000000..a2b48cad3 --- /dev/null +++ b/wai/http-conformance/objects/rcpt/0755611daa44a9474511562d9ac7e429d4be353fb387c54edd9a24217853ae75.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":0,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":3,"parent_receipt_hash":null,"root_hash":"9cf58f35f3c67de873d8751c94f65dd3e7bb6d3397eaf8a4c5a053042cea10da","sig":"de0352f2ea7398e64bdb768a81dc3073bcc36862060782df6897d490534f42a1d40c4cf867fdeefd5803aa6ec2c6558b6bee6713ef29c2def7d8becc7a596d00","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":3} \ No newline at end of file diff --git a/wai/http-conformance/objects/rcpt/0c0b87f288cc459e99b914920284bf633eab89bf035078137b89936002072768.json b/wai/http-conformance/objects/rcpt/0c0b87f288cc459e99b914920284bf633eab89bf035078137b89936002072768.json new file mode 100644 index 000000000..5d8d9c83d --- /dev/null +++ b/wai/http-conformance/objects/rcpt/0c0b87f288cc459e99b914920284bf633eab89bf035078137b89936002072768.json @@ -0,0 +1 @@ +{"body":{"bytes_envelopes":14536,"conflicts":0,"deadline_misses":0,"objects":[{"bytes":12711,"evidence":"delivered-verified","layer":1,"object":"53a48057cb99b88fd235b9165799122df2d1b55a4b84245112a5a5ea948458ab","reason":null,"status":"applied","units":{"count":1,"from":0}},{"bytes":0,"evidence":"inclusion-verified","layer":2,"object":"753e8ac465d347547c688690eb51615f19118c0a4c17490499fbb31aefd5cd0c","reason":null,"status":"dropped","units":{"count":1,"from":0}}],"op_committed":{"wai-stage:layers":2},"op_delivered":{"wai-stage:layers":1},"op_requested":{"wai-stage:layers":2},"presented":{"wai-stage:layers":1},"stage_hash":null,"tier":"none","units_by_stage":[0,1],"vocabulary":"urn:wai:staged-delivery:1"},"class":"stage","issued_at":"2026-10-06T00:01:10Z","kind":"claim","parent_receipt_hash":"8754b1d2e028880a1d21fe29dca565c0588125ea25979e37e1d90723b674654c","seq":1,"sig":"b69f73df4488fa8eb031415191cd851cc18196f0068020f9250cbd69f0c68ae7f5e8912567b1455830c107c0c8b6532ad2ee903d6dc1683a7537bf3641850f0d","signer_id":"did:example:http-sink","signer_pubkey":"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653","subject":"1eb764beb4a8a739054341df8b5b7d065e490032b187a93787be15f1478080a8","subject_type":"object"} \ No newline at end of file diff --git a/wai/http-conformance/objects/rcpt/0c0de1e8d6eca027482086221a3022c5f130d46a45f27aedbf0377b62a0675c0.json b/wai/http-conformance/objects/rcpt/0c0de1e8d6eca027482086221a3022c5f130d46a45f27aedbf0377b62a0675c0.json new file mode 100644 index 000000000..3cb83df8f --- /dev/null +++ b/wai/http-conformance/objects/rcpt/0c0de1e8d6eca027482086221a3022c5f130d46a45f27aedbf0377b62a0675c0.json @@ -0,0 +1 @@ +{"body":{"group_id":0,"object_id":2,"observed_at":"2026-10-06T00:02:10Z","reason":"not-received","track_alias":3},"class":"delivery-gap","issued_at":"2026-10-06T00:02:10Z","kind":"claim","parent_receipt_hash":null,"seq":0,"sig":"2305227fe2c0111a0535e6fcfc1bb0e92706bd824e9b7cbe3a9ff843e8f17564bed3883c218b1ca9105d637eb9b5a080668d22a52a447c088ccb5dbfafb7ed09","signer_id":"did:example:http-relay","signer_pubkey":"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746","subject":"753e8ac465d347547c688690eb51615f19118c0a4c17490499fbb31aefd5cd0c","subject_type":"object"} \ No newline at end of file diff --git a/wai/http-conformance/objects/rcpt/144f86074d065d7f6f8bb30b3505870241b439eec6341f7b5dafa55eb5c5e936.json b/wai/http-conformance/objects/rcpt/144f86074d065d7f6f8bb30b3505870241b439eec6341f7b5dafa55eb5c5e936.json new file mode 100644 index 000000000..505ed3f91 --- /dev/null +++ b/wai/http-conformance/objects/rcpt/144f86074d065d7f6f8bb30b3505870241b439eec6341f7b5dafa55eb5c5e936.json @@ -0,0 +1 @@ +{"body":{"coverage":"Whole","energy_calibration_ref":null,"energy_counter_filtering":null,"energy_provenance":"Estimator","energy_uncertainty":null,"grade":"projected","joules_micro":1501,"method":"https://transaction.science/wai/extensions/energy-measurement","oer_receipt":null,"work_unit":"subpixel","work_units":1},"class":"energy","issued_at":"2026-10-06T00:02:00Z","kind":"claim","parent_receipt_hash":"8dc0815e30c004abc33175080c733c3aec1e0187ebd6f8374333f22373d9fe11","seq":4,"sig":"794648a32f165761c1ed74afca133dd48bec849ec58b58773d0e9330b45766cc554253a872c76bfdfb5f7b27a93aa5f8b1ccdc3cdf0903a9abfda26b7e4d5400","signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","subject_type":"object"} \ No newline at end of file diff --git a/wai/http-conformance/objects/rcpt/14c0d31e65ae842627c606581c492bad7a73ee9190fc7db33d15de262d535491.json b/wai/http-conformance/objects/rcpt/14c0d31e65ae842627c606581c492bad7a73ee9190fc7db33d15de262d535491.json new file mode 100644 index 000000000..4342a0167 --- /dev/null +++ b/wai/http-conformance/objects/rcpt/14c0d31e65ae842627c606581c492bad7a73ee9190fc7db33d15de262d535491.json @@ -0,0 +1 @@ +{"body":{"alg":"org.example.fingerprint","binding_type":"fingerprint","registry":"https://registry.example/soft-binding","resolution_endpoint":"https://origin.example/resolve","value":"0ff0"},"class":"soft-binding","issued_at":"2026-10-06T00:01:20Z","kind":"claim","parent_receipt_hash":null,"seq":0,"sig":"7b702db3b4c42f52e9c5379ab161909e21c49f2e41b613611a65e0f16c1176c18451abe4f1e7079b6100e019ef79297303afd969160543d33e322b80c66f3900","signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","subject":"922db9b513790966272927c4f542fb01cb25ff9c7b189fb6699bf92894c29a6f","subject_type":"object"} \ No newline at end of file diff --git a/wai/http-conformance/objects/rcpt/3fba8315347da348bbb2f7a4b3f5488ff18a2be9bfb95efc7921142969ec3807.json b/wai/http-conformance/objects/rcpt/3fba8315347da348bbb2f7a4b3f5488ff18a2be9bfb95efc7921142969ec3807.json new file mode 100644 index 000000000..3ed7c5479 --- /dev/null +++ b/wai/http-conformance/objects/rcpt/3fba8315347da348bbb2f7a4b3f5488ff18a2be9bfb95efc7921142969ec3807.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:04Z","group_id":0,"group_start":"2026-10-06T00:00:00Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":null,"root_hash":"43c3a4e8ff2e9cb84fa4529dd16c18008f7ac1b324117853c0f45871e46f85c7","sig":"7f0a32e960322763ba31d025cb4fefc0f9b210310c66b319baaeefe6f5845864e5338dc1ddd89a49a694e4e0342b23fb5bf4813dd546e9518b8f79d93b1c3c04","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":1} \ No newline at end of file diff --git a/wai/http-conformance/objects/rcpt/40ef9986d24aaf108f7d9ba9995c6add781cf5733e6d639d9a783ca0c0ddd61c.json b/wai/http-conformance/objects/rcpt/40ef9986d24aaf108f7d9ba9995c6add781cf5733e6d639d9a783ca0c0ddd61c.json new file mode 100644 index 000000000..b551e0d6b --- /dev/null +++ b/wai/http-conformance/objects/rcpt/40ef9986d24aaf108f7d9ba9995c6add781cf5733e6d639d9a783ca0c0ddd61c.json @@ -0,0 +1 @@ +{"body":{"group_id":0,"object_id":1,"observed_at":"2026-10-06T00:02:10Z","reason":"not-received","track_alias":3},"class":"delivery-gap","issued_at":"2026-10-06T00:02:11Z","kind":"claim","parent_receipt_hash":"0c0de1e8d6eca027482086221a3022c5f130d46a45f27aedbf0377b62a0675c0","seq":1,"sig":"1d6654635b68d4a253e5507efd4ce464e5204509900aa17025a961a3a48d38300bfab95cb8a53f6894158b4a498aed8f56efcccf9d6a031cbf3b68f0f768cd09","signer_id":"did:example:http-relay","signer_pubkey":"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746","subject":"753e8ac465d347547c688690eb51615f19118c0a4c17490499fbb31aefd5cd0c","subject_type":"object"} \ No newline at end of file diff --git a/wai/http-conformance/objects/rcpt/8754b1d2e028880a1d21fe29dca565c0588125ea25979e37e1d90723b674654c.json b/wai/http-conformance/objects/rcpt/8754b1d2e028880a1d21fe29dca565c0588125ea25979e37e1d90723b674654c.json new file mode 100644 index 000000000..f3e015885 --- /dev/null +++ b/wai/http-conformance/objects/rcpt/8754b1d2e028880a1d21fe29dca565c0588125ea25979e37e1d90723b674654c.json @@ -0,0 +1 @@ +{"body":{"bytes_envelopes":1825,"conflicts":0,"deadline_misses":0,"objects":[],"op_committed":null,"op_delivered":{"wai-stage:layers":0},"op_requested":null,"presented":{"wai-stage:layers":0},"stage_hash":null,"tier":"none","units_by_stage":[1],"vocabulary":"urn:wai:staged-delivery:1"},"class":"stage","issued_at":"2026-10-06T00:01:00Z","kind":"claim","parent_receipt_hash":null,"seq":0,"sig":"1a8121d6606828f292880f6e694bde4d81775eeb5568b4327773538ed072b38f7e32058b747eab0ef0ca2b96b8e524d886da715fd107238fcffbb97631443c0f","signer_id":"did:example:http-sink","signer_pubkey":"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653","subject":"1eb764beb4a8a739054341df8b5b7d065e490032b187a93787be15f1478080a8","subject_type":"object"} \ No newline at end of file diff --git a/wai/http-conformance/objects/rcpt/8dc0815e30c004abc33175080c733c3aec1e0187ebd6f8374333f22373d9fe11.json b/wai/http-conformance/objects/rcpt/8dc0815e30c004abc33175080c733c3aec1e0187ebd6f8374333f22373d9fe11.json new file mode 100644 index 000000000..7e77eb33e --- /dev/null +++ b/wai/http-conformance/objects/rcpt/8dc0815e30c004abc33175080c733c3aec1e0187ebd6f8374333f22373d9fe11.json @@ -0,0 +1 @@ +{"body":{"coverage":"Whole","energy_calibration_ref":null,"energy_counter_filtering":null,"energy_provenance":"Estimator","energy_uncertainty":null,"grade":"projected","joules_micro":1500,"method":"https://transaction.science/wai/extensions/energy-measurement","oer_receipt":null,"work_unit":"subpixel","work_units":1},"class":"energy","issued_at":"2026-10-06T00:01:50Z","kind":"claim","parent_receipt_hash":"b766a9838403aa0b33528ac30947c8f1e6fd993ba6434f112392f11b482464f0","seq":3,"sig":"5720366f082ed88f95c1cbc776e22b77fbcabe0b9670b5e8e5dc92dc4343a4426334bd93273900a4c728d954c486e7ad1bd619e0ab34396117e159fd9ed8bf0d","signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","subject":"6e485f112e849422fb9cfd279cb16b88c8d5ee1e72bafe4892992fc6bef88be9","subject_type":"object"} \ No newline at end of file diff --git a/wai/http-conformance/objects/rcpt/8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765.json b/wai/http-conformance/objects/rcpt/8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765.json new file mode 100644 index 000000000..1f8a1eebc --- /dev/null +++ b/wai/http-conformance/objects/rcpt/8ee7c8ed8cf488b5cffeb7c9c9c500d5f1f1aa5befacff41f2ac4a19bbbc9765.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:12Z","group_id":2,"group_start":"2026-10-06T00:00:08Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"e0e1d579fff00461f48124d31edac54e44cf157fd26307fca69de8e1c7cd9d30","root_hash":"12404b3b6734a2e2bde882ae596f31a132e4e30d05beb6ed589bcba1b084a187","sig":"0735c5eccc2e1ef109c5758a39357855359033fbedf08ad2f36cd60bff49c0ea04abb1f869bc3da279badbdfbd3849f5624697c85ad6760bc32a674445d23e00","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":1} \ No newline at end of file diff --git a/wai/http-conformance/objects/rcpt/95477f223c6bee89a57c7171c831352fd43f4a348f9d56f90e32e922c2c2b16f.json b/wai/http-conformance/objects/rcpt/95477f223c6bee89a57c7171c831352fd43f4a348f9d56f90e32e922c2c2b16f.json new file mode 100644 index 000000000..d67faa585 --- /dev/null +++ b/wai/http-conformance/objects/rcpt/95477f223c6bee89a57c7171c831352fd43f4a348f9d56f90e32e922c2c2b16f.json @@ -0,0 +1 @@ +{"energy_counter_filtering":"Unknown","energy_coverage":"Whole","energy_provenance":"OnChipCounter","energy_uncertainty":{"relative_ppm":44669,"window_us":3000000},"group_end":"2026-10-06T00:00:04Z","group_id":0,"group_start":"2026-10-06T00:00:00Z","joules_total":461538,"kind":"group","leaf_count":1,"parent_receipt_hash":null,"root_hash":"0164608dfa773751ba4de5edd778031dab500cbc8e9c76438377163773ec4c32","sig":"0a3f0fb7fb6a4e6b3dcc62aacfd85329df75639a1b7a62ff212657c0429326acc6166e0dba2b7b6290bc58c8a26e8eda3c935c3c15a284d66a8bd473bfd98a0b","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":4} \ No newline at end of file diff --git a/wai/http-conformance/objects/rcpt/b766a9838403aa0b33528ac30947c8f1e6fd993ba6434f112392f11b482464f0.json b/wai/http-conformance/objects/rcpt/b766a9838403aa0b33528ac30947c8f1e6fd993ba6434f112392f11b482464f0.json new file mode 100644 index 000000000..10e50c73c --- /dev/null +++ b/wai/http-conformance/objects/rcpt/b766a9838403aa0b33528ac30947c8f1e6fd993ba6434f112392f11b482464f0.json @@ -0,0 +1 @@ +{"body":{"bracket":{"busy":{"live_intervals":14,"longest_live_run":14,"mean_uw":30000000,"n":14,"sd_uw":115471},"completions":60,"idle":{"live_intervals":10,"longest_live_run":10,"mean_uw":20000000,"n":10,"sd_uw":115471},"in_flight":10,"opening":"from-rest","window_us":3000000},"build":{"profile":"release","target":"x86_64-unknown-linux-gnu","toolchain":"rustc 1.98.0 (example)"},"energy_calibration_ref":null,"energy_counter_filtering":"Unknown","energy_provenance":"OnChipCounter","energy_uncertainty":{"relative_ppm":44669,"window_us":3000000},"figure_index":0,"figure_profile":"moq-jwp:object","group":"95477f223c6bee89a57c7171c831352fd43f4a348f9d56f90e32e922c2c2b16f","joules_micro":461538,"power_reader":"reader:example"},"class":"measurement","issued_at":"2026-10-06T00:01:40Z","kind":"claim","parent_receipt_hash":"dae83c921710311cbd34d8d4a2bedab53cf56a9be5afea2dc3d264ee978a03c5","seq":2,"sig":"76511b02d5ab8b096df498cc4fa5b2ad29056c383e94c8c312606fec81ca2771875704c53399096bbdbe0eecd0548274cedaad7b37aa13dd14f422db7b9a1100","signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","subject":"e43e038c10c0494d9647444fd4daf54b19fc3a946a3c7980431bc6e0a236093b","subject_type":"object"} \ No newline at end of file diff --git a/wai/http-conformance/objects/rcpt/dae83c921710311cbd34d8d4a2bedab53cf56a9be5afea2dc3d264ee978a03c5.json b/wai/http-conformance/objects/rcpt/dae83c921710311cbd34d8d4a2bedab53cf56a9be5afea2dc3d264ee978a03c5.json new file mode 100644 index 000000000..e432999f6 --- /dev/null +++ b/wai/http-conformance/objects/rcpt/dae83c921710311cbd34d8d4a2bedab53cf56a9be5afea2dc3d264ee978a03c5.json @@ -0,0 +1 @@ +{"body":{"entry":"41","log":"https://log.example/","log_receipt":"01020304","profile":"https://log.example/profile"},"class":"log-inclusion","issued_at":"2026-10-06T00:01:30Z","kind":"claim","parent_receipt_hash":"14c0d31e65ae842627c606581c492bad7a73ee9190fc7db33d15de262d535491","seq":1,"sig":"4e37f4d1e84dd20646315da68569cbe25c36815999e23b9116555fffefd6d1d51213db21eda080658a7f8d42a61d8574060a96092fc288d9a808d52d6200f20e","signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","subject":"ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018","subject_type":"receipt"} \ No newline at end of file diff --git a/wai/http-conformance/objects/rcpt/e0e1d579fff00461f48124d31edac54e44cf157fd26307fca69de8e1c7cd9d30.json b/wai/http-conformance/objects/rcpt/e0e1d579fff00461f48124d31edac54e44cf157fd26307fca69de8e1c7cd9d30.json new file mode 100644 index 000000000..365c5db9c --- /dev/null +++ b/wai/http-conformance/objects/rcpt/e0e1d579fff00461f48124d31edac54e44cf157fd26307fca69de8e1c7cd9d30.json @@ -0,0 +1 @@ +{"group_end":"2026-10-06T00:00:08Z","group_id":1,"group_start":"2026-10-06T00:00:04Z","joules_total":0,"kind":"group","leaf_count":1,"parent_receipt_hash":"3fba8315347da348bbb2f7a4b3f5488ff18a2be9bfb95efc7921142969ec3807","root_hash":"24bc07e7f8394236e4365ba109e919ba9da1471ee4b0171c8879d27dd4bbe862","sig":"090d40d3f0e9a30a0086de99befe87dfb11fd12aff7f1fb470adc40c5edd03498675c2f4598842baec927bf5af1081941318b61bd93ad6809da4489c783c8500","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":1} \ No newline at end of file diff --git a/wai/http-conformance/objects/rcpt/ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018.json b/wai/http-conformance/objects/rcpt/ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018.json new file mode 100644 index 000000000..6a5d31fcd --- /dev/null +++ b/wai/http-conformance/objects/rcpt/ed784e0aa394c85918460cb0e818ef939b8674098bc2e38910bba7b2244df018.json @@ -0,0 +1 @@ +{"energy_coverage":"Whole","energy_provenance":"Estimator","group_end":"2026-10-06T00:00:04Z","group_id":0,"group_start":"2026-10-06T00:00:00Z","joules_total":1800,"kind":"group","leaf_count":2,"parent_receipt_hash":null,"root_hash":"27d390152992012872960999af3942bd9e794db9516bc915637316dee2850782","sig":"7296693723c77c5b6c2eb4f9f0b8bf66b9d825931473c51ec8329505a292e86b1063595188af10f6396148f7deafd52719b2f9792dd71436ee7920c83485a90c","sign_version":2,"signer_id":"did:web:origin.example#d1","signer_pubkey":"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d","track_alias":2} \ No newline at end of file diff --git a/wai/http-conformance/sets/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4 b/wai/http-conformance/sets/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4 new file mode 100644 index 000000000..4e335fe23 Binary files /dev/null and b/wai/http-conformance/sets/7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4 differ diff --git a/wai/wai-rs/Cargo.lock b/wai/wai-rs/Cargo.lock index 227ce0684..36e2bb1f3 100644 --- a/wai/wai-rs/Cargo.lock +++ b/wai/wai-rs/Cargo.lock @@ -722,6 +722,16 @@ dependencies = [ "cc", ] +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "console_log" version = "1.1.0" @@ -763,6 +773,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -1821,6 +1841,55 @@ dependencies = [ "jiff-tzdb", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.20", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + [[package]] name = "jobserver" version = "0.1.35" @@ -2248,6 +2317,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "opus" version = "0.3.1" @@ -3038,6 +3113,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" version = "1.15.1" @@ -3047,6 +3134,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" + [[package]] name = "rustls-webpki" version = "0.103.15" @@ -3064,12 +3178,30 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scan_fmt" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b53b0a5db882a8e2fdaae0a43f7b39e7e9082389e978398bdf223a55b581248" +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.9.0" @@ -3108,6 +3240,29 @@ dependencies = [ "zeroize", ] +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.28" @@ -3294,6 +3449,16 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + [[package]] name = "simd_helpers" version = "0.1.0" @@ -3303,6 +3468,12 @@ dependencies = [ "quote", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.12" @@ -3889,6 +4060,8 @@ dependencies = [ "ort", "pkg-config", "rav1e 0.8.1", + "rustls", + "rustls-platform-verifier", "serde", "serde_json", "sha2 0.10.9", @@ -3907,6 +4080,16 @@ dependencies = [ "serde_json", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -3997,6 +4180,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "1.0.9" @@ -4022,6 +4214,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/wai/wai-rs/Cargo.toml b/wai/wai-rs/Cargo.toml index 9668591fe..f93673075 100644 --- a/wai/wai-rs/Cargo.toml +++ b/wai/wai-rs/Cargo.toml @@ -65,6 +65,13 @@ byteorder = { version = "1", optional = true } # right libonnxruntime build at compile time; no manual brew install. ort = { version = "2.0.0-rc.12", default-features = false, features = ["std", "download-binaries", "ndarray", "tls-rustls"], optional = true } ndarray = { version = "0.17", optional = true } +# ---- the `wai` CLI's TLS client (optional, behind the `cli_tls` feature) ---- +# `wai http receipts --key-origin` retrieves an origin's key log over HTTPS itself +# (http-delivery §8.4), the origin's certificate validated against the operating system's +# trust store. The library never opens a connection: only the binary uses these, and no +# library or wasm32 build enables the feature. ring is the provider already in the graph. +rustls = { version = "0.23", default-features = false, features = ["std", "ring", "tls12"], optional = true } +rustls-platform-verifier = { version = "0.7", optional = true } zstd-safe = { version = "7", optional = true, features = ["std"] } # for bmshj2018 latent unpack (std → WriteBuf for Vec) # SHA-256 is WAI's digest for parameter-set identity, component identity and decoded-output # conformance digests (SPEC §3.1, §7 *Digests*, §7.1). Pure Rust, so it is always on: the @@ -218,6 +225,10 @@ receipt_claims = ["provenance", "oer", "jwp_hash", "staged", "dep:curve25519-dal # modules (§7.9). Adds no dependency: it signs and hashes with what `receipt_claims` # already brings, and a module digest is the SHA-256 `oer` already brings. key_discovery = ["receipt_claims"] +# The `wai` CLI's TLS client for anchoring an origin (`wai http receipts --key-origin`, +# http-delivery §8.4): rustls with the operating system's trust store. Built into the +# binary only; without it the CLI refuses to anchor rather than fetch over plaintext. +cli_tls = ["http", "key_discovery", "dep:rustls", "dep:rustls-platform-verifier"] # Emit a REAL C2PA manifest (not just the assertion shapes). Off by default: # the SDK is a large dependency chain and most WAI consumers only need the # binding types, which `provenance` already gives them. @@ -804,6 +815,18 @@ opt-level = 3 lto = true codegen-units = 1 +# The HTTP corpus's falsification (http-conformance/falsify/run.sh): one target +# directory, each patch rebuilt incrementally on the last; overflow checks and debug +# assertions on, as in the tests, so a patch is judged by the code's own checks; enough +# optimisation that each verify runs quickly. +[profile.falsify] +inherits = "dev" +opt-level = 1 +debug = false +incremental = true +overflow-checks = true +debug-assertions = true + [[bin]] name = "wai_score_vectors" path = "src/bin/wai_score_vectors.rs" diff --git a/wai/wai-rs/deny.toml b/wai/wai-rs/deny.toml new file mode 100644 index 000000000..b931fed7a --- /dev/null +++ b/wai/wai-rs/deny.toml @@ -0,0 +1,43 @@ +# The licences the `wai` binary's HTTP commands may carry with their TLS client (the +# `cli_tls` feature: rustls and the operating system's trust store), checked in CI beside +# wai-pack's (.github/workflows/wai-pack.yml, job `licences`), under the same allow-list. +# +# The graph is the binary's HTTP features on the hosts it is built for. The library's +# other features reach the check through wai-pack's graph. rustls-platform-verifier's +# wasm32 and Android dependencies are not in it: no wasm32 or Android build enables +# `cli_tls`, and on wasm32 the library opens no connection at all. + +[graph] +no-default-features = true +features = ["http", "video_manifest", "key_discovery", "cli_tls"] +targets = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", + "x86_64-pc-windows-msvc", +] + +[licenses] +allow = [ + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "MIT", + "MIT-0", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Zlib", + "CC0-1.0", + "Unicode-3.0", + "Unlicense", +] +confidence-threshold = 0.9 +unused-allowed-license = "allow" + +# wai's build script generates its C header with cbindgen, a build-time tool: it is not +# linked into the binary, and its output is not covered by its licence. As in wai-pack's +# deny.toml, the exception names the version reviewed. +[[licenses.exceptions]] +crate = "cbindgen@0.29.4" +allow = ["MPL-2.0"] diff --git a/wai/wai-rs/src/bin/wai_cli/http.rs b/wai/wai-rs/src/bin/wai_cli/http.rs index 1b0da2942..7beb60378 100644 --- a/wai/wai-rs/src/bin/wai_cli/http.rs +++ b/wai/wai-rs/src/bin/wai_cli/http.rs @@ -45,6 +45,25 @@ //! alias with 404). --live-swap serves OTHER's object under ALIAS (a test aid) //! wai http vectors gen|verify|diff … //! the corpus tool (as `wai_http_vectors`) +//! wai http receipts --url http://HOST:PORT/PATH/.wai[#frag] [--advertise CAP]… +//! [--held fetch|none] [--join] [--max-walk N] [--fetch-budget N] +//! [--deadline-ms MS] [--key-origin https://HOST[:PORT] +//! [--pin-key-log FILE | --test-plaintext-connect HOST:PORT]] +//! fetch the object by §7 (unless --held none), then its receipt index beside it and +//! the receipt files it locates, walking parents by their links, and print what each +//! delivery item and claim established (§8.1–§8.3): its evidence, how the content +//! hash was established, the property's reading, and, with a key log pinned or +//! anchored from the origin (§8.4), each signer's confirmation (§7.4 of +//! jwp-receipts). The origin's key log is retrieved over HTTPS by this command: TLS +//! to the origin's host (port 443 by default), its certificate validated for that +//! host against the operating system's trust store, no redirect followed (a build +//! with `cli_tls`; one without refuses to anchor, never fetching it over plaintext). +//! --test-plaintext-connect is a test aid: the log fetched over plain HTTP from +//! that address, reported with the anchor path `test-plaintext` as anchoring +//! nothing a verifier may rely on, and the command exits 1. Every exchange is +//! bounded by one deadline and each body by its cap. Exit status 1 when the index, +//! an item or the anchoring is refused (a build with `receipt_claims`; confirmation +//! needs `key_discovery`) //! wai http bench --url http://HOST:PORT/PATH[#frag] --runs N --advertise CAP… --out F //! [--refinement-deadline-ms MS] //! fetch an object N times by §7 over plain HTTP and write a measurement record: @@ -89,7 +108,7 @@ fn positional(args: &[String], flags: &[&str]) -> Vec { } fn usage() -> String { - "usage: wai http publish … --out \n wai http codecs …\n wai http origin --root [--port P] [--delay-range FIRST-LAST:MS]\n wai http hls --objects --out [--master NAME]\n wai http mpd --objects --out [--period-id ID] [--set-id N]\n wai http seal-playlist --key [--signer-id ID] [--content-id ID] [--parent ] [--out ]\n wai http check-playlist --receipt [--parent ] [--classical ]\n wai http vectors gen|verify|diff …\n wai http bench --url URL --runs N --advertise CAP [--advertise CAP]… --out F".into() + "usage: wai http publish … --out \n wai http codecs …\n wai http origin --root [--port P] [--delay-range FIRST-LAST:MS]\n wai http hls --objects --out [--master NAME]\n wai http mpd --objects --out [--period-id ID] [--set-id N]\n wai http seal-playlist --key [--signer-id ID] [--content-id ID] [--parent ] [--out ]\n wai http check-playlist --receipt [--parent ] [--classical ]\n wai http vectors gen|verify|diff …\n wai http receipts --url URL [--advertise CAP]… [--held fetch|none] [--join] [--max-walk N] [--fetch-budget N] [--deadline-ms MS] [--key-origin https://HOST[:PORT] [--pin-key-log FILE | --test-plaintext-connect HOST:PORT]]\n wai http bench --url URL --runs N --advertise CAP [--advertise CAP]… --out F".into() } pub fn main(args: &[String]) -> Result { @@ -103,6 +122,7 @@ pub fn main(args: &[String]) -> Result { Some("mpd") => crate::http_playlists::mpd(&args[1..]).map(|()| true), Some("seal-playlist") => crate::http_playlists::seal(&args[1..]).map(|()| true), Some("check-playlist") => crate::http_playlists::check(&args[1..]), + Some("receipts") => receipts_cmd(&args[1..]), _ => Err(usage()), } } @@ -425,17 +445,14 @@ fn resource(cfg: &OriginConfig, path: &str) -> Option<(Vec, Vec<(&'static st Some((prefix, max_age)) if rel.starts_with(prefix.as_str()) => origin::live_object_headers(&cid, codecs.as_deref(), *max_age), _ => origin::object_headers(&cid, codecs.as_deref()), } + } else if rel == ".well-known/wai-key-status" { + origin::key_status_headers(status_max_age(root, &bytes)) } else if rel.starts_with(".well-known/wai-") { origin::key_resource_headers() } else if name.ends_with(".wai.hp") { origin::immutable_headers("application/octet-stream") } else if name.ends_with(".wai.receipts") { - vec![ - ("Content-Type", "application/json".to_string()), - ("Cache-Control", "no-cache, no-transform".to_string()), - ("ETag", format!("\"{}\"", hex(&content_hash(&bytes)))), - ("Access-Control-Allow-Origin", "*".to_string()), - ] + origin::receipt_index_headers(&bytes) } else if name.ends_with(".m3u8") || name.ends_with(".mpd") { // A playlist changes at a live edge: revalidated on every use (§11). origin::playlist_headers(if name.ends_with(".mpd") { "application/dash+xml" } else { "application/vnd.apple.mpegurl" }) @@ -447,6 +464,24 @@ fn resource(cfg: &OriginConfig, path: &str) -> Option<(Vec, Vec<(&'static st Some((bytes, headers)) } +/// The `max-age` of the key status `status` served under `root` (§8.4): a quarter of the +/// `status_max_age_s` of the revision it names in the key log beside it. `None` where the +/// status or the log does not read, or names no revision of it. +#[cfg(feature = "key_discovery")] +fn status_max_age(root: &std::path::Path, status: &[u8]) -> Option { + let st = wai::key_log::KeyStatus::from_json(std::str::from_utf8(status).ok()?)?; + let log = std::fs::read_to_string(root.join(".well-known/wai-keys")).ok()?; + let head = wai::key_log::read_log(&st.origin, &log).ok()?.into_iter().find(|r| r.revision_hash() == st.head)?; + Some(wai::http_binding::keys::status_max_age_s(&head)) +} + +/// Without `key_discovery` the origin reads no status: it is served for revalidation on +/// every use. +#[cfg(not(feature = "key_discovery"))] +fn status_max_age(_: &std::path::Path, _: &[u8]) -> Option { + None +} + /// Write `bytes` to `s`, never past `until` (the end of the connection's lifetime): in /// pieces, each write waiting at most until then, so a peer that reads slowly cannot hold /// the connection longer. @@ -830,3 +865,852 @@ fn bench(args: &[String]) -> Result<(), String> { println!("wrote {out}"); Ok(()) } + +// ---------------------------------------------------------------- receipts (§8) + +/// A plain-HTTP `GET` of `path` from `addr` with `Host: host`, every step before +/// `deadline` (the lookup, each address tried in turn, every read and write), keeping at +/// most `max_body` bytes of the body (one more marks it longer), and following no +/// redirect. The head may be at most 64 KiB. +#[cfg(feature = "receipt_claims")] +fn get_bounded(addr: &str, host: &str, path: &str, headers: &[(&str, &str)], deadline: Instant, max_body: usize) + -> Result<(ResponseMeta, Vec), String> { + let addrs = resolve(addr.to_string(), deadline)?; + let mut s = DeadlineSocket { tcp: connect_any(&addrs, deadline)?, deadline }; + let req = request(path, host, headers); + exchange(&mut s, req.as_bytes(), deadline, max_body, false) +} + +/// A TCP connection every read and write of which is bounded by `deadline`: each call +/// waits at most what remains, and once the deadline has passed a call is `TimedOut` +/// without touching the socket. A peer that trickles bytes, or a TLS handshake that +/// loops over many reads, cannot extend it. +#[cfg(feature = "receipt_claims")] +struct DeadlineSocket { + tcp: TcpStream, + deadline: Instant, +} + +#[cfg(feature = "receipt_claims")] +impl DeadlineSocket { + fn remaining(&self) -> std::io::Result { + self.deadline + .checked_duration_since(Instant::now()) + .filter(|l| !l.is_zero()) + .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::TimedOut, "the deadline passed")) + } +} + +#[cfg(feature = "receipt_claims")] +fn timed_out(e: std::io::Error) -> std::io::Error { + match e.kind() { + std::io::ErrorKind::WouldBlock => std::io::Error::new(std::io::ErrorKind::TimedOut, "the deadline passed"), + _ => e, + } +} + +#[cfg(feature = "receipt_claims")] +impl Read for DeadlineSocket { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let d = self.remaining()?; + self.tcp.set_read_timeout(Some(d))?; + self.tcp.read(buf).map_err(timed_out) + } +} + +#[cfg(feature = "receipt_claims")] +impl Write for DeadlineSocket { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let d = self.remaining()?; + self.tcp.set_write_timeout(Some(d))?; + self.tcp.write(buf).map_err(timed_out) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.tcp.flush() + } +} + +/// The addresses `target` (`host:port`) names, looked up within `deadline`: the lookup +/// runs on a thread of its own, and a lookup still running when the deadline passes is +/// left to finish unread. +#[cfg(feature = "receipt_claims")] +fn resolve(target: String, deadline: Instant) -> Result, String> { + use std::net::ToSocketAddrs; + let (tx, rx) = std::sync::mpsc::channel(); + let t = target.clone(); + std::thread::spawn(move || { + let _ = tx.send(t.to_socket_addrs().map(|a| a.collect::>())); + }); + match rx.recv_timeout(left(deadline)?) { + Ok(Ok(a)) if !a.is_empty() => Ok(a), + Ok(Ok(_)) => Err(format!("{target}: no address")), + Ok(Err(e)) => Err(format!("{target}: {e}")), + Err(_) => Err(format!("{target}: the deadline passed resolving it")), + } +} + +/// A connection to the first of `addrs` that answers, each tried in turn within +/// `deadline`. +#[cfg(feature = "receipt_claims")] +fn connect_any(addrs: &[std::net::SocketAddr], deadline: Instant) -> Result { + let mut last = "no address".to_string(); + for a in addrs { + match TcpStream::connect_timeout(a, left(deadline)?) { + Ok(s) => return Ok(s), + Err(e) => last = format!("{a}: {e}"), + } + } + Err(last) +} + +/// What is left before `deadline`, or that it passed. +#[cfg(feature = "receipt_claims")] +fn left(deadline: Instant) -> Result { + deadline.checked_duration_since(Instant::now()).filter(|l| !l.is_zero()).ok_or("the deadline passed".to_string()) +} + +/// A `GET` request's bytes, asking for the body as it is stored and the connection closed. +#[cfg(feature = "receipt_claims")] +fn request(path: &str, host: &str, headers: &[(&str, &str)]) -> String { + let mut req = format!("GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\nAccept-Encoding: identity\r\n"); + for (k, v) in headers { + req.push_str(&format!("{k}: {v}\r\n")); + } + req.push_str("\r\n"); + req +} + +/// One exchange on `s`, a transport whose every read and write is bounded by +/// `deadline` ([`DeadlineSocket`], or TLS over one): send `req`, then read the response, +/// keeping at most `max_body` bytes of the body, reading it no further than one byte +/// past that. A chunked body is decoded; a body framed by `Content-Length` that ends +/// short is refused. Over TLS (`tls`), the end of a response is trusted only when the +/// peer closed with `close_notify` or the body's own framing ended it: a connection cut +/// before a framed body ends, or an unframed body without `close_notify`, is refused, so +/// no truncation passes for a whole response. +#[cfg(feature = "receipt_claims")] +fn exchange( + s: &mut S, + req: &[u8], + deadline: Instant, + max_body: usize, + tls: bool, +) -> Result<(ResponseMeta, Vec), String> { + left(deadline)?; + let io = |e: std::io::Error| match e.kind() { + std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut => "the deadline passed".to_string(), + _ => e.to_string(), + }; + s.write_all(req).map_err(io)?; + s.flush().map_err(io)?; + let mut raw = Vec::new(); + let mut chunk = [0u8; 64 * 1024]; + let mut head_end: Option = None; + let mut capped = false; + let mut clean_end = true; + loop { + left(deadline)?; + // Before the head ends, small reads, so little of the body comes with it; after, + // no further than one byte past the body's cap. + let want = match head_end { + None => 1024, + Some(e) => (max_body + 1).saturating_sub(raw.len() - e).clamp(1, chunk.len()), + }; + match s.read(&mut chunk[..want]) { + Ok(0) => break, + Ok(n) => raw.extend_from_slice(&chunk[..n]), + Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => { + clean_end = false; + break; + } + Err(e) => return Err(io(e)), + } + if head_end.is_none() { + head_end = raw.windows(4).position(|w| w == b"\r\n\r\n").map(|p| p + 4); + if head_end.is_none() && raw.len() > 64 * 1024 { + return Err("a response head past 64 KiB".into()); + } + } + if let Some(e) = head_end + && raw.len() - e > max_body + { + raw.truncate(e + max_body + 1); + capped = true; + break; + } + } + let end = head_end.ok_or("no header end")?; + let head = String::from_utf8_lossy(&raw[..end]).to_string(); + let mut lines = head.lines(); + let status: u16 = lines.next().and_then(|l| l.split_whitespace().nth(1)).and_then(|s| s.parse().ok()).ok_or("no status")?; + let mut meta = ResponseMeta { status, ..Default::default() }; + let (mut length, mut chunked) = (None::, false); + for l in lines { + if let Some((k, v)) = l.split_once(':') { + match k.trim().to_ascii_lowercase().as_str() { + "content-range" => meta.content_range = Some(v.trim().to_string()), + "content-encoding" => meta.content_encoding = Some(v.trim().to_string()), + "content-length" => length = Some(v.trim().parse().map_err(|_| "a Content-Length that is not a number")?), + "transfer-encoding" => chunked = v.split(',').any(|t| t.trim().eq_ignore_ascii_case("chunked")), + _ => {} + } + } + } + let body = raw[end..].to_vec(); + if capped { + return Ok((meta, body)); + } + if chunked { + return dechunk(&body).map(|b| (meta, b)).ok_or_else(|| "a chunked body that does not end".to_string()); + } + match length { + Some(n) if body.len() < n => Err("the body ended before its Content-Length".into()), + Some(n) => Ok((meta, body[..n].to_vec())), + None if tls && !clean_end => Err("the response's end is not authenticated: the connection closed without close_notify".into()), + None => Ok((meta, body)), + } +} + +/// A chunked body (RFC 9112 §7.1) decoded, or `None` when it does not end with its last +/// chunk. +#[cfg(feature = "receipt_claims")] +fn dechunk(mut b: &[u8]) -> Option> { + let mut out = Vec::new(); + loop { + let line = b.windows(2).position(|w| w == b"\r\n")?; + let size = std::str::from_utf8(&b[..line]).ok()?.split(';').next()?.trim(); + let n = usize::from_str_radix(size, 16).ok()?; + b = &b[line + 2..]; + if n == 0 { + return Some(out); + } + if b.len() < n + 2 || &b[n..n + 2] != b"\r\n" { + return None; + } + out.extend_from_slice(&b[..n]); + b = &b[n + 2..]; + } +} + +/// The host and path of a plain-HTTP URL, and the object's cid (from `#cid=` or the +/// `.wai` name). +#[cfg(feature = "receipt_claims")] +fn object_url(url: &str) -> Result<(String, String, [u8; 32], wai::http_binding::Fragment), String> { + let (base, frag) = parse_fragment(url); + let rest = base.strip_prefix("http://").ok_or("this command fetches over plain http:// only")?; + let (host, path) = rest.split_once('/').map(|(h, p)| (h.to_string(), format!("/{p}"))).ok_or("URL has no path")?; + let stem = path.rsplit('/').next().and_then(|n| n.strip_suffix(".wai")).ok_or("the URL names .wai")?; + let cid = frag.cid.or_else(|| unhex32(stem)).ok_or("no cid: name the object .wai or give #cid=")?; + Ok((host, path, cid, frag)) +} + +#[cfg(not(feature = "receipt_claims"))] +fn receipts_cmd(_: &[String]) -> Result { + Err("built without receipt_claims: `wai http receipts` needs it".into()) +} + +/// The largest object response `wai http receipts` keeps (§7): a byte range, or the rest +/// of an object, that runs past this ends the command. +#[cfg(feature = "receipt_claims")] +const MAX_OBJECT_RESPONSE: usize = 1 << 30; + +/// `wai http receipts`: §7 then §8.2 over plain HTTP, every exchange bounded by one +/// deadline (`--deadline-ms`, 60 s by default) and each body by its cap. Every receipt file +/// it fetches is checked as §8.1 states, by `http_binding::receipts`: no other path reads +/// one. With `--key-origin`, the origin's key log is anchored as §8.4 states before any +/// signer is confirmed ([`keys`]): retrieved over HTTPS by this command, with +/// `Cache-Control: no-cache`, no redirect followed, the retrieval built from that request +/// and its response. +#[cfg(feature = "receipt_claims")] +fn receipts_cmd(args: &[String]) -> Result { + use wai::http_binding::receipts::{check_index, Limits, ObjectHeld, ReceiptIndex, FETCH_BUDGET, MAX_INDEX_BYTES, PARENT_WALK_BOUND, RECEIPT_DIR}; + use wai::jwp_delivery::ReceiptChains; + let (host, path, cid, frag) = object_url(opt(args, "--url").ok_or("receipts needs --url")?)?; + let deadline_ms: u64 = opt(args, "--deadline-ms").map_or(Ok(60_000), str::parse).map_err(|_| "--deadline-ms")?; + let deadline = Instant::now() + Duration::from_millis(deadline_ms); + let adv: Vec = args.iter().enumerate().filter(|(_, a)| *a == "--advertise").filter_map(|(i, _)| args.get(i + 1).cloned()).collect(); + let mut report = serde_json::Map::new(); + report.insert("object".into(), json!(hex(&cid))); + let late = std::cell::Cell::new(false); + let held = match opt(args, "--held").unwrap_or("fetch") { + "none" => ObjectHeld::Nothing, + "fetch" => { + let mut s = FetchSession::new(cid, frag, FetchPolicy::default(), Selector::new(move |c| adv.iter().any(|a| a == c))); + let r = wai::http_binding::session::drive(&mut s, |w| { + let (p, range) = match w { + Want::HeadProof => (format!("{path}.hp"), None), + Want::Whole(_) => (path.clone(), None), + Want::Range(r, _) => (path.clone(), Some(r.header())), + Want::Rest(r, _) => (path.clone(), Some(format!("bytes={}-", r.first))), + }; + let hdrs: Vec<(&str, &str)> = range.as_deref().map(|r| ("Range", r)).into_iter().collect(); + match get_bounded(&host, &host, &p, &hdrs, deadline, MAX_OBJECT_RESPONSE) { + Ok(x) => Some(x), + Err(e) => { + late.set(late.get() || e.contains("deadline")); + None + } + } + }); + if late.get() { + return Err("the deadline passed fetching the object".into()); + } + report.insert("fetch".into(), json!(match &r { Ok(f) => f.verified_by.label(), Err(e) => e.code() })); + s.established().map_or(ObjectHeld::Nothing, ObjectHeld::Object) + } + other => return Err(format!("--held {other}: fetch or none")), + }; + report.insert("held".into(), json!(match held { ObjectHeld::Object(e) => e.by().label(), ObjectHeld::Nothing => "nothing" })); + let (index_meta, index) = get_bounded(&host, &host, &format!("{path}.receipts"), &[], deadline, MAX_INDEX_BYTES)?; + if index_meta.status != 200 { + return Err(format!("no receipt index is served beside the object ({})", index_meta.status)); + } + // Receipt files resolve against the object's URI (§8.1). + let dir = path.rsplit_once('/').map_or("", |(d, _)| d).to_string(); + // Each file is read no further than the fetcher allows: what remains of the index's + // bytes, and of the file's cap plus one byte (§8.1, §8.2). + let mut fetch = |h: &[u8; 32], limit: usize| -> Option> { + match get_bounded(&host, &host, &format!("{dir}/{RECEIPT_DIR}{}.json", hex(h)), &[], deadline, limit.saturating_sub(1)) { + Ok((m, b)) => (m.status == 200).then_some(b), + Err(e) => { + late.set(late.get() || e.contains("deadline")); + None + } + } + }; + let mut chains = if args.iter().any(|a| a == "--join") { ReceiptChains::new().with_join() } else { ReceiptChains::new() }; + let limits = Limits { + max_walk: opt(args, "--max-walk").map_or(Ok(PARENT_WALK_BOUND), str::parse).map_err(|_| "--max-walk")?, + fetch_budget: opt(args, "--fetch-budget").map_or(Ok(FETCH_BUDGET), str::parse).map_err(|_| "--fetch-budget")?, + }; + let mut all = true; + let keys = match keys(args, deadline, &mut report)? { + Ok(k) => k, + Err(r) => { + all = false; + report.insert("anchor".into(), json!({ "refused": r.code, "why": r.why })); + None + } + }; + // A test-only plaintext anchor confirms this run's signers and nothing more: the + // command does not succeed on it. + if report.get("anchor").and_then(|a| a.get("path")).and_then(|p| p.as_str()) == Some("test-plaintext") { + all = false; + } + let items = ReceiptIndex::from_bytes(&index).map(|i| i.delivery).unwrap_or_default(); + let checked = check_index(&cid, &index, &held, &mut fetch, &mut chains, 0, &limits, None); + if late.get() { + return Err("the deadline passed fetching receipt files".into()); + } + match checked { + Err(e) => { + all = false; + report.insert("index".into(), json!(e.to_string())); + } + Ok(r) => { + report.insert("index".into(), json!("ok")); + report.insert("files_fetched".into(), json!(r.fetched)); + let mut ds = Vec::new(); + for (k, d) in r.delivery.iter().enumerate() { + ds.push(match d { + Ok(v) => { + let mut o = json!({ "admission": v.admission.label(), "walked": v.walked, "evidence": v.evidence.label(), + "established": v.established.map(|e| e.label()), "reading": v.reading.as_ref().map(|x| format!("{x:?}")) }); + if let (Some((revs, anchors)), Some(Ok(it))) = (&keys, items.get(k)) { + o["confirm"] = json!(confirm_group(&v.group, &it.object_receipt, revs, anchors)); + } + o + } + Err(e) => { + all = false; + json!({ "refused": e.to_string() }) + } + }); + } + report.insert("delivery".into(), json!(ds)); + let mut cs = Vec::new(); + for c in &r.claims { + cs.push(match c { + Ok(v) => { + let mut o = json!({ "class": v.claim.class().label(), "attests": v.attests.label(), "parent": v.parent.map(|p| hex(&p)) }); + if let Some((revs, anchors)) = &keys { + o["confirm"] = json!(confirm_claim(&v.claim, revs, anchors)); + } + o + } + Err(e) => { + all = false; + json!({ "refused": e.to_string() }) + } + }); + } + report.insert("claims".into(), json!(cs)); + } + } + println!("{}", serde_json::to_string_pretty(&serde_json::Value::Object(report)).expect("json")); + Ok(all) +} + +/// The key log the signers are confirmed against, for `--key-origin ORIGIN` +/// (http-delivery §8.4), and the anchoring's refusal, if any: +/// +/// - with `--pin-key-log FILE`, pinned from that file; +/// - otherwise retrieved from the origin's key-log location over HTTPS by this command +/// itself ([`anchor_over_tls`]): TLS to the origin's host and port (443 by default), +/// its certificate validated for that host against the operating system's trust store, +/// asking with `Cache-Control: no-cache`, following no redirect, and the retrieval +/// built from that request and its response alone. A build without the `cli_tls` +/// feature refuses to anchor (`key_log_not_from_origin`): it never fetches the log +/// over plaintext; +/// - with `--test-plaintext-connect HOST:PORT`, a test aid: the log fetched over plain +/// HTTP from that address. The anchor is reported with the path `test-plaintext`, +/// never `fetched`, as anchoring nothing a verifier may rely on, and the command exits +/// with status 1. +#[cfg(all(feature = "receipt_claims", feature = "key_discovery"))] +type Keys = Option<(Vec, Vec)>; +#[cfg(all(feature = "receipt_claims", not(feature = "key_discovery")))] +type Keys = Option<((), ())>; + +/// Why anchoring was refused: the reason code, and what happened. +#[cfg(feature = "receipt_claims")] +struct AnchorRefusal { + code: String, + why: String, +} + +#[cfg(all(feature = "receipt_claims", feature = "key_discovery"))] +fn keys(args: &[String], deadline: Instant, report: &mut serde_json::Map) -> Result, String> { + use wai::http_binding::keys::{anchoring_request_headers, KEY_LOG_PATH}; + use wai::key_log::{anchor_log, AnchorPath, Anchoring}; + let Some(origin) = opt(args, "--key-origin") else { return Ok(Ok(None)) }; + if args.iter().any(|a| a == "--connect") { + return Err("--connect is gone: the key log is retrieved over HTTPS (--test-plaintext-connect HOST:PORT is a test aid that anchors nothing a verifier may rely on)".into()); + } + let refuse = |code: &str, why: String| Ok(Err(AnchorRefusal { code: code.into(), why })); + let not_from_origin = "key_log_not_from_origin"; + let (text, anchor) = match (opt(args, "--pin-key-log"), opt(args, "--test-plaintext-connect")) { + (Some(file), _) => { + let text = std::fs::read_to_string(file).map_err(|e| format!("{file}: {e}"))?; + match anchor_log(origin, &text, AnchorPath::Pinned, None) { + Anchoring::Anchored(a) => { + report.insert("anchor".into(), json!({ "revision": hex(&a.revision), "path": a.path.label() })); + (text, a) + } + Anchoring::NotAnchored(r) => return refuse(r.label(), "the pinned log does not anchor".into()), + Anchoring::Replaced => return refuse("key_log_replaced", "the pinned log does not anchor".into()), + } + } + (None, Some(addr)) => { + // A test aid: plain HTTP from a stated address. What it retrieves anchors this + // run's confirmations and nothing a verifier may rely on. + let host = origin.split_once("://").map_or(origin, |(_, h)| h); + let (meta, body) = get_bounded(addr, host, KEY_LOG_PATH, &anchoring_request_headers(), deadline, + wai::http_binding::keys::MAX_KEY_LOG_BYTES)?; + if meta.status != 200 { + return refuse(not_from_origin, format!("the answer is a {}, not a 200; no redirect is followed", meta.status)); + } + if body.len() > wai::http_binding::keys::MAX_KEY_LOG_BYTES { + return refuse("key_log_too_large", "a key log longer than its cap".into()); + } + let text = String::from_utf8_lossy(&body).into_owned(); + match anchor_log(origin, &text, AnchorPath::TestPlaintext { acquired_at: now_rfc3339() }, None) { + Anchoring::Anchored(a) => { + report.insert("anchor".into(), json!({ "revision": hex(&a.revision), "path": a.path.label(), "relied_on": false, + "note": "a plaintext retrieval made for tests: it anchors nothing a verifier may rely on" })); + (text, a) + } + Anchoring::NotAnchored(r) => return refuse(r.label(), "the log does not anchor".into()), + Anchoring::Replaced => return refuse("key_log_replaced", "the log does not anchor".into()), + } + } + (None, None) => match anchor_over_tls(origin, deadline) { + Ok((text, a)) => { + report.insert("anchor".into(), json!({ "revision": hex(&a.revision), "path": a.path.label() })); + (text, a) + } + Err(r) => return Ok(Err(r)), + }, + }; + let revs = wai::key_log::read_log(origin, &text).map_err(|r| r.label().to_string())?; + Ok(Ok(Some((revs, vec![anchor])))) +} + +/// Now, as RFC 3339 UTC: when a log was acquired. +#[cfg(all(feature = "receipt_claims", feature = "key_discovery"))] +fn now_rfc3339() -> String { + let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs()); + wai::claim_receipt::rfc3339_utc(now) +} + +/// An `https` origin as §8.4 anchors it: `https://` and an authority, a host and an +/// optional port (443 when absent), with no user information, path, query or fragment. +#[cfg(all(feature = "receipt_claims", feature = "key_discovery", feature = "cli_tls"))] +#[derive(Debug, PartialEq, Eq)] +struct HttpsOrigin { + /// The host, as SNI and the certificate check name it (an IPv6 literal without its + /// brackets). + host: String, + port: u16, + /// The authority as the origin states it: the `Host` header and the URL requested. + authority: String, +} + +#[cfg(all(feature = "receipt_claims", feature = "key_discovery", feature = "cli_tls"))] +impl HttpsOrigin { + fn parse(origin: &str) -> Option { + let authority = origin.strip_prefix("https://")?; + if authority.is_empty() || authority.contains(['/', '?', '#', '@', ' ', '\\']) { + return None; + } + let port_of = |p: &str| (!p.is_empty() && p.bytes().all(|b| b.is_ascii_digit())).then(|| p.parse::().ok()).flatten().filter(|p| *p != 0); + let (host, port) = match authority.strip_prefix('[') { + Some(rest) => { + let (h, after) = rest.split_once(']')?; + let port = match after { + "" => 443, + p => port_of(p.strip_prefix(':')?)?, + }; + (h.to_string(), port) + } + None => match authority.split_once(':') { + Some((h, p)) => (h.to_string(), port_of(p)?), + None => (authority.to_string(), 443), + }, + }; + if host.is_empty() { + return None; + } + Some(HttpsOrigin { host, port, authority: authority.to_string() }) + } + + /// The URL of the origin's key log, as requested. + fn key_log_location(&self) -> String { + format!("https://{}{}", self.authority, wai::http_binding::keys::KEY_LOG_PATH) + } +} + +/// §8.4 over TLS: resolve the origin's host, then [`anchor_from`] with the operating +/// system's trust store. +#[cfg(all(feature = "receipt_claims", feature = "key_discovery", feature = "cli_tls"))] +fn anchor_over_tls(origin: &str, deadline: Instant) -> Result<(String, wai::key_log::Anchor), AnchorRefusal> { + use rustls_platform_verifier::BuilderVerifierExt; + let refuse = |why: String| AnchorRefusal { code: "key_log_not_from_origin".into(), why }; + let o = HttpsOrigin::parse(origin).ok_or_else(|| refuse("not an https origin: https:// and a host, with no path".into()))?; + let target = if o.host.contains(':') { format!("[{}]:{}", o.host, o.port) } else { format!("{}:{}", o.host, o.port) }; + let addrs = resolve(target, deadline).map_err(refuse)?; + let provider = std::sync::Arc::new(rustls::crypto::ring::default_provider()); + let config = rustls::ClientConfig::builder_with_provider(provider) + .with_safe_default_protocol_versions() + .and_then(|b| b.with_platform_verifier()) + .map_err(|e| refuse(format!("TLS: {e}")))? + .with_no_client_auth(); + anchor_from(&o, &addrs, std::sync::Arc::new(config), deadline) +} + +/// A build without TLS never anchors an origin: it would have to fetch the log over +/// plaintext. +#[cfg(all(feature = "receipt_claims", feature = "key_discovery", not(feature = "cli_tls")))] +fn anchor_over_tls(_: &str, _: Instant) -> Result<(String, wai::key_log::Anchor), AnchorRefusal> { + Err(AnchorRefusal { + code: "key_log_not_from_origin".into(), + why: "this build has no TLS client (the cli_tls feature): it never fetches a key log over plaintext".into(), + }) +} + +/// §8.4: `GET` the key log of `o` over TLS from the first of `addrs` that answers, SNI +/// and the certificate check naming the origin's host under `config`, and anchor the +/// origin from it. The retrieval is what this request asked for and what answered it on +/// the same connection: the origin's key-log location, the status received, no redirect +/// followed. A log longer than its cap is `key_log_too_large`, read no further than one +/// byte past it. +#[cfg(all(feature = "receipt_claims", feature = "key_discovery", feature = "cli_tls"))] +fn anchor_from( + o: &HttpsOrigin, + addrs: &[std::net::SocketAddr], + config: std::sync::Arc, + deadline: Instant, +) -> Result<(String, wai::key_log::Anchor), AnchorRefusal> { + use wai::http_binding::keys::{anchor_retrieved, anchoring_request_headers, Retrieval, KEY_LOG_PATH, MAX_KEY_LOG_BYTES}; + let refuse = |why: String| AnchorRefusal { code: "key_log_not_from_origin".into(), why }; + let (meta, body) = get_tls(addrs, &o.host, &o.authority, KEY_LOG_PATH, &anchoring_request_headers(), deadline, + MAX_KEY_LOG_BYTES, config).map_err(refuse)?; + let location = o.key_log_location(); + let retrieval = Retrieval { requested: &location, answered: &location, status: meta.status }; + let origin = format!("https://{}", o.authority); + let a = anchor_retrieved(&origin, &retrieval, &body, &now_rfc3339(), None) + .map_err(|e| AnchorRefusal { code: e.code().to_string(), why: e.to_string() })?; + Ok((String::from_utf8_lossy(&body).into_owned(), a)) +} + +/// A `GET` over TLS: connect to `addr`, handshake naming `host` (SNI, and the name the +/// certificate is validated for), send the request with `Host: authority`, and read the +/// response as [`exchange`] does, its end authenticated. +#[cfg(all(feature = "receipt_claims", feature = "cli_tls"))] +#[allow(clippy::too_many_arguments)] +fn get_tls( + addrs: &[std::net::SocketAddr], + host: &str, + authority: &str, + path: &str, + headers: &[(&str, &str)], + deadline: Instant, + max_body: usize, + config: std::sync::Arc, +) -> Result<(ResponseMeta, Vec), String> { + let name = rustls::pki_types::ServerName::try_from(host.to_string()).map_err(|_| format!("{host}: not a server name"))?; + let tcp = DeadlineSocket { tcp: connect_any(addrs, deadline)?, deadline }; + let conn = rustls::ClientConnection::new(config, name).map_err(|e| format!("TLS: {e}"))?; + let mut s = rustls::StreamOwned::new(conn, tcp); + let req = request(path, authority, headers); + exchange(&mut s, req.as_bytes(), deadline, max_body, true).map_err(|e| format!("TLS: {e}")) +} + +#[cfg(all(feature = "receipt_claims", not(feature = "key_discovery")))] +fn keys(args: &[String], _: Instant, _: &mut serde_json::Map) -> Result, String> { + if opt(args, "--key-origin").is_some() { + return Err("built without key_discovery: --key-origin needs it".into()); + } + Ok(Ok(None)) +} + +#[cfg(all(feature = "receipt_claims", feature = "key_discovery"))] +fn confirm_group(g: &wai::jwp_delivery::GroupReceipt, o: &wai::jwp_delivery::ObjectReceipt, revs: &[wai::key_log::Revision], anchors: &[wai::key_log::Anchor]) -> Option { + wai::http_binding::keys::confirm_delivery(g, o, revs, anchors).map(|c| c.verdict()) +} + +#[cfg(all(feature = "receipt_claims", not(feature = "key_discovery")))] +fn confirm_group(_: &wai::jwp_delivery::GroupReceipt, _: &wai::jwp_delivery::ObjectReceipt, _: &(), _: &()) -> Option { + None +} + +#[cfg(all(feature = "receipt_claims", feature = "key_discovery"))] +fn confirm_claim(c: &wai::claim_receipt::ClaimReceipt, revs: &[wai::key_log::Revision], anchors: &[wai::key_log::Anchor]) -> String { + wai::http_binding::keys::confirm_claim(c, revs, anchors).verdict() +} + +#[cfg(all(feature = "receipt_claims", not(feature = "key_discovery")))] +fn confirm_claim(_: &wai::claim_receipt::ClaimReceipt, _: &(), _: &()) -> String { + String::new() +} + +/// §8.4 over TLS, against servers under a test CA (`tests/fixtures/tls/`: a CA and +/// certificates for `origin.example` and `other.example`, test keys that attest nothing). +/// The client trusts that CA alone here; the command trusts the operating system's store. +#[cfg(all(test, feature = "receipt_claims", feature = "key_discovery", feature = "cli_tls"))] +mod tls_tests { + use super::*; + use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; + use std::net::SocketAddr; + use std::sync::{mpsc, Arc}; + + fn fixture(name: &str) -> Vec { + std::fs::read(format!("{}/tests/fixtures/tls/{name}", env!("CARGO_MANIFEST_DIR"))).expect("a TLS fixture") + } + + fn provider() -> Arc { + Arc::new(rustls::crypto::ring::default_provider()) + } + + fn client() -> Arc { + let mut roots = rustls::RootCertStore::empty(); + roots.add(CertificateDer::from(fixture("ca.cert.der"))).expect("the test CA"); + Arc::new( + rustls::ClientConfig::builder_with_provider(provider()) + .with_safe_default_protocol_versions() + .expect("versions") + .with_root_certificates(roots) + .with_no_client_auth(), + ) + } + + #[derive(Clone, Copy)] + enum Mode { + /// The key log, `Content-Length` framed, closed with `close_notify`. + Log, + /// A redirect to a mirror. + Redirect, + /// The key log unframed, the connection cut without `close_notify`. + Unframed, + /// No TLS at all: a plain-HTTP server. + Plain, + /// A peer that reads the ClientHello and then sends a byte at a time, slowly, + /// never finishing the handshake. + Drip, + /// The key log padded past its cap, `Content-Length` framed, closed with + /// `close_notify`. + OverCap, + } + + /// A server with the certificate of `name` answering one connection as `mode` says; + /// what it saw, the SNI and the request head (or, for `Plain`, the first bytes). + fn server(name: &'static str, mode: Mode) -> (SocketAddr, mpsc::Receiver<(Option, Vec)>) { + let l = TcpListener::bind("127.0.0.1:0").expect("bind"); + let addr = l.local_addr().expect("addr"); + let (tx, rx) = mpsc::channel(); + let body = std::fs::read(format!("{}/../http-conformance/.well-known/wai-keys", env!("CARGO_MANIFEST_DIR"))).expect("the corpus log"); + std::thread::spawn(move || { + let Ok((mut tcp, _)) = l.accept() else { return }; + let _ = tcp.set_read_timeout(Some(Duration::from_secs(10))); + if let Mode::Plain = mode { + let mut first = [0u8; 8]; + let n = tcp.read(&mut first).unwrap_or(0); + let _ = tcp.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + let _ = tx.send((None, first[..n].to_vec())); + return; + } + if let Mode::Drip = mode { + let mut first = [0u8; 512]; + let n = tcp.read(&mut first).unwrap_or(0); + let _ = tx.send((None, first[..n].to_vec())); + // A handshake record's header, then its body a byte at a time: every read + // the client makes gets a byte before any timeout of its own runs out. + for b in [0x16u8, 0x03, 0x03, 0x40, 0x00].into_iter().chain(std::iter::repeat(0x02).take(100)) { + if tcp.write_all(&[b]).is_err() { + return; + } + std::thread::sleep(Duration::from_millis(300)); + } + return; + } + let config = rustls::ServerConfig::builder_with_provider(provider()) + .with_safe_default_protocol_versions() + .expect("versions") + .with_no_client_auth() + .with_single_cert( + vec![CertificateDer::from(fixture(&format!("{name}.cert.der")))], + PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(fixture(&format!("{name}.key.der")))), + ) + .expect("a server certificate"); + let conn = rustls::ServerConnection::new(Arc::new(config)).expect("a server connection"); + let mut s = rustls::StreamOwned::new(conn, tcp); + let mut head = Vec::new(); + let mut b = [0u8; 1]; + while !head.ends_with(b"\r\n\r\n") { + match s.read(&mut b) { + Ok(1) => head.push(b[0]), + _ => break, + } + } + let sni = s.conn.server_name().map(String::from); + let answer: Vec = match mode { + Mode::Log => [format!("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n", body.len()).into_bytes(), body].concat(), + Mode::OverCap => { + let mut b = body; + b.resize(1_048_621, b' '); + [format!("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n", b.len()).into_bytes(), b].concat() + } + Mode::Redirect => b"HTTP/1.1 302 Found\r\nLocation: https://mirror.example/.well-known/wai-keys\r\nContent-Length: 0\r\n\r\n".to_vec(), + _ => [b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n".to_vec(), body].concat(), + }; + let _ = s.write_all(&answer); + if !matches!(mode, Mode::Unframed) { + s.conn.send_close_notify(); + } + let _ = s.flush(); + let _ = tx.send((sni, head)); + }); + (addr, rx) + } + + fn origin() -> HttpsOrigin { + HttpsOrigin::parse("https://origin.example").expect("an https origin") + } + + fn deadline() -> Instant { + Instant::now() + Duration::from_secs(10) + } + + #[test] + fn an_origin_anchors_from_a_tls_retrieval_it_made() { + let (addr, seen) = server("origin", Mode::Log); + let (text, anchor) = anchor_from(&origin(), &[addr], client(), deadline()).unwrap_or_else(|r| panic!("{}: {}", r.code, r.why)); + assert_eq!(anchor.path.label(), "fetched"); + assert!(text.contains("wai-key-log")); + let (sni, head) = seen.recv().expect("the server's view"); + assert_eq!(sni.as_deref(), Some("origin.example"), "SNI names the origin's host"); + let head = String::from_utf8(head).expect("a request head"); + assert!(head.starts_with("GET /.well-known/wai-keys HTTP/1.1\r\n"), "{head}"); + assert!(head.contains("\r\nHost: origin.example\r\n") && head.contains("\r\nCache-Control: no-cache\r\n"), "{head}"); + } + + #[test] + fn a_certificate_for_another_host_anchors_nothing() { + let (addr, _) = server("other", Mode::Log); + let r = anchor_from(&origin(), &[addr], client(), deadline()).expect_err("another host's certificate"); + assert_eq!(r.code, "key_log_not_from_origin"); + assert!(r.why.contains("certificate"), "{}", r.why); + } + + #[test] + fn a_plaintext_server_is_never_read() { + let (addr, seen) = server("origin", Mode::Plain); + let r = anchor_from(&origin(), &[addr], client(), deadline()).expect_err("no TLS"); + assert_eq!(r.code, "key_log_not_from_origin"); + let (_, first) = seen.recv().expect("the server's view"); + assert_eq!(first.first(), Some(&0x16), "the client opened with a TLS handshake, never a plaintext request"); + } + + #[test] + fn a_redirect_over_tls_anchors_nothing() { + let (addr, _) = server("origin", Mode::Redirect); + let r = anchor_from(&origin(), &[addr], client(), deadline()).expect_err("a redirect"); + assert_eq!(r.code, "key_log_not_from_origin"); + assert!(r.why.contains("200"), "{}", r.why); + } + + #[test] + fn a_response_whose_end_is_not_authenticated_anchors_nothing() { + let (addr, _) = server("origin", Mode::Unframed); + let r = anchor_from(&origin(), &[addr], client(), deadline()).expect_err("cut without close_notify"); + assert!(r.why.contains("close_notify"), "{}", r.why); + } + + /// The deadline bounds the whole exchange, the handshake's many reads among them: a + /// peer that sends a byte before each read's own wait runs out still ends at it. + #[test] + fn a_trickling_peer_ends_at_the_deadline() { + let (addr, _) = server("origin", Mode::Drip); + let r = anchor_from(&origin(), &[addr], client(), Instant::now() + Duration::from_millis(1500)).expect_err("a peer that never finishes"); + assert_eq!(r.code, "key_log_not_from_origin"); + assert!(r.why.contains("deadline"), "{}", r.why); + } + + /// A key log longer than its cap is refused, read no further than one byte past it, + /// however it is framed and closed. + #[test] + fn a_key_log_past_its_cap_anchors_nothing() { + let (addr, _) = server("origin", Mode::OverCap); + let r = anchor_from(&origin(), &[addr], client(), deadline()).expect_err("a log past its cap"); + assert_eq!(r.code, "key_log_too_large", "{}", r.why); + } + + /// Each address a name resolves to is tried in turn: a server on 127.0.0.1 alone is + /// reached through `localhost`, whichever address the lookup gives first. + #[test] + fn every_address_is_tried_in_turn() { + let l = TcpListener::bind("127.0.0.1:0").expect("bind"); + let port = l.local_addr().expect("addr").port(); + let addrs = resolve(format!("localhost:{port}"), deadline()).expect("localhost resolves"); + let mut tried = addrs.clone(); + tried.retain(|a| a.is_ipv6()); + tried.extend(addrs.iter().filter(|a| a.is_ipv4())); + assert!(connect_any(&tried, deadline()).is_ok(), "{tried:?}"); + let dead = TcpListener::bind("127.0.0.1:0").expect("bind").local_addr().expect("addr"); + assert!(connect_any(&[dead, l.local_addr().expect("addr")], deadline()).is_ok(), "a refused address is passed over"); + } + + #[test] + fn an_https_origin_is_a_host_and_a_port() { + let o = |s: &str| HttpsOrigin::parse(s).map(|o| (o.host, o.port, o.authority)); + assert_eq!(o("https://origin.example"), Some(("origin.example".into(), 443, "origin.example".into()))); + assert_eq!(o("https://origin.example:8443"), Some(("origin.example".into(), 8443, "origin.example:8443".into()))); + assert_eq!(o("https://[::1]:9443"), Some(("::1".into(), 9443, "[::1]:9443".into()))); + assert_eq!(o("https://[::1]"), Some(("::1".into(), 443, "[::1]".into()))); + for bad in ["http://origin.example", "https://", "https://origin.example/", "https://u@origin.example", "https://origin.example:", + "https://origin.example:0", "https://origin.example:65536", "https://origin.example:+1", "https://origin.example?x", "https://[::1]x"] { + assert_eq!(o(bad), None, "{bad}"); + } + assert_eq!(origin().key_log_location(), "https://origin.example/.well-known/wai-keys"); + } +} diff --git a/wai/wai-rs/src/bin/wai_cli/http_vectors.rs b/wai/wai-rs/src/bin/wai_cli/http_vectors.rs index 3e6da62aa..e205ac216 100644 --- a/wai/wai-rs/src/bin/wai_cli/http_vectors.rs +++ b/wai/wai-rs/src/bin/wai_cli/http_vectors.rs @@ -37,6 +37,8 @@ use wai::pin::HeldBytes; #[path = "http_vectors_playlists.rs"] mod playlists; +#[path = "http_vectors_receipts.rs"] +mod receipts; /// The committed vectors the corpus carries objects of, relative to the corpus. const STREAM_WAI: &str = "../wai-rs/tests/vectors/int_mlicpp/stream.wai"; @@ -487,6 +489,15 @@ fn select(c: &Corpus, input: &Value) -> Result { /// a playlist's layer. The sink is read before the producer writes the proof, so a case /// refused for both is refused for its sink. fn fetch(c: &Corpus, input: &Value) -> Result { + Ok(match fetch_session(c, input)?.1 { + Ok(f) => fetched(&f), + Err(e) => err(&e), + }) +} + +/// [`fetch`]'s session and its outcome: what a receipt case holds of an object it +/// fetched (`FetchSession::established`, http-delivery §8.3). +fn fetch_session(c: &Corpus, input: &Value) -> Result<(FetchSession, Result), Stop> { let env = c.original(input)?; let mut served = env.clone(); let tamper = &input["tamper"]; @@ -544,10 +555,7 @@ fn fetch(c: &Corpus, input: &Value) -> Result { } Some((meta, body)) }); - Ok(match r { - Ok(f) => fetched(&f), - Err(e) => err(&e), - }) + Ok((s, r)) } /// A fetch on a clock (§10.1, §10.2), driven by a script of steps, each at a time on @@ -800,7 +808,7 @@ fn outcome(c: &Corpus, op: &str, input: &Value) -> Result { Ok(t) => json!({ "n": t.n, "uri": t.uri }), Err(e) => err(&e), }, - other => match playlists::outcome(c, other, input) { + other => match playlists::outcome(c, other, input).or_else(|| receipts::outcome(c, other, input)) { Some(r) => r?, None => return Err(format!("unknown op {other}").into()), }, @@ -1208,6 +1216,32 @@ fn json_lines(cases: &[(String, &str, Value, Value)]) -> Vec { out.into_bytes() } +/// Whether an outcome gives the refusal `code`: as its own `error`, or, for an outcome of +/// several steps or checks, as the `error` of its last step, or the `error` or `index` +/// verdict of any check it holds. +fn gives(v: &Value, code: &str) -> bool { + if v["error"] == code { + return true; + } + if let Some(last) = v["steps"].as_array().and_then(|a| a.last()) { + return last["error"] == code; + } + v["checks"].as_array().into_iter().flatten().any(|c| { + c["index"] == code || ["delivery", "claims"].iter().any(|k| c[*k].as_array().into_iter().flatten().any(|x| x["error"] == code)) + }) +} + +/// Whether an outcome holds a refusal anywhere: its own `error` or `inert`, a step's +/// `error`, or a check's index or item refused (for the fuzz's tallies). +fn refuses(v: &Value) -> bool { + v.get("error").is_some() + || v.get("inert").is_some() + || v["steps"].as_array().into_iter().flatten().any(|s| s.get("error").is_some()) + || v["checks"].as_array().into_iter().flatten().any(|c| { + c["index"] != "ok" || ["delivery", "claims"].iter().any(|k| c[*k].as_array().into_iter().flatten().any(|x| x.get("error").is_some())) + }) +} + /// Every generated file, by path relative to the corpus. fn generate(dir: &Path) -> Result>, String> { check_sources(dir)?; @@ -1256,11 +1290,14 @@ fn generate(dir: &Path) -> Result>, String> { // The playlists, their receipts and their cases (§8.6, §10.4, §11, Appendix F). let playlist_files = playlists::generate(&mut corpus)?; list.extend(playlists::cases(&corpus)?); + // The receipts, keys, grants and parameter sets, and their cases (§8.1–§8.5, §10.5). + let receipt_files = receipts::generate(&mut corpus)?; + list.extend(receipts::cases(&corpus)?); let mut out = Vec::new(); for c in &list { let expect = eval(&corpus, c.op, &c.input).map_err(|e| format!("{}: {e}", c.name))?; if let Some(code) = c.intends { - if expect["error"] != code { + if !gives(&expect, code) { return Err(format!("{} exists for {code}, and this build gives {expect}", c.name)); } } @@ -1355,6 +1392,7 @@ fn generate(dir: &Path) -> Result>, String> { } } files.extend(playlist_files); + files.extend(receipt_files); files.insert("cases.json".into(), json_lines(&out)); Ok(files) } @@ -1541,7 +1579,7 @@ fn fuzz(dir: &Path, seed: u64, count: usize) -> Result<(Vec, usize, BTreeMap let family = name.rsplit_once('_').map_or(name.as_str(), |(f, _)| f).to_string(); let tally = stats.entry(family).or_default(); tally.0 += 1; - if expect.get("error").is_some() || expect.get("inert").is_some() { + if refuses(&expect) { tally.1 += 1; } out.push((name, op, input, expect)); @@ -1774,6 +1812,11 @@ fn fuzz(dir: &Path, seed: u64, count: usize) -> Result<(Vec, usize, BTreeMap for (name, op, input) in playlists::fuzz_cases(&fuzz_corpus, &mut s, i)? { push(name, op, input)?; } + // The receipt side: receipt files, indexes, grants files and key logs, each changed + // one way or not at all. + for (name, op, input) in receipts::fuzz_cases(&fuzz_corpus, &mut s, i)? { + push(name, op, input)?; + } } let n = out.len(); Ok((json_lines(&out), n, stats)) diff --git a/wai/wai-rs/src/bin/wai_cli/http_vectors_receipts.rs b/wai/wai-rs/src/bin/wai_cli/http_vectors_receipts.rs new file mode 100644 index 000000000..cf62eeb16 --- /dev/null +++ b/wai/wai-rs/src/bin/wai_cli/http_vectors_receipts.rs @@ -0,0 +1,1312 @@ +//! The receipt, key, grant and parameter-set side of the HTTP delivery corpus +//! (http-delivery §8.1–§8.5, §10.5): group receipts and claims served as receipt files +//! named by their link hash, a receipt index beside each object that has receipts, an +//! origin's key log, key status and grants file, and a parameter set served by its +//! digest; and the cases that hold all of it — receipt files read under their names, +//! indexes checked item by item against what a client holds of the object (the whole +//! envelope, the cid a head proof established, or nothing), chains walked by their +//! parents' links, claims by their subjects and parents, key logs anchored from their +//! origin only and taken through caches, grants files read as locators, and sets held +//! only as their pins and jwp-receipts §7.9 hold them. +//! +//! The corpus's keys are seeds of one byte repeated, published here: they attest +//! nothing. The origin is `https://origin.example`, an example host serving nothing. + +#[cfg(not(feature = "key_discovery"))] +use std::collections::BTreeMap; + +#[cfg(not(feature = "key_discovery"))] +use serde_json::Value; + +#[cfg(not(feature = "key_discovery"))] +use super::{Case, Corpus, Stop}; + +#[cfg(feature = "key_discovery")] +pub(super) use built::{cases, fuzz_cases, generate, outcome}; + +/// The ops this side evaluates. +const OPS: &[&str] = &["receipt_file", "receipts", "key_log", "grants_file", "param_set"]; + +#[cfg(not(feature = "key_discovery"))] +const NEEDS: &str = "built without key_discovery: the receipt and key cases need it (build with --features http,video_manifest,key_discovery)"; + +#[cfg(not(feature = "key_discovery"))] +pub(super) fn generate(_: &mut Corpus) -> Result>, String> { + Err(NEEDS.into()) +} + +#[cfg(not(feature = "key_discovery"))] +pub(super) fn cases(_: &Corpus) -> Result, String> { + Err(NEEDS.into()) +} + +#[cfg(not(feature = "key_discovery"))] +pub(super) fn outcome(_: &Corpus, op: &str, _: &Value) -> Option> { + OPS.contains(&op).then(|| Err(Stop::Fault(NEEDS.into()))) +} + +#[cfg(not(feature = "key_discovery"))] +pub(super) fn fuzz_cases(_: &Corpus, _: &mut u64, _: usize) -> Result, String> { + Err(NEEDS.into()) +} + +#[cfg(feature = "key_discovery")] +mod built { + use std::collections::BTreeMap; + + use ed25519_dalek::SigningKey; + use serde_json::{json, Value}; + + use super::super::{case, fetch_session, next, pick, refused, strs, Case, Corpus, Stop, STREAM_WAI}; + use super::OPS; + use wai::claim_receipt::{stage_point, BindingType, ClaimBody, ClaimReceipt, DeliveryGapClaim, EnergyClaim, GapReason, LawfulBasisClaim, + LogInclusionClaim, SoftBindingClaim, StageClaim, StageEvidence, StageObject, StageStatus, SubjectType, STAGE_VOCABULARY}; + use wai::energy_binding::Coverage; + use wai::energy_class::{CounterFiltering, Uncertainty}; + use wai::container::{PinRefusal, Prior}; + use wai::energy_class::EnergyProvenance; + use wai::http_binding::keys::{ + anchor_retrieved, check_fetched_set, confirm_claim, confirm_delivery, grants_file, hold_cached_revisions, key_log_url, + read_grants_file, KeysRefusal, Retrieval, + }; + use wai::http_binding::receipts::{ + check_index, claim_file, group_file, index_file, read_claim_file, read_group_file, receipt_path, ClaimVerdict, + DeliveryVerdict, IndexItem, Limits, ObjectHeld, ReceiptIndex, ReceiptRefusal, Recovery, FETCH_BUDGET, MAX_INDEX_BYTES, + MAX_RECEIPT_FILE_BYTES, PARENT_WALK_BOUND, + }; + use wai::http_binding::{content_hash, hex, unhex32}; + use wai::jwp_delivery::{GroupBuilder, GroupReceipt, ObjectLabel, ObjectReceipt, PropertyReading, ReceiptChains, WaiReceiptProperty}; + use wai::jwp_hash::EstablishedHash; + use wai::key_log::{ + commitment, log_json, Anchor, KeyEntry, KeyStatus, ModuleForm, ModuleGrant, Revision, SinkKeyLog, + }; + use wai::pin::HeldBytes; + + /// The origin every key, grant and receipt signer of this side is under. + pub(crate) const ORIGIN: &str = "https://origin.example"; + const PUBLISHER: u8 = 0x61; + const PUBLISHER_ID: &str = "did:web:origin.example#d1"; + const ROOT: u8 = 0x60; + const NEXT_ROOT: u8 = 0x6f; + const GRANTER: u8 = 0x63; + const GRANTER_ID: &str = "did:web:origin.example#g1"; + const SINK: u8 = 0x64; + const SINK_ID: &str = "did:example:http-sink"; + const RELAY: u8 = 0x66; + const RELAY_ID: &str = "did:example:http-relay"; + /// A second publisher, whose delivery of O7 an index may list before the first's. + const OTHER: u8 = 0x67; + const OTHER_ID: &str = "did:example:other-publisher"; + const DEEP_RCPT: &str = "cases/rcpt-deep"; + /// The claim-conformance vector whose `measurement` body the corpus's measurement + /// claim takes, about another object. + const MEASUREMENT_VECTOR: &str = "../claim-conformance/vectors.json"; + const METHOD: &str = "https://transaction.science/wai/extensions/energy-measurement"; + const KEY_LOG: &str = ".well-known/wai-keys"; + const STATUS: &str = ".well-known/wai-key-status"; + const OLDER_LOG: &str = "cases/keys/wai-keys-r0"; + const REVOKED_LOG: &str = "cases/keys/wai-keys-revoked"; + const RCPT: &str = "objects/rcpt"; + const CASE_RCPT: &str = "cases/rcpt"; + const SET_CAP: &str = "wai.neural.int_mlicpp"; + const MLICPP_SET: &str = "../wai-rs/tests/vectors/int_mlicpp/model.wmb"; + /// The status is frozen at this time: §7.9 holds the set only with `now` inside + /// its `status_max_age_s`, as at [`NOW_HELD`]; at any real time it is stale. + const STATUS_AT: &str = "2026-10-06T00:00:00Z"; + const NOW_HELD: &str = "2026-10-06T00:30:00Z"; + const NOW_STALE: &str = "2026-10-06T02:00:00Z"; + const ACQUIRED: &str = "2026-10-06T00:00:00Z"; + + fn key(seed: u8) -> SigningKey { + SigningKey::from_bytes(&[seed; 32]) + } + + fn pk(seed: u8) -> [u8; 32] { + key(seed).verifying_key().to_bytes() + } + + fn fault(e: impl std::fmt::Display) -> String { + e.to_string() + } + + // ------------------------------------------------------------- the material + + /// One group: its receipt and its objects' receipts. + struct Group { + receipt: GroupReceipt, + objects: Vec, + } + + /// Everything this side signs, computed from the corpus's objects. + struct Material { + groups: Vec, + /// Claims by label. + claims: BTreeMap<&'static str, ClaimReceipt>, + /// Each indexed object: its id, its delivery items as (group, object) indices, and + /// the labels of the claims it lists. + indexes: Vec<(&'static str, Vec<(usize, usize)>, Vec<&'static str>)>, + r0: Revision, + r1: Revision, + r1_revoked: Revision, + grant: ModuleGrant, + set: Vec, + } + + fn object<'a>(c: &'a Corpus, id: &str) -> Result<&'a Vec, String> { + c.objects.get(id).ok_or(format!("no object {id}")) + } + + fn time(s: u64) -> String { + format!("2026-10-06T00:{:02}:{:02}Z", s / 60, s % 60) + } + + /// The class of the corpus's measured figure: an on-chip counter at 44669 ppm over 3 s. + fn chip() -> EnergyProvenance { + EnergyProvenance::on_chip_counter(Uncertainty::new(44_669, 3_000_000).expect("a declared uncertainty"), CounterFiltering::Unknown) + .expect("a class") + } + + /// A group of `ids` on `track`, at `group_id`, after `parent`. + fn group(c: &Corpus, track: u64, group_id: u64, ids: &[(&'static str, u64, Option)], parent: Option<&Group>) -> Result { + let mut b = GroupBuilder::default(); + for (id, j, l) in ids { + b.add(content_hash(object(c, id)?), *j, "origin", l.clone()); + } + let (start, end) = (time(4 * group_id), time(4 * group_id + 4)); + let (receipt, objects) = b + .finish(&key(PUBLISHER), PUBLISHER_ID, track, group_id, &start, &end, parent.map(|p| p.receipt.link_hash()), true) + .ok_or("the objects compose to no class")?; + Ok(Group { receipt, objects }) + } + + fn revision(prev: Option<&Revision>, issued_at: &str, revoked: Vec<[u8; 32]>) -> Result { + let r = Revision { + origin: ORIGIN.into(), + seq: prev.map_or(0, |p| p.seq + 1), + prev: prev.map(Revision::revision_hash), + issued_at: issued_at.into(), + root: [0; 32], + next_root: commitment(&pk(NEXT_ROOT)), + keys: vec![ + KeyEntry::active("d1", pk(PUBLISHER), &["delivery", "energy/Estimator", "log-inclusion", "soft-binding"]), + KeyEntry::active("g1", pk(GRANTER), &["module/decoder"]), + ], + revoked_modules: revoked, + grant_max_s: 86_400, + status_max_age_s: 3_600, + sig: [0; 64], + }; + Revision::seal(&key(ROOT), r).map_err(|e| e.label().to_string()) + } + + fn stage(objects: Vec, units_by_stage: Vec, presented: u64, delivered: u64, requested: Option, bytes: u64) -> StageClaim { + StageClaim { + vocabulary: STAGE_VOCABULARY.into(), + op_requested: requested.map(stage_point), + op_committed: requested.map(stage_point), + op_delivered: Some(stage_point(delivered)), + presented: stage_point(presented), + units_by_stage, + tier: "none".into(), + stage_hash: None, + objects, + conflicts: 0, + deadline_misses: 0, + bytes_envelopes: bytes, + } + } + + fn material(c: &Corpus) -> Result { + let unmetered = || Some(ObjectLabel::Unmetered); + let estimator = || Some(ObjectLabel::Class(EnergyProvenance::Estimator)); + // Track 1: an on-demand sequence, one group per segment, chained. + let t0 = group(c, 1, 0, &[("O9a", 0, unmetered())], None)?; + let t1 = group(c, 1, 1, &[("O9b", 0, unmetered())], Some(&t0))?; + let t2 = group(c, 1, 2, &[("O9c", 0, unmetered())], Some(&t1))?; + // Track 2: two objects with signed figures of a stated class. + let v = group(c, 2, 0, &[("O3", 1500, estimator()), ("O7", 300, estimator())], None)?; + // Track 3: a stage set's base and its two layers. + let s = group(c, 3, 0, &[("O10", 0, unmetered()), ("O10L1", 0, unmetered()), ("O10L2", 0, unmetered())], None)?; + let o10 = object(c, "O10")?.len() as u64; + let l1 = object(c, "O10L1")?; + let base = content_hash(object(c, "O10")?); + let seal = |signer: u8, id: &str, t: SubjectType, subject: [u8; 32], seq: u64, at: &str, body: ClaimBody, parent: Option<[u8; 32]>| { + ClaimReceipt::seal(&key(signer), id, t, subject, seq, at, body, parent).map_err(|r| format!("a corpus claim: {r}")) + }; + // A sink's two stage claims about O10: before its layers, and after layer 1 was + // applied and layer 2 abandoned (http-delivery §10.5). + let c0 = seal(SINK, SINK_ID, SubjectType::Object, base, 0, &time(60), ClaimBody::Stage(stage(Vec::new(), vec![1], 0, 0, None, o10)), None)?; + let objs = vec![ + StageObject { layer: 1, object: content_hash(l1), units: (0, 1), bytes: l1.len() as u64, status: StageStatus::Applied, + evidence: StageEvidence::DeliveredVerified, reason: None }, + StageObject { layer: 2, object: content_hash(object(c, "O10L2")?), units: (0, 1), bytes: 0, status: StageStatus::Dropped, + evidence: StageEvidence::InclusionVerified, reason: None }, + ]; + let c1 = seal(SINK, SINK_ID, SubjectType::Object, base, 1, &time(70), ClaimBody::Stage(stage(objs, vec![0, 1], 1, 1, Some(2), o10 + l1.len() as u64)), + Some(c0.link_hash()))?; + // The publisher's soft binding of O7, which O3 is listed as derived from, and its + // log-inclusion claim about track 2's group, chained to it. + let soft = ClaimBody::SoftBinding(SoftBindingClaim { + alg: "org.example.fingerprint".into(), + registry: "https://registry.example/soft-binding".into(), + binding_type: BindingType::Fingerprint, + value: vec![0x0f, 0xf0], + resolution_endpoint: "https://origin.example/resolve".into(), + }); + let s0 = seal(PUBLISHER, PUBLISHER_ID, SubjectType::Object, content_hash(object(c, "O7")?), 0, &time(80), soft, None)?; + let logged = ClaimBody::LogInclusion(LogInclusionClaim { + log: "https://log.example/".into(), + profile: "https://log.example/profile".into(), + entry: "41".into(), + log_receipt: vec![1, 2, 3, 4], + }); + let l0 = seal(PUBLISHER, PUBLISHER_ID, SubjectType::Receipt, v.receipt.link_hash(), 1, &time(90), logged, Some(s0.link_hash()))?; + let mut claims = BTreeMap::new(); + claims.insert("c0", c0); + claims.insert("c1", c1); + claims.insert("s0", s0); + claims.insert("l0", l0); + // Track 4: an object whose leaf carries an on-chip counter's figure, and the + // publisher's measurement report of that figure (jwp-receipts §2.4.8), the body + // of claim-conformance's `measurement_object_accept` about this object. + let w = group(c, 4, 0, &[("O5", 461_538, Some(ObjectLabel::Class(chip())))], None)?; + let doc: Value = serde_json::from_slice(&std::fs::read(c.dir.join(MEASUREMENT_VECTOR)).map_err(fault)?).map_err(fault)?; + let vector = doc["vectors"].as_array().into_iter().flatten().find(|v| v["name"] == "measurement_object_accept").ok_or("no measurement vector")?; + let mut body = ClaimReceipt::from_json(vector["receipt"].as_str().ok_or("receipt")?).map_err(|r| r.to_string())?.body; + let ClaimBody::Measurement(mm) = &mut body else { return Err("the vector is not a measurement".into()) }; + mm.figure.figure_index = Some(0); + mm.figure.group = Some(w.receipt.link_hash()); + let m0 = seal(PUBLISHER, PUBLISHER_ID, SubjectType::Object, content_hash(object(c, "O5")?), 2, &time(100), body, Some(claims["l0"].link_hash()))?; + // The publisher's energy claims about O3: its leaf's figure, and another. + let energy = |j: u64| ClaimBody::Energy(EnergyClaim::new(j, &EnergyProvenance::Estimator, Coverage::Whole, "projected", METHOD, 1, "subpixel", None)); + let o3 = content_hash(object(c, "O3")?); + let e0 = seal(PUBLISHER, PUBLISHER_ID, SubjectType::Object, o3, 3, &time(110), energy(1500), Some(m0.link_hash()))?; + let e1 = seal(PUBLISHER, PUBLISHER_ID, SubjectType::Object, o3, 4, &time(120), energy(1501), Some(e0.link_hash()))?; + // A relay's delivery-gap claims about O10L2: at its coordinates, and at O10L1's. + let gap = |object_id: u64| ClaimBody::DeliveryGap(DeliveryGapClaim { track_alias: 3, group_id: 0, object_id, + reason: GapReason::NotReceived, observed_at: time(130) }); + let l2 = content_hash(object(c, "O10L2")?); + let g0 = seal(RELAY, RELAY_ID, SubjectType::Object, l2, 0, &time(130), gap(2), None)?; + let g1 = seal(RELAY, RELAY_ID, SubjectType::Object, l2, 1, &time(131), gap(1), Some(g0.link_hash()))?; + claims.insert("m0", m0); + claims.insert("e0", e0); + claims.insert("e1", e1); + claims.insert("g0", g0); + claims.insert("g1", g1); + let groups = vec![t0, t1, t2, v, s, w]; + let indexes = vec![ + ("O9a", vec![(0, 0)], vec![]), + ("O9b", vec![(1, 0)], vec![]), + ("O9c", vec![(2, 0)], vec![]), + ("O3", vec![(3, 0)], vec!["s0", "l0", "e0", "e1"]), + ("O7", vec![(3, 1)], vec!["s0"]), + ("O10", vec![(4, 0)], vec!["c1"]), + ("O10L1", vec![(4, 1)], vec![]), + ("O10L2", vec![(4, 2)], vec!["g0", "g1"]), + ("O5", vec![(5, 0)], vec!["m0"]), + ]; + let set = std::fs::read(c.dir.join(MLICPP_SET)).map_err(|e| format!("{MLICPP_SET}: {e}"))?; + let digest = wai::pin::sha256(&set); + let r0 = revision(None, "2026-10-01T00:00:00Z", Vec::new())?; + let r1 = revision(Some(&r0), "2026-10-05T00:00:00Z", Vec::new())?; + let r1_revoked = revision(Some(&r0), "2026-10-05T00:00:00Z", vec![digest])?; + let grant = ModuleGrant::seal(&key(GRANTER), GRANTER_ID, ORIGIN, digest, ModuleForm::Decoder, &[SET_CAP], "2026-10-05T12:00:00Z", "2026-10-06T12:00:00Z") + .map_err(|e| e.label().to_string())?; + Ok(Material { groups, claims, indexes, r0, r1, r1_revoked, grant, set }) + } + + /// The claims and the group the cases serve from `cases/rcpt/` beside the published + /// ones (§8.2): a second publisher's delivery of O7; the lawful-basis, recovery, + /// operating-point and session classes, whose bodies are claim-conformance's + /// accepted vectors retargeted to the corpus's objects and signed with its keys. + struct Extras { + other: Group, + claims: BTreeMap<&'static str, ClaimReceipt>, + /// Each case index: its file, its object, its delivery items and its claims. + indexes: Vec<(&'static str, &'static str, Vec, Vec<&'static str>)>, + } + + fn extras(c: &Corpus, m: &Material) -> Result { + let doc: Value = serde_json::from_slice(&std::fs::read(c.dir.join(MEASUREMENT_VECTOR)).map_err(fault)?).map_err(fault)?; + let vector = |name: &str| doc["vectors"].as_array().into_iter().flatten().find(|v| v["name"] == name).ok_or(format!("no vector {name}")); + let body = |name: &str, member: Option<&str>| -> Result { + let v = vector(name)?; + let text = match member { + None => v["receipt"].as_str(), + Some(k) => v["context"][k].as_str(), + } + .ok_or(format!("{name}: no receipt"))?; + Ok(ClaimReceipt::from_json(text).map_err(|r| r.to_string())?.body) + }; + let seal = |signer: u8, id: &str, subject: [u8; 32], at: u64, body: ClaimBody| { + ClaimReceipt::seal(&key(signer), id, SubjectType::Object, subject, 0, &time(at), body, None).map_err(|r| format!("a case claim: {r}")) + }; + let (o3, o5, o7) = (content_hash(object(c, "O3")?), content_hash(object(c, "O5")?), content_hash(object(c, "O7")?)); + // O7 delivered again, under a second publisher's key. + let mut b = GroupBuilder::default(); + b.add(o7, 300, "origin", Some(ObjectLabel::Class(EnergyProvenance::Estimator))); + let (receipt, objects) = b.finish(&key(OTHER), OTHER_ID, 7, 0, &time(0), &time(4), None, true).ok_or("the other publisher's group")?; + let other = Group { receipt, objects }; + let mut claims = BTreeMap::new(); + // Lawful basis: the publisher's declaration about O3, and about O7; a sink's + // decisions on a declared purpose, on an undeclared one, and against O7's. + let declaration = body("lawful_basis_declaration_accept", None)?; + let ld0 = seal(PUBLISHER, PUBLISHER_ID, o3, 200, declaration.clone())?; + let ld7 = seal(PUBLISHER, PUBLISHER_ID, o7, 201, declaration)?; + let decide = |on: [u8; 32], purpose: Option<&str>| -> Result { + let mut b = body("lawful_basis_decision_accept", None)?; + let ClaimBody::LawfulBasis(LawfulBasisClaim::Decision { declaration, purpose: p, .. }) = &mut b else { + return Err("the vector is not a decision".into()); + }; + *declaration = on; + if let Some(x) = purpose { + *p = x.into(); + } + Ok(b) + }; + claims.insert("ldd0", seal(SINK, SINK_ID, o3, 210, decide(ld0.link_hash(), None)?)?); + claims.insert("ldd1", seal(SINK, SINK_ID, o3, 211, decide(ld0.link_hash(), Some("dpv:PersonalisedAdvertising"))?)?); + claims.insert("ldd2", seal(SINK, SINK_ID, o3, 212, decide(ld7.link_hash(), None)?)?); + claims.insert("ld0", ld0); + claims.insert("ld7", ld7); + // Binding recovery: the publisher's soft binding of O3 and the relay's; records + // of a value found in O7 matching the publisher's, of another value, and naming + // the relay's. + let sb = body("binding_recovery_accept", Some("declaration"))?; + let sb3 = seal(PUBLISHER, PUBLISHER_ID, o3, 220, sb.clone())?; + let sbr = seal(RELAY, RELAY_ID, o3, 221, sb)?; + let record = |on: [u8; 32], value: Option<&[u8]>| -> Result { + let mut b = body("binding_recovery_accept", None)?; + let ClaimBody::BindingRecovery(r) = &mut b else { return Err("the vector is not a recovery".into()) }; + r.declaration = on; + r.observed = Some(o7); + if let Some(v) = value { + r.value = v.to_vec(); + } + Ok(b) + }; + claims.insert("rr0", seal(SINK, SINK_ID, o3, 230, record(sb3.link_hash(), None)?)?); + claims.insert("rr1", seal(SINK, SINK_ID, o3, 231, record(sb3.link_hash(), Some(b"another payload"))?)?); + claims.insert("rr2", seal(SINK, SINK_ID, o3, 232, record(sbr.link_hash(), None)?)?); + claims.insert("sb3", sb3); + claims.insert("sbr", sbr); + // An operating point whose figure is O5's leaf (moq-jwp:object), and one whose + // figure disagrees with it. + let point = |joules: u64| -> Result { + let mut b = body("operating_point_accept", None)?; + let ClaimBody::OperatingPoint(o) = &mut b else { return Err("the vector is not an operating point".into()) }; + o.figure.figure_profile = "moq-jwp:object".into(); + o.figure.figure_index = Some(0); + o.figure.group = Some(m.groups[5].receipt.link_hash()); + o.figure.joules_micro = joules; + Ok(b) + }; + claims.insert("op0", seal(PUBLISHER, PUBLISHER_ID, o5, 240, point(461_538)?)?); + claims.insert("op1", seal(PUBLISHER, PUBLISHER_ID, o5, 241, point(461_539)?)?); + // A session claim, whose context no index carries. + claims.insert("rs0", seal(SINK, SINK_ID, o3, 250, body("session_accept", None)?)?); + let (i3, i5, i7) = (item(m, 3, 0), item(m, 5, 0), item(m, 3, 1)); + let io = item_for(&other.receipt, &other.objects[0]); + let indexes = vec![ + ("cases/receipts/O3-lawful-basis.wai.receipts", "O3", vec![i3.clone()], vec!["ldd0", "ldd1", "ldd2"]), + ("cases/receipts/O3-binding-recovery.wai.receipts", "O3", vec![i3.clone()], vec!["rr0", "rr1", "rr2"]), + ("cases/receipts/O3-session.wai.receipts", "O3", vec![i3], vec!["rs0"]), + ("cases/receipts/O5-operating-point.wai.receipts", "O5", vec![i5], vec!["op0", "op1"]), + ("cases/receipts/O7-other-first.wai.receipts", "O7", vec![io.clone(), i7.clone()], vec![]), + ("cases/receipts/O7-publisher-first.wai.receipts", "O7", vec![i7, io.clone()], vec![]), + ("cases/receipts/O7-other-only.wai.receipts", "O7", vec![io], vec![]), + ]; + Ok(Extras { other, claims, indexes }) + } + + fn item(m: &Material, g: usize, o: usize) -> IndexItem { + item_for(&m.groups[g].receipt, &m.groups[g].objects[o]) + } + + fn item_for(gr: &GroupReceipt, ob: &ObjectReceipt) -> IndexItem { + let (acquisition, evidence) = match &ob.label { + Some(ObjectLabel::Class(p)) => (p.label().to_string(), wai::jwp_delivery::property_evidence(p)), + _ => (String::new(), Vec::new()), + }; + IndexItem { + property: WaiReceiptProperty { + content_hash: ob.content_hash, + joules_micro: ob.joules_micro, + acquisition, + acquisition_evidence: evidence, + group_receipt: gr.link_hash(), + }, + object_receipt: ob.clone(), + } + } + + fn index_path(c: &Corpus, id: &str) -> Result { + Ok(format!("objects/{}.wai.receipts", hex(&content_hash(object(c, id)?)))) + } + + /// The committed file of the object `id`, as `objects.json` lists it: what a case + /// that holds no objects in memory (the fuzz) reads. + fn object_file_of(c: &Corpus, id: &str) -> Result { + let listing: Value = serde_json::from_slice(&c.file("objects.json")?).map_err(fault)?; + listing[id]["path"].as_str().map(String::from).ok_or(format!("objects.json lists no {id}")) + } + + /// Every file of this side, by path; each is also put where the cases read it. + pub(crate) fn generate(c: &mut Corpus) -> Result>, String> { + let m = material(c)?; + let mut out = BTreeMap::new(); + for g in &m.groups { + out.insert(format!("objects/{}", receipt_path(&g.receipt.link_hash())), group_file(&g.receipt)?); + } + for cl in m.claims.values() { + out.insert(format!("objects/{}", receipt_path(&cl.link_hash())), claim_file(cl)); + } + for (id, items, claims) in &m.indexes { + let items: Vec = items.iter().map(|&(g, o)| item(&m, g, o)).collect(); + let names: Vec<[u8; 32]> = claims.iter().map(|l| m.claims[l].link_hash()).collect(); + out.insert(index_path(c, id)?, index_file(&content_hash(object(c, id)?), &items, &names)?); + } + out.insert(KEY_LOG.into(), log_json(&[m.r0.clone(), m.r1.clone()]).into_bytes()); + out.insert(OLDER_LOG.into(), log_json(std::slice::from_ref(&m.r0)).into_bytes()); + out.insert(REVOKED_LOG.into(), log_json(&[m.r0.clone(), m.r1_revoked.clone()]).into_bytes()); + out.insert(STATUS.into(), KeyStatus::seal(&key(ROOT), &m.r1, STATUS_AT).to_json().into_bytes()); + let digest = wai::pin::sha256(&m.set); + // The grants file lists, beside the grant, one signed by a key the log does not + // list: each is checked on its own. + let stray = ModuleGrant::seal(&key(0x65), "did:web:origin.example#x9", ORIGIN, digest, ModuleForm::Decoder, &[SET_CAP], + "2026-10-05T12:00:00Z", "2026-10-06T12:00:00Z").map_err(|e| e.label().to_string())?; + out.insert(format!(".well-known/wai-grants/{}.json", hex(&digest)), grants_file(&digest, &[stray, m.grant.clone()])); + out.insert(format!("sets/{}", hex(&digest)), m.set.clone()); + // A group whose signature was changed, served under the link hash of what it then + // is, and an index of O9a naming it. + let mut bad = m.groups[0].receipt.clone(); + bad.sig[5] ^= 0x10; + out.insert(format!("{CASE_RCPT}/{}.json", hex(&bad.link_hash())), group_file(&bad)?); + let mut it = item(&m, 0, 0); + it.property.group_receipt = bad.link_hash(); + let cid = it.property.content_hash; + out.insert("cases/receipts/O9a-bad-signature.wai.receipts".into(), index_file(&cid, &[it], &[])?); + // A group in the legacy payload, whose link hash does not cover its parent, and an + // index of O1 naming it (§8.1). + let o1 = content_hash(object(c, "O1")?); + let mut b = GroupBuilder::default(); + b.add(o1, 0, "origin", None); + let (legacy, lo) = b.finish(&key(PUBLISHER), PUBLISHER_ID, 6, 1, &time(0), &time(4), Some(m.groups[0].receipt.link_hash()), false) + .ok_or("a legacy group")?; + out.insert(format!("{CASE_RCPT}/{}.json", hex(&legacy.link_hash())), group_file(&legacy)?); + let li = IndexItem { + property: WaiReceiptProperty { content_hash: o1, joules_micro: 0, acquisition: String::new(), acquisition_evidence: Vec::new(), + group_receipt: legacy.link_hash() }, + object_receipt: lo[0].clone(), + }; + out.insert("cases/receipts/O1-legacy.wai.receipts".into(), index_file(&o1, &[li], &[])?); + // A chain of 66 groups over objects the corpus does not carry: from the last, 65 + // parents back to the chain's start; from the one before, 64 (§8.1's bound). + let mut deep: Vec<(GroupReceipt, ObjectReceipt)> = Vec::new(); + for i in 0..66u64 { + let h = content_hash(format!("deep chain object {i}").as_bytes()); + let mut b = GroupBuilder::default(); + b.add(h, 0, "origin", Some(ObjectLabel::Unmetered)); + let parent = deep.last().map(|(g, _)| g.link_hash()); + let (g, mut o) = b.finish(&key(PUBLISHER), PUBLISHER_ID, 5, i, &time(0), &time(4), parent, true).ok_or("a deep group")?; + out.insert(format!("{DEEP_RCPT}/{}.json", hex(&g.link_hash())), group_file(&g)?); + deep.push((g, o.remove(0))); + } + for i in [64usize, 65] { + let (g, o) = &deep[i]; + let it = IndexItem { + property: WaiReceiptProperty { content_hash: o.content_hash, joules_micro: 0, acquisition: String::new(), + acquisition_evidence: Vec::new(), group_receipt: g.link_hash() }, + object_receipt: o.clone(), + }; + out.insert(format!("cases/receipts/deep-{i}.wai.receipts"), index_file(&o.content_hash, &[it], &[])?); + } + // O9c's index with its one item fifty times: each name is fetched once. + let it = item(&m, 2, 0); + let o9c = it.property.content_hash; + out.insert("cases/receipts/O9c-x50.wai.receipts".into(), index_file(&o9c, &vec![it; 50], &[])?); + // Indexes at and past the caps on items and claims (§8.2): items that do not read, + // claim entries that are not link hashes, and the count read first. + let o9a = content_hash(object(c, "O9a")?); + let capped = |items: usize, claims: usize| { + let mut mm = serde_json::Map::new(); + mm.insert("kind".into(), json!(wai::http_binding::receipts::INDEX_KIND)); + mm.insert("object".into(), json!(hex(&o9a))); + mm.insert("delivery".into(), Value::Array(vec![json!({}); items])); + mm.insert("claims".into(), Value::Array(vec![json!("x"); claims])); + wai::jcs::jcs(&Value::Object(mm)).map(String::into_bytes).map_err(|e| e.to_string()) + }; + out.insert("cases/receipts/O9a-256-items.wai.receipts".into(), capped(256, 256)?); + out.insert("cases/receipts/O9a-257-items.wai.receipts".into(), capped(257, 0)?); + out.insert("cases/receipts/O9a-257-claims.wai.receipts".into(), capped(0, 257)?); + let x = extras(c, &m)?; + out.insert(format!("{CASE_RCPT}/{}.json", hex(&x.other.receipt.link_hash())), group_file(&x.other.receipt)?); + for cl in x.claims.values() { + out.insert(format!("{CASE_RCPT}/{}.json", hex(&cl.link_hash())), claim_file(cl)); + } + for (path, id, items, names) in &x.indexes { + let names: Vec<[u8; 32]> = names.iter().map(|l| x.claims[l].link_hash()).collect(); + out.insert(path.to_string(), index_file(&content_hash(object(c, id)?), items, &names)?); + } + for (k, b) in &out { + c.extra.insert(k.clone(), b.clone()); + } + Ok(out) + } + + // ---------------------------------------------------------------- the outcomes + + fn text(c: &Corpus, path: &str) -> Result { + String::from_utf8(c.file(path)?).map_err(|_| Stop::Fault(format!("{path} is not UTF-8"))) + } + + /// Replace each `[from, to]` once, as the playlist side does; a `from` not in the text + /// is a fault of the case. + fn edit(mut t: String, edits: &Value) -> Result { + for e in edits.as_array().into_iter().flatten() { + let (from, to) = (e[0].as_str().ok_or("an edit's from")?, e[1].as_str().ok_or("an edit's to")?); + if !t.contains(from) { + return Err(Stop::Fault(format!("the edit's {from:?} is not in the text"))); + } + t = t.replacen(from, to, 1); + } + Ok(t) + } + + fn hash_of(v: &Value, what: &str) -> Result<[u8; 32], Stop> { + v.as_str().and_then(unhex32).ok_or_else(|| Stop::Fault(format!("{what} is not a hash"))) + } + + fn refusal(r: &ReceiptRefusal) -> Value { + let mut v = json!({ "error": r.code(), "step": r.step() }); + if let ReceiptRefusal::ParentNotHeld { walked, stop } = r { + v["walked"] = json!(walked); + v["stop"] = json!(stop); + } + v + } + + fn keys_refusal(r: &KeysRefusal) -> Value { + json!({ "error": r.code() }) + } + + /// `receipt_file`: read `file` (after `edit`) as the `kind` of receipt file named + /// `name`, by default its own file name. + fn receipt_file(c: &Corpus, input: &Value) -> Result { + let path = input["file"].as_str().ok_or("file")?; + let t = edit(text(c, path)?, &input["edit"])?; + let stem = path.rsplit('/').next().and_then(|n| n.strip_suffix(".json")).unwrap_or(""); + let name = match &input["name"] { + Value::Null => hash_of(&json!(stem), "the file's name")?, + v => hash_of(v, "name")?, + }; + let r = match input["kind"].as_str() { + Some("group") => read_group_file(&name, t.as_bytes()).map(|g| g.link_hash()), + Some("claim") => read_claim_file(&name, t.as_bytes()).map(|c| c.link_hash()), + _ => return Err("kind is group or claim".into()), + }; + Ok(match r { + Ok(h) => json!({ "link": hex(&h) }), + Err(e) => refusal(&e), + }) + } + + /// Where a case's receipt files are served from: the directories `rcpt` (by default + /// `objects/rcpt`), each searched for `.json`; `omit` lists hashes served + /// nothing; `as` serves at a hash the file at a path; `pad` serves a file padded with + /// spaces to a length, and `pad_all` every file. + struct Served<'a> { + c: &'a Corpus, + dirs: Vec, + omit: Vec<[u8; 32]>, + swap: BTreeMap<[u8; 32], String>, + pad: BTreeMap<[u8; 32], usize>, + pad_all: Option, + } + + impl Served<'_> { + fn of<'a>(c: &'a Corpus, v: &Value) -> Result, Stop> { + let dirs = match v["rcpt"].as_array() { + Some(a) => a.iter().map(|d| d.as_str().map(String::from).ok_or("an rcpt directory")).collect::>()?, + None => vec![RCPT.to_string()], + }; + let omit = v["omit"].as_array().into_iter().flatten().map(|h| hash_of(h, "omit")).collect::>()?; + let mut swap = BTreeMap::new(); + for (h, p) in v["as"].as_object().into_iter().flatten() { + swap.insert(hash_of(&json!(h), "as")?, p.as_str().ok_or("an as path")?.to_string()); + } + let mut pad = BTreeMap::new(); + for (h, n) in v["pad"].as_object().into_iter().flatten() { + pad.insert(hash_of(&json!(h), "pad")?, n.as_u64().ok_or("a pad length")? as usize); + } + let pad_all = v["pad_all"].as_u64().map(|n| n as usize); + Ok(Served { c, dirs, omit, swap, pad, pad_all }) + } + + fn get(&self, h: &[u8; 32]) -> Option> { + if self.omit.contains(h) { + return None; + } + let mut b = match self.swap.get(h) { + Some(p) => self.c.file(p).ok(), + None => self.dirs.iter().find_map(|d| self.c.file(&format!("{d}/{}.json", hex(h))).ok()), + }?; + if let Some(n) = self.pad.get(h).or(self.pad_all.as_ref()) { + if b.len() < *n { + b.resize(*n, b' '); + } + } + Some(b) + } + } + + /// The key material a case holds: the revisions and anchors of each step of `log` + /// (as `key_log` runs them), or none. + fn key_material(c: &Corpus, steps: &Value) -> Result<(Vec, Vec), Stop> { + let mut state = KeyState::default(); + for s in steps.as_array().into_iter().flatten() { + if let Err(e) = state.step(c, s)? { + return Err(Stop::Fault(format!("the case's key material does not anchor: {}", e.code()))); + } + } + Ok((state.held, state.anchor.into_iter().collect())) + } + + fn reading(r: &PropertyReading) -> Value { + match r { + PropertyReading::Unmetered => json!("unmetered"), + PropertyReading::Unsigned => json!("unsigned"), + PropertyReading::Signed { class, by } => json!(format!("signed class={} by={}", class.label(), hex(by))), + } + } + + fn delivery(v: &DeliveryVerdict, item: Option<&IndexItem>, keys: Option<&(Vec, Vec)>) -> Value { + let mut out = json!({ + "admission": v.admission.label(), + "walked": v.walked, + "evidence": v.evidence.label(), + "established": v.established.map(|e| e.label()), + "reading": v.reading.as_ref().map_or(Value::Null, reading), + }); + if let (Some((revs, anchors)), Some(i)) = (keys, item) { + out["confirm"] = json!(confirm_delivery(&v.group, &i.object_receipt, revs, anchors).map(|c| c.verdict())); + } + out + } + + fn claim(v: &ClaimVerdict, keys: Option<&(Vec, Vec)>) -> Value { + let mut out = json!({ + "class": v.claim.class().label(), + "attests": v.attests.label(), + "subject": hex(&v.claim.subject), + "parent": v.parent.map(|p| hex(&p)), + }); + if let Some((revs, anchors)) = keys { + out["confirm"] = json!(confirm_claim(&v.claim, revs, anchors).verdict()); + } + out + } + + /// What a check's client holds of its object: `"none"`; `"whole"`, the object's + /// bytes (or `held_object`'s); or `{"fetch": {…}}`, a fetch of the object as the + /// `fetch` op runs one, its session's established hash, nothing where it was refused. + fn held(c: &Corpus, chk: &Value, bytes: &[u8]) -> Result<(ObjectHeld, Value), Stop> { + match &chk["held"] { + Value::String(s) if s == "none" => Ok((ObjectHeld::Nothing, Value::Null)), + Value::String(s) if s == "whole" => { + let b = match chk["held_object"].as_str() { + Some(o) => object(c, o)?.clone(), + None => bytes.to_vec(), + }; + Ok((ObjectHeld::Object(EstablishedHash::of_envelope(&b)), Value::Null)) + } + Value::Object(m) if m.contains_key("fetch") => { + let mut input = m["fetch"].clone(); + input["envelope_hex"] = json!(hex(bytes)); + let (s, r) = fetch_session(c, &input)?; + let fetched = match &r { + Ok(f) => json!(f.verified_by.label()), + Err(e) => json!(e.code()), + }; + Ok((s.established().map_or(ObjectHeld::Nothing, ObjectHeld::Object), fetched)) + } + _ => Err("held is none, whole or {fetch}".into()), + } + } + + /// `receipts`: a client's checks of objects' receipt indexes, in order, with one set + /// of chains (`join` declared or not), one parent bound (`max_walk`) and fetch budget + /// per index (`fetch_budget`), the receipt files served as `serve` says, with + /// `recovery` (`observed_value`, `exact_match_algs`) the context of jwp-receipts + /// §3.2's recovery, its publisher keys those of the delivery receipts earlier checks + /// verified over their objects' bytes; and, with `keys`, each accepted item's signer + /// confirmed against the key material those steps give (§7.4). A check names its + /// object by id, by `object_file`, or by `object_cid` (held `none`); `pad_index` pads + /// its index with spaces to a length. + fn receipts(c: &Corpus, input: &Value) -> Result { + let served = Served::of(c, &input["serve"])?; + let mut chains = if input["join"].as_bool() == Some(true) { ReceiptChains::new().with_join() } else { ReceiptChains::new() }; + let limits = Limits { + max_walk: input["max_walk"].as_u64().map_or(PARENT_WALK_BOUND, |b| b as usize), + fetch_budget: input["fetch_budget"].as_u64().map_or(FETCH_BUDGET, |b| b as usize), + }; + let keys = match &input["keys"] { + Value::Null => None, + k => Some(key_material(c, k)?), + }; + let observed = match input["recovery"]["observed_value"].as_str() { + Some(h) => Some(super::super::unhex(h)?), + None => None, + }; + let exact: Vec = + strs(&input["recovery"]["exact_match_algs"]).into_iter().map(|alg| wai::claim_receipt::ExactMatch { alg }).collect(); + let matchers: Vec<&dyn wai::claim_receipt::SoftBindingMatcher> = + exact.iter().map(|m| m as &dyn wai::claim_receipt::SoftBindingMatcher).collect(); + let mut publishers: Vec<([u8; 32], [u8; 32])> = Vec::new(); + let mut out = Vec::new(); + for chk in input["checks"].as_array().ok_or("checks")? { + let (bytes, cid) = match (chk["object_cid"].as_str(), chk["object_file"].as_str()) { + (Some(h), _) => (Vec::new(), hash_of(&json!(h), "object_cid")?), + (None, Some(f)) => { + let b = c.file(f)?; + let h = content_hash(&b); + (b, h) + } + (None, None) => { + let b = object(c, chk["object"].as_str().ok_or("object")?)?.clone(); + let h = content_hash(&b); + (b, h) + } + }; + let path = match chk["index"].as_str() { + Some(p) => p.to_string(), + None => format!("objects/{}.wai.receipts", hex(&cid)), + }; + let mut ix = edit(text(c, &path)?, &chk["edit_index"])?.into_bytes(); + if let Some(n) = chk["pad_index"].as_u64() { + if ix.len() < n as usize { + ix.resize(n as usize, b' '); + } + } + let (h, fetched) = held(c, chk, &bytes)?; + let mut fetch = |x: &[u8; 32], _: usize| served.get(x); + let items: Vec> = ReceiptIndex::from_bytes(&ix) + .map(|i| i.delivery.into_iter().map(Result::ok).collect()) + .unwrap_or_default(); + let recovery = observed.as_ref().map(|v| Recovery { publishers: &publishers, observed_value: v, matchers: &matchers }); + let report = check_index(&cid, &ix, &h, &mut fetch, &mut chains, 0, &limits, recovery.as_ref()); + let mut entry = match &report { + Ok(r) => json!({ + "index": "ok", + "delivery": r.delivery.iter().enumerate().map(|(k, d)| match d { + Ok(v) => delivery(v, items.get(k).and_then(Option::as_ref), keys.as_ref()), + Err(e) => refusal(e), + }).collect::>(), + "claims": r.claims.iter().map(|x| match x { + Ok(v) => claim(v, keys.as_ref()), + Err(e) => refusal(e), + }).collect::>(), + "files_fetched": r.fetched, + }), + Err(e) => json!({ "index": e.code() }), + }; + if !fetched.is_null() { + entry["fetched"] = fetched; + } + out.push(entry); + // The delivery receipts this check verified over the object's bytes: publisher + // keys a later check may recover through. + if let Ok(r) = report { + publishers.extend(r.delivery.iter().flatten().filter(|v| v.held.is_some()).map(|v| (cid, v.group.signer_pubkey))); + } + } + Ok(json!({ "checks": out })) + } + + /// A client's key-log state across `key_log` steps. + #[derive(Default)] + struct KeyState { + anchor: Option, + held: Vec, + } + + impl KeyState { + /// One step: `{"anchor": file, "retrieval": {requested, answered, status}}` + /// anchors (or moves the anchor) from a log retrieved so; `{"pin": file}` pins it; + /// `{"cached": file}` takes a log through a cache. `origin` defaults to the + /// corpus's; `edit` changes the log's text first. + fn step(&mut self, c: &Corpus, s: &Value) -> Result, Stop> { + let origin = s["origin"].as_str().unwrap_or(ORIGIN); + let file = s["anchor"].as_str().or(s["cached"].as_str()).or(s["pin"].as_str()).ok_or("a step names a log")?; + let mut body = edit(text(c, file)?, &s["edit"])?; + // `pad`: the log served with spaces after it, to a length in bytes. + if let Some(n) = s["pad"].as_u64() { + while body.len() < n as usize { + body.push(' '); + } + } + if !s["pin"].is_null() { + let a = match wai::key_log::anchor_log(origin, &body, wai::key_log::AnchorPath::Pinned, None) { + wai::key_log::Anchoring::Anchored(a) => a, + _ => return Err("a pinned log that does not anchor".into()), + }; + self.held = wai::key_log::read_log(origin, &body).map_err(|e| Stop::Fault(e.label().into()))?; + let r = json!({ "anchored": hex(&a.revision), "path": a.path.label() }); + self.anchor = Some(a); + return Ok(Ok(r)); + } + if !s["cached"].is_null() { + let Some(a) = &self.anchor else { return Err("a cached log before any anchor".into()) }; + return Ok(hold_cached_revisions(origin, body.as_bytes(), a, &mut self.held).map(|n| json!({ "added": n }))); + } + let loc = key_log_url(origin); + let r = &s["retrieval"]; + let requested = r["requested"].as_str().unwrap_or(&loc); + let answered = r["answered"].as_str().unwrap_or(requested); + let status = r["status"].as_u64().unwrap_or(200) as u16; + let ret = Retrieval { requested, answered, status }; + let current = self.anchor.as_ref().map(|a| (a, self.held.as_slice())); + Ok(match anchor_retrieved(origin, &ret, body.as_bytes(), ACQUIRED, current) { + Ok(a) => { + if let Ok(revs) = wai::key_log::read_log(origin, &body) { + for r in revs { + if !self.held.iter().any(|h| h.revision_hash() == r.revision_hash()) { + self.held.push(r); + } + } + } + let v = json!({ "anchored": hex(&a.revision), "path": a.path.label() }); + self.anchor = Some(a); + Ok(v) + } + Err(e) => Err(e), + }) + } + } + + /// `key_log`: the steps in order, each giving what it did or its reason; a step + /// refused leaves the state as it was. + fn key_log(c: &Corpus, input: &Value) -> Result { + let mut st = KeyState::default(); + let mut out = Vec::new(); + for s in input["steps"].as_array().ok_or("steps")? { + out.push(match st.step(c, s)? { + Ok(v) => v, + Err(e) => keys_refusal(&e), + }); + } + Ok(json!({ "steps": out })) + } + + /// `grants_file`: read `file` (after `edit`) as the grants file of `module` (by + /// default the digest in its name); each grant's reading by §7.9's check 1. + fn grants(c: &Corpus, input: &Value) -> Result { + let path = input["file"].as_str().ok_or("file")?; + let t = edit(text(c, path)?, &input["edit"])?; + let stem = path.rsplit('/').next().and_then(|n| n.strip_suffix(".json")).unwrap_or(""); + let module = match &input["module"] { + Value::Null => hash_of(&json!(stem), "the file's name")?, + v => hash_of(v, "module")?, + }; + Ok(match read_grants_file(&module, t.as_bytes()) { + Ok(gs) => json!({ "grants": gs.iter().map(|g| match ModuleGrant::from_json(g) { + Ok(_) => "read".to_string(), + Err(r) => r.label().to_string(), + }).collect::>() }), + Err(e) => keys_refusal(&e), + }) + } + + /// `param_set`: a set served as `served`, held for `capability` under a pin of + /// `sha256`, at a sink with the key material of `keys`, the statuses and the grants + /// file it fetched, holding the capability's sets to §7.9 for `hold_to` (absent: not + /// held to it), at `now` (null: no trusted clock). + fn param_set(c: &Corpus, input: &Value) -> Result { + let cap = input["capability"].as_str().ok_or("capability")?; + let sha = input["sha256"].as_str().ok_or("sha256")?; + let prior = Prior::new("set", sha, "integer-exact"); + let served = c.file(input["served"].as_str().ok_or("served")?)?; + let (revs, anchors) = key_material(c, &input["keys"])?; + let statuses: Vec = strs(&input["statuses"]).iter().map(|p| text(c, p)).collect::>()?; + // A pin whose digest is not 64 lowercase hexadecimal digits locates no grants + // file: the pin's own check refuses it (pin_malformed). + let grants = match (input["grants"].as_str(), unhex32(sha)) { + (Some(p), Some(d)) => read_grants_file(&d, &c.file(p)?).map_err(|e| Stop::Fault(format!("the grants file: {e}")))?, + _ => Vec::new(), + }; + let mut sink = SinkKeyLog::new(revs, anchors).with_statuses(statuses).with_grants(grants); + if let Some(origins) = input["hold_to"].as_array() { + let o: Vec<&str> = origins.iter().filter_map(Value::as_str).collect(); + sink = sink.hold_pinned_sets(cap, &o); + } + if let Some(now) = input["now"].as_str() { + sink = sink.at(now); + } + Ok(match check_fetched_set(cap, &prior, &HeldBytes::File(&served), Some(&sink)) { + Ok(_) => json!({ "held": true }), + Err(e) => match &e { + PinRefusal::Revoked { revoked_by, .. } => json!({ "error": e.code(), "revoked_by": revoked_by }), + PinRefusal::NotAuthorised { check, reason, .. } => json!({ "error": e.code(), "check": check, "reason": reason }), + _ => json!({ "error": e.code() }), + }, + }) + } + + pub(crate) fn outcome(c: &Corpus, op: &str, input: &Value) -> Option> { + if !OPS.contains(&op) { + return None; + } + Some(match op { + "receipt_file" => receipt_file(c, input), + "receipts" => receipts(c, input), + "key_log" => key_log(c, input), + "grants_file" => grants(c, input), + _ => param_set(c, input), + }) + } + + // ------------------------------------------------------------------ the cases + + fn link_file(h: &[u8; 32]) -> String { + format!("{RCPT}/{}.json", hex(h)) + } + + fn chk(id: &str, held: Value) -> Value { + json!({ "object": id, "held": held }) + } + + pub(crate) fn cases(c: &Corpus) -> Result, String> { + let m = material(c)?; + let mut v = Vec::new(); + let g = |i: usize| m.groups[i].receipt.link_hash(); + let cl = |l: &str| m.claims[l].link_hash(); + let whole = json!("whole"); + let none = json!("none"); + let digest = hex(&wai::pin::sha256(&m.set)); + + // §8.1: receipt files under their names. + v.push(case("receipt_file_group", "receipt_file", json!({ "file": link_file(&g(0)), "kind": "group" }))); + v.push(case("receipt_file_claim", "receipt_file", json!({ "file": link_file(&cl("c1")), "kind": "claim" }))); + v.push(refused("receipt_file_name_mismatch", "receipt_file", json!({ "file": link_file(&g(0)), "name": hex(&g(1)), "kind": "group" }), "receipt_name_mismatch")); + v.push(refused("receipt_file_group_as_claim", "receipt_file", json!({ "file": link_file(&g(0)), "kind": "claim" }), "wrong_kind")); + v.push(refused("receipt_file_claim_as_group", "receipt_file", json!({ "file": link_file(&cl("s0")), "kind": "group" }), "wrong_kind")); + v.push(refused("receipt_file_kind_null", "receipt_file", + json!({ "file": link_file(&g(0)), "kind": "group", "edit": [["\"kind\":\"group\"", "\"kind\":null"]] }), "wrong_kind")); + v.push(refused("receipt_file_member_twice", "receipt_file", + json!({ "file": link_file(&g(0)), "kind": "group", "edit": [["{", "{\"group_id\":0,"]] }), "malformed")); + v.push(refused("receipt_file_claim_member_twice", "receipt_file", + json!({ "file": link_file(&cl("c1")), "kind": "claim", "edit": [["{", "{\"seq\":1,"]] }), "malformed")); + + // §8.2: a chain walked from nothing held; one level up; again at the head; in order. + v.push(case("receipts_walk_two", "receipts", json!({ "checks": [chk("O9c", whole.clone())] }))); + v.push(case("receipts_walk_one", "receipts", json!({ "checks": [chk("O9b", whole.clone())] }))); + v.push(case("receipts_in_order", "receipts", json!({ "checks": [chk("O9a", whole.clone()), chk("O9b", whole.clone()), chk("O9c", whole.clone()), chk("O9c", whole.clone())] }))); + v.push(case("receipts_back_after_head", "receipts", json!({ "checks": [chk("O9c", whole.clone()), chk("O9a", none.clone())] }))); + v.push(case("receipts_join_anchors", "receipts", json!({ "join": true, "checks": [chk("O9c", whole.clone())], + "serve": { "omit": [hex(&g(0)), hex(&g(1))] } }))); + // §8.1: a parent not served, a walk past its bound, a parent under another name. + v.push(refused("receipts_parent_not_served", "receipts", json!({ "checks": [chk("O9c", whole.clone())], "serve": { "omit": [hex(&g(0))] } }), "parent_not_held")); + v.push(refused("receipts_walk_bound", "receipts", json!({ "max_walk": 1, "checks": [chk("O9c", whole.clone())] }), "parent_not_held")); + v.push(refused("receipts_parent_renamed", "receipts", json!({ "checks": [chk("O9c", whole.clone())], "serve": { "as": { hex(&g(0)): link_file(&g(1)) } } }), "parent_not_held")); + // The group itself not served, served under another name, or with its signature + // changed. + v.push(refused("receipts_group_not_served", "receipts", json!({ "checks": [chk("O9a", whole.clone())], "serve": { "omit": [hex(&g(0))] } }), "receipt_not_served")); + v.push(refused("receipts_group_renamed", "receipts", json!({ "checks": [chk("O9a", whole.clone())], "serve": { "as": { hex(&g(0)): link_file(&g(2)) } } }), "receipt_name_mismatch")); + v.push(refused("receipts_group_bad_signature", "receipts", json!({ "serve": { "rcpt": [RCPT, CASE_RCPT] }, + "checks": [{ "object": "O9a", "held": "whole", "index": "cases/receipts/O9a-bad-signature.wai.receipts" }] }), "signature_verify_failed")); + // §8.3: the cid a head proof established; a fetch refused holds nothing; another + // object's bytes in hand. + let fetch_o3 = json!({ "fetch": { "advertises": ["wai.video.av1"], "applies": ["wai.meta.hdr10plus"] } }); + v.push(case("receipts_head_proof", "receipts", json!({ "checks": [chk("O3", fetch_o3.clone())] }))); + // A byte of the head changed in transit: the head proof refuses it. + let o3 = object(c, "O3")?; + let at = 200; + let tampered = json!({ "fetch": { "advertises": ["wai.video.av1"], "applies": ["wai.meta.hdr10plus"], "tamper": { "patch_object": [[at, format!("{:02x}", o3[at] ^ 1)]] } } }); + v.push(case("receipts_fetch_refused_holds_nothing", "receipts", json!({ "checks": [chk("O3", tampered)] }))); + v.push(refused("receipts_other_object_in_hand", "receipts", json!({ "checks": [{ "object": "O9a", "held": "whole", "held_object": "O9b" }] }), "content_hash_mismatch")); + // §10.5: a layer not received is inclusion-verified; the base and its stage claim. + v.push(case("receipts_stage_set", "receipts", json!({ "checks": [chk("O10", whole.clone()), chk("O10L1", whole.clone()), chk("O10L2", none.clone())] }))); + v.push(case("receipts_layer_fetched", "receipts", json!({ "checks": [chk("O10L1", json!({ "fetch": { "listed_as_layer": true } }))] }))); + // Claims: the ancestor and a receipt claim with its parent; the object's own soft + // binding; a claim's parent not served. + v.push(case("receipts_claims_O3", "receipts", json!({ "checks": [chk("O3", whole.clone())] }))); + v.push(case("receipts_claims_O7", "receipts", json!({ "checks": [chk("O7", whole.clone())] }))); + v.push(refused("receipts_claim_parent_not_served", "receipts", json!({ "checks": [chk("O10", whole.clone())], "serve": { "omit": [hex(&cl("c0"))] } }), "parent_not_held")); + // The index and its items: another object's index; an item about another object; + // a property that disagrees with the signed leaf; members not of the form. + v.push(refused("receipts_index_other_object", "receipts", json!({ "checks": [{ "object": "O9b", "held": "whole", "index": index_path(c, "O9a")? }] }), "receipt_not_about_object")); + let p3 = item(&m, 3, 0).property; + let p_tail = format!("\"group_receipt\":\"{}\",\"joules_micro\":1500}}", hex(&p3.group_receipt)); + v.push(refused("receipts_property_disagrees", "receipts", json!({ "checks": [{ "object": "O3", "held": "whole", + "edit_index": [[p_tail.clone(), p_tail.replace(":1500}", ":1501}")]] }] }), "property_disagrees")); + let p9 = item(&m, 0, 0).property; + let ch = format!("\"content_hash\":\"{}\",\"group_receipt\"", hex(&p9.content_hash)); + v.push(refused("receipts_item_not_about_object", "receipts", json!({ "checks": [{ "object": "O9a", "held": "whole", + "edit_index": [[ch.clone(), ch.replacen(&hex(&p9.content_hash), &"00".repeat(32), 1)]] }] }), "receipt_not_about_object")); + v.push(refused("receipts_index_member_added", "receipts", json!({ "checks": [{ "object": "O9a", "held": "whole", "edit_index": [["{", "{\"note\":1,"]] }] }), "receipt_index_malformed")); + v.push(refused("receipts_item_member_added", "receipts", json!({ "checks": [{ "object": "O3", "held": "whole", + "edit_index": [["{\"object_receipt\"", "{\"note\":1,\"object_receipt\""]] }] }), "receipt_index_malformed")); + v.push(refused("receipts_claim_entry_not_a_hash", "receipts", json!({ "checks": [{ "object": "O7", "held": "whole", + "edit_index": [[hex(&cl("s0")), "s0"]] }] }), "receipt_index_malformed")); + // §8.2 step 6: a measurement report against the figure it names, held only for a + // delivered object; energy claims against the delivered object's figure; a gap + // against the receipt at its coordinates. + v.push(case("receipts_measurement_figure", "receipts", json!({ "checks": [chk("O5", whole.clone())] }))); + v.push(refused("receipts_measurement_figure_not_held", "receipts", json!({ "checks": [chk("O5", none.clone())] }), "figure_not_held")); + v.push(refused("receipts_energy_not_delivered", "receipts", json!({ "checks": [chk("O3", none.clone())] }), "context_not_held")); + v.push(case("receipts_delivery_gap", "receipts", json!({ "checks": [chk("O10L2", none.clone())] }))); + // §8.2: a claim about a receipt attaches only through an item that passed: O3's + // item and its log-inclusion claim listed in O9a's index. + let o3_index = String::from_utf8(c.file(&index_path(c, "O3")?)?).map_err(|_| "not UTF-8")?; + let o3_item = { + let v: Value = serde_json::from_str(&o3_index).map_err(fault)?; + wai::jcs::jcs(&v["delivery"][0]).map_err(fault)? + }; + v.push(refused("receipts_claim_through_foreign_item", "receipts", json!({ "checks": [{ "object": "O9a", "held": "whole", + "edit_index": [["\"claims\":[]", format!("\"claims\":[\"{}\"]", hex(&cl("l0")))], ["\"delivery\":[", format!("\"delivery\":[{o3_item},")]] }] }), + "receipt_not_about_object")); + // §8.2, jwp-receipts §3.2: a soft binding of another object attests the ancestor only + // through a recovery: O7's delivery receipt verified first gives its publisher key. + let recovery = |value: &str, algs: Value| json!({ "observed_value": value, "exact_match_algs": algs }); + let o7_then_o3 = json!([chk("O7", whole.clone()), chk("O3", whole.clone())]); + v.push(case("receipts_ancestor_recovered", "receipts", json!({ "recovery": recovery("0ff0", json!(["org.example.fingerprint"])), "checks": o7_then_o3 }))); + v.push(refused("receipts_ancestor_no_publisher", "receipts", json!({ "recovery": recovery("0ff0", json!(["org.example.fingerprint"])), + "checks": [chk("O3", whole.clone())] }), "receipt_not_about_object")); + v.push(refused("receipts_ancestor_value_mismatch", "receipts", json!({ "recovery": recovery("0000", json!(["org.example.fingerprint"])), + "checks": o7_then_o3 }), "recovery_value_mismatch")); + v.push(refused("receipts_ancestor_no_matcher", "receipts", json!({ "recovery": recovery("0ff0", json!([])), "checks": o7_then_o3 }), + "recovery_no_matcher")); + v.push(refused("receipts_ancestor_any_object", "receipts", json!({ "checks": [{ "object": "O9a", "held": "whole", + "edit_index": [["\"claims\":[]", format!("\"claims\":[\"{}\"]", hex(&cl("s0")))]] }] }), "receipt_not_about_object")); + // §8.1: a legacy group is refused, its name not fixing its bytes. + v.push(refused("receipts_legacy_group", "receipts", json!({ "serve": { "rcpt": [RCPT, CASE_RCPT] }, + "checks": [{ "object": "O1", "held": "whole", "index": "cases/receipts/O1-legacy.wai.receipts" }] }), "receipt_legacy_group")); + // §8.1: 64 parents and no more, whatever bound a client asks for. + let deep = |i: usize, walk: Value| -> Result { + let ix = format!("cases/receipts/deep-{i}.wai.receipts"); + let cid = serde_json::from_slice::(&c.file(&ix)?).map_err(fault)?["object"].clone(); + Ok(json!({ "serve": { "rcpt": [DEEP_RCPT] }, "max_walk": walk, "checks": [{ "object_cid": cid, "held": "none", "index": ix }] })) + }; + v.push(case("receipts_walk_64", "receipts", deep(64, Value::Null)?)); + v.push(refused("receipts_walk_65", "receipts", deep(65, Value::Null)?, "parent_not_held")); + v.push(refused("receipts_walk_65_asks_1000", "receipts", deep(65, json!(1000))?, "parent_not_held")); + // §8.2: each name fetched once; a fetch budget; files and indexes over their caps. + v.push(case("receipts_items_fetch_each_name_once", "receipts", json!({ "checks": [{ "object": "O9c", "held": "whole", + "index": "cases/receipts/O9c-x50.wai.receipts" }] }))); + v.push(refused("receipts_fetch_budget", "receipts", json!({ "fetch_budget": 2, "checks": [chk("O9c", whole.clone())] }), "parent_not_held")); + v.push(refused("receipts_file_too_large", "receipts", json!({ "serve": { "pad": { hex(&g(0)): MAX_RECEIPT_FILE_BYTES + 1 } }, + "checks": [chk("O9a", whole.clone())] }), "receipt_too_large")); + v.push(refused("receipts_parent_too_large", "receipts", json!({ "serve": { "pad": { hex(&g(0)): MAX_RECEIPT_FILE_BYTES + 1 } }, + "checks": [chk("O9b", whole.clone())] }), "parent_not_held")); + v.push(case("receipts_file_at_its_cap", "receipts", json!({ "serve": { "pad": { hex(&g(0)): MAX_RECEIPT_FILE_BYTES } }, + "checks": [chk("O9a", whole.clone())] }))); + v.push(refused("receipts_index_too_large", "receipts", json!({ "checks": [{ "object": "O9a", "held": "whole", "pad_index": MAX_INDEX_BYTES + 1 }] }), + "receipt_index_too_large")); + v.push(case("receipts_index_at_its_cap", "receipts", json!({ "checks": [{ "object": "O9a", "held": "whole", "pad_index": MAX_INDEX_BYTES }] }))); + v.push(case("receipts_index_256_items", "receipts", json!({ "checks": [{ "object": "O9a", "held": "whole", "index": "cases/receipts/O9a-256-items.wai.receipts" }] }))); + v.push(refused("receipts_index_257_items", "receipts", json!({ "checks": [{ "object": "O9a", "held": "whole", "index": "cases/receipts/O9a-257-items.wai.receipts" }] }), + "receipt_index_too_large")); + v.push(refused("receipts_index_257_claims", "receipts", json!({ "checks": [{ "object": "O9a", "held": "whole", "index": "cases/receipts/O9a-257-claims.wai.receipts" }] }), + "receipt_index_too_large")); + v.push(refused("receipt_file_legacy_group", "receipt_file", json!({ "file": format!("{CASE_RCPT}/{}.json", hex(&legacy_link(c)?)), "kind": "group" }), + "receipt_legacy_group")); + // §8.2: at most 64 MiB of receipt files read for one index. The walk of 64 parents + // reads 65 files: at 1 MiB each it passes the budget, at a million bytes it does not. + let deep_padded = |pad: usize| json!({ "serve": { "rcpt": [DEEP_RCPT], "pad_all": pad }, + "checks": [{ "object_cid": hex(&deep_cid(c, 64).unwrap_or_default()), "held": "none", "index": "cases/receipts/deep-64.wai.receipts" }] }); + v.push(refused("receipts_fetch_bytes_exhausted", "receipts", deep_padded(MAX_RECEIPT_FILE_BYTES), "parent_not_held")); + v.push(case("receipts_fetch_bytes_within", "receipts", deep_padded(1_000_000))); + // §8.2 step 6 for the classes the published indexes carry none of: lawful-basis + // decisions against the declaration fetched by its link, recovery records, + // operating points, and a session claim, whose context no index carries. + let x = extras(c, &m)?; + let xl = |l: &str| hex(&x.claims[l].link_hash()); + let both = json!({ "rcpt": [RCPT, CASE_RCPT] }); + let on = |id: &str, ix: &str, held: &Value| json!({ "object": id, "held": held, "index": format!("cases/receipts/{ix}.wai.receipts") }); + v.push(refused("receipts_lawful_basis", "receipts", json!({ "serve": both, "checks": [on("O3", "O3-lawful-basis", &whole)] }), + "decision_purpose_not_declared")); + v.push(refused("receipts_lawful_basis_declaration_not_served", "receipts", json!({ "serve": { "rcpt": [RCPT, CASE_RCPT], "omit": [xl("ld0")] }, + "checks": [on("O3", "O3-lawful-basis", &whole)] }), "context_not_held")); + // §8.1 first: another claim's bytes under the declaration's name. + v.push(refused("receipts_lawful_basis_declaration_renamed", "receipts", json!({ "serve": { "rcpt": [RCPT, CASE_RCPT], + "as": { xl("ld0"): format!("{CASE_RCPT}/{}.json", xl("ld7")) } }, "checks": [on("O3", "O3-lawful-basis", &whole)] }), "receipt_name_mismatch")); + let watermark = json!({ "observed_value": "00", "exact_match_algs": ["org.example.watermark.1"] }); + v.push(refused("receipts_binding_recovery", "receipts", json!({ "serve": both, "recovery": watermark, + "checks": [on("O3", "O3-binding-recovery", &whole)] }), "recovery_value_mismatch")); + // A recovery record's step 6 needs a matcher, not the object. + v.push(refused("receipts_binding_recovery_object_not_held", "receipts", json!({ "serve": both, "recovery": watermark, + "checks": [on("O3", "O3-binding-recovery", &none)] }), "recovery_signer_mismatch")); + v.push(refused("receipts_binding_recovery_no_matcher", "receipts", json!({ "serve": both, + "checks": [on("O3", "O3-binding-recovery", &whole)] }), "recovery_no_matcher")); + v.push(refused("receipts_operating_point", "receipts", json!({ "serve": both, "checks": [on("O5", "O5-operating-point", &whole)] }), + "figure_disagrees")); + v.push(refused("receipts_operating_point_not_held", "receipts", json!({ "serve": both, "checks": [on("O5", "O5-operating-point", &none)] }), + "context_not_held")); + v.push(refused("receipts_reconstruct_session", "receipts", json!({ "serve": both, "checks": [on("O3", "O3-session", &whole)] }), + "context_not_held")); + // jwp-receipts §3.2's P: of the publisher keys held for the ancestor, the + // declaration's signer's, whichever delivery came first; with none of them, the + // mismatch. + let ancestor = |ix: &str| json!({ "serve": both, "recovery": { "observed_value": "0ff0", "exact_match_algs": ["org.example.fingerprint"] }, + "checks": [on("O7", ix, &whole), chk("O3", whole.clone())] }); + v.push(case("receipts_ancestor_other_publisher_first", "receipts", ancestor("O7-other-first"))); + v.push(case("receipts_ancestor_publisher_first", "receipts", ancestor("O7-publisher-first"))); + v.push(refused("receipts_ancestor_other_publisher_only", "receipts", ancestor("O7-other-only"), "recovery_signer_mismatch")); + // §7.4 beside each verdict: the signers confirmed against the origin's key log, + // anchored from its location. + let anchored = json!([{ "anchor": KEY_LOG }]); + v.push(case("receipts_confirmed", "receipts", json!({ "keys": anchored.clone(), + "checks": [chk("O3", whole.clone()), chk("O10", whole.clone())] }))); + + // §8.4: anchoring from the origin only. + let loc = key_log_url(ORIGIN); + v.push(case("key_log_anchor", "key_log", json!({ "steps": [{ "anchor": KEY_LOG }] }))); + v.push(refused_step("key_log_anchor_redirect", json!({ "steps": [{ "anchor": KEY_LOG, "retrieval": { "requested": loc, "answered": "https://mirror.example/.well-known/wai-keys" } }] }), "key_log_not_from_origin")); + v.push(refused_step("key_log_anchor_elsewhere", json!({ "steps": [{ "anchor": KEY_LOG, "retrieval": { "requested": "https://mirror.example/.well-known/wai-keys" } }] }), "key_log_not_from_origin")); + v.push(refused_step("key_log_anchor_not_200", json!({ "steps": [{ "anchor": KEY_LOG, "retrieval": { "status": 304 } }] }), "key_log_not_from_origin")); + v.push(refused_step("key_log_anchor_plain_http", json!({ "steps": [{ "anchor": KEY_LOG, "origin": "http://origin.example" }] }), "key_log_not_from_origin")); + v.push(refused_step("key_log_anchor_other_origin", json!({ "steps": [{ "anchor": KEY_LOG, "origin": "https://other.example" }] }), "origin_mismatch")); + v.push(refused_step("key_log_anchor_malformed", json!({ "steps": [{ "anchor": KEY_LOG, "edit": [["{\"kind\":\"wai-key-log\"", "{\"kind\":\"wai-key-log\",\"note\":1"]] }] }), "key_log_malformed")); + v.push(case("key_log_anchor_forward", "key_log", json!({ "steps": [{ "anchor": OLDER_LOG }, { "anchor": KEY_LOG }] }))); + v.push(case("key_log_anchor_rolled_back", "key_log", json!({ "steps": [{ "anchor": KEY_LOG }, { "anchor": OLDER_LOG }] }))); + // Through a cache: revisions added, the anchor kept; an older copy reported. + v.push(case("key_log_cached_newer", "key_log", json!({ "steps": [{ "anchor": OLDER_LOG }, { "cached": KEY_LOG }] }))); + v.push(case("key_log_cached_rolled_back", "key_log", json!({ "steps": [{ "anchor": KEY_LOG }, { "cached": OLDER_LOG }] }))); + v.push(case("key_log_cached_other_origin", "key_log", json!({ "steps": [{ "anchor": KEY_LOG }, { "cached": KEY_LOG, "origin": "https://other.example" }] }))); + // §8.4: a key log is read to 1 MiB; a longer one is refused, anchored or cached. + let cap = wai::http_binding::keys::MAX_KEY_LOG_BYTES; + v.push(case("key_log_anchor_at_its_cap", "key_log", json!({ "steps": [{ "anchor": KEY_LOG, "pad": cap }] }))); + v.push(refused_step("key_log_anchor_too_large", json!({ "steps": [{ "anchor": KEY_LOG, "pad": cap + 1 }] }), "key_log_too_large")); + v.push(refused_step("key_log_cached_too_large", json!({ "steps": [{ "anchor": OLDER_LOG }, { "cached": KEY_LOG, "pad": cap + 1 }] }), + "key_log_too_large")); + + // §8.4: the grants file, a locator. + let gfile = format!(".well-known/wai-grants/{digest}.json"); + v.push(case("grants_file", "grants_file", json!({ "file": gfile }))); + v.push(refused("grants_file_other_module", "grants_file", json!({ "file": gfile, "module": "00".repeat(32) }), "grants_file_malformed")); + v.push(refused("grants_file_member_added", "grants_file", json!({ "file": gfile, "edit": [["{\"grants\"", "{\"note\":1,\"grants\""]] }), "grants_file_malformed")); + v.push(refused("grants_file_wrong_kind", "grants_file", json!({ "file": gfile, "edit": [["wai-module-grants", "wai-module-grant"]] }), "grants_file_malformed")); + + // §8.5: the set served by its digest, held only by its pin and §7.9. + let set = json!({ "capability": SET_CAP, "sha256": digest, "served": format!("sets/{digest}"), "keys": anchored, + "statuses": [STATUS], "grants": gfile, "hold_to": [ORIGIN], "now": NOW_HELD }); + let with = |k: &str, val: Value| { + let mut s = set.clone(); + s[k] = val; + s + }; + v.push(case("param_set_held", "param_set", set.clone())); + v.push(refused("param_set_status_stale", "param_set", with("now", json!(NOW_STALE)), "prior_not_authorised")); + v.push(refused("param_set_no_clock", "param_set", with("now", Value::Null), "prior_not_authorised")); + v.push(refused("param_set_no_grant", "param_set", with("grants", Value::Null), "prior_not_authorised")); + v.push(refused("param_set_other_bytes", "param_set", with("served", json!(STREAM_WAI)), "prior_not_held")); + v.push(refused("param_set_revoked", "param_set", with("keys", json!([{ "anchor": REVOKED_LOG }])), "prior_revoked")); + let mut not_held_to = set.clone(); + not_held_to.as_object_mut().expect("an object").remove("hold_to"); + v.push(case("param_set_not_held_to", "param_set", not_held_to.clone())); + let mut revoked_not_held_to = not_held_to; + revoked_not_held_to["keys"] = json!([{ "anchor": REVOKED_LOG }]); + v.push(refused("param_set_revoked_not_held_to", "param_set", revoked_not_held_to, "prior_revoked")); + v.push(case("param_set_pinned_anchor", "param_set", with("keys", json!([{ "pin": KEY_LOG }])))); + v.push(refused("param_set_sha_uppercase", "param_set", with("sha256", json!(digest.to_uppercase())), "pin_malformed")); + Ok(v) + } + + /// The link hash of the corpus's legacy group, from the index that names it. + fn legacy_link(c: &Corpus) -> Result<[u8; 32], String> { + let v: Value = serde_json::from_slice(&c.file("cases/receipts/O1-legacy.wai.receipts")?).map_err(fault)?; + v["delivery"][0]["property"]["group_receipt"].as_str().and_then(unhex32).ok_or("the legacy index names no group".into()) + } + + /// The object the deep chain's index `deep-` is about. + fn deep_cid(c: &Corpus, i: usize) -> Result<[u8; 32], String> { + let v: Value = serde_json::from_slice(&c.file(&format!("cases/receipts/deep-{i}.wai.receipts"))?).map_err(fault)?; + v["object"].as_str().and_then(unhex32).ok_or("the deep index names no object".into()) + } + + /// A `key_log` case whose last step exists for `code`. + fn refused_step(name: &str, input: Value, code: &'static str) -> Case { + refused(name, "key_log", input, code) + } + + // ------------------------------------------------------------------ the fuzz + + /// One member of `t` changed one way, or nothing (one time in five). + fn perturb(s: &mut u64, t: &str, members: &[&str]) -> Value { + let m = pick(s, members); + if !t.contains(m) || next(s) % 5 == 0 { + return json!([]); + } + let whole = !m.ends_with(':') && !m.ends_with('['); + let to = match next(s) % 10 { + 0 => format!("{m}0"), + 1 => m.replacen(':', ":\"", 1) + "\"", + 2 => m.replacen("\":", "x\":", 1), + 3 => format!("\"note\":1,{m}"), + 4 => m.replacen(':', ":-", 1), + 5 => m.replacen(':', ":1.0e0,\"z\":", 1), + 6 => m.to_uppercase(), + 7 if whole => format!("{m},{m}"), + 8 if m.ends_with('"') => format!("{}\\u{:x}\"", &m[..m.len() - 1], [0xd800u32, 0xdfff][(next(s) % 2) as usize]), + _ => m.replacen(':', ":null,\"y\":", 1), + }; + json!([[m, to]]) + } + + /// One iteration of the receipt-side fuzz families: a receipt file, an index, a grants + /// file and a key log, each changed one way or not at all, read as a client reads them. + pub(crate) fn fuzz_cases(c: &Corpus, s: &mut u64, i: usize) -> Result, String> { + let mut v = Vec::new(); + let rdir = c.dir.join(RCPT); + let mut files: Vec = std::fs::read_dir(&rdir).map_err(fault)?.filter_map(|e| e.ok()).map(|e| e.file_name().to_string_lossy().into_owned()).collect(); + files.sort(); + if files.is_empty() { + return Err(format!("{} holds no receipt file", rdir.display())); + } + let f = &files[(next(s) % files.len() as u64) as usize]; + let path = format!("{RCPT}/{f}"); + let t = String::from_utf8(c.file(&path)?).map_err(|_| "not UTF-8")?; + let kind = if t.contains("\"kind\":\"group\"") { "group" } else { "claim" }; + let members = ["\"group_id\":", "\"leaf_count\":", "\"track_alias\":", "\"kind\":\"group\"", "\"kind\":\"claim\"", "\"sign_version\":2", "\"seq\":", + "\"subject_type\":\"object\"", "\"parent_receipt_hash\":", "\"class\":\"stage\"", "\"tier\":\"none\"", "\"units_by_stage\":[", "\"joules_total\":", + "\"signer_id\":\"did:web:origin.example#d1\"", "\"value\":\"0ff0\"", "\"body\":{"]; + let as_kind = if next(s) % 6 == 0 { if kind == "group" { "claim" } else { "group" } } else { kind }; + v.push((format!("fuzz_receipt_file_{i}"), "receipt_file", json!({ "file": path, "kind": as_kind, "edit": perturb(s, &t, &members) }))); + // An index, its object held whole or not at all. + let ids = ["O9a", "O9b", "O9c", "O3", "O7", "O10", "O10L1", "O10L2"]; + let id = pick(s, &ids); + let ofile = object_file_of(c, id)?; + let ix = String::from_utf8(c.file(&format!("objects/{}.wai.receipts", hex(&content_hash(&c.file(&ofile)?))))?).map_err(|_| "not UTF-8")?; + let members = ["\"kind\":\"wai-receipt-index\"", "\"delivery\":[", "\"claims\":[", "\"property\":{", "\"object_receipt\":{", "\"joules_micro\":", + "\"acquisition\":", "\"acquisition_evidence\":", "\"leaf_index\":", "\"direction_bits\":", "\"object_id\":", "\"leaf_version\":2", + "\"siblings\":[", "\"origin\":\"origin\"", "\"energy_provenance\":\"Estimator\"", "\"group_id\":"]; + let held = if next(s) % 2 == 0 { "whole" } else { "none" }; + v.push((format!("fuzz_receipt_index_{i}"), "receipts", json!({ "checks": [{ "object_file": ofile, "held": held, + "edit_index": perturb(s, &ix, &members) }] }))); + // The grants file. + let gdir = c.dir.join(".well-known/wai-grants"); + let gname = std::fs::read_dir(&gdir).map_err(fault)?.filter_map(|e| e.ok()).map(|e| e.file_name().to_string_lossy().into_owned()).min().ok_or("no grants file")?; + let gpath = format!(".well-known/wai-grants/{gname}"); + let gt = String::from_utf8(c.file(&gpath)?).map_err(|_| "not UTF-8")?; + let members = ["\"kind\":\"wai-module-grants\"", "\"module\":", "\"grants\":[", "\"kind\":\"wai-module-grant\"", "\"form\":\"decoder\"", "\"capabilities\":[", + "\"not_after\":", "\"not_before\":", "\"origin\":\"https://origin.example\"", "\"signer_id\":"]; + v.push((format!("fuzz_grants_file_{i}"), "grants_file", json!({ "file": gpath, "edit": perturb(s, >, &members) }))); + // A key log anchored from a retrieval whose URLs or status may be off. + let lt = String::from_utf8(c.file(KEY_LOG)?).map_err(|_| "not UTF-8")?; + let members = ["\"kind\":\"wai-key-log\"", "\"revisions\":[", "\"seq\":", "\"prev\":", "\"issued_at\":", "\"status\":\"active\"", "\"may_sign\":[", + "\"grant_max_s\":", "\"status_max_age_s\":", "\"origin\":\"https://origin.example\"", "\"revoked_modules\":["]; + let loc = key_log_url(ORIGIN); + let urls = [loc.as_str(), "https://origin.example/.well-known/wai-keys/", "https://ORIGIN.example/.well-known/wai-keys", "https://origin.example:443/.well-known/wai-keys", + "http://origin.example/.well-known/wai-keys", "https://origin.example/.well-known/WAI-KEYS", "https://origin.example//.well-known/wai-keys"]; + // Each way off is rare enough that about half the cases anchor. + let requested = if next(s) % 8 == 0 { pick(s, &urls) } else { loc.as_str() }; + let answered = if next(s) % 8 == 0 { pick(s, &urls) } else { requested }; + let status = if next(s) % 8 == 0 { [301u64, 302, 304, 203][(next(s) % 4) as usize] } else { 200 }; + let origin = if next(s) % 8 == 0 { pick(s, &["https://origin.example/", "HTTPS://origin.example", "https://origin.example:443", "http://origin.example"]) } else { ORIGIN }; + let edit = if next(s) % 3 == 0 { perturb(s, <, &members) } else { json!([]) }; + v.push((format!("fuzz_key_log_{i}"), "key_log", json!({ "steps": [{ "anchor": KEY_LOG, "origin": origin, "edit": edit, + "retrieval": { "requested": requested, "answered": answered, "status": status } }] }))); + Ok(v) + } + +} diff --git a/wai/wai-rs/src/energy_figure.rs b/wai/wai-rs/src/energy_figure.rs index 2c61edb78..d70df27f7 100644 --- a/wai/wai-rs/src/energy_figure.rs +++ b/wai/wai-rs/src/energy_figure.rs @@ -218,6 +218,34 @@ impl EnergyFigures for DeliveredObject<'_> { } } +/// A JWP object (`moq-jwp:object`) accepted under its group against the content hash the +/// verifier established for it: recomputed over the envelope, or the cid a verified head +/// proof established for an object fetched in part (http-delivery §8.3). Its figure is +/// [`DeliveredObject`]'s. +pub struct HeldObject<'a> { + pub object: &'a crate::jwp_delivery::ObjectReceipt, + pub group: &'a crate::jwp_delivery::GroupReceipt, + pub established: &'a crate::jwp_hash::EstablishedHash, +} + +impl EnergyFigures for HeldObject<'_> { + fn figure_view(&self) -> Option { + self.object.accept_hash(self.group, self.established).ok()?; + let class = self.object.label.as_ref().and_then(|l| l.class().cloned()); + Some(FigureView { + profile: "moq-jwp:object".into(), + identity: self.object.content_hash, + group: Some(self.group.link_hash()), + figures: vec![Figure { + index: Some(self.object.merkle_proof.leaf_index), + joules_micro: self.object.joules_micro, + class, + }], + signer: Some(self.group.signer_pubkey), + }) + } +} + /// The receipt families whose figure sits in `joules_micro` beside an optional or /// always-present `energy_provenance`, signed by `signer_pubkey`, identified by /// `receipt_hash()`. diff --git a/wai/wai-rs/src/http_binding/keys.rs b/wai/wai-rs/src/http_binding/keys.rs new file mode 100644 index 000000000..68832c797 --- /dev/null +++ b/wai/wai-rs/src/http_binding/keys.rs @@ -0,0 +1,303 @@ +//! Keys, statuses, grants and parameter sets over HTTP (http-delivery §8.4, §8.5). +//! +//! What is served here is jwp-receipts §7's, checked by `key_log` and nowhere else: +//! +//! - an origin's key log at [`KEY_LOG_PATH`] (§7.2). A verifier anchors an origin +//! (§7.3) only from a log retrieved from that location over HTTPS, answered `200` +//! with no redirect ([`anchor_retrieved`], `key_log_not_from_origin` otherwise); a log +//! retrieved through any cache adds revisions, each checked against its parent, and +//! never moves the anchor ([`hold_cached_revisions`]); +//! - a key status at [`STATUS_PATH`] and an origin's module grants at +//! `/.well-known/wai-grants/.json` (§7.9). The grants file is a locator: +//! each grant in it is handed to §7.9's checks on its own ([`read_grants_file`]); +//! - a parameter set at `/`, named by the digest its pin states. The name +//! locates it and proves nothing: the bytes are held only as SPEC §3.1's checks hold +//! them, check 5 being §7.9's ([`check_fetched_set`]), so `pin::verify_bytes_with` and +//! `int_payload::resolve` decide, never the URL. +//! +//! A module grant is §7.9's form. WAI carries a JCP grant only as a reference by its +//! content hash inside the receipt that names it (`quantum_ops::GrantRef`, as a +//! playlist receipt carries its publisher's grant, §8.6); this binding serves no JCP +//! grant, and a client checks such a reference only as the receipt carrying it binds it. +//! +//! Nothing here performs I/O or reads a clock. + +use std::collections::BTreeMap; + +use serde_json::{Map, Value}; + +use super::hex; +use crate::container::{PinRefusal, Prior}; +use crate::jwp_delivery::ObjectReceipt; +use crate::key_log::{ + anchor_log, read_log, Anchor, AnchorPath, Anchoring, Confirmation, Item, ModuleGrant, Reason, Revision, SinkKeyLog, +}; +use crate::pin::{HeldBytes, PinAuthority, VerifiedRef}; + +pub use crate::signer_id::{key_log_url, KEY_LOG_PATH}; + +/// Where an origin serves its key status (§8.4). +pub const STATUS_PATH: &str = "/.well-known/wai-key-status"; + +/// The most bytes of a key log a client reads (§8.4); a longer one is +/// `key_log_too_large`, whatever its first bytes say. A log is a few revisions of a few +/// keys each, kilobytes long; the cap bounds what one retrieval costs. +/// +/// cbindgen:ignore +pub const MAX_KEY_LOG_BYTES: usize = 1 << 20; +/// Where an origin serves its module grants, one file per module digest (§8.4). +pub const GRANTS_PATH: &str = "/.well-known/wai-grants/"; +/// `kind` of a module grants file (§8.4). +pub const GRANTS_KIND: &str = "wai-module-grants"; + +/// The URL of `origin`'s key status. +pub fn status_url(origin: &str) -> String { + format!("{origin}{STATUS_PATH}") +} + +/// The URL of `origin`'s grants file for the module or parameter set `digest`. +pub fn grants_url(origin: &str, digest: &[u8; 32]) -> String { + format!("{origin}{GRANTS_PATH}{}.json", hex(digest)) +} + +/// The URL of the parameter set whose digest is `sha256` (64 lowercase hexadecimal +/// digits, as a pin states it) under `base` (§8.5). +pub fn set_url(base: &str, sha256: &str) -> String { + format!("{}/{sha256}", base.trim_end_matches('/')) +} + +/// The headers a verifier sends when it retrieves a key log to anchor its origin (§8.4): +/// it asks every cache on the way to revalidate with the origin. +pub fn anchoring_request_headers() -> Vec<(&'static str, &'static str)> { + vec![("Cache-Control", "no-cache")] +} + +/// The largest `max-age`, in seconds, a key status may be served with (§8.4): a quarter +/// of the head's `status_max_age_s`, so a status stays fresh at the sink for most of its +/// life after any cache served it. +pub fn status_max_age_s(head: &Revision) -> u64 { + head.status_max_age_s / 4 +} + +/// Why a key log, a grants file or a status was not taken (http-delivery §13, and +/// jwp-receipts §7.7 unchanged). +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum KeysRefusal { + /// §8.4: a log offered for anchoring that was not retrieved from the origin's own + /// key-log location over HTTPS, answered `200` with no redirect + /// (`key_log_not_from_origin`). + NotFromOrigin(&'static str), + /// §8.4: a grants file not of its form, or for another module than the one it is + /// fetched for (`grants_file_malformed`). + GrantsFileMalformed(&'static str), + /// §8.4: a key log longer than [`MAX_KEY_LOG_BYTES`] (`key_log_too_large`). + TooLarge, + /// jwp-receipts §7.3, §7.7: the log's own reason. + Log(Reason), +} + +impl KeysRefusal { + /// The reason code. + pub fn code(&self) -> &'static str { + match self { + KeysRefusal::NotFromOrigin(_) => "key_log_not_from_origin", + KeysRefusal::GrantsFileMalformed(_) => "grants_file_malformed", + KeysRefusal::TooLarge => "key_log_too_large", + KeysRefusal::Log(r) => r.label(), + } + } +} + +impl std::fmt::Display for KeysRefusal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + KeysRefusal::NotFromOrigin(why) | KeysRefusal::GrantsFileMalformed(why) => write!(f, "{}: {why}", self.code()), + KeysRefusal::Log(r) => f.write_str(r.label()), + KeysRefusal::TooLarge => f.write_str(self.code()), + } + } +} + +impl std::error::Error for KeysRefusal {} + +/// How the answer to a key-log request reached the verifier: the URL it requested, the +/// URL that answered (another one after a redirect a client library followed), and the +/// status. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Retrieval<'a> { + pub requested: &'a str, + pub answered: &'a str, + pub status: u16, +} + +/// §8.4 and jwp-receipts §7.3: anchor `origin` from `body`, a key log retrieved as +/// `retrieval` states at `acquired_at`, the verifier holding `current` (its anchor and +/// revisions) where it has one. Refused (`key_log_not_from_origin`) unless `origin` is an +/// `https` origin, the request was for its key-log location ([`key_log_url`]), and the +/// answer is a `200` from that same URL; a body longer than [`MAX_KEY_LOG_BYTES`] is +/// `key_log_too_large`. Then §7.3's checks and moves, as `key_log` +/// makes them: the log's reason, `key_log_rolled_back` for an older copy, and +/// `key_log_replaced` for a log that neither contains the anchor nor supersedes it. +pub fn anchor_retrieved( + origin: &str, + retrieval: &Retrieval<'_>, + body: &[u8], + acquired_at: &str, + current: Option<(&Anchor, &[Revision])>, +) -> Result { + let location = key_log_url(origin); + if !origin.starts_with("https://") { + return Err(KeysRefusal::NotFromOrigin("the origin is not an https origin")); + } + if retrieval.requested != location { + return Err(KeysRefusal::NotFromOrigin("the request was not for the origin's key-log location")); + } + if retrieval.answered != location { + return Err(KeysRefusal::NotFromOrigin("the answer came from another URL: a redirect was followed")); + } + if retrieval.status != 200 { + return Err(KeysRefusal::NotFromOrigin("the answer is not a 200")); + } + if body.len() > MAX_KEY_LOG_BYTES { + return Err(KeysRefusal::TooLarge); + } + let text = std::str::from_utf8(body).map_err(|_| KeysRefusal::Log(Reason::KeyLogMalformed))?; + match anchor_log(origin, text, AnchorPath::Fetched { acquired_at: acquired_at.into() }, current) { + Anchoring::Anchored(a) => Ok(a), + Anchoring::NotAnchored(r) => Err(KeysRefusal::Log(r)), + Anchoring::Replaced => Err(KeysRefusal::Log(Reason::KeyLogReplaced)), + } +} + +/// §8.4: take the revisions of a key log retrieved other than to anchor — through any +/// cache, a mirror, or the origin without the anchoring rules. The log is read as §7.3 +/// reads one (its form, rules 1–3 for `origin`, its first `seq` 0, rules 4–6 between +/// consecutive revisions; that rule's reason). One whose latest revision is a trusted +/// revision older than `anchor` — the anchor's held ancestors — is an older copy +/// (`key_log_rolled_back`) and adds nothing. Otherwise every revision of it the +/// verifier does not hold is added to `held`, and §7.4's head walk decides what each is +/// worth; the anchor never moves. Returns how many were added. `anchor` must be for +/// `origin` (`origin_not_anchored`). +pub fn hold_cached_revisions(origin: &str, body: &[u8], anchor: &Anchor, held: &mut Vec) -> Result { + if anchor.origin != origin { + return Err(KeysRefusal::Log(Reason::OriginNotAnchored)); + } + if body.len() > MAX_KEY_LOG_BYTES { + return Err(KeysRefusal::TooLarge); + } + let text = std::str::from_utf8(body).map_err(|_| KeysRefusal::Log(Reason::KeyLogMalformed))?; + let revs = read_log(origin, text).map_err(KeysRefusal::Log)?; + let by: BTreeMap<[u8; 32], &Revision> = + held.iter().filter(|r| r.check_held(origin).is_ok()).map(|r| (r.revision_hash(), r)).collect(); + let mut trusted = vec![anchor.revision]; + while let Some(p) = by.get(trusted.last().expect("non-empty")).and_then(|r| r.prev) { + if !by.contains_key(&p) || trusted.contains(&p) { + break; + } + trusted.push(p); + } + let latest = revs.last().expect("a log is non-empty").revision_hash(); + if latest != anchor.revision && trusted.contains(&latest) { + return Err(KeysRefusal::Log(Reason::KeyLogRolledBack)); + } + let mut have: Vec<[u8; 32]> = held.iter().map(Revision::revision_hash).collect(); + let mut added = 0; + for r in revs { + let h = r.revision_hash(); + if !have.contains(&h) { + have.push(h); + held.push(r); + added += 1; + } + } + Ok(added) +} + +/// §8.4: read the grants file fetched for `module`: JSON as jwp-receipts §3.1 step 1 +/// reads it, with exactly the members `kind` (`wai-module-grants`), `module` (64 lowercase +/// hexadecimal digits, the digest it is fetched for) and `grants` (an array), or +/// `grants_file_malformed`. Returns each grant's text, to be checked on its own by §7.9 +/// (`key_log::module_check`, `key_log::SinkKeyLog::with_grants`): its canonical form, or, +/// for one outside the canonical subset, its JSON as read, which check 1 refuses. +pub fn read_grants_file(module: &[u8; 32], bytes: &[u8]) -> Result, KeysRefusal> { + let bad = KeysRefusal::GrantsFileMalformed; + let text = std::str::from_utf8(bytes).map_err(|_| bad("not UTF-8"))?; + let v = crate::claim_receipt::parse_strict(text).map_err(|_| bad("not JSON as a receipt is read"))?; + let m = v.as_object().ok_or(bad("not an object"))?; + const MEMBERS: [&str; 3] = ["kind", "module", "grants"]; + if m.len() != MEMBERS.len() || MEMBERS.iter().any(|k| !m.contains_key(*k)) { + return Err(bad("the members are not kind, module and grants")); + } + if m["kind"].as_str() != Some(GRANTS_KIND) { + return Err(bad("kind is not wai-module-grants")); + } + if m["module"].as_str().and_then(super::unhex32) != Some(*module) { + return Err(bad("module is not the digest the file is fetched for")); + } + let grants = m["grants"].as_array().ok_or(bad("grants is not an array"))?; + Ok(grants.iter().map(|g| crate::jcs::jcs(g).unwrap_or_else(|_| g.to_string())).collect()) +} + +/// A grants file (§8.4) for `module`, listing `grants`, in JCS. +pub fn grants_file(module: &[u8; 32], grants: &[ModuleGrant]) -> Vec { + let mut m = Map::new(); + m.insert("kind".into(), GRANTS_KIND.into()); + m.insert("module".into(), hex(module).into()); + let gs: Vec = grants + .iter() + .map(|g| crate::claim_receipt::parse_strict(&g.to_json()).expect("a grant's canonical form reads")) + .collect(); + m.insert("grants".into(), Value::Array(gs)); + crate::jcs::jcs(&Value::Object(m)).expect("a grants file is inside the canonical subset").into_bytes() +} + +/// §8.5: hold a parameter set fetched by the digest `prior` states, as SPEC §3.1's +/// checks hold it over the bytes `served` (`pin::verify_bytes_with`, reading the forms +/// SPEC registers), check 5 run by `sink` where the sink holds a key log: jwp-receipts +/// §7.9 with the statuses and grants it fetched as §8.4 states. The URL the bytes came +/// from counts for nothing: bytes whose digest is not the pin's are not held +/// (`prior_not_held`), and a set §7.9 refuses is not held (`prior_revoked`, +/// `prior_not_authorised`). A set that is not held leaves its capability unsupported for +/// the envelope, and dispatch continues (SPEC §4, §7.1). +pub fn check_fetched_set( + capability: &str, + prior: &Prior, + served: &HeldBytes<'_>, + sink: Option<&SinkKeyLog>, +) -> Result { + crate::pin::verify_bytes_with( + capability, + prior, + std::slice::from_ref(served), + crate::pin::READABLE_FORMS, + sink.map(|s| s as &dyn PinAuthority), + ) +} + +/// The files a set listing names, each with the digest its URL is formed from (§8.5): +/// a set in the form `wai.prior.set` is fetched as its listing, by the pin's digest, then +/// each file by its own digest. The listing's own reading (`set_malformed`). +pub fn set_files(listing: &[u8]) -> Result, PinRefusal> { + crate::pin::parse_listing(listing) +} + +/// jwp-receipts §7.4 for the group of a delivery item accepted from an index (§8.2), +/// with its object receipt: its signer confirmed against the revisions and anchors the +/// verifier holds — anchored over HTTP as §8.4 states, or pinned. Reported beside the +/// item's verdict, never folded into it. `None` where the group or the object receipt +/// does not verify, which an accepted item's always do. +pub fn confirm_delivery( + group: &crate::jwp_delivery::GroupReceipt, + object: &ObjectReceipt, + revisions: &[Revision], + anchors: &[Anchor], +) -> Option { + let item = Item::from_group(group, &[object]).ok()?; + Some(crate::key_log::confirm(&item, revisions, anchors, None)) +} + +/// jwp-receipts §7.4 for a claim accepted from an index (§8.2), reported beside its +/// verdict. +pub fn confirm_claim(claim: &crate::claim_receipt::ClaimReceipt, revisions: &[Revision], anchors: &[Anchor]) -> Confirmation { + crate::key_log::confirm(&Item::from_claim(claim), revisions, anchors, None) +} diff --git a/wai/wai-rs/src/http_binding/mod.rs b/wai/wai-rs/src/http_binding/mod.rs index 975ad5cec..4811d0f4e 100644 --- a/wai/wai-rs/src/http_binding/mod.rs +++ b/wai/wai-rs/src/http_binding/mod.rs @@ -17,7 +17,13 @@ //! - [`dash`]: `wai.mpd` on the classical timeline (§11.1); //! - `playlist_receipt` (with `video_manifest`): playlist receipts, sealed and checked, //! and a live revision chain (§8.6, §11.6); -//! - [`origin`]: an origin's responses (§5.3) and byte-range answers. +//! - [`origin`]: an origin's responses (§5.3) and byte-range answers; +//! - `receipts` (with `receipt_claims`): receipt files named by link hash, an object's +//! receipt index, and the checks a client runs on what they locate, an object fetched +//! in part attested through the cid its head proof established (§8.1–§8.3, §10.5); +//! - `keys` (with `key_discovery`): key logs anchored over HTTP and taken through caches, +//! statuses, grants files, and parameter sets fetched by their digest and held only as +//! their pins and jwp-receipts §7.9 hold them (§8.4, §8.5). //! //! Nothing here performs I/O: the `wai http` commands and the reference origin do. @@ -28,11 +34,15 @@ pub mod dash_read; pub mod head; pub mod head_proof; pub mod hls; +#[cfg(feature = "key_discovery")] +pub mod keys; pub mod origin; pub mod plan; pub mod playlist; #[cfg(feature = "video_manifest")] pub mod playlist_receipt; +#[cfg(feature = "receipt_claims")] +pub mod receipts; pub mod session; pub mod uri; @@ -42,6 +52,8 @@ mod tests; mod tests_abr; #[cfg(test)] mod tests_playlists; +#[cfg(all(test, feature = "receipt_claims"))] +mod tests_receipts; /// The proposed media type of an object (§3). It is not registered. pub const MEDIA_TYPE: &str = "application/wai"; diff --git a/wai/wai-rs/src/http_binding/origin.rs b/wai/wai-rs/src/http_binding/origin.rs index 111108acf..ff226d9e9 100644 --- a/wai/wai-rs/src/http_binding/origin.rs +++ b/wai/wai-rs/src/http_binding/origin.rs @@ -66,6 +66,32 @@ pub fn key_resource_headers() -> Vec<(&'static str, String)> { ] } +/// The headers for a receipt index `.wai.receipts` (§8.2): a mutable locator, +/// revalidated on every use, with an `ETag` of its own (its content hash). +pub fn receipt_index_headers(index: &[u8]) -> Vec<(&'static str, String)> { + vec![ + ("Content-Type", "application/json".to_string()), + ("Cache-Control", "no-cache, no-transform".to_string()), + ("ETag", format!("\"{}\"", hex(&super::content_hash(index)))), + ("Access-Control-Allow-Origin", "*".to_string()), + ] +} + +/// The headers for a key status (§8.4): `max-age` at most a quarter of the +/// `status_max_age_s` of the head it names, where the origin knows it; revalidated on +/// every use where it does not. +pub fn key_status_headers(max_age_s: Option) -> Vec<(&'static str, String)> { + let cc = match max_age_s { + Some(s) => format!("max-age={s}, no-transform"), + None => "no-cache, no-transform".to_string(), + }; + vec![ + ("Content-Type", "application/json".to_string()), + ("Cache-Control", cc), + ("Access-Control-Allow-Origin", "*".to_string()), + ] +} + /// Whether a `Range` is honoured under `If-Range` (RFC 9110 §13.1.5): with no /// `If-Range`, yes; with one, only when it is the representation's strong `etag` /// exactly. A weak validator never matches, and a date is not used for an object. diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs new file mode 100644 index 000000000..bdec9436b --- /dev/null +++ b/wai/wai-rs/src/http_binding/receipts.rs @@ -0,0 +1,845 @@ +//! Receipts over HTTP (http-delivery §8.1–§8.3, §10.5): receipt files named by their +//! link hash, an object's receipt index, and the checks a client runs on what they locate. +//! +//! Every check here is jwp-receipts' own, bound to HTTP carriage and never re-created: +//! +//! - a receipt file `rcpt/.json` holds one group receipt in the labelled payload, or +//! one claim, read as §3 step 1 or §3.1 steps 1–2 read it, and is refused unless its +//! link hash is `h` (`receipt_name_mismatch`). A legacy group is refused +//! (`receipt_legacy_group`): its link hash does not cover its parent, so its name does +//! not fix its bytes; +//! - an index `.wai.receipts` is a locator: each delivery item and each claim it +//! lists is checked on its own, and one that fails is ignored with its reason; +//! - a delivery item's group goes through §3 steps 1–3 against the client's +//! [`ReceiptChains`], the parents it lacks fetched by their links up to 64, and its +//! object receipt through steps 4–5 against what the client holds of the object: the +//! content hash recomputed over the envelope, the cid a verified head proof establishes +//! for an object fetched in part (§8.3, [`EstablishedHash`]), or nothing, for an object +//! it did not receive, which is then `inclusion-verified` and never delivered (§10.5); +//! - a claim goes through §3.1 steps 1–6: its subject the cid, or a group a delivery +//! item that passed names; its parent fetched by its link; and step 6 run against the +//! object receipts of the delivery items that passed, or the claims they name, a claim +//! whose step 6 needs a receipt the client does not hold not accepted. A soft-binding +//! claim about another object attests that object, the ancestor, only once §3.2's +//! recovery has passed; +//! - one index check fetches each name once, at most [`FETCH_BUDGET`] files and +//! [`MAX_INDEX_FETCH_BYTES`] in all, none over [`MAX_RECEIPT_FILE_BYTES`], from an +//! index of at most [`MAX_INDEX_BYTES`] listing at most [`MAX_INDEX_ITEMS`] delivery +//! items and [`MAX_INDEX_CLAIMS`] claims. A declaration a claim names is fetched by +//! its link and read as §8.1 reads every file fetched by name. +//! +//! Nothing here performs I/O: `fetch` gives the bytes served at `rcpt/.json` beside +//! the object, or `None` where nothing is served. + +use std::collections::BTreeMap; + +use serde_json::{Map, Value}; + +use super::{hex, unhex32}; +use crate::claim_receipt::{ + ClaimBody, ClaimClass, ClaimReceipt, LawfulBasisClaim, Reason as ClaimReason, Rejection, SoftBindingMatcher, SubjectType, +}; +use crate::energy_figure::{EnergyFigures, HeldObject}; +use crate::jwp_delivery::{ + Admission, DeliveryError, Evidence, GroupReceipt, ObjectReceipt, PropertyReading, Reason, ReceiptChains, WaiReceiptProperty, +}; +use crate::jwp_hash::{EstablishedHash, Establishment}; + +/// The directory of receipt files, resolved against an object's URI (§8.1). +pub const RECEIPT_DIR: &str = "rcpt/"; + +/// `kind` of a receipt index (§8.2). +pub const INDEX_KIND: &str = "wai-receipt-index"; + +/// How many parents a client fetches for one group, at most (§8.1). A group whose +/// chain is further back than this stays `parent_not_held`. A client MAY hold a lower +/// bound, never a higher one. A client that checks receipts as they arrive holds its +/// chains and walks only across what it missed, so the bound is a gap, not a history. +/// +/// cbindgen:ignore +pub const PARENT_WALK_BOUND: usize = 64; + +/// The most bytes a receipt file may hold (§8.1); one over it is `receipt_too_large`. +/// A file holds one receipt, a group's or a claim's, of a few kilobytes; the largest +/// claim bodies list objects or stages by the hundred and stay well inside this. +/// +/// cbindgen:ignore +pub const MAX_RECEIPT_FILE_BYTES: usize = 1 << 20; + +/// The most bytes a receipt index may hold (§8.2); one over it is +/// `receipt_index_too_large`. It holds the most delivery items and claims an index may +/// list, each item a property and an object receipt of under a kilobyte, with room. +/// +/// cbindgen:ignore +pub const MAX_INDEX_BYTES: usize = 4 << 20; + +/// The most delivery items an index may list (§8.2); more is `receipt_index_too_large`. +/// An object is delivered under one group per track and publisher that carries it, and +/// an index lists those, not every hop's copy. +/// +/// cbindgen:ignore +pub const MAX_INDEX_ITEMS: usize = 256; + +/// The most claims an index may list (§8.2); more is `receipt_index_too_large`. Claims +/// about one object chain per signer, so an index lists each signer's latest few, never +/// a log. +/// +/// cbindgen:ignore +pub const MAX_INDEX_CLAIMS: usize = 256; + +/// The most receipt files a client fetches to check one index (§8.2), each name fetched +/// once; past it a fetch is `receipt_fetch_budget_exhausted`. A client MAY hold a lower +/// budget, never a higher one. It covers every item and claim an index may list, a +/// declaration for each claim, and full parent walks for a few groups. +/// +/// cbindgen:ignore +pub const FETCH_BUDGET: usize = 1024; + +/// The most bytes of receipt files a client reads to check one index (§8.2). Each file +/// is read only as far as what remains of this budget and of its own cap (one byte past +/// [`MAX_RECEIPT_FILE_BYTES`]); a file that reaches the end of the budget is refused, +/// and once the budget is spent no file is fetched (`receipt_fetch_bytes_exhausted`). +/// Without it, [`FETCH_BUDGET`] files at their cap would let one index make a client +/// read and hold about a gibibyte. A client MAY hold a lower budget, never a higher one. +/// +/// cbindgen:ignore +pub const MAX_INDEX_FETCH_BYTES: usize = 64 << 20; + +/// The path of the receipt file whose link hash is `h`, relative to the object (§8.1). +pub fn receipt_path(h: &[u8; 32]) -> String { + format!("{RECEIPT_DIR}{}.json", hex(h)) +} + +/// The last path segment of an object's receipt index, `.wai.receipts` (§8.2). +pub fn index_name(cid: &[u8; 32]) -> String { + format!("{}.wai.receipts", hex(cid)) +} + +/// Why a receipt file, an index, an item of it, or a claim it lists was refused or +/// ignored (http-delivery §13, and jwp-receipts' reasons unchanged). +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ReceiptRefusal { + /// §8.2: the index, or an item of it, is not of its form (`receipt_index_malformed`). + IndexMalformed(&'static str), + /// §8.2: the index holds more bytes, delivery items or claims than a client reads + /// (`receipt_index_too_large`). + IndexTooLarge(&'static str), + /// §8.2: an index, a delivery item or a claim about another object + /// (`receipt_not_about_object`). + NotAboutObject, + /// §8.1: nothing is served at the receipt file's name (`receipt_not_served`). + NotServed, + /// §8.1: a receipt file over [`MAX_RECEIPT_FILE_BYTES`] (`receipt_too_large`). + TooLarge, + /// §8.2: the index's fetch budget is spent (`receipt_fetch_budget_exhausted`). + BudgetExhausted, + /// §8.2: the bytes a client reads for one index are spent + /// (`receipt_fetch_bytes_exhausted`). + BytesExhausted, + /// §8.1: the file's link hash is not its name (`receipt_name_mismatch`). + NameMismatch, + /// §8.1: a group receipt in the legacy payload, whose link hash does not cover its + /// parent, so the name does not fix the bytes (`receipt_legacy_group`). + LegacyGroup, + /// jwp-receipts §3, §3.3: a group or object receipt, or a property, refused. + Delivery(DeliveryError), + /// jwp-receipts §3.1: a claim refused. + Claim(Rejection), + /// §8.2: a claim whose §3.1 step 6 needs a receipt the client does not hold, other + /// than a `measurement` claim's figure (`figure_not_held`) (`context_not_held`). + ContextNotHeld, + /// §8.2, jwp-receipts §3.2: a soft-binding claim about another object whose recovery + /// the client ran and which failed, with the recovery's reason. + Recovery(Rejection), + /// §8.1, jwp-receipts §3 step 3 and §3.1 step 5: a parent the client could not + /// recover (`parent_not_held`), after `walked` parents fetched; `stop` says why the + /// walk ended: `bound`, `another_chain`, or the reason the parent fetched last was + /// refused or not fetched. + ParentNotHeld { walked: usize, stop: String }, +} + +impl ReceiptRefusal { + /// The reason code. + pub fn code(&self) -> &'static str { + match self { + ReceiptRefusal::IndexMalformed(_) => "receipt_index_malformed", + ReceiptRefusal::IndexTooLarge(_) => "receipt_index_too_large", + ReceiptRefusal::NotAboutObject => "receipt_not_about_object", + ReceiptRefusal::NotServed => "receipt_not_served", + ReceiptRefusal::TooLarge => "receipt_too_large", + ReceiptRefusal::BudgetExhausted => "receipt_fetch_budget_exhausted", + ReceiptRefusal::BytesExhausted => "receipt_fetch_bytes_exhausted", + ReceiptRefusal::NameMismatch => "receipt_name_mismatch", + ReceiptRefusal::LegacyGroup => "receipt_legacy_group", + ReceiptRefusal::Delivery(e) => e.reason.label(), + ReceiptRefusal::Claim(r) | ReceiptRefusal::Recovery(r) => r.reason.label(), + ReceiptRefusal::ContextNotHeld => "context_not_held", + ReceiptRefusal::ParentNotHeld { .. } => "parent_not_held", + } + } + + /// The jwp-receipts step a refusal of jwp-receipts' own was raised at, where it has + /// one; `context_not_held` is step 6's. + pub fn step(&self) -> Option { + match self { + ReceiptRefusal::Delivery(e) => e.step, + ReceiptRefusal::Claim(r) | ReceiptRefusal::Recovery(r) => Some(r.step), + ReceiptRefusal::ContextNotHeld => Some(6), + _ => None, + } + } +} + +impl std::fmt::Display for ReceiptRefusal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ReceiptRefusal::IndexMalformed(why) => write!(f, "receipt_index_malformed: {why}"), + ReceiptRefusal::IndexTooLarge(why) => write!(f, "receipt_index_too_large: {why}"), + ReceiptRefusal::ParentNotHeld { walked, stop } => write!(f, "parent_not_held: {walked} parents fetched, stopped at {stop}"), + other => match other.step() { + Some(s) => write!(f, "{} at step {s}", other.code()), + None => f.write_str(other.code()), + }, + } + } +} + +impl std::error::Error for ReceiptRefusal {} + +fn malformed_group() -> ReceiptRefusal { + ReceiptRefusal::Delivery(DeliveryError { step: Some(1), reason: Reason::Malformed }) +} + +/// §8.1: read the receipt file named `name` as a group receipt: JSON as §3.1 step 1 +/// reads it, then §3 step 1 (`malformed`, `wrong_kind`, `group_label_invalid`), then the +/// payload (`receipt_legacy_group` for the legacy one), then the name +/// (`receipt_name_mismatch`). The signature is not checked here. +pub fn read_group_file(name: &[u8; 32], bytes: &[u8]) -> Result { + let text = std::str::from_utf8(bytes).map_err(|_| malformed_group())?; + let v = crate::claim_receipt::parse_strict(text).map_err(|_| malformed_group())?; + let g = GroupReceipt::from_json(&v).map_err(ReceiptRefusal::Delivery)?; + if g.label.is_none() { + return Err(ReceiptRefusal::LegacyGroup); + } + if g.link_hash() != *name { + return Err(ReceiptRefusal::NameMismatch); + } + Ok(g) +} + +/// §8.1: read the receipt file named `name` as a claim: §3.1 steps 1–2, then the name +/// (`receipt_name_mismatch`). The signature is not checked here. +pub fn read_claim_file(name: &[u8; 32], bytes: &[u8]) -> Result { + let text = std::str::from_utf8(bytes) + .map_err(|_| ReceiptRefusal::Claim(Rejection { step: 1, reason: ClaimReason::Malformed }))?; + let c = ClaimReceipt::from_json(text).map_err(ReceiptRefusal::Claim)?; + if c.link_hash() != *name { + return Err(ReceiptRefusal::NameMismatch); + } + Ok(c) +} + +/// A group receipt's file (§8.1): its JSON form (jwp-receipts §2.4.1) in JCS. +pub fn group_file(g: &GroupReceipt) -> Result, String> { + crate::jcs::jcs(&g.to_json()).map(String::into_bytes).map_err(|e| e.to_string()) +} + +/// A claim's file (§8.1): its JCS form. +pub fn claim_file(c: &ClaimReceipt) -> Vec { + c.to_json().into_bytes() +} + +// ───────────────────────────── fetching ───────────────────────────── + +/// The receipt files one index check fetches (§8.1, §8.2): each name fetched once and +/// what was served remembered, at most a budget of fetches (never above +/// [`FETCH_BUDGET`]) and of bytes ([`MAX_INDEX_FETCH_BYTES`]), and a file over +/// [`MAX_RECEIPT_FILE_BYTES`] refused. +pub struct ReceiptFetcher<'f> { + fetch: &'f mut dyn FnMut(&[u8; 32], usize) -> Option>, + memo: BTreeMap<[u8; 32], Served>, + fetched: usize, + budget: usize, + bytes: usize, +} + +/// What one name gave, kept for the rest of the index's check. +enum Served { + Bytes(Vec), + Nothing, + /// The file that reached the end of the index's bytes. + PastBytes, +} + +impl<'f> ReceiptFetcher<'f> { + /// A fetcher over `fetch` with `budget` fetches, at most [`FETCH_BUDGET`]. `fetch` + /// gives what is served at a name, reading at most the number of bytes it is given; + /// anything more it returns is taken as unread. + pub fn new(fetch: &'f mut dyn FnMut(&[u8; 32], usize) -> Option>, budget: usize) -> Self { + ReceiptFetcher { fetch, memo: BTreeMap::new(), fetched: 0, budget: budget.min(FETCH_BUDGET), bytes: 0 } + } + + /// How many files were fetched (each name once). + pub fn fetched(&self) -> usize { + self.fetched + } + + /// The file named `h`: `receipt_not_served`, `receipt_too_large`, or, past the + /// budget, `receipt_fetch_budget_exhausted`. Each fetch is asked for no more than + /// what remains of the bytes ([`MAX_INDEX_FETCH_BYTES`]) and of the file's cap plus + /// one byte, so the bytes read never pass the budget; a file that reaches the end of + /// the bytes is refused (`receipt_fetch_bytes_exhausted`), since whether it ends + /// there is not read, and once they are spent no further file is fetched. + pub fn get(&mut self, h: &[u8; 32]) -> Result, ReceiptRefusal> { + if !self.memo.contains_key(h) { + if self.fetched >= self.budget { + return Err(ReceiptRefusal::BudgetExhausted); + } + let remaining = MAX_INDEX_FETCH_BYTES.saturating_sub(self.bytes); + if remaining == 0 { + return Err(ReceiptRefusal::BytesExhausted); + } + self.fetched += 1; + let limit = remaining.min(MAX_RECEIPT_FILE_BYTES + 1); + let served = match (self.fetch)(h, limit) { + None => Served::Nothing, + Some(mut b) => { + b.truncate(limit); + self.bytes += b.len(); + if b.len() == remaining { Served::PastBytes } else { Served::Bytes(b) } + } + }; + self.memo.insert(*h, served); + } + match &self.memo[h] { + Served::Nothing => Err(ReceiptRefusal::NotServed), + Served::PastBytes => Err(ReceiptRefusal::BytesExhausted), + Served::Bytes(b) if b.len() > MAX_RECEIPT_FILE_BYTES => Err(ReceiptRefusal::TooLarge), + Served::Bytes(b) => Ok(b.clone()), + } + } +} + +// ───────────────────────────── the index ───────────────────────────── + +/// One delivery item of an index: a `WAI_RECEIPT` property and the object receipt it +/// describes (§8.2). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct IndexItem { + pub property: WaiReceiptProperty, + pub object_receipt: ObjectReceipt, +} + +/// A receipt index as read (§8.2): the object it names, and each delivery item and claim +/// link it lists, each read on its own. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ReceiptIndex { + pub object: [u8; 32], + pub delivery: Vec>, + pub claims: Vec>, +} + +impl ReceiptIndex { + /// Read an index: at most [`MAX_INDEX_BYTES`] (`receipt_index_too_large`), JSON as + /// jwp-receipts §3.1 step 1 reads it, with exactly the members `kind` + /// (`wai-receipt-index`), `object` (64 lowercase hexadecimal digits), `delivery` and + /// `claims` (arrays), or the whole index is refused (`receipt_index_malformed`), as it + /// is when it lists more than [`MAX_INDEX_ITEMS`] delivery items or + /// [`MAX_INDEX_CLAIMS`] claims (`receipt_index_too_large`). Each delivery item is an + /// object with exactly the members `property` and `object_receipt` + /// (`receipt_index_malformed`); its property reads as moq-streaming-format §5's + /// (`property_invalid`) and its object receipt as jwp-receipts §2.4.2's (that + /// reading's reason). Each claim entry is a link hash (`receipt_index_malformed`). An + /// item that does not read is kept as its refusal. + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() > MAX_INDEX_BYTES { + return Err(ReceiptRefusal::IndexTooLarge("more bytes than a client reads")); + } + let bad = ReceiptRefusal::IndexMalformed; + let text = std::str::from_utf8(bytes).map_err(|_| bad("not UTF-8"))?; + let v = crate::claim_receipt::parse_strict(text).map_err(|_| bad("not JSON as a receipt is read"))?; + let m = v.as_object().ok_or(bad("not an object"))?; + const MEMBERS: [&str; 4] = ["kind", "object", "delivery", "claims"]; + if m.len() != MEMBERS.len() || MEMBERS.iter().any(|k| !m.contains_key(*k)) { + return Err(bad("the members are not kind, object, delivery and claims")); + } + if m["kind"].as_str() != Some(INDEX_KIND) { + return Err(bad("kind is not wai-receipt-index")); + } + let object = m["object"].as_str().and_then(unhex32).ok_or(bad("object is not a content hash"))?; + let delivery = m["delivery"].as_array().ok_or(bad("delivery is not an array"))?; + let claims = m["claims"].as_array().ok_or(bad("claims is not an array"))?; + if delivery.len() > MAX_INDEX_ITEMS { + return Err(ReceiptRefusal::IndexTooLarge("more delivery items than a client reads")); + } + if claims.len() > MAX_INDEX_CLAIMS { + return Err(ReceiptRefusal::IndexTooLarge("more claims than a client reads")); + } + Ok(ReceiptIndex { + object, + delivery: delivery.iter().map(read_item).collect(), + claims: claims.iter().map(|c| c.as_str().and_then(unhex32).ok_or(bad("a claim entry is not a link hash"))).collect(), + }) + } +} + +fn read_item(v: &Value) -> Result { + let bad = ReceiptRefusal::IndexMalformed("a delivery item is not an object with exactly property and object_receipt"); + let m = v.as_object().ok_or_else(|| bad.clone())?; + if m.len() != 2 || !m.contains_key("property") || !m.contains_key("object_receipt") { + return Err(bad); + } + let property = WaiReceiptProperty::from_json(&m["property"]) + .ok_or(ReceiptRefusal::Delivery(DeliveryError { step: None, reason: Reason::PropertyInvalid }))?; + let object_receipt = ObjectReceipt::from_json(&m["object_receipt"]).map_err(ReceiptRefusal::Delivery)?; + Ok(IndexItem { property, object_receipt }) +} + +/// An index's bytes (§8.2), in JCS, listing `items` and `claims` for the object `cid`. +pub fn index_file(cid: &[u8; 32], items: &[IndexItem], claims: &[[u8; 32]]) -> Result, String> { + let mut m = Map::new(); + m.insert("kind".into(), INDEX_KIND.into()); + m.insert("object".into(), hex(cid).into()); + m.insert( + "delivery".into(), + Value::Array( + items + .iter() + .map(|i| { + let mut it = Map::new(); + it.insert("property".into(), i.property.to_json()); + it.insert("object_receipt".into(), i.object_receipt.to_json()); + Value::Object(it) + }) + .collect(), + ), + ); + m.insert("claims".into(), Value::Array(claims.iter().map(|c| Value::from(hex(c))).collect())); + crate::jcs::jcs(&Value::Object(m)).map(String::into_bytes).map_err(|e| e.to_string()) +} + +// ───────────────────────────── delivery items ───────────────────────────── + +/// What a client holds of the object an index is about. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ObjectHeld { + /// The object, whole or, over HTTP, in part: the content hash jwp-receipts §3 step 4 + /// compares with ([`EstablishedHash`]). + Object(EstablishedHash), + /// Nothing: the client did not receive it (it abandoned it, or never asked). + Nothing, +} + +/// A delivery item that passed (§8.2): how its group joined the client's chain, the +/// parents fetched for it, what the object receipt establishes, and, for a delivered +/// object, how its content hash was established and the property's reading. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DeliveryVerdict { + pub admission: Admission, + pub walked: usize, + pub evidence: Evidence, + pub established: Option, + pub reading: Option, + pub group: GroupReceipt, + pub object: ObjectReceipt, + /// The hash the object receipt was accepted against, for a delivered object. + pub held: Option, +} + +/// §3 step 3 for `group` against `chains`, recovering the parents it lacks (§8.1): while +/// the group is `parent_not_held`, fetch the receipt file its missing parent's link +/// names, read it as a group file, require it on the group's key and track, and offer it +/// to the chains — at most `max_walk` files, never more than [`PARENT_WALK_BOUND`]. Once +/// one is admitted, the parents fetched before it are offered again, nearest that one +/// first, then `group`. Returns how `group` was admitted and how many files were fetched. +fn admit_walking( + chains: &mut ReceiptChains, + group: &GroupReceipt, + fetch: &mut ReceiptFetcher<'_>, + now_ms: u64, + max_walk: usize, +) -> Result<(Admission, usize), ReceiptRefusal> { + match chains.admit(group, now_ms) { + Ok(a) => return Ok((a, 0)), + Err(e) if e.reason != Reason::ParentNotHeld => return Err(ReceiptRefusal::Delivery(e)), + Err(_) => {} + } + let bound = max_walk.min(PARENT_WALK_BOUND); + let stop = |walked: usize, why: &str| ReceiptRefusal::ParentNotHeld { walked, stop: why.to_string() }; + let mut pending: Vec = Vec::new(); + let mut link = group.parent_receipt_hash; + let mut fetched = 0usize; + loop { + // §3 step 3 says `parent_not_held` only of a group that names a parent. + let Some(l) = link else { return Err(stop(fetched, "parent_not_held")) }; + if fetched == bound { + return Err(stop(fetched, "bound")); + } + let bytes = match fetch.get(&l) { + Ok(b) => b, + Err(ReceiptRefusal::TooLarge) => return Err(stop(fetched + 1, "receipt_too_large")), + Err(r) => return Err(stop(fetched, r.code())), + }; + fetched += 1; + let p = read_group_file(&l, &bytes).map_err(|r| stop(fetched, r.code()))?; + // A group of another chain is never this group's predecessor (§3 step 3). + if p.signer_pubkey != group.signer_pubkey || p.track_alias != group.track_alias { + return Err(stop(fetched, "another_chain")); + } + match chains.admit(&p, now_ms) { + Ok(_) => break, + Err(e) if e.reason == Reason::ParentNotHeld => { + link = p.parent_receipt_hash; + pending.push(p); + } + Err(e) => return Err(stop(fetched, e.reason.label())), + } + } + for p in pending.iter().rev() { + chains.admit(p, now_ms).map_err(|e| stop(fetched, e.reason.label()))?; + } + chains.admit(group, now_ms).map(|a| (a, fetched)).map_err(ReceiptRefusal::Delivery) +} + +/// §8.2's check of one delivery item of the index of the object `cid`, in order: +/// +/// 1. the property's `content_hash` and the object receipt's are `cid` +/// (`receipt_not_about_object`); +/// 2. the group receipt is fetched at `rcpt/.json` and read as +/// a group file of that name ([`read_group_file`]); +/// 3. jwp-receipts §3 steps 1–3 against `chains`, recovering missing parents as §8.1 +/// states, at most `max_walk` of them (never more than [`PARENT_WALK_BOUND`]); +/// 4. steps 4–5 for the object receipt, step 4 against what the client holds: the +/// object (`delivered-verified`), or nothing (`inclusion-verified`, never delivered); +/// 5. the property, against the object receipt for a delivered object +/// (moq-streaming-format §5: `property_invalid`, `property_disagrees`), alone for one +/// not delivered, which gives no reading. +pub fn check_delivery( + cid: &[u8; 32], + item: &IndexItem, + held: &ObjectHeld, + fetch: &mut ReceiptFetcher<'_>, + chains: &mut ReceiptChains, + now_ms: u64, + max_walk: usize, +) -> Result { + if item.property.content_hash != *cid || item.object_receipt.content_hash != *cid { + return Err(ReceiptRefusal::NotAboutObject); + } + let name = item.property.group_receipt; + let bytes = fetch.get(&name)?; + let group = read_group_file(&name, &bytes)?; + let (admission, walked) = admit_walking(chains, &group, fetch, now_ms, max_walk)?; + let established = match held { + ObjectHeld::Object(e) => Some(*e), + ObjectHeld::Nothing => None, + }; + let evidence = chains + .verify_admitted_object(&group, &item.object_receipt, established.as_ref()) + .map_err(ReceiptRefusal::Delivery)?; + let reading = match &established { + Some(e) => Some(item.property.check_against_object_hash(&item.object_receipt, &group, e).map_err(ReceiptRefusal::Delivery)?), + None => { + item.property.check().map_err(ReceiptRefusal::Delivery)?; + None + } + }; + Ok(DeliveryVerdict { + admission, + walked, + evidence, + established: established.map(|e| e.by()), + reading, + group, + object: item.object_receipt.clone(), + held: established, + }) +} + +// ───────────────────────────── claims ───────────────────────────── + +/// What an accepted claim attests (§8.2). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Attests { + /// The object in hand: its subject is the cid. + Object, + /// A group receipt a delivery item that passed names: its subject is that group's + /// link hash. + Receipt, + /// Another object, the ancestor the object in hand was derived from: a soft-binding + /// claim whose subject is not the cid, through which jwp-receipts §3.2's recovery + /// passed. It says nothing of the object in hand. + Ancestor, +} + +impl Attests { + /// `object`, `receipt` or `ancestor`. + pub fn label(self) -> &'static str { + match self { + Attests::Object => "object", + Attests::Receipt => "receipt", + Attests::Ancestor => "ancestor", + } + } +} + +/// A claim that passed (§8.2): the claim, what it attests, and whether it starts its +/// signer's chain or links to the parent fetched for it. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ClaimVerdict { + pub claim: ClaimReceipt, + pub attests: Attests, + /// The parent fetched and linked to, `None` for a claim that starts its chain. + pub parent: Option<[u8; 32]>, +} + +/// What a client brings to jwp-receipts §3.2's recovery through a soft binding: the +/// publisher keys of delivery receipts it has verified under §3 over the objects' bytes, +/// each with the content hash it is for; the value the registered algorithm computed from +/// the object in hand; and its matchers, which step 6 also uses for a `binding-recovery` +/// record (§2.6.3), whether or not the object is held. Without it, or without the +/// object in hand or a publisher key for the soft binding's subject, no recovery +/// through a soft binding is run. +pub struct Recovery<'a> { + pub publishers: &'a [([u8; 32], [u8; 32])], + pub observed_value: &'a [u8], + pub matchers: &'a [&'a dyn SoftBindingMatcher], +} + +/// §3.1 step 6 for `claim` over HTTP (§8.2): against the delivery items of the index +/// that passed (`passed`, in the index's order), and the declarations the claim names, +/// fetched by their links. A check whose receipt the client does not hold is +/// `figure_not_held` for a `measurement` claim and `context_not_held` for any other. +fn step6( + claim: &ClaimReceipt, + passed: &[DeliveryVerdict], + fetch: &mut ReceiptFetcher<'_>, + matchers: &[&dyn SoftBindingMatcher], +) -> Result<(), ReceiptRefusal> { + let c = ReceiptRefusal::Claim; + let delivered = || passed.iter().filter(|v| v.held.is_some()); + match &claim.body { + // §2.4.5: against the object receipt for its subject, verified under §3, and the + // OER receipt it names. + ClaimBody::Energy(e) => { + let mut any = false; + for v in delivered() { + any = true; + let class = v.object.label.as_ref().and_then(|l| l.class()); + claim.check_energy_against_labelled_object(&v.object.content_hash, v.object.joules_micro, class).map_err(c)?; + } + if !any || e.oer_receipt.is_some() { + return Err(ReceiptRefusal::ContextNotHeld); + } + Ok(()) + } + // §2.4.8, §2.4.9: against the receipt carrying the figure — for `moq-jwp:object`, + // an object receipt accepted under the group the claim names. + ClaimBody::Measurement(_) | ClaimBody::OperatingPoint(_) => { + let group = match &claim.body { + ClaimBody::Measurement(m) => m.figure.group, + ClaimBody::OperatingPoint(o) => o.figure.group, + _ => None, + }; + let view = delivered().find(|v| Some(v.group.link_hash()) == group).and_then(|v| { + HeldObject { object: &v.object, group: &v.group, established: v.held.as_ref()? }.figure_view() + }); + match view { + Some(view) => claim.check_figure_against(Some(&view)).map_err(c), + None if claim.needs_figure() => Err(c(Rejection { step: 6, reason: ClaimReason::FigureNotHeld })), + None => Err(ReceiptRefusal::ContextNotHeld), + } + } + // §2.4.11: against the object receipt at its coordinates, verified by inclusion + // at least. + ClaimBody::DeliveryGap(g) => { + let at = passed.iter().find(|v| { + (v.object.track_alias, v.object.group_id, v.object.object_id) == (g.track_alias, g.group_id, g.object_id) + }); + let v = at.ok_or(ReceiptRefusal::ContextNotHeld)?; + claim.check_gap_against(v.object.track_alias, v.object.group_id, v.object.object_id, &v.object.content_hash).map_err(c) + } + // §2.4.6: against the declaration it names, fetched by its link. + ClaimBody::LawfulBasis(LawfulBasisClaim::Decision { declaration, .. }) => { + let d = declaration_of(declaration, fetch)?; + claim.check_decision_against(&d).map_err(c) + } + // §2.6.3: against its declaration together with the publisher key of a verified + // delivery receipt for the declaration's subject. + ClaimBody::BindingRecovery(r) => { + if passed.is_empty() { + return Err(ReceiptRefusal::ContextNotHeld); + } + let d = declaration_of(&r.declaration, fetch)?; + let publisher = passed + .iter() + .map(|v| v.group.signer_pubkey) + .find(|k| *k == d.signer_pubkey) + .unwrap_or(passed[0].group.signer_pubkey); + claim.check_recovery_against(&d, &publisher, matchers).map_err(c) + } + // §2.4.12: its outputs, received objects and envelopes are the sink's, not + // served beside an object. + ClaimBody::ReconstructSession(_) => Err(ReceiptRefusal::ContextNotHeld), + _ => Ok(()), + } +} + +/// A declaration a claim names, fetched as `rcpt/.json` and read as §8.1 reads +/// every file fetched by name ([`read_claim_file`]: §3.1 steps 1–2, then the name, +/// `receipt_name_mismatch`). Not served is `context_not_held`. Its steps 3–4 are step +/// 6's, which reports a declaration that does not verify as `context_invalid`. +fn declaration_of(link: &[u8; 32], fetch: &mut ReceiptFetcher<'_>) -> Result { + let bytes = match fetch.get(link) { + Ok(b) => b, + Err(ReceiptRefusal::NotServed) => return Err(ReceiptRefusal::ContextNotHeld), + Err(r) => return Err(r), + }; + read_claim_file(link, &bytes) +} + +/// §8.2's check of one claim the index of the object `cid` lists by its link hash +/// `name`, against the delivery items of the index that passed, in order: +/// +/// 1. it is fetched at `rcpt/.json` and reads as a claim of that name +/// ([`read_claim_file`]); +/// 2. its subject is `cid`, or, for a claim about a receipt, the link hash of the group of +/// a delivery item that passed; any other claim is `receipt_not_about_object`, except a +/// soft-binding claim about another object when the client holds the object (`held`) +/// and `recovery` holds a publisher key for that object: the key equal to the claim's +/// `signer_pubkey` where it holds one, else the first, whose recovery then reports the +/// signer mismatch; +/// 3. jwp-receipts §3.1 steps 1–4; +/// 4. step 5: a claim that names a parent has it fetched at `rcpt/.json`, read +/// as a claim of that name and verified (`parent_not_held` otherwise), and links to it +/// (`chain_break` otherwise); +/// 5. step 6 against the items that passed and the declarations the claim names +/// (`figure_not_held`, `context_not_held` where the client holds no such receipt); or, +/// for a soft binding of another object, jwp-receipts §3.2's recovery with the +/// object in hand's content hash, whose reason a failure reports. +pub fn check_claim( + cid: &[u8; 32], + passed: &[DeliveryVerdict], + held: &ObjectHeld, + name: &[u8; 32], + fetch: &mut ReceiptFetcher<'_>, + recovery: Option<&Recovery<'_>>, +) -> Result { + let bytes = fetch.get(name)?; + let claim = read_claim_file(name, &bytes)?; + let groups: Vec<[u8; 32]> = passed.iter().map(|v| v.group.link_hash()).collect(); + // §3.2's P for the subject: of the publisher keys held for it, the declaration's own + // signer's where one is held, so an earlier delivery under another key cannot block a + // true recovery; else the first, whose recovery reports the mismatch. + let publisher = |subject: &[u8; 32]| { + let r = recovery.filter(|_| matches!(held, ObjectHeld::Object(_)))?; + let mut keys = r.publishers.iter().filter(|(h, _)| h == subject).map(|(_, k)| *k); + let first = keys.next()?; + Some(if first == claim.signer_pubkey { first } else { keys.find(|k| *k == claim.signer_pubkey).unwrap_or(first) }) + }; + let attests = match claim.subject_type { + SubjectType::Object if claim.subject == *cid => Attests::Object, + SubjectType::Receipt if groups.contains(&claim.subject) => Attests::Receipt, + SubjectType::Object if claim.class() == ClaimClass::SoftBinding && publisher(&claim.subject).is_some() => Attests::Ancestor, + _ => return Err(ReceiptRefusal::NotAboutObject), + }; + claim.verify().map_err(ReceiptRefusal::Claim)?; + let parent = match claim.parent_receipt_hash { + None => None, + Some(p) => { + let stop = |walked: usize, why: &str| ReceiptRefusal::ParentNotHeld { walked, stop: why.to_string() }; + let pb = match fetch.get(&p) { + Ok(b) => b, + Err(ReceiptRefusal::TooLarge) => return Err(stop(1, "receipt_too_large")), + Err(r) => return Err(stop(0, r.code())), + }; + let prev = read_claim_file(&p, &pb).map_err(|r| stop(1, r.code()))?; + prev.verify().map_err(|r| stop(1, r.reason.label()))?; + claim.check_parent(Some(&prev)).map_err(ReceiptRefusal::Claim)?; + Some(p) + } + }; + let no_matchers: [&dyn SoftBindingMatcher; 0] = []; + let matchers = recovery.map_or(&no_matchers[..], |r| r.matchers); + if attests == Attests::Ancestor { + let r = recovery.expect("a publisher key came from the recovery"); + let key = publisher(&claim.subject).expect("checked above"); + crate::claim_receipt::recover(&claim, &key, r.observed_value, Some(*cid), matchers).map_err(ReceiptRefusal::Recovery)?; + } else { + step6(&claim, passed, fetch, matchers)?; + } + Ok(ClaimVerdict { claim, attests, parent }) +} + +// ───────────────────────────── the whole index ───────────────────────────── + +/// What a client established from an index (§8.2): each delivery item's verdict or +/// reason, and each claim's, in the index's order, and how many receipt files it fetched. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct IndexReport { + pub delivery: Vec>, + pub claims: Vec>, + pub fetched: usize, +} + +/// The bounds a client checks an index under (§8.1, §8.2): the parents walked for one +/// group and the receipt files fetched for the index, each clamped to the protocol's. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Limits { + pub max_walk: usize, + pub fetch_budget: usize, +} + +impl Default for Limits { + fn default() -> Self { + Limits { max_walk: PARENT_WALK_BOUND, fetch_budget: FETCH_BUDGET } + } +} + +/// §8.2: check the index `index` of the object `cid` against what the client holds of +/// the object, with the receipt files `fetch` serves and the client's `chains`, under +/// `limits`, and, where the client brings one, `recovery`'s context. The index is refused +/// whole when it does not read (`receipt_index_malformed`), is too large +/// (`receipt_index_too_large`) or names another object (`receipt_not_about_object`); +/// otherwise each delivery item is checked in order ([`check_delivery`]), then each claim +/// ([`check_claim`]) against the items that passed. Each name is fetched once, at most +/// `limits.fetch_budget` files and [`MAX_INDEX_FETCH_BYTES`] bytes in all; `fetch` is +/// given, with each name, the most bytes it may read of it. +#[allow(clippy::too_many_arguments)] +pub fn check_index( + cid: &[u8; 32], + index: &[u8], + held: &ObjectHeld, + fetch: &mut dyn FnMut(&[u8; 32], usize) -> Option>, + chains: &mut ReceiptChains, + now_ms: u64, + limits: &Limits, + recovery: Option<&Recovery<'_>>, +) -> Result { + let ix = ReceiptIndex::from_bytes(index)?; + if ix.object != *cid { + return Err(ReceiptRefusal::NotAboutObject); + } + let mut fetcher = ReceiptFetcher::new(fetch, limits.fetch_budget); + let delivery: Vec> = ix + .delivery + .iter() + .map(|i| match i { + Ok(item) => check_delivery(cid, item, held, &mut fetcher, chains, now_ms, limits.max_walk), + Err(e) => Err(e.clone()), + }) + .collect(); + let passed: Vec = delivery.iter().filter_map(|d| d.as_ref().ok().cloned()).collect(); + // A recovery through a soft binding runs only for an object the client holds (the + // value it compares is computed from it): check_claim gates it on `held`. The + // matchers serve step 6 of a recovery record whether or not the object is held. + let claims = ix + .claims + .iter() + .map(|c| match c { + Ok(name) => check_claim(cid, &passed, held, name, &mut fetcher, recovery), + Err(e) => Err(e.clone()), + }) + .collect(); + Ok(IndexReport { delivery, claims, fetched: fetcher.fetched() }) +} diff --git a/wai/wai-rs/src/http_binding/session.rs b/wai/wai-rs/src/http_binding/session.rs index fd00472db..20ad9792f 100644 --- a/wai/wai-rs/src/http_binding/session.rs +++ b/wai/wai-rs/src/http_binding/session.rs @@ -389,6 +389,21 @@ impl FetchSession { self.result.as_ref().and_then(|r| r.as_ref().ok()) } + /// The content hash jwp-receipts §3 step 4 compares the delivered object's receipt + /// with (http-delivery §8.3), once the session has delivered: recomputed over the + /// whole envelope it fetched and verified against the cid, or, where it fetched the + /// object in part, the cid its head proof verified, every component it delivered + /// checked against its entry's digest. `None` before a delivery, after a refusal, and + /// for a layer whose request was abandoned, of which nothing was received. Only a + /// session builds the second kind, so a cid nothing verified never stands in for one. + pub fn established(&self) -> Option { + let f = self.done()?; + match f.verified_by { + VerifiedBy::WholeObject => f.envelope.as_deref().map(crate::jwp_hash::EstablishedHash::of_envelope), + VerifiedBy::HeadProof => Some(crate::jwp_hash::EstablishedHash::by_head_proof(self.cid)), + } + } + /// The base, as soon as it verified (§10.1): the picked component and the companions /// that are not refinements, before any refinement has arrived or been abandoned. Its /// `refinements` are empty; [`Self::done`] adds them. A layer, and an object fetched diff --git a/wai/wai-rs/src/http_binding/tests_receipts.rs b/wai/wai-rs/src/http_binding/tests_receipts.rs new file mode 100644 index 000000000..3ed7b798a --- /dev/null +++ b/wai/wai-rs/src/http_binding/tests_receipts.rs @@ -0,0 +1,833 @@ +//! Unit tests of receipts over HTTP (§8.1–§8.3, §10.5) and, with `key_discovery`, of +//! keys, statuses, grants and parameter sets (§8.4, §8.5). + +use std::collections::BTreeMap; + +use ed25519_dalek::SigningKey; + +use super::head_proof::HeadProof; +use super::plan::FetchPolicy; +use super::receipts::*; +use super::session::{drive, FetchSession, ResponseMeta, Selector, VerifiedBy, Want}; +use super::{content_hash, origin, Fragment}; +use crate::claim_receipt::{BindingType, ClaimBody, ClaimReceipt, LogInclusionClaim, SoftBindingClaim, SubjectType}; +use crate::container::{Rendition, WaiMulti}; +use crate::jwp_delivery::{ + resign, Admission, Evidence, GroupBuilder, GroupReceipt, ObjectLabel, ObjectReceipt, PropertyReading, Reason, ReceiptChains, + WaiReceiptProperty, +}; +use crate::jwp_hash::{EstablishedHash, Establishment}; + +fn key(s: u8) -> SigningKey { + SigningKey::from_bytes(&[s; 32]) +} + +fn pattern(len: usize, a: usize, b: usize) -> Vec { + (0..len).map(|i| ((a * i + b) % 256) as u8).collect() +} + +/// Appendix D's shape, its payloads varied by `v`: two AV1 renditions and a companion. +fn envelope(v: usize) -> Vec { + WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![ + Rendition::media("wai.video.av1", "av1", pattern(3000, 7, v)).with_id("low").with_payload_digest(), + Rendition::media("wai.video.av1", "av1", pattern(50_000, 13, v)).with_id("high").with_payload_digest(), + Rendition::companion("wai.meta.hdr10plus", pattern(200, 31, v), "wai.video.av1").with_id("hdr") + .with_payload_digest().with_companion_of_id("high"), + ]).to_bytes().unwrap() +} + +const SIGNER: &str = "did:web:origin.example#d1"; + +/// A track of `n` labelled groups under key `seed`, each with the one object +/// `envelope(i)`, chained; and the receipt files an origin serves, by link hash. +struct Track { + envs: Vec>, + groups: Vec, + objects: Vec, + files: BTreeMap<[u8; 32], Vec>, +} + +fn track(seed: u8, track_alias: u64, n: usize) -> Track { + let mut t = Track { envs: Vec::new(), groups: Vec::new(), objects: Vec::new(), files: BTreeMap::new() }; + for i in 0..n { + let env = envelope(i + 10 * seed as usize); + let mut b = GroupBuilder::default(); + b.add(content_hash(&env), 0, "origin", Some(ObjectLabel::Unmetered)); + let parent = t.groups.last().map(GroupReceipt::link_hash); + let (g, mut o) = b + .finish(&key(seed), SIGNER, track_alias, i as u64, "2026-10-06T00:00:00Z", "2026-10-06T00:00:04Z", parent, true) + .unwrap(); + t.files.insert(g.link_hash(), group_file(&g).unwrap()); + t.envs.push(env); + t.groups.push(g); + t.objects.push(o.remove(0)); + } + t +} + +fn item(g: &GroupReceipt, o: &ObjectReceipt) -> IndexItem { + IndexItem { + property: WaiReceiptProperty { + content_hash: o.content_hash, + joules_micro: 0, + acquisition: String::new(), + acquisition_evidence: Vec::new(), + group_receipt: g.link_hash(), + }, + object_receipt: o.clone(), + } +} + +fn whole(env: &[u8]) -> ObjectHeld { + ObjectHeld::Object(EstablishedHash::of_envelope(env)) +} + +fn check( + t: &Track, + files: &BTreeMap<[u8; 32], Vec>, + i: usize, + held: &ObjectHeld, + chains: &mut ReceiptChains, + bound: usize, +) -> Result { + let cid = content_hash(&t.envs[i]); + let index = index_file(&cid, &[item(&t.groups[i], &t.objects[i])], &[]).unwrap(); + let mut fetch = |h: &[u8; 32], _: usize| files.get(h).cloned(); + let r = check_index(&cid, &index, held, &mut fetch, chains, 0, &Limits { max_walk: bound, fetch_budget: FETCH_BUDGET }, None).unwrap(); + r.delivery.into_iter().next().unwrap() +} + +/// One delivery item checked against the files `files` serves. +fn deliver( + cid: &[u8; 32], + it: &IndexItem, + held: &ObjectHeld, + files: &BTreeMap<[u8; 32], Vec>, + chains: &mut ReceiptChains, +) -> Result { + let mut fetch = |h: &[u8; 32], _: usize| files.get(h).cloned(); + let mut f = ReceiptFetcher::new(&mut fetch, FETCH_BUDGET); + check_delivery(cid, it, held, &mut f, chains, 0, PARENT_WALK_BOUND) +} + +/// One claim checked against the items that passed and the files `files` serves. +fn claim_of( + cid: &[u8; 32], + passed: &[DeliveryVerdict], + name: &[u8; 32], + files: &BTreeMap<[u8; 32], Vec>, +) -> Result { + let mut fetch = |h: &[u8; 32], _: usize| files.get(h).cloned(); + let mut f = ReceiptFetcher::new(&mut fetch, FETCH_BUDGET); + check_claim(cid, passed, &ObjectHeld::Nothing, name, &mut f, None) +} + +/// B's retrofit: §3 step 4 compares with the hash established for the object, recomputed +/// over its bytes or the cid a head proof verified; anything else is a mismatch. +#[test] +fn step_4_compares_the_established_hash() { + let t = track(0x61, 1, 1); + let (g, o, env) = (&t.groups[0], &t.objects[0], &t.envs[0]); + assert_eq!(o.accept(g, env), Ok(())); + assert_eq!(o.accept_hash(g, &EstablishedHash::of_envelope(env)), Ok(())); + assert_eq!(o.accept_hash(g, &EstablishedHash::by_head_proof(content_hash(env))), Ok(())); + let other = EstablishedHash::of_envelope(b"another object"); + let e = o.accept_hash(g, &other).unwrap_err(); + assert_eq!((e.step, e.reason), (Some(4), Reason::ContentHashMismatch)); + assert_eq!(o.verify_against_hash(g, None), Ok(Evidence::InclusionVerified)); + assert_eq!(o.verify_against_hash(g, Some(&EstablishedHash::by_head_proof(o.content_hash))), Ok(Evidence::DeliveredVerified)); + let p = item(g, o).property; + assert_eq!(p.check_against_object_hash(o, g, &EstablishedHash::by_head_proof(o.content_hash)), Ok(PropertyReading::Unmetered)); + assert_eq!(p.check_against_object_hash(o, g, &other).unwrap_err().reason, Reason::ContentHashMismatch); + assert_eq!(p.check_against_object(o, g, env), Ok(PropertyReading::Unmetered)); +} + +/// An answerer for `env` and its proof, as an origin answers each request. +fn answer(env: &[u8], proof: &[u8], w: &Want) -> Option<(ResponseMeta, Vec)> { + let range = match w { + Want::HeadProof => return Some((ResponseMeta { status: 200, ..Default::default() }, proof.to_vec())), + Want::Whole(_) => None, + Want::Range(r, _) => Some(r.header()), + Want::Rest(r, _) => Some(format!("bytes={}-", r.first)), + }; + let s = origin::serve(env.len() as u64, range.as_deref()); + Some((ResponseMeta { status: s.status, content_range: s.content_range, content_encoding: None }, env[s.body.start as usize..s.body.end as usize].to_vec())) +} + +/// §8.3: a session that verified the head by its proof and the component by its digest +/// establishes the cid; one that fetched whole recomputes it; one refused establishes +/// nothing. +#[test] +fn a_session_establishes_only_what_it_verified() { + let env = envelope(1); + let cid = content_hash(&env); + let proof = HeadProof::build(&env).unwrap().to_bytes(); + let mut s = FetchSession::new(cid, Fragment::default(), FetchPolicy::default(), Selector::new(|c| c == "wai.video.av1")); + let f = drive(&mut s, |w| answer(&env, &proof, w)).unwrap(); + assert_eq!(f.verified_by, VerifiedBy::HeadProof); + let e = s.established().unwrap(); + assert_eq!((*e.hash(), e.by()), (cid, Establishment::HeadProof)); + // An object its URI says is below one chunk is fetched whole: the hash is recomputed + // over it. + let small = WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![ + Rendition::media("wai.video.av1", "av1", pattern(100, 7, 1)).with_id("low").with_payload_digest(), + ]).to_bytes().unwrap(); + let small_proof = HeadProof::build(&small).unwrap().to_bytes(); + let frag = Fragment { n: Some(small.len() as u64), ..Default::default() }; + let mut s = FetchSession::new(content_hash(&small), frag, FetchPolicy::default(), Selector::new(|c| c == "wai.video.av1")); + let f = drive(&mut s, |w| answer(&small, &small_proof, w)).unwrap(); + assert_eq!(f.verified_by, VerifiedBy::WholeObject); + assert_eq!(s.established(), Some(EstablishedHash::of_envelope(&small))); + // A head changed in transit: refused, nothing established. + let mut bad = env.clone(); + bad[100] ^= 1; + let mut s = FetchSession::new(cid, Fragment::default(), FetchPolicy::default(), Selector::new(|c| c == "wai.video.av1")); + assert!(drive(&mut s, |w| answer(&bad, &proof, w)).is_err()); + assert_eq!(s.established(), None); +} + +/// §8.1: a receipt file is named by its link hash; a file read under another name, or +/// read as the other kind, or not JSON as a receipt is read, is refused. +#[test] +fn receipt_files_are_named_by_their_link_hash() { + let t = track(0x61, 1, 2); + let (g0, g1) = (&t.groups[0], &t.groups[1]); + let b0 = &t.files[&g0.link_hash()]; + assert_eq!(read_group_file(&g0.link_hash(), b0).as_ref(), Ok(g0)); + assert_eq!(read_group_file(&g1.link_hash(), b0), Err(ReceiptRefusal::NameMismatch)); + let text = String::from_utf8(b0.clone()).unwrap(); + let twice = text.replacen("{", "{\"group_id\":0,", 1); + assert_eq!(read_group_file(&g0.link_hash(), twice.as_bytes()).unwrap_err().code(), "malformed"); + assert_eq!(read_claim_file(&g0.link_hash(), b0).unwrap_err().code(), "wrong_kind"); + assert_eq!(receipt_path(&[0xab; 32]), format!("rcpt/{}.json", "ab".repeat(32))); +} + +/// §8.2: an index is read item by item; one item that does not read is kept as its +/// reason, and only an index not of its form is refused whole. +#[test] +fn an_index_reads_item_by_item() { + let t = track(0x61, 1, 1); + let cid = content_hash(&t.envs[0]); + let good = index_file(&cid, &[item(&t.groups[0], &t.objects[0])], &[[7; 32]]).unwrap(); + let ix = ReceiptIndex::from_bytes(&good).unwrap(); + assert_eq!((ix.object, ix.delivery.len(), ix.claims.len()), (cid, 1, 1)); + let text = String::from_utf8(good).unwrap(); + let broken_item = text.replacen("\"object_receipt\":{", "\"note\":1,\"object_receipt\":{", 1); + let ix = ReceiptIndex::from_bytes(broken_item.as_bytes()).unwrap(); + assert_eq!(ix.delivery[0].as_ref().unwrap_err().code(), "receipt_index_malformed"); + let broken_claim = text.replacen(&"07".repeat(32), "07", 1); + assert_eq!(ReceiptIndex::from_bytes(broken_claim.as_bytes()).unwrap().claims[0].as_ref().unwrap_err().code(), "receipt_index_malformed"); + let extra = text.replacen("{", "{\"extra\":1,", 1); + assert_eq!(ReceiptIndex::from_bytes(extra.as_bytes()).unwrap_err().code(), "receipt_index_malformed"); + let bad_property = text.replacen("\"acquisition\":\"\"", "\"acquisition\":0", 1); + assert_eq!(ReceiptIndex::from_bytes(bad_property.as_bytes()).unwrap().delivery[0].as_ref().unwrap_err().code(), "property_invalid"); + // An index about another object is refused whole. + let mut fetch = |h: &[u8; 32], _: usize| t.files.get(h).cloned(); + let other = content_hash(b"another"); + let e = check_index(&other, text.as_bytes(), &ObjectHeld::Nothing, &mut fetch, &mut ReceiptChains::new(), 0, &Limits::default(), None).unwrap_err(); + assert_eq!(e, ReceiptRefusal::NotAboutObject); +} + +/// §8.1, §8.2: a client that holds nothing of a chain walks back to its start by the +/// parents' links; a later check finds the group at the head; an earlier object's group +/// is admitted again; an object not received is inclusion-verified. +#[test] +fn a_delivery_item_walks_to_its_chain() { + let t = track(0x61, 1, 3); + let mut chains = ReceiptChains::new(); + let v = check(&t, &t.files, 2, &whole(&t.envs[2]), &mut chains, PARENT_WALK_BOUND).unwrap(); + assert_eq!((v.admission, v.walked, v.evidence), (Admission::Linked, 2, Evidence::DeliveredVerified)); + assert_eq!((v.established, v.reading), (Some(Establishment::Envelope), Some(PropertyReading::Unmetered))); + let v = check(&t, &t.files, 2, &whole(&t.envs[2]), &mut chains, PARENT_WALK_BOUND).unwrap(); + assert_eq!((v.admission, v.walked), (Admission::Linked, 0)); + let v = check(&t, &t.files, 1, &ObjectHeld::Nothing, &mut chains, PARENT_WALK_BOUND).unwrap(); + assert_eq!((v.admission, v.walked, v.evidence, v.reading), (Admission::AlreadyAdmitted, 0, Evidence::InclusionVerified, None)); + // §8.3: the cid a head proof established attests the object in part. + let v = check(&t, &t.files, 0, &ObjectHeld::Object(EstablishedHash::by_head_proof(content_hash(&t.envs[0]))), &mut chains, 64).unwrap(); + assert_eq!((v.evidence, v.established), (Evidence::DeliveredVerified, Some(Establishment::HeadProof))); + // Another object's bytes in hand: step 4 refuses. + let e = check(&t, &t.files, 0, &whole(&t.envs[1]), &mut chains, 64).unwrap_err(); + assert_eq!((e.code(), e.step()), ("content_hash_mismatch", Some(4))); +} + +/// §8.1: a parent the walk cannot recover leaves the group `parent_not_held`, saying +/// why; a joining client anchors instead and walks nothing. +#[test] +fn an_unrecovered_parent_is_parent_not_held() { + let t = track(0x61, 1, 3); + let pnh = |walked: usize, stop: &str| ReceiptRefusal::ParentNotHeld { walked, stop: stop.into() }; + assert_eq!(check(&t, &t.files, 2, &ObjectHeld::Nothing, &mut ReceiptChains::new(), 1).unwrap_err(), pnh(1, "bound")); + let mut gone = t.files.clone(); + gone.remove(&t.groups[0].link_hash()); + assert_eq!(check(&t, &gone, 2, &ObjectHeld::Nothing, &mut ReceiptChains::new(), 64).unwrap_err(), pnh(1, "receipt_not_served")); + let mut swapped = t.files.clone(); + swapped.insert(t.groups[0].link_hash(), t.files[&t.groups[1].link_hash()].clone()); + assert_eq!(check(&t, &swapped, 2, &ObjectHeld::Nothing, &mut ReceiptChains::new(), 64).unwrap_err(), pnh(2, "receipt_name_mismatch")); + let v = check(&t, &gone, 2, &ObjectHeld::Nothing, &mut ReceiptChains::new().with_join(), 64).unwrap(); + assert_eq!((v.admission, v.walked), (Admission::Anchor, 0)); + // A parent link that names a group of another key is never a predecessor. + let other = track(0x62, 1, 1); + let mut b = GroupBuilder::default(); + b.add(content_hash(&t.envs[0]), 0, "origin", Some(ObjectLabel::Unmetered)); + let (g, o) = b.finish(&key(0x61), SIGNER, 1, 5, "2026-10-06T00:00:00Z", "2026-10-06T00:00:04Z", Some(other.groups[0].link_hash()), true).unwrap(); + let mut files = other.files.clone(); + files.insert(g.link_hash(), group_file(&g).unwrap()); + let index = index_file(&o[0].content_hash, &[item(&g, &o[0])], &[]).unwrap(); + let mut fetch = |h: &[u8; 32], _: usize| files.get(h).cloned(); + let r = check_index(&o[0].content_hash, &index, &ObjectHeld::Nothing, &mut fetch, &mut ReceiptChains::new(), 0, &Limits::default(), None).unwrap(); + assert_eq!(r.delivery[0].as_ref().unwrap_err(), &pnh(1, "another_chain")); +} + +/// §8.2's item checks, each with its reason: another object, a group not served, a +/// signature that does not verify, a property that disagrees with the signed leaf. +#[test] +fn a_delivery_item_is_refused_for_its_reason() { + let t = track(0x61, 1, 1); + let (g, o) = (&t.groups[0], &t.objects[0]); + let cid = o.content_hash; + let mut chains = ReceiptChains::new(); + let mut it = item(g, o); + it.property.content_hash = [9; 32]; + assert_eq!(deliver(&cid, &it, &ObjectHeld::Nothing, &t.files, &mut chains), Err(ReceiptRefusal::NotAboutObject)); + let mut it = item(g, o); + it.property.group_receipt = [9; 32]; + assert_eq!(deliver(&cid, &it, &ObjectHeld::Nothing, &t.files, &mut chains), Err(ReceiptRefusal::NotServed)); + // A signature changed: served under the link hash of the bytes as changed. + let mut bad = g.clone(); + bad.sig[0] ^= 1; + let files: BTreeMap<[u8; 32], Vec> = [(bad.link_hash(), group_file(&bad).unwrap())].into(); + let it = item(&bad, o); + let e = deliver(&cid, &it, &ObjectHeld::Nothing, &files, &mut chains).unwrap_err(); + assert_eq!((e.code(), e.step()), ("signature_verify_failed", Some(2))); + // A property stating a figure the unmetered leaf does not. + let mut it = item(g, o); + it.property.joules_micro = 5; + it.property.acquisition = "Estimator".into(); + assert_eq!(deliver(&cid, &it, &whole(&t.envs[0]), &t.files, &mut chains).unwrap_err().code(), "property_disagrees"); + // The same property beside an object not received gives no reading, and passes. + assert_eq!(deliver(&cid, &it, &ObjectHeld::Nothing, &t.files, &mut chains).unwrap().reading, None); + // A resigned group whose root no longer covers the object: step 5. + let mut moved = g.clone(); + moved.root_hash = [3; 32]; + let moved = resign(moved, &key(0x61)); + let files: BTreeMap<[u8; 32], Vec> = [(moved.link_hash(), group_file(&moved).unwrap())].into(); + let e = deliver(&cid, &item(&moved, o), &ObjectHeld::Nothing, &files, &mut ReceiptChains::new()).unwrap_err(); + assert_eq!((e.code(), e.step()), ("inclusion_proof_failed", Some(5))); +} + +fn soft(signer: u8, subject: [u8; 32], seq: u64, parent: Option<[u8; 32]>) -> ClaimReceipt { + let body = ClaimBody::SoftBinding(SoftBindingClaim { + alg: "org.example.fp".into(), + registry: "https://registry.example/soft-binding".into(), + binding_type: BindingType::Fingerprint, + value: vec![0, 0xff], + resolution_endpoint: "https://origin.example/resolve".into(), + }); + ClaimReceipt::seal(&key(signer), SIGNER, SubjectType::Object, subject, seq, "2026-10-06T00:00:05Z", body, parent).unwrap() +} + +fn logged(signer: u8, subject: [u8; 32], seq: u64, parent: Option<[u8; 32]>) -> ClaimReceipt { + let body = ClaimBody::LogInclusion(LogInclusionClaim { + log: "https://log.example/".into(), + profile: "https://log.example/profile".into(), + entry: "1".into(), + log_receipt: vec![1, 2, 3], + }); + ClaimReceipt::seal(&key(signer), SIGNER, SubjectType::Receipt, subject, seq, "2026-10-06T00:00:06Z", body, parent).unwrap() +} + +/// §8.2: a claim about the object attests it; one about a listed group attests that +/// receipt; a soft-binding claim about another object attests the ancestor; any other +/// claim about something else is not about the object. A claim's parent is fetched by its +/// link. +#[test] +fn claims_are_checked_by_subject_and_parent() { + let t = track(0x61, 1, 1); + let cid = t.objects[0].content_hash; + let group = t.groups[0].link_hash(); + let c0 = soft(0x63, cid, 0, None); + let c1 = logged(0x63, group, 1, Some(c0.link_hash())); + let ancestor = soft(0x64, [5; 32], 0, None); + let elsewhere = logged(0x65, [6; 32], 0, None); + let mut files = t.files.clone(); + for c in [&c0, &c1, &ancestor, &elsewhere] { + files.insert(c.link_hash(), claim_file(c)); + } + let names = [c0.link_hash(), c1.link_hash(), ancestor.link_hash(), elsewhere.link_hash(), [8; 32]]; + let index = index_file(&cid, &[item(&t.groups[0], &t.objects[0])], &names).unwrap(); + let mut fetch = |h: &[u8; 32], _: usize| files.get(h).cloned(); + let r = check_index(&cid, &index, &ObjectHeld::Nothing, &mut fetch, &mut ReceiptChains::new(), 0, &Limits::default(), None).unwrap(); + let got: Vec), &'static str>> = + r.claims.iter().map(|c| c.as_ref().map(|v| (v.attests, v.parent)).map_err(ReceiptRefusal::code)).collect(); + // A soft binding of another object, with no recovery run, is about another object. + assert_eq!(got, vec![ + Ok((Attests::Object, None)), + Ok((Attests::Receipt, Some(c0.link_hash()))), + Err("receipt_not_about_object"), + Err("receipt_not_about_object"), + Err("receipt_not_served"), + ]); + let passed = vec![deliver(&cid, &item(&t.groups[0], &t.objects[0]), &ObjectHeld::Nothing, &files, &mut ReceiptChains::new()).unwrap()]; + // A parent not served, or served under another name. + let mut no_parent = files.clone(); + no_parent.remove(&c0.link_hash()); + assert_eq!(claim_of(&cid, &passed, &c1.link_hash(), &no_parent).unwrap_err(), ReceiptRefusal::ParentNotHeld { walked: 0, stop: "receipt_not_served".into() }); + let mut wrong = files.clone(); + wrong.insert(c0.link_hash(), claim_file(&ancestor)); + assert_eq!(claim_of(&cid, &passed, &c1.link_hash(), &wrong).unwrap_err(), ReceiptRefusal::ParentNotHeld { walked: 1, stop: "receipt_name_mismatch".into() }); + // A parent that is another signer's claim: the link does not hold (chain_break). + let c1x = logged(0x63, group, 1, Some(ancestor.link_hash())); + let mut other = files.clone(); + other.insert(c1x.link_hash(), claim_file(&c1x)); + assert_eq!(claim_of(&cid, &passed, &c1x.link_hash(), &other).unwrap_err().code(), "chain_break"); + // With no item that passed, a claim about the group is about nothing the client holds. + assert_eq!(claim_of(&cid, &[], &c1.link_hash(), &files).unwrap_err(), ReceiptRefusal::NotAboutObject); +} + +/// A track whose one object's leaf carries `label` at `joules`. +fn labelled_track(seed: u8, track_alias: u64, joules: u64, label: ObjectLabel) -> Track { + let env = envelope(200 + seed as usize); + let mut b = GroupBuilder::default(); + b.add(content_hash(&env), joules, "origin", Some(label)); + let (g, mut o) = b.finish(&key(seed), SIGNER, track_alias, 0, "2026-10-06T00:00:00Z", "2026-10-06T00:00:04Z", None, true).unwrap(); + let files = [(g.link_hash(), group_file(&g).unwrap())].into(); + Track { envs: vec![env], groups: vec![g], objects: vec![o.remove(0)], files } +} + +fn chip() -> crate::energy_class::EnergyProvenance { + crate::energy_class::EnergyProvenance::on_chip_counter( + crate::energy_class::Uncertainty::new(44_669, 3_000_000).unwrap(), + crate::energy_class::CounterFiltering::Unknown, + ) + .unwrap() +} + +/// The `measurement` claim of claim-conformance's `measurement_object_accept`, about the +/// object `subject` at leaf 0 of `group`, signed by `signer`. +fn measurement(signer: u8, subject: [u8; 32], group: [u8; 32], seq: u64) -> ClaimReceipt { + let doc: serde_json::Value = + serde_json::from_slice(&std::fs::read(concat!(env!("CARGO_MANIFEST_DIR"), "/../claim-conformance/vectors.json")).unwrap()).unwrap(); + let v = doc["vectors"].as_array().unwrap().iter().find(|v| v["name"] == "measurement_object_accept").unwrap(); + let mut body = ClaimReceipt::from_json(v["receipt"].as_str().unwrap()).unwrap().body; + let ClaimBody::Measurement(m) = &mut body else { panic!("a measurement") }; + m.figure.figure_index = Some(0); + m.figure.group = Some(group); + ClaimReceipt::seal(&key(signer), SIGNER, SubjectType::Object, subject, seq, "2026-10-06T00:00:07Z", body, None).unwrap() +} + +fn energy(signer: u8, subject: [u8; 32], joules: u64, oer: Option, seq: u64) -> ClaimReceipt { + let e = crate::claim_receipt::EnergyClaim::new(joules, &chip(), crate::energy_binding::Coverage::Whole, "measured", + "https://transaction.science/wai/extensions/energy-measurement", 1, "subpixel", oer); + ClaimReceipt::seal(&key(signer), SIGNER, SubjectType::Object, subject, seq, "2026-10-06T00:00:08Z", ClaimBody::Energy(e), None).unwrap() +} + +fn gap(signer: u8, subject: [u8; 32], object_id: u64) -> ClaimReceipt { + let g = crate::claim_receipt::DeliveryGapClaim { + track_alias: 4, + group_id: 0, + object_id, + reason: crate::claim_receipt::GapReason::NotReceived, + observed_at: "2026-10-06T00:00:09Z".into(), + }; + ClaimReceipt::seal(&key(signer), "did:example:relay", SubjectType::Object, subject, 0, "2026-10-06T00:00:09Z", ClaimBody::DeliveryGap(g), None) + .unwrap() +} + +/// §8.2, jwp-receipts §3.1 step 6: a claim is checked against the object receipts of the +/// items that passed. A `measurement` claim with no figure held is `figure_not_held`; an +/// `energy` claim with no delivered object, or naming an OER receipt the client does not +/// hold, is `context_not_held`; a `delivery-gap` claim needs the receipt at its +/// coordinates, verified by inclusion at least. None is accepted on steps 1–5 alone. +#[test] +fn step_6_runs_against_the_items_that_passed() { + let t = labelled_track(0x61, 4, 461_538, ObjectLabel::Class(chip())); + let (g, o) = (&t.groups[0], &t.objects[0]); + let cid = o.content_hash; + let m = measurement(0x61, cid, g.link_hash(), 0); + let ok = energy(0x66, cid, 461_538, None, 0); + let other_figure = energy(0x67, cid, 461_539, None, 0); + let named_oer = energy(0x68, cid, 461_538, Some(format!("sha256:{}", "00".repeat(32))), 0); + let at = gap(0x69, cid, 0); + let elsewhere = gap(0x6a, cid, 1); + let mut files = t.files.clone(); + let claims = [&m, &ok, &other_figure, &named_oer, &at, &elsewhere]; + for c in claims { + files.insert(c.link_hash(), claim_file(c)); + } + let names: Vec<[u8; 32]> = claims.iter().map(|c| c.link_hash()).collect(); + let mut it = item(g, o); + it.property.joules_micro = 461_538; + it.property.acquisition = "OnChipCounter".into(); + it.property.acquisition_evidence = crate::jwp_delivery::property_evidence(&chip()); + let index = index_file(&cid, &[it], &names).unwrap(); + let run = |held: &ObjectHeld| { + let mut fetch = |h: &[u8; 32], _: usize| files.get(h).cloned(); + let r = check_index(&cid, &index, held, &mut fetch, &mut ReceiptChains::new(), 0, &Limits::default(), None).unwrap(); + assert!(r.delivery[0].is_ok(), "{:?}", r.delivery[0]); + r.claims.iter().map(|c| c.as_ref().map(|v| v.attests.label()).map_err(|e| (e.code(), e.step()))).collect::>() + }; + assert_eq!(run(&whole(&t.envs[0])), vec![ + Ok("object"), + Ok("object"), + Err(("energy_disagrees_with_delivery", Some(6))), + Err(("context_not_held", Some(6))), + Ok("object"), + Err(("context_not_held", Some(6))), + ]); + // The object not received: no figure, no delivered object; the gap still has its + // receipt, by inclusion. + assert_eq!(run(&ObjectHeld::Nothing), vec![ + Err(("figure_not_held", Some(6))), + Err(("context_not_held", Some(6))), + Err(("context_not_held", Some(6))), + Err(("context_not_held", Some(6))), + Ok("object"), + Err(("context_not_held", Some(6))), + ]); +} + +/// §8.2: a claim about a receipt attaches only through a delivery item that passed for +/// this object: one about the group of an item refused, or of another object, is not +/// about this object. +#[test] +fn a_claim_about_a_receipt_needs_an_item_that_passed() { + let a = track(0x61, 1, 1); + let b = track(0x62, 2, 1); + let cid = a.objects[0].content_hash; + let about_b = logged(0x63, b.groups[0].link_hash(), 0, None); + let mut files = a.files.clone(); + files.extend(b.files.clone()); + files.insert(about_b.link_hash(), claim_file(&about_b)); + // B's item listed in A's index: refused, so B's group is no group of A's. + let index = index_file(&cid, &[item(&b.groups[0], &b.objects[0]), item(&a.groups[0], &a.objects[0])], &[about_b.link_hash()]).unwrap(); + let mut fetch = |h: &[u8; 32], _: usize| files.get(h).cloned(); + let r = check_index(&cid, &index, &whole(&a.envs[0]), &mut fetch, &mut ReceiptChains::new(), 0, &Limits::default(), None).unwrap(); + assert_eq!(r.delivery[0].as_ref().unwrap_err(), &ReceiptRefusal::NotAboutObject); + assert!(r.delivery[1].is_ok()); + assert_eq!(r.claims[0].as_ref().unwrap_err(), &ReceiptRefusal::NotAboutObject); +} + +/// §8.2, jwp-receipts §3.2: a soft binding of another object attests that ancestor only +/// once the recovery passed: a publisher key from a delivery receipt the client verified +/// for the ancestor, the declaration signed by it, and a matcher that matches. Otherwise +/// it is about another object, or the recovery's reason. +#[test] +fn a_soft_binding_attests_an_ancestor_only_through_recovery() { + let a = track(0x61, 1, 1); + let b = track(0x62, 2, 1); + let (ancestor, cid) = (a.objects[0].content_hash, b.objects[0].content_hash); + let by_publisher = soft(0x61, ancestor, 0, None); + let by_another = soft(0x64, ancestor, 0, None); + let mut files = b.files.clone(); + files.insert(by_publisher.link_hash(), claim_file(&by_publisher)); + files.insert(by_another.link_hash(), claim_file(&by_another)); + let index = index_file(&cid, &[item(&b.groups[0], &b.objects[0])], &[by_publisher.link_hash(), by_another.link_hash()]).unwrap(); + let publishers = [(ancestor, key(0x61).verifying_key().to_bytes())]; + let exact = crate::claim_receipt::ExactMatch { alg: "org.example.fp".into() }; + let matchers: [&dyn crate::claim_receipt::SoftBindingMatcher; 1] = [&exact]; + let run = |held: &ObjectHeld, rec: Option<&Recovery<'_>>| { + let mut fetch = |h: &[u8; 32], _: usize| files.get(h).cloned(); + let r = check_index(&cid, &index, held, &mut fetch, &mut ReceiptChains::new(), 0, &Limits::default(), rec).unwrap(); + r.claims.iter().map(|c| c.as_ref().map(|v| v.attests.label()).map_err(|e| e.code())).collect::>() + }; + let held = whole(&b.envs[0]); + assert_eq!(run(&held, None), vec![Err("receipt_not_about_object"), Err("receipt_not_about_object")]); + let good = Recovery { publishers: &publishers, observed_value: &[0, 0xff], matchers: &matchers }; + assert_eq!(run(&held, Some(&good)), vec![Ok("ancestor"), Err("recovery_signer_mismatch")]); + let other_value = Recovery { publishers: &publishers, observed_value: &[1], matchers: &matchers }; + assert_eq!(run(&held, Some(&other_value))[0], Err("recovery_value_mismatch")); + let no_matcher = Recovery { publishers: &publishers, observed_value: &[0, 0xff], matchers: &[] }; + assert_eq!(run(&held, Some(&no_matcher))[0], Err("recovery_no_matcher")); + // Nothing held: no value to compare, no recovery. + assert_eq!(run(&ObjectHeld::Nothing, Some(&good))[0], Err("receipt_not_about_object")); +} + +/// §8.1: a legacy group's link hash does not cover its parent, so other bytes read under +/// its name; over HTTP it is refused, and so is the copy with its parent rewritten. +#[test] +fn a_legacy_group_is_refused_over_http() { + let env = envelope(7); + let mut b = GroupBuilder::default(); + b.add(content_hash(&env), 0, "origin", None); + let (g, o) = b.finish(&key(0x61), SIGNER, 1, 1, "2026-10-06T00:00:00Z", "2026-10-06T00:00:04Z", Some([4; 32]), false).unwrap(); + let mut forged = g.clone(); + forged.parent_receipt_hash = None; + assert_eq!(forged.link_hash(), g.link_hash(), "the legacy link hash does not cover the parent"); + for bytes in [group_file(&g).unwrap(), group_file(&forged).unwrap()] { + assert_eq!(read_group_file(&g.link_hash(), &bytes), Err(ReceiptRefusal::LegacyGroup)); + } + let files: BTreeMap<[u8; 32], Vec> = [(g.link_hash(), group_file(&forged).unwrap())].into(); + let index = index_file(&o[0].content_hash, &[item(&g, &o[0])], &[]).unwrap(); + let mut fetch = |h: &[u8; 32], _: usize| files.get(h).cloned(); + let r = check_index(&o[0].content_hash, &index, &whole(&env), &mut fetch, &mut ReceiptChains::new(), 0, &Limits::default(), None).unwrap(); + assert_eq!(r.delivery[0].as_ref().unwrap_err(), &ReceiptRefusal::LegacyGroup); +} + +/// §8.1: a client walks at most 64 parents for one group, whatever bound it asks for. +#[test] +fn the_walk_is_bounded_at_64() { + let t = track(0x62, 1, 66); + let wide = Limits { max_walk: 1000, fetch_budget: FETCH_BUDGET }; + let at = |i: usize| { + let cid = content_hash(&t.envs[i]); + let index = index_file(&cid, &[item(&t.groups[i], &t.objects[i])], &[]).unwrap(); + let mut fetch = |h: &[u8; 32], _: usize| t.files.get(h).cloned(); + check_index(&cid, &index, &ObjectHeld::Nothing, &mut fetch, &mut ReceiptChains::new(), 0, &wide, None).unwrap().delivery.remove(0) + }; + // 64 parents reach the chain's start; 65 do not. + assert_eq!(at(64).map(|v| (v.admission, v.walked)), Ok((Admission::Linked, 64))); + assert_eq!(at(65).unwrap_err(), ReceiptRefusal::ParentNotHeld { walked: 64, stop: "bound".into() }); +} + +/// §8.2: one index check fetches each name once, at most its budget, no file over its +/// cap, from an index within its caps. +#[test] +fn one_index_fetches_each_name_once_within_its_bounds() { + let t = track(0x63, 1, 70); + let last = t.groups.len() - 1; + let cid = content_hash(&t.envs[last]); + let items: Vec = (0..50).map(|_| item(&t.groups[last], &t.objects[last])).collect(); + let index = index_file(&cid, &items, &[]).unwrap(); + let count = std::cell::Cell::new(0usize); + let run = |limits: &Limits, files: &BTreeMap<[u8; 32], Vec>| { + count.set(0); + let mut fetch = |h: &[u8; 32], _: usize| { + count.set(count.get() + 1); + files.get(h).cloned() + }; + check_index(&cid, &index, &ObjectHeld::Nothing, &mut fetch, &mut ReceiptChains::new(), 0, limits, None).unwrap() + }; + // Fifty items naming one group: the group and 64 parents, fetched once. + let r = run(&Limits::default(), &t.files); + assert_eq!((r.fetched, count.get()), (65, 65)); + assert!(r.delivery.iter().all(|d| d.as_ref().unwrap_err() == &ReceiptRefusal::ParentNotHeld { walked: 64, stop: "bound".into() })); + // A budget of ten: the walk stops where it is spent. + let r = run(&Limits { max_walk: 64, fetch_budget: 10 }, &t.files); + assert_eq!((r.fetched, count.get()), (10, 10)); + assert_eq!(r.delivery[0].as_ref().unwrap_err(), &ReceiptRefusal::ParentNotHeld { walked: 9, stop: "receipt_fetch_budget_exhausted".into() }); + // A budget above the protocol's is the protocol's. + assert_eq!(ReceiptFetcher::new(&mut |_: &[u8; 32], _: usize| None, usize::MAX).get(&[0; 32]), Err(ReceiptRefusal::NotServed)); + // A receipt file over its cap. + let mut big = t.files.clone(); + let mut padded = big[&t.groups[last].link_hash()].clone(); + padded.resize(MAX_RECEIPT_FILE_BYTES + 1, b' '); + big.insert(t.groups[last].link_hash(), padded); + assert_eq!(run(&Limits::default(), &big).delivery[0].as_ref().unwrap_err(), &ReceiptRefusal::TooLarge); + // An index over its caps: bytes, items, claims. + let mut long = index.clone(); + long.resize(MAX_INDEX_BYTES + 1, b' '); + assert_eq!(ReceiptIndex::from_bytes(&long).unwrap_err().code(), "receipt_index_too_large"); + let many: Vec = (0..=MAX_INDEX_ITEMS).map(|_| item(&t.groups[last], &t.objects[last])).collect(); + assert_eq!(ReceiptIndex::from_bytes(&index_file(&cid, &many, &[]).unwrap()).unwrap_err().code(), "receipt_index_too_large"); + let claims = vec![[1u8; 32]; MAX_INDEX_CLAIMS + 1]; + assert_eq!(ReceiptIndex::from_bytes(&index_file(&cid, &[], &claims).unwrap()).unwrap_err().code(), "receipt_index_too_large"); + assert!(ReceiptIndex::from_bytes(&index_file(&cid, &many[1..], &claims[1..]).unwrap()).is_ok()); +} + +/// §8.2: the bytes read for one index are bounded too. Each fetch is asked for no more +/// than what remains of them (and of the file's cap plus one byte), so what is read never +/// passes them; the file that reaches their end is refused, and none is fetched after. +#[test] +fn the_bytes_read_for_one_index_are_bounded() { + let cap = MAX_RECEIPT_FILE_BYTES; + let full = MAX_INDEX_FETCH_BYTES / cap; + let asked = std::cell::RefCell::new(Vec::new()); + let read = std::cell::Cell::new(0usize); + // A fetch that reads no more than it is asked for. + let mut fetch = |h: &[u8; 32], limit: usize| { + asked.borrow_mut().push(limit); + let n = (if h[0] == 0xff { 10 * cap } else { cap }).min(limit); + read.set(read.get() + n); + Some(vec![b' '; n]) + }; + let mut f = ReceiptFetcher::new(&mut fetch, FETCH_BUDGET); + for i in 0..full - 1 { + assert!(f.get(&[i as u8; 32]).is_ok(), "file {i}"); + } + assert_eq!(f.get(&[(full - 1) as u8; 32]), Err(ReceiptRefusal::BytesExhausted), "the file that reaches the end"); + assert_eq!(asked.borrow().last(), Some(&cap), "asked for what remained, not the cap plus one"); + let n = asked.borrow().len(); + assert_eq!(f.get(&[0xfe; 32]), Err(ReceiptRefusal::BytesExhausted)); + assert_eq!(asked.borrow().len(), n, "nothing is fetched once the bytes are spent"); + assert_eq!(read.get(), MAX_INDEX_FETCH_BYTES, "what was read never passes the budget"); + assert!(f.get(&[0; 32]).is_ok(), "a name already fetched is kept"); + // A fetch that returns more than it was asked for: the rest is taken as unread, and + // the file that then reaches the end is refused, remembered so. + let mut fetch = |h: &[u8; 32], _: usize| Some(vec![b' '; if h[0] == 0xff { 10 * cap } else { cap - 1 }]); + let mut f = ReceiptFetcher::new(&mut fetch, FETCH_BUDGET); + for i in 0..full { + assert!(f.get(&[i as u8; 32]).is_ok()); + } + assert_eq!(f.get(&[0xff; 32]), Err(ReceiptRefusal::BytesExhausted)); + assert_eq!(f.get(&[0xff; 32]), Err(ReceiptRefusal::BytesExhausted)); + // A file that ends inside what remains is read whole. + let mut fetch = |h: &[u8; 32], _: usize| Some(vec![b' '; if h[0] == 0xee { 10 } else { cap - 1 }]); + let mut f = ReceiptFetcher::new(&mut fetch, FETCH_BUDGET); + for i in 0..full { + assert!(f.get(&[i as u8; 32]).is_ok()); + } + assert_eq!(f.get(&[0xee; 32]).map(|b| b.len()), Ok(10)); + assert_eq!(ReceiptRefusal::BytesExhausted.code(), "receipt_fetch_bytes_exhausted"); +} + +#[cfg(feature = "key_discovery")] +mod keys { + use super::super::keys::*; + use super::key; + use crate::container::Prior; + use crate::key_log::{commitment, log_json, KeyEntry, KeyStatus, ModuleForm, ModuleGrant, Revision, SinkKeyLog}; + use crate::pin::{sha256_hex, HeldBytes, VerifiedRef}; + + const ORIGIN: &str = "https://origin.example"; + + fn revision(prev: Option<&Revision>, issued_at: &str, revoked: Vec<[u8; 32]>) -> Revision { + let r = Revision { + origin: ORIGIN.into(), + seq: prev.map_or(0, |p| p.seq + 1), + prev: prev.map(Revision::revision_hash), + issued_at: issued_at.into(), + root: [0; 32], + next_root: commitment(&key(0x6f).verifying_key().to_bytes()), + keys: vec![ + KeyEntry::active("d1", key(0x61).verifying_key().to_bytes(), &["delivery"]), + KeyEntry::active("g1", key(0x63).verifying_key().to_bytes(), &["module/decoder"]), + ], + revoked_modules: revoked, + grant_max_s: 86_400, + status_max_age_s: 3_600, + sig: [0; 64], + }; + Revision::seal(&key(0x60), r).unwrap() + } + + fn log_bytes(revs: &[Revision]) -> Vec { + log_json(revs).into_bytes() + } + + /// §8.4: an origin is anchored only from its own location over HTTPS, answered 200 + /// with no redirect; a cached copy adds revisions and never moves the anchor, and an + /// older copy is reported. + #[test] + fn a_key_log_anchors_only_from_its_origin() { + let r0 = revision(None, "2026-10-01T00:00:00Z", vec![]); + let r1 = revision(Some(&r0), "2026-10-02T00:00:00Z", vec![]); + let loc = key_log_url(ORIGIN); + assert_eq!(loc, "https://origin.example/.well-known/wai-keys"); + let ok = Retrieval { requested: &loc, answered: &loc, status: 200 }; + let a = anchor_retrieved(ORIGIN, &ok, &log_bytes(&[r0.clone()]), "2026-10-03T00:00:00Z", None).unwrap(); + assert_eq!(a.revision, r0.revision_hash()); + let mirror = "https://mirror.example/.well-known/wai-keys"; + for (r, why) in [ + (Retrieval { requested: &loc, answered: mirror, status: 200 }, "redirect"), + (Retrieval { requested: mirror, answered: mirror, status: 200 }, "another location"), + (Retrieval { requested: &loc, answered: &loc, status: 304 }, "not a 200"), + ] { + assert_eq!(anchor_retrieved(ORIGIN, &r, &log_bytes(&[r0.clone()]), "t", None).unwrap_err().code(), "key_log_not_from_origin", "{why}"); + } + let plain = "http://origin.example"; + let pl = key_log_url(plain); + let r = Retrieval { requested: &pl, answered: &pl, status: 200 }; + assert_eq!(anchor_retrieved(plain, &r, &log_bytes(&[r0.clone()]), "t", None).unwrap_err().code(), "key_log_not_from_origin"); + // Through a cache: the newer copy adds r1; the anchor stays at r0. + let mut held = vec![r0.clone()]; + assert_eq!(hold_cached_revisions(ORIGIN, &log_bytes(&[r0.clone(), r1.clone()]), &a, &mut held), Ok(1)); + assert_eq!(held.len(), 2); + // Anchored at r1, a cached copy ending at r0 is an older copy. + let a1 = anchor_retrieved(ORIGIN, &ok, &log_bytes(&[r0.clone(), r1.clone()]), "2026-10-03T00:00:00Z", Some((&a, &held))).unwrap(); + assert_eq!(a1.revision, r1.revision_hash()); + assert_eq!(hold_cached_revisions(ORIGIN, &log_bytes(&[r0.clone()]), &a1, &mut held).unwrap_err().code(), "key_log_rolled_back"); + // A log of another origin is not this one's. + assert_eq!(hold_cached_revisions("https://other.example", &log_bytes(&[r0]), &a1, &mut held).unwrap_err().code(), "origin_not_anchored"); + assert_eq!(status_max_age_s(&r1), 900); + } + + /// §8.4: a key log longer than its cap is refused whole, over the anchoring path and + /// through a cache, whatever its first bytes hold; one at the cap reads as usual. + #[test] + fn a_key_log_past_its_cap_is_refused() { + let r0 = revision(None, "2026-10-01T00:00:00Z", vec![]); + let loc = key_log_url(ORIGIN); + let ok = Retrieval { requested: &loc, answered: &loc, status: 200 }; + let mut at_cap = log_bytes(&[r0.clone()]); + at_cap.resize(MAX_KEY_LOG_BYTES, b' '); + let a = anchor_retrieved(ORIGIN, &ok, &at_cap, "t", None).expect("a log at the cap anchors"); + let mut past = at_cap.clone(); + past.push(b' '); + assert_eq!(anchor_retrieved(ORIGIN, &ok, &past, "t", None).unwrap_err().code(), "key_log_too_large"); + let mut held = vec![r0]; + assert_eq!(hold_cached_revisions(ORIGIN, &past, &a, &mut held).unwrap_err().code(), "key_log_too_large"); + } + + /// §8.4: a grants file is read whole, and each grant in it is handed on as it is, + /// to be checked on its own. + #[test] + fn a_grants_file_is_a_locator() { + let module = [7u8; 32]; + let g = ModuleGrant::seal(&key(0x63), "did:web:origin.example#g1", ORIGIN, module, ModuleForm::Decoder, &["wai.neural.int_mlicpp"], + "2026-10-01T00:00:00Z", "2026-10-01T12:00:00Z").unwrap(); + let file = grants_file(&module, &[g.clone()]); + assert_eq!(read_grants_file(&module, &file).unwrap(), vec![g.to_json()]); + assert_eq!(read_grants_file(&[8; 32], &file).unwrap_err().code(), "grants_file_malformed"); + let text = String::from_utf8(file).unwrap(); + let extra = text.replacen("{", "{\"note\":1,", 1); + assert_eq!(read_grants_file(&module, extra.as_bytes()).unwrap_err().code(), "grants_file_malformed"); + // A grant that does not read stays in the list, for §7.9's check 1 to refuse. + let junk = text.replacen("\"grants\":[", "\"grants\":[1.5,", 1); + let got = read_grants_file(&module, junk.as_bytes()).unwrap(); + assert_eq!(got.len(), 2); + assert!(ModuleGrant::from_json(&got[0]).is_err()); + assert_eq!(grants_url(ORIGIN, &module), format!("https://origin.example/.well-known/wai-grants/{}.json", "07".repeat(32))); + } + + /// §8.5: a set fetched by its digest is held only as its pin and §7.9 hold it; the + /// name proves nothing. + #[test] + fn a_fetched_set_is_held_only_by_its_pin_and_its_grant() { + let set = b"a parameter set's bytes".to_vec(); + let digest: [u8; 32] = crate::pin::sha256(&set); + let r0 = revision(None, "2026-10-01T00:00:00Z", vec![]); + let loc = key_log_url(ORIGIN); + let ok = Retrieval { requested: &loc, answered: &loc, status: 200 }; + let a = anchor_retrieved(ORIGIN, &ok, &log_bytes(&[r0.clone()]), "2026-10-01T00:00:00Z", None).unwrap(); + let g = ModuleGrant::seal(&key(0x63), "did:web:origin.example#g1", ORIGIN, digest, ModuleForm::Decoder, &["wai.neural.int_mlicpp"], + "2026-10-01T00:00:00Z", "2026-10-01T12:00:00Z").unwrap(); + let grants = read_grants_file(&digest, &grants_file(&digest, &[g])).unwrap(); + let status = KeyStatus::seal(&key(0x60), &r0, "2026-10-01T05:00:00Z").to_json(); + let cap = "wai.neural.int_mlicpp"; + let prior = Prior::new("set", sha256_hex(&set), "integer-exact"); + let sink = |now: &str| { + SinkKeyLog::new(vec![r0.clone()], vec![a.clone()]).with_statuses(vec![status.clone()]).with_grants(grants.clone()) + .hold_pinned_sets(cap, &[ORIGIN]).at(now) + }; + assert_eq!(check_fetched_set(cap, &prior, &HeldBytes::File(&set), Some(&sink("2026-10-01T06:00:00Z"))), Ok(VerifiedRef::File { index: 0 })); + let stale = check_fetched_set(cap, &prior, &HeldBytes::File(&set), Some(&sink("2026-10-01T08:00:00Z"))).unwrap_err(); + assert_eq!((stale.code(), format!("{stale:?}").contains("module_status_stale")), ("prior_not_authorised", true)); + // Other bytes served under the set's name are not the set. + assert_eq!(check_fetched_set(cap, &prior, &HeldBytes::File(b"other bytes"), Some(&sink("2026-10-01T06:00:00Z"))).unwrap_err().code(), "prior_not_held"); + // A digest the log revokes is refused whatever is served. + let r1 = revision(Some(&r0), "2026-10-01T01:00:00Z", vec![digest]); + let revoked = SinkKeyLog::new(vec![r0.clone(), r1.clone()], vec![a.clone()]); + assert_eq!(check_fetched_set(cap, &prior, &HeldBytes::File(&set), Some(&revoked)).unwrap_err().code(), "prior_revoked"); + assert_eq!(set_url("https://origin.example/sets/", &prior.sha256), format!("https://origin.example/sets/{}", prior.sha256)); + } + + /// §8.5 through the integer decode's own entry: `int_payload::resolve` holds the + /// fetched set only as the pin and §7.9 hold it. + #[cfg(feature = "neural_int")] + #[test] + fn a_fetched_set_resolves_only_through_its_pin() { + let set = std::fs::read(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/vectors/int_mlicpp/model.wmb")).unwrap(); + let cap = "wai.neural.int_mlicpp"; + let prior = Prior::new("set", sha256_hex(&set), "integer-exact"); + let held = [HeldBytes::File(&set)]; + assert!(crate::int_payload::resolve(cap, Some(&prior), &held, None).unwrap().is_pinned()); + let other = [HeldBytes::File(b"not the set")]; + assert_eq!(crate::int_payload::resolve(cap, Some(&prior), &other, None).unwrap_err().code(), "prior_not_held"); + let digest: [u8; 32] = crate::pin::sha256(&set); + let r0 = revision(None, "2026-10-01T00:00:00Z", vec![]); + let r1 = revision(Some(&r0), "2026-10-01T01:00:00Z", vec![digest]); + let anchor = crate::key_log::Anchor::pinned(ORIGIN, r1.revision_hash()); + let sink = SinkKeyLog::new(vec![r0, r1], vec![anchor]); + assert_eq!(crate::int_payload::resolve(cap, Some(&prior), &held, Some(&sink)).unwrap_err().code(), "prior_revoked"); + } +} diff --git a/wai/wai-rs/src/jwp_delivery.rs b/wai/wai-rs/src/jwp_delivery.rs index f1c9f33b0..b741b22be 100644 --- a/wai/wai-rs/src/jwp_delivery.rs +++ b/wai/wai-rs/src/jwp_delivery.rs @@ -44,6 +44,7 @@ //! [`GroupBuilder`] is not a publishing path. use crate::claim_receipt::{ClaimBody, ClaimReceipt}; +use crate::jwp_hash::EstablishedHash; use crate::energy_binding::Coverage; use crate::energy_class::{ check_label, json_fields, label_from_json, AcquisitionError, AcquisitionKind, CounterFiltering, EnergyProvenance, @@ -521,9 +522,11 @@ impl GroupReceipt { pub fn from_json(v: &Value) -> Result { let bad = DeliveryError::at(1, Reason::Malformed); let m = v.as_object().ok_or(bad)?; - match m.get("kind").and_then(Value::as_str) { + // §3 step 1: a `kind` that is not "group", of whatever type, is `wrong_kind`; one + // that is absent is `malformed`. + match m.get("kind") { None => return Err(bad), - Some("group") => {} + Some(k) if k.as_str() == Some("group") => {} Some(_) => return Err(DeliveryError::at(1, Reason::WrongKind)), } if GROUP_MEMBERS.iter().any(|k| !m.contains_key(*k)) @@ -683,8 +686,16 @@ impl ObjectReceipt { /// `signer_pubkey`; and, where the leaf is labelled, the figure, its class, /// the class's evidence and the origin. pub fn accept(&self, group: &GroupReceipt, envelope: &[u8]) -> Result<(), DeliveryError> { + self.accept_hash(group, &EstablishedHash::of_envelope(envelope)) + } + + /// §3 steps 1–5 with step 4's comparison made against `established`: the content + /// hash recomputed over the envelope ([`ObjectReceipt::accept`]), or, for an object + /// received in part over HTTP, the cid its verified head proof establishes + /// (http-delivery §8.3). The object receipt then attests the object that hash names. + pub fn accept_hash(&self, group: &GroupReceipt, established: &EstablishedHash) -> Result<(), DeliveryError> { group.verify()?; - if crate::jwp_hash::content_hash(envelope) != self.content_hash { + if *established.hash() != self.content_hash { return Err(DeliveryError::at(4, Reason::ContentHashMismatch)); } self.verify_inclusion(group) @@ -700,20 +711,28 @@ impl ObjectReceipt { /// Step 3 is the caller's: [`ReceiptChains::verify_object`] runs it against the /// chain of the group's (`signer_pubkey`, `track_alias`). pub fn verify_against(&self, group: &GroupReceipt, envelope: Option<&[u8]>) -> Result { - group.verify()?; - self.evidence_under(group, envelope) + self.verify_against_hash(group, envelope.map(EstablishedHash::of_envelope).as_ref()) } - /// [`ObjectReceipt::verify_against`] under a group whose signature has verified: - /// steps 4 and 5. - fn evidence_under(&self, group: &GroupReceipt, envelope: Option<&[u8]>) -> Result { - if let Some(e) = envelope - && crate::jwp_hash::content_hash(e) != self.content_hash + /// [`ObjectReceipt::verify_against`] with step 4 run against `established` where + /// the verifier holds the object, whole or, over HTTP, in part + /// ([`ObjectReceipt::accept_hash`]); `None`, it holds nothing of it, and the object + /// is [`Evidence::InclusionVerified`]. + pub fn verify_against_hash(&self, group: &GroupReceipt, established: Option<&EstablishedHash>) -> Result { + group.verify()?; + self.evidence_under(group, established) + } + + /// [`ObjectReceipt::verify_against_hash`] under a group whose signature has + /// verified: steps 4 and 5. + fn evidence_under(&self, group: &GroupReceipt, established: Option<&EstablishedHash>) -> Result { + if let Some(e) = established + && *e.hash() != self.content_hash { return Err(DeliveryError::at(4, Reason::ContentHashMismatch)); } self.verify_inclusion(group)?; - Ok(if envelope.is_some() { Evidence::DeliveredVerified } else { Evidence::InclusionVerified }) + Ok(if established.is_some() { Evidence::DeliveredVerified } else { Evidence::InclusionVerified }) } /// Carried over MoQ (moq-streaming-format §4.1): the receipt is for the object of MOQT @@ -766,9 +785,11 @@ impl ObjectReceipt { pub fn from_json(v: &Value) -> Result { let bad = DeliveryError::at(4, Reason::Malformed); let m = v.as_object().ok_or(bad)?; - match m.get("kind").and_then(Value::as_str) { + // A `kind` that is not "object", of whatever type, is `wrong_kind`; one that is + // absent is `malformed`. + match m.get("kind") { None => return Err(bad), - Some("object") => {} + Some(k) if k.as_str() == Some("object") => {} Some(_) => return Err(DeliveryError::at(4, Reason::WrongKind)), } if OBJECT_MEMBERS.iter().any(|k| !m.contains_key(*k)) @@ -1593,7 +1614,29 @@ impl ReceiptChains { envelope: Option<&[u8]>, ) -> Result { self.accept_group(group)?; - object.evidence_under(group, envelope) + object.evidence_under(group, envelope.map(EstablishedHash::of_envelope).as_ref()) + } + + /// §3 steps 4 and 5 for `object` under `group`, which this verifier has admitted to + /// its chain (or is the head of it), with step 4 against `established` + /// ([`ObjectReceipt::verify_against_hash`]). The group's signature was verified when + /// it was admitted, and is not verified again. A group the chains have not admitted is + /// refused `parent_not_held`: an object is never accepted under a group step 3 has not + /// passed. + pub fn verify_admitted_object( + &self, + group: &GroupReceipt, + object: &ObjectReceipt, + established: Option<&EstablishedHash>, + ) -> Result { + let link = group.link_hash(); + let admitted = self.chains.get(&(group.signer_pubkey, group.track_alias)).is_some_and(|c| { + c.head_link == Some(link) || c.links.get(&link) == Some(&group.parent_receipt_hash) + }); + if !admitted { + return Err(DeliveryError::at(3, Reason::ParentNotHeld)); + } + object.evidence_under(group, established) } } @@ -1870,13 +1913,33 @@ impl WaiReceiptProperty { object: &ObjectReceipt, group: &GroupReceipt, envelope: &[u8], + ) -> Result { + self.check_against_object_hash(object, group, &EstablishedHash::of_envelope(envelope)) + } + + /// [`WaiReceiptProperty::check_against_object`] with the object accepted under + /// `established` ([`ObjectReceipt::accept_hash`]): the hash recomputed over its + /// envelope, or, for an object received in part over HTTP, the cid its verified head + /// proof establishes (http-delivery §8.3). + pub fn check_against_object_hash( + &self, + object: &ObjectReceipt, + group: &GroupReceipt, + established: &EstablishedHash, ) -> Result { let class = self.check()?; let disagrees = DeliveryError::of(Reason::PropertyDisagrees); if self.content_hash != object.content_hash || self.group_receipt != group.link_hash() { return Err(disagrees); } - object.accept(group, envelope)?; + object.accept_hash(group, established)?; + self.reading_of(object, group, class) + } + + /// The property's reading against an object accepted under `group`: its class + /// restated by the object's labelled leaf, or not. + fn reading_of(&self, object: &ObjectReceipt, group: &GroupReceipt, class: Option) -> Result { + let disagrees = DeliveryError::of(Reason::PropertyDisagrees); match (&object.label, class) { (None, None) => Ok(PropertyReading::Unmetered), (None, Some(_)) => Ok(PropertyReading::Unsigned), diff --git a/wai/wai-rs/src/jwp_delivery/tests.rs b/wai/wai-rs/src/jwp_delivery/tests.rs index 7ce505609..1d54719ab 100644 --- a/wai/wai-rs/src/jwp_delivery/tests.rs +++ b/wai/wai-rs/src/jwp_delivery/tests.rs @@ -412,6 +412,24 @@ fn a_sign_version_other_than_2_is_group_label_invalid() { assert!(GroupReceipt::from_json(&g.to_json()).is_ok()); } +/// §3 step 1 refuses a `kind` other than the receipt's as `wrong_kind` whatever its JSON +/// type; only a `kind` that is absent is `malformed`. So does an object receipt's reading. +#[test] +fn a_kind_of_another_type_is_wrong_kind() { + let (g, o) = group(&[(1, None)], false); + for v in [json!(null), json!(1), json!(["group"]), json!("object")] { + let mut j = g.to_json(); + j["kind"] = v.clone(); + assert_eq!(GroupReceipt::from_json(&j), Err(DeliveryError::at(1, Reason::WrongKind)), "{v}"); + let mut k = o[0].to_json(); + k["kind"] = if v == json!("object") { json!("group") } else { v.clone() }; + assert_eq!(ObjectReceipt::from_json(&k), Err(DeliveryError::at(4, Reason::WrongKind)), "{v}"); + } + let mut j = g.to_json(); + j.as_object_mut().unwrap().remove("kind"); + assert_eq!(GroupReceipt::from_json(&j), Err(DeliveryError::at(1, Reason::Malformed))); +} + /// RFC 8032 §5.1.3 gives every point one encoding: `y = p + 3` encodes a point /// of large order that a reducing decoder would accept, and it is refused as a /// key, at step 1, before any signature is examined. diff --git a/wai/wai-rs/src/jwp_hash.rs b/wai/wai-rs/src/jwp_hash.rs index d20bc358c..db6de21d4 100644 --- a/wai/wai-rs/src/jwp_hash.rs +++ b/wai/wai-rs/src/jwp_hash.rs @@ -18,6 +18,65 @@ pub fn content_hash(envelope_bytes: &[u8]) -> [u8; 32] { *h.finalize().as_bytes() } +/// How a verifier came to hold the content hash it compares an object receipt's with +/// (jwp-receipts §3 step 4). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Establishment { + /// Recomputed over the envelope bytes the verifier received. + Envelope, + /// The cid a verified head proof establishes, for an object the verifier received + /// only in part over HTTP, each component it uses checked against its entry's + /// digest (http-delivery §6, §7, §8.3). + HeadProof, +} + +impl Establishment { + /// `envelope` or `head-proof`. + pub fn label(self) -> &'static str { + match self { + Establishment::Envelope => "envelope", + Establishment::HeadProof => "head-proof", + } + } +} + +/// The content hash jwp-receipts §3 step 4 compares an object receipt's `content_hash` +/// with, and how the verifier came to hold it. It is built only from bytes — the +/// envelope, hashed here ([`EstablishedHash::of_envelope`]) — or by an HTTP fetch whose +/// head proof verified against the cid and whose every used component matched its +/// entry's digest (`http_binding::session::FetchSession::established`). A value a receipt +/// or an index states never becomes one, so a check over it cannot pass on the receipt's +/// own word. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct EstablishedHash { + hash: [u8; 32], + by: Establishment, +} + +impl EstablishedHash { + /// The content hash recomputed over `envelope`, the bytes as received. + pub fn of_envelope(envelope: &[u8]) -> EstablishedHash { + EstablishedHash { hash: content_hash(envelope), by: Establishment::Envelope } + } + + /// The cid a head proof verified, for an object received in part + /// (http-delivery §8.3). Built only by a fetch session that verified it. + #[cfg_attr(not(feature = "http"), allow(dead_code))] + pub(crate) fn by_head_proof(cid: [u8; 32]) -> EstablishedHash { + EstablishedHash { hash: cid, by: Establishment::HeadProof } + } + + /// The content hash. + pub fn hash(&self) -> &[u8; 32] { + &self.hash + } + + /// How it was established. + pub fn by(&self) -> Establishment { + self.by + } +} + #[cfg(test)] mod tests { use super::*; @@ -31,4 +90,14 @@ mod tests { let hex: String = h.iter().map(|b| format!("{b:02x}")).collect(); assert_eq!(hex, "af904358ba08fb542a845ce37edb3e5ee7f56eb362dfb1ee10b209e167d420de"); } + + /// An established hash over bytes is the content hash of those bytes, marked as + /// recomputed: the one public way to build one. + #[test] + fn an_established_hash_is_recomputed_from_bytes() { + let e = EstablishedHash::of_envelope(b"claim-conformance: canonical fixture object"); + assert_eq!(*e.hash(), content_hash(b"claim-conformance: canonical fixture object")); + assert_eq!(e.by(), Establishment::Envelope); + assert_eq!(EstablishedHash::by_head_proof([7; 32]).by().label(), "head-proof"); + } } diff --git a/wai/wai-rs/src/key_log.rs b/wai/wai-rs/src/key_log.rs index dea0c2257..8f3ed3a03 100644 --- a/wai/wai-rs/src/key_log.rs +++ b/wai/wai-rs/src/key_log.rs @@ -772,6 +772,11 @@ pub enum AnchorPath { Pinned, /// Retrieved from the origin's key-log location over HTTPS, at `acquired_at`. Fetched { acquired_at: String }, + /// A test aid's record: a log a test fetched over plain HTTP at `acquired_at`. It is + /// held as a fetched record is, so a test can exercise confirmation, and is labelled + /// `test-plaintext` wherever it is reported: it anchors nothing a verifier may rely + /// on, and no retrieval path in this crate builds one. + TestPlaintext { acquired_at: String }, } impl AnchorPath { @@ -779,6 +784,7 @@ impl AnchorPath { match self { AnchorPath::Pinned => "pinned", AnchorPath::Fetched { .. } => "fetched", + AnchorPath::TestPlaintext { .. } => "test-plaintext", } } } @@ -843,6 +849,7 @@ pub fn anchor_log(origin: &str, log_text: &str, path: AnchorPath, current: Optio let fresh = match &path { AnchorPath::Pinned => Anchor::pinned(origin, latest), AnchorPath::Fetched { acquired_at } => Anchor::fetched(origin, latest, acquired_at), + AnchorPath::TestPlaintext { .. } => Anchor { path: path.clone(), ..Anchor::fetched(origin, latest, "") }, }; let Some((cur, held)) = current.filter(|_| path != AnchorPath::Pinned) else { return Anchoring::Anchored(fresh); diff --git a/wai/wai-rs/tests/cli_http.rs b/wai/wai-rs/tests/cli_http.rs index 6396b36ea..3af2e0a87 100644 --- a/wai/wai-rs/tests/cli_http.rs +++ b/wai/wai-rs/tests/cli_http.rs @@ -620,3 +620,256 @@ fn origin_replays_a_live_edge() { let (h, b) = get(o.port, "/live/720/1200.5.wai", &[("Range", "bytes=0-7")]); assert!(h.starts_with("HTTP/1.1 206") && b == part[..8], "{h}"); } + +/// `wai http receipts` (§8.1–§8.3) against the reference origin serving the corpus: +/// O3 fetched by its head proof, its group accepted on that cid, and its claims (the +/// ancestor's soft binding and a receipt claim with its parent fetched by link), each +/// signer confirmed under the pinned key log; O9c's group walked back two parents; a +/// layer not fetched inclusion-verified; and a walk past its bound refused. +#[cfg(feature = "key_discovery")] +#[test] +fn receipts_checks_an_index_over_http() { + let o = Origin::start(&corpus(), &[]); + let url = |id: &str| { + let p = object(id); + format!("http://127.0.0.1:{}/{}", o.port, p.strip_prefix(corpus()).unwrap().to_str().unwrap()) + }; + let log = corpus().join(".well-known/wai-keys"); + let run = wai(&["http", "receipts", "--url", &url("O3"), "--advertise", "wai.video.av1", "--pin-key-log", log.to_str().unwrap(), + "--key-origin", "https://origin.example"]); + let out = String::from_utf8_lossy(&run.stdout); + let v: serde_json::Value = serde_json::from_str(&out).unwrap_or_else(|e| panic!("{e}: {out}{}", String::from_utf8_lossy(&run.stderr))); + assert_eq!((v["fetch"].as_str(), v["held"].as_str()), (Some("head-proof"), Some("head-proof")), "{v}"); + let d = &v["delivery"][0]; + assert_eq!((d["evidence"].as_str(), d["established"].as_str()), (Some("delivered-verified"), Some("head-proof")), "{v}"); + assert!(d["confirm"].as_str().unwrap().starts_with("confirmed origin=https://origin.example key_id=d1"), "{v}"); + // O3's claims: a soft binding of O7, with no recovery run here, is about another + // object; a receipt claim with its parent; an energy claim checked at step 6 against + // the object's figure, and one that disagrees with it. + assert!(v["claims"][0]["refused"].as_str().unwrap().starts_with("receipt_not_about_object"), "{v}"); + assert_eq!(v["claims"][1]["attests"], "receipt"); + assert!(v["claims"][1]["parent"].is_string(), "{v}"); + assert_eq!(v["claims"][2]["attests"], "object"); + assert!(v["claims"][3]["refused"].as_str().unwrap().starts_with("energy_disagrees_with_delivery"), "{v}"); + assert!(!run.status.success(), "a refused claim makes the exit status 1"); + let run = wai(&["http", "receipts", "--url", &url("O9c"), "--advertise", "wai.video.av1"]); + let v: serde_json::Value = serde_json::from_slice(&run.stdout).unwrap(); + assert!(run.status.success(), "{v}"); + assert_eq!((v["delivery"][0]["walked"].as_u64(), v["delivery"][0]["admission"].as_str()), (Some(2), Some("accept")), "{v}"); + let run = wai(&["http", "receipts", "--url", &url("O10L2"), "--held", "none"]); + let v: serde_json::Value = serde_json::from_slice(&run.stdout).unwrap(); + assert_eq!((v["held"].as_str(), v["delivery"][0]["evidence"].as_str()), (Some("nothing"), Some("inclusion-verified")), "{v}"); + let run = wai(&["http", "receipts", "--url", &url("O9c"), "--held", "none", "--max-walk", "1"]); + assert!(!run.status.success()); + let v: serde_json::Value = serde_json::from_slice(&run.stdout).unwrap(); + assert!(v["delivery"][0]["refused"].as_str().unwrap().starts_with("parent_not_held"), "{v}"); + // The origin serves the index for revalidation and the status for a quarter of its + // freshness at most. + let cid = hex(&content_hash(&std::fs::read(object("O3")).unwrap())); + let (h, _) = get(o.port, &format!("/objects/{cid}.wai.receipts"), &[]); + assert!(h.contains("Cache-Control: no-cache, no-transform"), "{h}"); + let (h, _) = get(o.port, "/.well-known/wai-key-status", &[]); + assert!(h.contains("Cache-Control: max-age=900, no-transform"), "{h}"); +} + +/// A test server for `wai http receipts`: each connection's request head is kept, and it +/// answers `mode`: `log` (the corpus's key log, 200), `redirect` (302 to another host), +/// `hang` (a status line and headers, then nothing), `stream` (a body that does not end). +#[cfg(feature = "key_discovery")] +fn test_server(mode: &'static str) -> (u16, std::sync::Arc>>) { + let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let port = l.local_addr().unwrap().port(); + let seen = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); + let kept = seen.clone(); + std::thread::spawn(move || { + for s in l.incoming().flatten() { + let kept = kept.clone(); + std::thread::spawn(move || { + let mut s = s; + if mode == "plain" { + // What a client sends first, recorded as it arrives, answered in plain + // HTTP with the key log. + let _ = s.set_read_timeout(Some(std::time::Duration::from_secs(10))); + let mut first = [0u8; 512]; + let n = s.read(&mut first).unwrap_or(0); + kept.lock().unwrap().push(String::from_utf8_lossy(&first[..n]).into_owned()); + let body = std::fs::read(corpus().join(".well-known/wai-keys")).unwrap(); + let _ = s.write_all(format!("HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n", body.len()).as_bytes()); + let _ = s.write_all(&body); + return; + } + let mut head = Vec::new(); + let mut b = [0u8; 1]; + while !head.ends_with(b"\r\n\r\n") && s.read(&mut b).map_or(false, |n| n == 1) { + head.push(b[0]); + } + kept.lock().unwrap().push(String::from_utf8_lossy(&head).into_owned()); + match mode { + "log" => { + let body = std::fs::read(corpus().join(".well-known/wai-keys")).unwrap(); + let _ = s.write_all(format!("HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n", body.len()).as_bytes()); + let _ = s.write_all(&body); + } + "biglog" => { + // The log padded past its cap (1 MiB), framed by Content-Length. + let mut body = std::fs::read(corpus().join(".well-known/wai-keys")).unwrap(); + body.resize(1_048_621, b' '); + let _ = s.write_all(format!("HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n", body.len()).as_bytes()); + let _ = s.write_all(&body); + } + "redirect" => { + let _ = s.write_all(b"HTTP/1.1 302 Found\r\nLocation: https://mirror.example/.well-known/wai-keys\r\nContent-Length: 0\r\n\r\n"); + } + "hang" => { + let _ = s.write_all(b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n"); + std::thread::sleep(std::time::Duration::from_secs(30)); + } + _ => { + // A body that does not end before any deadline the test gives: 64 KiB + // every 5 ms, for longer than a minute, until the client hangs up. + let _ = s.write_all(b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n"); + let chunk = vec![b' '; 64 << 10]; + for _ in 0..(1 << 14) { + if s.write_all(&chunk).is_err() { + break; + } + std::thread::sleep(std::time::Duration::from_millis(5)); + } + } + } + }); + } + }); + (port, seen) +} + +/// §8.4's test aid: `--test-plaintext-connect` fetches the key log over plain HTTP from a +/// stated address, as the TLS path asks for it (revalidating, no redirect followed). What +/// it retrieves confirms this run's signers and is labelled `test-plaintext`, never +/// `fetched`, as anchoring nothing a verifier may rely on, and the command fails; a +/// redirect anchors nothing. `--connect` is gone. +#[cfg(feature = "key_discovery")] +#[test] +fn receipts_labels_a_plaintext_test_anchor_as_such() { + let o = Origin::start(&corpus(), &[]); + let p = object("O3"); + let url = format!("http://127.0.0.1:{}/{}", o.port, p.strip_prefix(corpus()).unwrap().to_str().unwrap()); + let (kp, seen) = test_server("log"); + let run = wai(&["http", "receipts", "--url", &url, "--held", "none", "--key-origin", "https://origin.example", + "--test-plaintext-connect", &format!("127.0.0.1:{kp}")]); + let v: serde_json::Value = serde_json::from_slice(&run.stdout).unwrap_or_else(|e| panic!("{e}: {}", String::from_utf8_lossy(&run.stderr))); + assert_eq!(v["anchor"]["path"], "test-plaintext", "{v}"); + assert_eq!(v["anchor"]["relied_on"], false, "{v}"); + assert!(v["anchor"]["note"].as_str().unwrap().contains("anchors nothing a verifier may rely on"), "{v}"); + assert!(v["delivery"][0]["confirm"].as_str().unwrap().starts_with("confirmed origin=https://origin.example key_id=d1"), "{v}"); + // Every confirmation names the test anchor, and no anchor anywhere is `fetched`. + let confirms: Vec<&str> = ["delivery", "claims"].iter().flat_map(|k| v[*k].as_array().unwrap().iter()) + .filter_map(|x| x["confirm"].as_str()).collect(); + assert!(confirms.iter().filter(|c| c.starts_with("confirmed")).count() >= 2, "{v}"); + for c in confirms.iter().filter(|c| c.starts_with("confirmed")) { + assert!(c.contains(" anchor=test-plaintext"), "{c}"); + } + let out = String::from_utf8_lossy(&run.stdout); + assert!(!out.contains("anchor=fetched") && !out.contains("\"fetched\""), "{v}"); + let req = seen.lock().unwrap().join(""); + assert!(req.starts_with("GET /.well-known/wai-keys HTTP/1.1\r\n"), "{req}"); + assert!(req.contains("\r\nHost: origin.example\r\n") && req.contains("\r\nCache-Control: no-cache\r\n"), "{req}"); + // A redirect is not followed, and the answer anchors nothing. + let (rp, seen) = test_server("redirect"); + let run = wai(&["http", "receipts", "--url", &url, "--held", "none", "--key-origin", "https://origin.example", + "--test-plaintext-connect", &format!("127.0.0.1:{rp}")]); + assert!(!run.status.success()); + let v: serde_json::Value = serde_json::from_slice(&run.stdout).unwrap(); + assert_eq!(v["anchor"]["refused"], "key_log_not_from_origin", "{v}"); + assert!(v["delivery"][0].get("confirm").is_none(), "{v}"); + assert_eq!(seen.lock().unwrap().len(), 1, "the redirect was followed"); + // A log past its cap is refused, read no further than one byte past it. + let (bp, _) = test_server("biglog"); + let run = wai(&["http", "receipts", "--url", &url, "--held", "none", "--key-origin", "https://origin.example", + "--test-plaintext-connect", &format!("127.0.0.1:{bp}")]); + let v: serde_json::Value = serde_json::from_slice(&run.stdout).unwrap(); + assert_eq!(v["anchor"]["refused"], "key_log_too_large", "{v}"); + // The flag that let any server stand in for the origin is gone. + let run = wai(&["http", "receipts", "--url", &url, "--held", "none", "--key-origin", "https://origin.example", "--connect", &format!("127.0.0.1:{kp}")]); + assert!(!run.status.success() && String::from_utf8_lossy(&run.stderr).contains("--connect is gone")); +} + +/// The test aid's anchor never lets the command succeed: on an object whose index passes +/// whole (O7), the same check exits 0 with the log pinned and 1 with it fetched over +/// plaintext from a stated address. +#[cfg(feature = "key_discovery")] +#[test] +fn receipts_never_succeeds_on_a_test_anchor() { + let o = Origin::start(&corpus(), &[]); + let p = object("O7"); + let url = format!("http://127.0.0.1:{}/{}", o.port, p.strip_prefix(corpus()).unwrap().to_str().unwrap()); + let log = corpus().join(".well-known/wai-keys"); + let pinned = wai(&["http", "receipts", "--url", &url, "--held", "none", "--key-origin", "https://origin.example", + "--pin-key-log", log.to_str().unwrap()]); + let v: serde_json::Value = serde_json::from_slice(&pinned.stdout).unwrap_or_else(|e| panic!("{e}: {}", String::from_utf8_lossy(&pinned.stderr))); + assert!(pinned.status.success(), "a pinned anchor on a whole index succeeds: {v}"); + assert_eq!(v["anchor"]["path"], "pinned", "{v}"); + let (kp, _) = test_server("log"); + let test = wai(&["http", "receipts", "--url", &url, "--held", "none", "--key-origin", "https://origin.example", + "--test-plaintext-connect", &format!("127.0.0.1:{kp}")]); + let w: serde_json::Value = serde_json::from_slice(&test.stdout).unwrap(); + assert_eq!(w["anchor"]["path"], "test-plaintext", "{w}"); + let strip = |x: &serde_json::Value| x.as_array().map(|d| json_without_confirm(d)).unwrap_or_default(); + assert_eq!((strip(&w["delivery"]), &w["index"]), (strip(&v["delivery"]), &v["index"]), "the same check but for its anchor"); + assert!(!test.status.success(), "a plaintext anchor never makes the command succeed: {w}"); +} + +/// Delivery verdicts with their confirmations dropped: the part an anchor does not +/// decide. +fn json_without_confirm(d: &[serde_json::Value]) -> Vec { + d.iter().map(|x| { let mut x = x.clone(); x.as_object_mut().map(|m| m.remove("confirm")); x }).collect() +} + +/// §8.4: without the test aid, the key log is retrieved over TLS from the origin itself, +/// so a server that speaks plain HTTP at the origin's address is never asked for it in +/// plaintext and anchors nothing; a build without TLS asks nothing at all. +#[cfg(feature = "key_discovery")] +#[test] +fn receipts_never_anchors_over_plaintext() { + let o = Origin::start(&corpus(), &[]); + let p = object("O3"); + let url = format!("http://127.0.0.1:{}/{}", o.port, p.strip_prefix(corpus()).unwrap().to_str().unwrap()); + let (kp, seen) = test_server("plain"); + let run = wai(&["http", "receipts", "--url", &url, "--held", "none", "--key-origin", &format!("https://127.0.0.1:{kp}")]); + assert!(!run.status.success()); + let v: serde_json::Value = serde_json::from_slice(&run.stdout).unwrap_or_else(|e| panic!("{e}: {}", String::from_utf8_lossy(&run.stderr))); + assert_eq!(v["anchor"]["refused"], "key_log_not_from_origin", "{v}"); + assert!(v["anchor"].get("path").is_none(), "{v}"); + assert!(v["delivery"][0].get("confirm").is_none(), "{v}"); + let sent = seen.lock().unwrap().join(""); + assert!(!sent.contains("GET "), "a plaintext request reached the server: {sent:?}"); + // With TLS the client opens with a handshake, unless the host has no trust store to + // validate against, when it refuses before it connects. + #[cfg(feature = "cli_tls")] + assert!(sent.starts_with('\u{16}') || (sent.is_empty() && v["anchor"]["why"].as_str().unwrap().contains("TLS")), + "the client opened with a TLS handshake: {sent:?} {v}"); + #[cfg(not(feature = "cli_tls"))] + { + assert!(sent.is_empty(), "a build without TLS connects to nothing: {sent:?}"); + assert!(v["anchor"]["why"].as_str().unwrap().contains("TLS"), "{v}"); + } +} + +/// §8.2 over HTTP: a server that stops after its headers, or never ends its body, costs +/// `wai http receipts` no more than its deadline and the index's cap. The hanging server +/// holds its connection open for 30 s and the stream runs past a minute, so a client +/// without the deadline, or without the cap, reports something else. +#[cfg(feature = "key_discovery")] +#[test] +fn receipts_is_bounded_by_its_deadline_and_caps() { + let cid = "31abc1da2cdf927191f3f447f68508ead30d078690cb7bea2435ae3d1df5f4f3"; + let (hp, _) = test_server("hang"); + let run = wai(&["http", "receipts", "--held", "none", "--deadline-ms", "1500", "--url", &format!("http://127.0.0.1:{hp}/o/{cid}.wai")]); + assert!(!run.status.success()); + assert!(String::from_utf8_lossy(&run.stderr).contains("deadline"), "{}", String::from_utf8_lossy(&run.stderr)); + let (sp, _) = test_server("stream"); + let run = wai(&["http", "receipts", "--held", "none", "--deadline-ms", "30000", "--url", &format!("http://127.0.0.1:{sp}/o/{cid}.wai")]); + assert!(!run.status.success()); + let v: serde_json::Value = serde_json::from_slice(&run.stdout).unwrap_or_else(|e| panic!("{e}: {}", String::from_utf8_lossy(&run.stderr))); + assert!(v["index"].as_str().unwrap().starts_with("receipt_index_too_large"), "{v}"); +} diff --git a/wai/wai-rs/tests/fixtures/tls/ca.cert.der b/wai/wai-rs/tests/fixtures/tls/ca.cert.der new file mode 100644 index 000000000..c076470b2 Binary files /dev/null and b/wai/wai-rs/tests/fixtures/tls/ca.cert.der differ diff --git a/wai/wai-rs/tests/fixtures/tls/origin.cert.der b/wai/wai-rs/tests/fixtures/tls/origin.cert.der new file mode 100644 index 000000000..f903bb8e9 Binary files /dev/null and b/wai/wai-rs/tests/fixtures/tls/origin.cert.der differ diff --git a/wai/wai-rs/tests/fixtures/tls/origin.key.der b/wai/wai-rs/tests/fixtures/tls/origin.key.der new file mode 100644 index 000000000..c39b849bd Binary files /dev/null and b/wai/wai-rs/tests/fixtures/tls/origin.key.der differ diff --git a/wai/wai-rs/tests/fixtures/tls/other.cert.der b/wai/wai-rs/tests/fixtures/tls/other.cert.der new file mode 100644 index 000000000..82cb26a33 Binary files /dev/null and b/wai/wai-rs/tests/fixtures/tls/other.cert.der differ diff --git a/wai/wai-rs/tests/fixtures/tls/other.key.der b/wai/wai-rs/tests/fixtures/tls/other.key.der new file mode 100644 index 000000000..5a663fa9c Binary files /dev/null and b/wai/wai-rs/tests/fixtures/tls/other.key.der differ