diff --git a/.github/workflows/wai-conformance.yml b/.github/workflows/wai-conformance.yml index ee42c862c..4862d0178 100644 --- a/.github/workflows/wai-conformance.yml +++ b/.github/workflows/wai-conformance.yml @@ -112,7 +112,7 @@ jobs: run: cargo build --no-default-features --features world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,receipt_claims - name: Library unit tests (the fixed-point determinism floors) working-directory: wai/wai-rs - run: cargo test --lib --no-default-features --features world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,oer,receipt_claims + run: cargo test --lib --no-default-features --features world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,oer,receipt_claims,key_discovery # The JPEG AI exact decoder reproduces across platforms only if no platform math # library is on its path. Two guards, each failing the step: clippy's # disallowed-methods (wai-rs/clippy.toml), which each module on the decode path @@ -202,6 +202,31 @@ jobs: if: matrix.os == 'linux' working-directory: wai/wai-web run: cargo test --locked + # jwp-receipts §7.1, §7.8: no `did:web`/`did:key` literal in a tracked .rs, .json or + # .js file under wai/ names a real web origin — every key the tree signs with has a + # published seed. Ungated: it reads the tree through `git ls-files` and classifies + # with `signer_id`, which builds with no feature. + - name: Every signer id in the tree is a label + working-directory: wai/wai-rs + run: cargo test --no-default-features --test signer_ids_are_labels + # jwp-receipts §7: `wai-keys` builds a key log from operator-supplied seeds, appends + # revocations and rotations, signs statuses and module grants, verifies a log as + # §7.3 anchors one, and checks a DID document against it. The test drives the binary. + - name: wai-keys — build, check and publish an origin key log + 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) + 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 # 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 @@ -212,7 +237,7 @@ jobs: working-directory: wai/wai-rs run: | set -euo pipefail - F=world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,quantum,quantum_ops,oer,receipt_claims + F=world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,quantum,quantum_ops,oer,receipt_claims,key_discovery v() { echo "::group::$1"; cargo run -q --no-default-features --features "$F" --bin "$2" -- verify "$3"; echo "::endgroup::"; } v worlds wai_world_vectors ../world-conformance v spatial-audio wai_audio_vectors ../audio-conformance @@ -229,6 +254,7 @@ jobs: v jwp wai_jwp_vectors ../jwp-conformance v claims wai_claim_vectors ../claim-conformance v pin wai_pin_vectors ../pin-conformance + v signers wai_signer_vectors ../signer-conformance # The pin corpus's second implementation: Python standard library only, sharing no # code with wai-rs. One leg is enough — it checks digests and listing rules, which # do not depend on the architecture — and the macOS host has python3. @@ -257,7 +283,7 @@ jobs: shell: pwsh working-directory: wai/wai-rs run: | - $F = "world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,quantum,quantum_ops,oer,receipt_claims" + $F = "world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,quantum,quantum_ops,oer,receipt_claims,key_discovery" function v($grp, $bin, $path) { Write-Output "::group::$grp" cargo run -q --no-default-features --features $F --bin $bin -- verify $path @@ -279,6 +305,7 @@ jobs: v jwp wai_jwp_vectors ../jwp-conformance v claims wai_claim_vectors ../claim-conformance v pin wai_pin_vectors ../pin-conformance + v signers wai_signer_vectors ../signer-conformance # The generated v2 vocabulary (wai/vocab/v2.*) is checked as a consumer uses it: # sample manifests, typed and untyped, WAI1 and WAI2, are expanded through the @@ -306,11 +333,13 @@ jobs: # code with wai-rs: claim_verify.py (every claim vector, the canonical fixture, and the # bracket arithmetic of energy-measurement §6.2 recomputed with Python's integers), # differential.py (both verifiers' verdicts on generated inputs the corpus does not hold, - # under a fixed seed, so a run is reproducible), and jwp_verify.py (every JWP vector). + # under a fixed seed, so a run is reproducible), jwp_verify.py (every JWP vector), and + # signer_verify.py (every key-discovery vector and the canonical fixture, with its + # verdicts required to equal wai_signer_vectors' line for line). # Each exits non-zero on any failure or any difference, and the step fails with it. # One platform: what they check is the reading of the text, not the architecture. independent-verifiers: - name: independent verifiers · claims and JWP (Python) + name: independent verifiers · claims, JWP and key discovery (Python) runs-on: linux steps: - uses: actions/checkout@v5 @@ -335,13 +364,14 @@ jobs: shell: bash run: python -m pip install cryptography - # differential.py drives wai_claim_vectors' `verdicts` mode. - - name: Build the corpus verifier wai-rs ships + # differential.py drives wai_claim_vectors' `verdicts` mode; the key-discovery + # verdicts are compared with wai_signer_vectors'. + - name: Build the corpus verifiers wai-rs ships working-directory: wai/wai-rs shell: bash - run: cargo build --no-default-features --features receipt_claims --bin wai_claim_vectors --bin wai_jwp_vectors + run: cargo build --no-default-features --features key_discovery --bin wai_claim_vectors --bin wai_jwp_vectors --bin wai_signer_vectors - - name: claim_verify.py, differential.py, jwp_verify.py + - name: claim_verify.py, differential.py, jwp_verify.py, signer_verify.py working-directory: wai shell: bash run: | @@ -349,6 +379,10 @@ jobs: python3 claim-conformance/claim_verify.py claim-conformance/vectors.json python3 claim-conformance/differential.py "$GITHUB_WORKSPACE/wai/wai-rs/target/debug/wai_claim_vectors" --random 2000 --seed 1 python3 jwp-conformance/jwp_verify.py jwp-conformance/vectors.json + python3 signer-conformance/signer_verify.py signer-conformance/vectors.json + python3 signer-conformance/signer_verify.py verdicts signer-conformance/vectors.json > "$RUNNER_TEMP/signer-py.tsv" + "$GITHUB_WORKSPACE/wai/wai-rs/target/debug/wai_signer_vectors" verdicts signer-conformance/vectors.json > "$RUNNER_TEMP/signer-rs.tsv" + diff "$RUNNER_TEMP/signer-py.tsv" "$RUNNER_TEMP/signer-rs.tsv" byte-exact: name: cross-arch byte-equality · ${{ matrix.os }} diff --git a/.github/workflows/wai-energy-receipts.yml b/.github/workflows/wai-energy-receipts.yml index 8e432ec3f..c994a08a6 100644 --- a/.github/workflows/wai-energy-receipts.yml +++ b/.github/workflows/wai-energy-receipts.yml @@ -136,10 +136,11 @@ jobs: run: cargo test --locked --features full --lib # The HTTP handler's POST /verify must accept a receipt in either form — - # as sealed, or labelled as wai_quantum_meter emits it. The verdict + # as sealed, or labelled as wai_quantum_meter emits it — and report the + # signer's level beside validity (jwp-receipts §7.1, §7.4). The verdict # logic is a plain function, tested natively; the wasm32-wasip2 build of # the component is not part of this workflow. - - name: wai-quantum-serve — POST /verify accepts either form + - name: wai-quantum-serve — POST /verify accepts either form and reports the signer's level working-directory: wai/wai-quantum-serve shell: bash run: cargo test --locked --lib diff --git a/.github/workflows/wai-neural-gates.yml b/.github/workflows/wai-neural-gates.yml index 9b3f14c85..4952426eb 100644 --- a/.github/workflows/wai-neural-gates.yml +++ b/.github/workflows/wai-neural-gates.yml @@ -21,6 +21,10 @@ name: WAI neural sink gates # primary), SPEC §4 step 6's order of reasons after a set_incomplete decode, and the # replicate_* reason the strict WAI2 policy reports for an entry it leaves out, # and a candidate held at the sink leaving the strict path's reason unchanged. +# With `key_discovery` as well, it runs SPEC §3.1's check 5 at the neural sink: a +# registry given a key log refuses the committed int_mlicpp set once the log revokes +# its digest, and when the sink holds no fresh status, and decodes it while a grant +# holds it. on: push: @@ -31,6 +35,11 @@ on: - 'wai/wai-rs/src/codecs/mod.rs' - 'wai/wai-rs/src/codecs/prior_form.rs' - 'wai/wai-rs/src/pin.rs' + - 'wai/wai-rs/src/key_log.rs' + - 'wai/wai-rs/src/key_log/**' + - 'wai/wai-rs/src/signer_id.rs' + - 'wai/wai-rs/src/claim_receipt.rs' + - 'wai/wai-rs/src/claim_text.rs' - 'wai/wai-rs/src/ffi.rs' - 'wai/wai-rs/src/ffi_neural.rs' - 'wai/wai-rs/tests/ffi_neural_pin.rs' @@ -51,6 +60,11 @@ on: - 'wai/wai-rs/src/codecs/mod.rs' - 'wai/wai-rs/src/codecs/prior_form.rs' - 'wai/wai-rs/src/pin.rs' + - 'wai/wai-rs/src/key_log.rs' + - 'wai/wai-rs/src/key_log/**' + - 'wai/wai-rs/src/signer_id.rs' + - 'wai/wai-rs/src/claim_receipt.rs' + - 'wai/wai-rs/src/claim_text.rs' - 'wai/wai-rs/src/ffi.rs' - 'wai/wai-rs/src/ffi_neural.rs' - 'wai/wai-rs/tests/ffi_neural_pin.rs' @@ -103,6 +117,15 @@ jobs: decode_multi_strict_reports_replicate_reasons strict_gate_ignores_a_held_capability_it_cannot_dispatch \ 2>&1 | tee "$RUNNER_TEMP/neural-gates.log" grep -q "test result: ok. 12 passed; 0 failed" "$RUNNER_TEMP/neural-gates.log" + - name: A key log refuses a revoked pinned set at the neural sink (SPEC §3.1 check 5) + working-directory: wai/wai-rs + shell: bash + run: | + set -euo pipefail + cargo test --no-default-features --features neural,key_discovery --test neural_native_decode -- \ + --exact int_mlicpp_revoked_set_is_refused_through_the_key_log \ + 2>&1 | tee "$RUNNER_TEMP/neural-key-log.log" + grep -q "test result: ok. 1 passed; 0 failed" "$RUNNER_TEMP/neural-key-log.log" # Library unit tests that exist only under `neural` and need no model: the # bmshj2018 CDF table cache stays bounded and evicts least recently used; # the ONNX reader finds a tensor kept in another file, wherever it sits; and a diff --git a/.github/workflows/wai-section-refs.yml b/.github/workflows/wai-section-refs.yml new file mode 100644 index 000000000..69ba8df38 --- /dev/null +++ b/.github/workflows/wai-section-refs.yml @@ -0,0 +1,37 @@ +name: WAI section references + +# Every section that SPEC.md or an extension cites in another WAI text exists: +# a link followed by §N, a link whose text holds §N, or an extension's name (or +# SPEC) in prose followed by §N, including references that break across lines. +# A citation of a section that was never written — as transparency §6's pointer +# to "jwp-receipts §7" was, before that section existed — fails the job. Research +# notes are dated records and are not read. The check is +# scripts/check-wai-section-refs.py; it needs Python and nothing else. + +on: + push: + branches: [main] + paths: + - 'wai/extensions/**' + - 'wai/SPEC.md' + - 'scripts/check-wai-section-refs.py' + - '.github/workflows/wai-section-refs.yml' + pull_request: + paths: + - 'wai/extensions/**' + - 'wai/SPEC.md' + - 'scripts/check-wai-section-refs.py' + - '.github/workflows/wai-section-refs.yml' + +jobs: + section-refs: + name: cited sections exist + runs-on: linux + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Every cited section exists + shell: bash + run: python scripts/check-wai-section-refs.py diff --git a/scripts/check-wai-section-refs.py b/scripts/check-wai-section-refs.py new file mode 100644 index 000000000..e4a7a0cb1 --- /dev/null +++ b/scripts/check-wai-section-refs.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Every section a WAI specification text cites in another WAI text exists. + +Reads wai/SPEC.md and wai/extensions/*.md. In each, a reference to a section of +SPEC.md or of an extension is checked against the headings of the file it names. Three +spellings are recognised, including where the reference breaks across lines: + + [text](X.md) §N a link, then the section + [text §N](X.md) the section inside the link text + X §N, X.md §N, `X` §N an extension's file name (or SPEC) in prose, then the section + +where §N is a section number of one or more parts (§7, §2.4.3), and a range written +§A–§B checks both ends. A heading is numbered when it begins `#`…, then the number, as +in `## 7.`, `### 6.4 ` or `## 7.1.`. Files under wai/research/ are dated records, kept as +written, and are not read. + +Exit status 1, with one line per reference that names a missing section; 0 otherwise. + + python3 scripts/check-wai-section-refs.py [repo-root] +""" +import os, re, sys + +ROOT = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +WAI = os.path.join(ROOT, "wai") +EXT = os.path.join(WAI, "extensions") + +HEADING = re.compile(r"^#{1,6}[ \t]+([0-9]+(?:\.[0-9]+)*)\.?(?=[ \t])", re.M) +SECTION = r"§[ \t]*([0-9]+(?:\.[0-9]+)*)" +RANGE = re.compile(r"[ \t]*[–-][ \t]*§[ \t]*([0-9]+(?:\.[0-9]+)*)") + + +def sections(path): + with open(path, encoding="utf-8") as f: + return set(HEADING.findall(f.read())) + + +def texts(): + out = [os.path.join(WAI, "SPEC.md")] + out += sorted(os.path.join(EXT, n) for n in os.listdir(EXT) if n.endswith(".md")) + return out + + +def main(): + files = texts() + known = {os.path.normpath(p): sections(p) for p in files} + stems = sorted((os.path.splitext(os.path.basename(p))[0] for p in files if os.path.dirname(p) == EXT), + key=len, reverse=True) + by_stem = {os.path.splitext(os.path.basename(p))[0]: os.path.normpath(p) for p in files if os.path.dirname(p) == EXT} + by_stem["SPEC"] = os.path.normpath(os.path.join(WAI, "SPEC.md")) + # A file name in prose, not inside a link target or a longer word or path. + bare = re.compile(r"(?[%3A]#` names a web origin, which publishes at + `/.well-known/wai-keys` a **key log**: revisions signed by a root key that + pre-commits to its successor, each listing the origin's keys, the authorities each + holds (`delivery`, `energy/`, `lawful-basis/declaration` or + `/decision`, `log-inclusion`, `soft-binding`, `binding-recovery`, `measurement`, + `operating-point`, `receipt`, `module/decoder`, `module/reconstruct`) and whether it + is active, retired or revoked. The path is published now; its registration in the IANA Well-Known URIs + registry is to be requested. A `did.json` on the same origin SHOULD list exactly + the log's active keys, and the log governs. +- A verifier that has anchored the origin — pinned out of band, or fetched from the + origin over HTTPS and recorded as such — confirms an accepted receipt, claim or + module grant against the log, offline, and reports `confirmed`, `refused` or + `unconfirmed` with a named reason beside the item's own verdict, never folded into + it. An item needs every authority the item fixes: a group receipt needs `delivery` + and `energy/` for each class its objects' labelled leaves and its labelled + total carry, another family's receipt `receipt` and `energy/` for each class + its figures carry, a claim its class's authority, and an energy authority is read from any + `energy_provenance` a claim's body carries, whatever its class — so a `measurement` + or `operating-point` claim needs its class's authority and the energy authority of + the figure it restates, pinned-only for a hardware class. An Ed25519 `did:key` + names one key, and one naming a key other than the signer's is refused; a `did:key` + longer than any Ed25519 key's spelling is a label, refused before it is decoded. The head walk + ignores revisions that are not held, unattached, or signed by a key their parent + does not authorise; the owner's rotation supersedes a stolen root's revisions; an + older log retrieved after a newer one was anchored is reported as rolled back. +- Pinned-only authorities: the hardware-class energy authorities and the + lawful-basis authorities, and any authority registered for pre-emption, are + confirmed only under a pinned anchor that has not moved (`anchor_not_pinned`); + every confirmation reports whether its anchor was pinned or fetched. A pinned record + moved forward by a later retrieval stays pinned and keeps the revision that was + pinned, and a pre-emption authority is read from that revision only. Beside an item + that needs a hardware-class authority, a verifier reports whether its key also + holds `delivery` (`also_delivers`); the recommendation that it not is a SHOULD. +- Revocation refuses every item under a revoked key, whatever time the item carries. + A retired key's untimed items stay confirmable until the key is revoked, and are + reported as untimed. +- §7.9, decoder and reconstruct modules: a module, or a parameter set an envelope + pins, is identified by the SHA-256 of its bytes and authorised by a signed **module + grant** from a key holding `module/
`, for named capabilities and a bounded + window. A grant is held only from one of the sink's **module origins** — an origin + its relying party names, or the origin under which it confirmed the envelope's + delivery — so anchoring an origin for its receipts does not make it a source of + modules. The key log revokes modules by digest (`revoked_modules`, append-only), and + a digest revoked in the head of any origin the sink has anchored is not held, + whichever origin grants it. Each log declares `grant_max_s` and `status_max_age_s` + under normative ceilings; a signed **key status** from the head's root, re-issued on + each heartbeat and held only if it names the head or an ancestor of it, keeps a sink + current. Eight ordered checks decide whether a sink runs a module; every failure — + including no network, a stale status and no trusted clock — means the module is not + held, and dispatch falls back: a sink never reports a file inert because of module + authorisation while a fallback remains, and an encoder whose capability needs a + module declares one. The kill latency `grant_max_s + status_max_age_s` is a + normative parameter. The ceilings are 86400 s for `grant_max_s` and 3600 s for + `status_max_age_s`. +- Module revocation through the parameter-set pin. [`SPEC.md`](SPEC.md) §4 folds the + hook into its steps: a sink that runs a module obtained apart from an envelope holds + it under §7.9, or the capability is not advertised for that envelope. SPEC §3.1's + check 5, reserved until now, runs at a sink that holds a key log, keyed on the pin's + `form` and `sha256`, before check 6. It has two parts. Revocation runs for every + capability a pin applies to and needs no grant, status or clock: a set whose digest + the head the sink reaches from any anchor it holds revokes is refused with + `prior_revoked`, whichever check a grant would fail first. Authorisation runs where + the relying party holds the capability's pinned sets to §7.9, its choice per + capability: §7.9 authorises the set as a decoder module, and any other failure, + offline and no grant included, is `prior_not_authorised`. Dispatch moves to the + fallback as for any refused pin. An envelope that pins no set does not escape check + 5: the set an unpinned decode reads is checked under each digest a pin over it could + name. A sink configured with no key log does not run check 5. + `key_log::module_check` implements §7.9's checks; `key_log::SinkKeyLog` runs check 5 + in the reference dispatcher, given to `pin::verify_with` or to + `neural::ModelRegistry::with_key_log`, which also runs it before an unpinned decode. + Library tests in CI dispatch a revoked set to its fallback, report `prior_revoked` + when none remains, refuse a revoked set whose grant fails an earlier check, whose + sink holds no grant, or whose capability is not held to §7.9, and fall back when the + sink is offline; the neural sink gates run the same refusals through + `neural::decode_envelope` over the committed int_mlicpp set, pinned and unpinned. A + key log that names the subject's origin holds for one envelope, so a registry given + one decodes that envelope only. The reference dispatcher runs no module obtained + apart from an envelope; `wai-web` and the C ABI take no key log. + `neural::ModelRegistry` gains a private field, so a struct literal no longer builds + one; build it with `ModelRegistry::new()` and its builder methods. +- `wai-rs`: `signer_id` (ungated, no dependency) reads a `signer_id`; + `key_log` (feature `key_discovery`, no new dependency, checked to build for wasm32) + implements §7. Outside the crate, an item to confirm is read from bytes that + verified: `Item::from_claim` from an accepted claim, `Item::from_group` from a group + receipt and the objects included under it, `Item::from_family` from another + family's receipt through its figure view, given the `signer_id` and signed time the + same receipt carries. `wai-keys` builds an origin's key log from seeds the operator names, + appends retirements, revocations, revoked modules and rotations, signs statuses and + module grants, verifies a log as §7.3 anchors one — afresh, or as a later retrieval + against a fetched or pinned anchor already held — and emits and checks a `did.json`. + It creates no key material, never prints a seed, and refuses a published test key + unless told the log is a test log. +- `signer-conformance/`: a corpus of anchoring, confirmation and module vectors with + declared verdicts, generated and verified by `wai_signer_vectors` in CI on every + platform of the conformance matrix, with a canonical fixture computed by a separate + implementation (`signer_verify.py`), which also reproduces every vector's verdict. CI + runs it on the linux runner and requires its verdicts to equal `wai_signer_vectors`' + line for line (jwp-receipts §5.8). +- No wire format or committed signed vector changes. Every receipt and claim verifies + as before, and every vector of the claim corpus, those of the `measurement` and + `operating-point` classes included, keeps its outcome. The reference meters, whose seeds are published, now write a label as + `signer_id` rather than a real origin, so their signed output differs from before; + the world demo's signer is a label too. A CI test holds every `did:web` literal in + a tracked `.rs`, `.json` or `.js` file under `wai/` (`signer-conformance/` aside) + that names a domain, and every origin-bound one, to a reserved example name. `wai-quantum-serve`'s `/verify` + reports the signer's level beside validity. +- transparency §6 pointed to "jwp-receipts §7" when no §7 existed; it now points at + this section. SPEC §6.2–§6.3, confidentiality §6–§7 and jwp-receipts §6.6 no longer + say that a relay signs, or that checking a receipt needs no key: it needs the + signer's verifying key, which the receipt carries. prior-carriage §6 no longer says + WAI has no key discovery. +- Not done here: a key log for `wai-web` and the C ABI; an independent time that could + reinstate an item signed before its key was compromised; sub-delegation between + module grants; an authority for pre-emption, relay attestation or reconciliation, + which have no class; checking a log's evidence under its profile; the keys an OER + receipt is checked with. + ### Typed receipt classes, and a second (soft) binding class - [`extensions/jwp-receipts.md`](extensions/jwp-receipts.md) §2.4.3–§2.4.7: every diff --git a/wai/SPEC.md b/wai/SPEC.md index f2672bfab..04ce24cb2 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -128,7 +128,8 @@ All WAI multi-byte integers are little-endian unless stated otherwise. - `model_requirement.fallback` — a second capability the sink MAY use if it lacks the primary, or if the envelope's pin does not resolve for the primary at the sink (§4 step 3). `null` means the file is inert without the - primary. + primary. An encoder whose primary capability decodes with a module a + sink obtains apart from the envelope does not write `null` (§4). - `conditioning.kind` — short codec id matching the capability. Helps multiplexers route bytes without parsing the full capability string. - `conditioning.prior` — OPTIONAL. The pin (§3.1) of the parameter set the @@ -233,8 +234,21 @@ order, and the first that fails is the reason reported: This is the sink's gap, and it is never reported as a mismatch. 4. `form_required`: `form` is absent and the capability is not registered with a one-file set. -5. `prior_revoked`: reserved, for a revocation of the pinned set, keyed on the - pin's `form` and `sha256`. +5. `prior_revoked` or `prior_not_authorised`: the sink holds a key log + ([jwp-receipts](extensions/jwp-receipts.md) §7), and it refuses the set this + pin names, which it identifies by the pin's `sha256` in the pin's `form`. The + reason is `prior_revoked` when that digest is in the `revoked_modules` of the + head the sink reaches from any anchor it holds, or, where none can be reached, + of that anchor revision (§7.9 check 5). A revocation needs no grant, status or + clock, so this part runs for every capability a pin applies to, and whichever + check of §7.9 a grant fails first. Where the sink also holds the capability's + pinned parameter sets to §7.9 (§4, *Module authorisation*), §7.9 authorises the + set as a decoder module, and the reason is `prior_not_authorised` when §7.9 does + not hold it for any other reason — no grant from one of the sink's module + origins, a granting key §7.4 does not confirm, no fresh status because the sink + is offline or has no clock it trusts, a grant outside its window or for another + capability — or the sink holds no grant for the set. The sink reports §7.9's + check and reason with it. A sink that holds no key log skips this check. 6. `prior_not_held`: the sink holds no parameter set for the capability whose digest, in the pin's form, equals `sha256`; a file the sink cannot read is not held. A sink that holds the set a well-formed `supersedes` names — a file when @@ -250,7 +264,9 @@ order, and the first that fails is the reason reported: These checks read the envelope, the registry, the forms the sink can read and the bytes it holds, and nothing that depends on how the sink was built, so two conforming sinks that read the same forms and hold the same bytes report the same -reason. Refusal reasons are snake_case; tier names and class labels are +reason. Check 5 also reads what the sink holds under jwp-receipts §7.9 and its +clock, so two sinks that hold the same key-log material and agree on the time +report the same reason there. Refusal reasons are snake_case; tier names and class labels are kebab-case. **Decoding against a verified set.** A pinned decode MUST read its parameters @@ -341,16 +357,18 @@ A conforming sink MUST implement this algorithm: 1. Read the manifest. If `wai` major version is unknown → reject. 2. Let `cap = model_requirement.capability`. -3. If `cap` is dispatchable (below), the sink advertises `cap`, and — when a pin applies to `cap` (§3.1) and the - manifest carries `conditioning.prior` — the pin resolves at the sink for - `cap`, decode `payload` via the codec registered for `cap` (§5), against the - verified set, and return. A pin that does not resolve makes `cap` unsupported - for this envelope: keep its reason and continue at step 4. +3. If `cap` is dispatchable (below), the sink advertises `cap` — holding under + jwp-receipts §7.9 any module it obtained apart from the envelope to decode + `cap` (*Module authorisation*, below) — and, when a pin applies to `cap` + (§3.1) and the manifest carries `conditioning.prior`, the pin resolves at the + sink for `cap`, decode `payload` via the codec registered for `cap` (§5), + against the verified set, and return. A pin that does not resolve makes `cap` + unsupported for this envelope: keep its reason and continue at step 4. 4. Else, let `fb = model_requirement.fallback`. -5. If `fb != null`, `fb` is dispatchable and the sink advertises `fb`, and — when a pin applies to `fb` - and the manifest carries `conditioning.prior` — the pin resolves at the sink - for `fb`, decode via `fb`'s codec and return. A pin that does not resolve: keep - its reason. +5. If `fb != null`, `fb` is dispatchable and the sink advertises `fb` on the same + terms, and, when a pin applies to `fb` and the manifest carries + `conditioning.prior`, the pin resolves at the sink for `fb`, decode via `fb`'s + codec and return. A pin that does not resolve: keep its reason. 6. Else the file is **inert at this sink** — return an error to the calling application that names why: the reason kept at step 3, else the one kept at step 5, else `missing_capability`. @@ -379,6 +397,37 @@ If the manifest declares `security.encryption` other than `"none"` (§6), the sink MUST recover the plaintext payload before step 3 and run this algorithm on the plaintext; see §6.4. +**Module authorisation.** A module is code a sink obtains apart from the envelope +and runs to decode a capability, or to rebuild a reconstruction from what a decoder +produced: a decoder or reconstruct module +([`extensions/jwp-receipts.md`](extensions/jwp-receipts.md) §7.9). A sink that +decodes `cap` with one MUST hold it under §7.9 for the envelope before it runs it, from one of the sink's module origins (§7.9): the origins its +relying party names, and the origin under which it confirmed the envelope's +delivery. A module that is not held — not granted by a module origin, revoked, or +not checkable because the sink is offline or has no clock it trusts — leaves `cap` +not advertised for that envelope at steps 3 and 5. A sink that holds a key log +refuses a parameter set whose digest the key log revokes (§3.1 check 5, +`prior_revoked`). A sink MAY also hold the parameter sets envelopes pin to §7.9, by +its relying party's choice per capability; for such a capability §3.1's check 5 +also refuses a pinned set §7.9 does not hold (`prior_not_authorised`). A refused +pin does not resolve. An envelope that pins no set is decoded against the set the +sink holds for `cap`, and a sink that holds a key log runs check 5 over that set +too, identified by each digest a pin over it could name: it does not decode `cap` +for that envelope when any of them is revoked, or, for a capability it holds to +§7.9, when §7.9 holds the set under none of them. Either way +the algorithm above runs unchanged, so a sink MUST NOT report a file inert because +of module authorisation while a fallback it can decode without that module remains, +and an encoder emitting a capability whose decode needs a module MUST declare such a +fallback (§3). A sink configured with no key log holds no module under §7.9, so it +runs no module obtained apart from an envelope, and it does not run check 5. The +reference dispatcher in `wai-rs` runs no module obtained apart from an envelope; it +runs check 5 when the sink is configured with a key log (`key_log::SinkKeyLog`, +feature `key_discovery`, given to `pin::verify_with` or to +`neural::ModelRegistry::with_key_log`, which also runs it before an unpinned +decode): the revocation part for every capability a pin applies to, and §7.9's +authorisation for the capabilities the sink holds to it. `wai-web` and the C ABI +take no key log. + --- ## 5. Registered capabilities (the zeroth menu) @@ -912,23 +961,34 @@ membership is established out of band via MLS; each member derives the derive the key and the file is **inert at that sink** — the confidentiality analog of a missing capability. +This is the content key. Checking a receipt needs no content key, only +the signer's verifying key, which the receipt itself carries +(`signer_pubkey`): where a signer publishes its verifying keys, and how +a verifier confirms whose a key is, is +[`extensions/jwp-receipts.md`](extensions/jwp-receipts.md) §7. + ### 6.3 Relay transparency and attestation A WAI relay or multiplexer that carries an encrypted payload MUST treat it as opaque: it MUST NOT require the plaintext to route, meter, or attest, and MUST NOT attempt capability dispatch on ciphertext. The JWP receipt chain ([`extensions/jwp-receipts.md`](extensions/jwp-receipts.md)) -commits over the **ciphertext** content hash, so per-object delivery and -integrity are attested end-to-end without any relay holding the key. Each -object's figure, class and origin are signed where its leaf is labelled, and -the group total, its class, its coverage and the parent link are signed under -the labelled group payload (jwp-receipts §2.2–§2.3). An object's figure and -origin beside a legacy leaf are the publisher's statement, unsigned beside the -signed bytes; a legacy group payload signs the total, but no class, coverage or -parent link. Content secrecy (SFrame/MLS) and delivery attestation (JWP) are -independent layers over the same bytes and compose: a relay proves it -forwarded the exact sealed bytes it was given while never learning what -they say. +commits over the **ciphertext** content hash, so each object's integrity, +and its delivery under the publisher's signature, are attested end-to-end +without any relay holding the content key. Each object's figure, class and +origin are signed where its leaf is labelled, and the group total, its class, +its coverage and the parent link are signed under the labelled group payload +(jwp-receipts §2.2–§2.3). An object's figure and origin beside a legacy leaf +are the publisher's statement, unsigned beside the signed bytes; such an +object's figure is signed only where an `energy` claim restates it +(jwp-receipts §2.4.5). A legacy group payload signs the total, but no class, +coverage or parent link. +Content secrecy (SFrame/MLS) and delivery attestation (JWP) are independent +layers over the same bytes and compose: a relay forwards the exact sealed +bytes and their receipts unaltered, signs nothing in the publisher's name +(jwp-receipts §4.3), and never learns what the bytes say. Checking the +attestation needs the publisher's verifying key, which the receipt carries, +never the content key; whose key it is, jwp-receipts §7 confirms. ### 6.4 Decrypt-then-dispatch diff --git a/wai/extensions/confidentiality.md b/wai/extensions/confidentiality.md index ca1580b7b..a7f5334a8 100644 --- a/wai/extensions/confidentiality.md +++ b/wai/extensions/confidentiality.md @@ -191,15 +191,22 @@ MUST NOT attempt capability dispatch on ciphertext. Attestation composes with confidentiality because the JWP receipt chain ([`jwp-receipts.md`](jwp-receipts.md)) commits over the **ciphertext** -content hash. A relay signs that it forwarded the exact sealed bytes it -was handed — with the per-object energy and delivery lineage bound into -the Merkle leaf — while never learning what they say. Content secrecy +content hash. The publisher signs the group's Merkle root over the +objects' leaves, and a Merkle leaf commits to the object's content hash +and, for a labelled object, its figure, class and origin (jwp-receipts +§2.2); an unlabelled object's figure is signed only where an `energy` +claim restates it (jwp-receipts §2.4.5). A relay forwards the +sealed bytes and their receipts unaltered, signs nothing in the +publisher's name (jwp-receipts §4.3), and never learns what the bytes +say. Content secrecy (SFrame/MLS) and delivery attestation (JWP) are independent layers over the same bytes: - SFrame answers *what does it say* — only members can. -- JWP answers *who delivered it and at what energy* — anyone can verify, - no key required. +- JWP answers *which bytes the publisher signed for, and the group's + declared energy* — anyone can check it with the publisher's verifying + key, which the receipt carries, and no content key; whose key it is, + [`jwp-receipts.md`](jwp-receipts.md) §7 confirms. --- @@ -218,8 +225,10 @@ the same bytes: object sizes/timing are visible to relays by design. This extension protects payload content, not traffic analysis. - **Trust boundary.** A relay is untrusted for confidentiality (it never - holds a key) and semi-trusted only for the attestation it signs under - its own key; the two roles do not overlap. + holds a content key) and signs nothing in the JWP chain (jwp-receipts + §4.3). A relay attestation, where a deployment wants one, is a separate + receipt under the relay's own key, and no class for it is registered + yet. --- diff --git a/wai/extensions/energy-measurement.md b/wai/extensions/energy-measurement.md index 34dbd295b..f3cbeeed3 100644 --- a/wai/extensions/energy-measurement.md +++ b/wai/extensions/energy-measurement.md @@ -92,6 +92,18 @@ carries the class as an optional signed label (interactive-worlds §8, and §2.8 here). It is signed under its own domain, `wai:world-receipt\x01`, not the base group receipt's, and nothing above changes it. +Whose key signed a figure's class can be confirmed under +[jwp-receipts](jwp-receipts.md) §7: an origin the verifier has anchored lists +the key with the authority `energy/` followed by the class. That is the +origin's statement of which of its keys states figures of that class, and a key +given one class cannot state another. For the hardware classes the +confirmation holds only under an anchor the relying party pinned, never one +fetched on first use (jwp-receipts §7.5). It is not evidence about the +instrument (§2.2), and no confirmation ranks a figure above its class (§2). +jwp-receipts §7.5 recommends that a hardware-class figure be signed by a key +held by the component that signs the reading, apart from the key that signs +delivery, and has a verifier report whether the key also holds `delivery`. + ## 2. The acquisition class (REQUIRED) A receipt this extension governs (§1) that carries a non-zero energy figure @@ -272,7 +284,9 @@ single class put them at one trust level. latter.) A signature settles who said a number. It says nothing about how far the number -can be from the energy actually spent. The declared uncertainty (§2.3) is the +can be from the energy actually spent. Confirming whose key it is +([jwp-receipts](jwp-receipts.md) §7) settles which origin stands behind the key, +and for which class, and nothing about the reading. The declared uncertainty (§2.3) is the statement about that, and the class says how much of it the declaration covers. For `CalibratedInstrument` it covers the instrument's error as established by its calibration, together with the measurement's own scatter. For diff --git a/wai/extensions/jwp-receipts.md b/wai/extensions/jwp-receipts.md index 2c42c799a..76e9d2c97 100644 --- a/wai/extensions/jwp-receipts.md +++ b/wai/extensions/jwp-receipts.md @@ -1,8 +1,11 @@ # WAI Extension: JWP Receipt Chain -> Status: Draft. Sub-spec of WAI v1.0. The verifier of the reference -> implementation is in -> https://git.transaction.science/Transaction-Science/open-standards/src/branch/main/wai/wai-rs. +> Status: Draft. Sub-spec of WAI v1.0. Canonical text: `wai/extensions/jwp-receipts.md` +> in the open-standards repository (https://git.transaction.science/Transaction-Science/open-standards). +> The verifier of the reference implementation is in +> https://git.transaction.science/Transaction-Science/open-standards/src/branch/main/wai/wai-rs: +> `claim_receipt`, `jwp_delivery`, and for §7 `signer_id` and `key_log` (feature +> `key_discovery`). Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY**, **REQUIRED**, **OPTIONAL** are RFC 2119 / RFC 8174. @@ -45,6 +48,18 @@ In scope: for objects whose bytes changed in normal distribution — and the **ancestor rule** (§3.2): a receipt reached through it attests the object the one in hand was derived from, never the object in hand. +- **Key discovery and signing authority** (§7): a form of `signer_id` + that names the web origin a signer publishes its keys under; a signed, + chained **key log** the origin publishes at a well-known location, + listing each key with what it may sign and whether it is active, + retired or revoked; and a confirmation a verifier runs after the + signature, against an origin it has anchored, reported beside the + receipt's own verdict. It adds no member and changes no signed byte. +- **Module authorisation** (§7.9): who may authorise the decoder and + reconstruct modules a sink runs, and the parameter sets an envelope + pins; their revocation by digest; the rule that a sink which cannot + establish a module's authorisation falls back rather than run it; and + the longest a revoked module keeps running. Out of scope: @@ -59,9 +74,9 @@ Out of scope: sits **above** it. A receipt's content hash is computed over the full WAI envelope (`WAI1` magic + manifest + payload), not over the payload alone. -- Key discovery. Every signature here, `claim` signatures included, - presumes the verifier already holds the signer's key by some means - this extension does not specify. +- A trust store. §7 says what a verifier checks against an origin it + has anchored and how it records the anchor; which origins a relying + party anchors, and how it provisions a pinned anchor, is its own. - A soft-binding algorithm, a resolution service, or a transparency log. This extension names them by identifier and locator (§2.4.7, §2.6) and defines none of them. @@ -197,7 +212,7 @@ Required fields: | `group_start` | string | RFC 3339 timestamp of the first object. | | `group_end` | string | RFC 3339 timestamp of the last object. | | `signer_pubkey` | 32 B hex | Ed25519 verifying key per RFC 8032. | -| `signer_id` | string | Bound identity string (DID, URN, or display). | +| `signer_id` | string | Bound identity string (DID, URN, or display). In §7.1's origin-bound form it names the web origin whose key log lists `signer_pubkey`; an Ed25519 `did:key` names one key; any other form is a label, and a verifier reads nothing from it. | | `sig` | 64 B hex | Ed25519 signature over §2.3 payload. | | `parent_receipt_hash` | 32 B hex or `null` | BLAKE3 of the previous group receipt's canonical bytes under this publisher key; `null` for the first group. | @@ -292,7 +307,7 @@ is present and `null`, so one claim has one canonical form. | `issued_at` | string | A date-time (below) at which the signer issued the claim. Self-asserted, like `group_end`. | | `body` | object | Class-scoped; its members are fixed by `class` and by nothing else (§2.4.5–§2.4.9, §2.6). | | `signer_pubkey` | 32 B hex | Ed25519 verifying key per RFC 8032. | -| `signer_id` | string | Bound identity string. | +| `signer_id` | string | Bound identity string, as for a group receipt (§2.4.1, §7.1). | | `parent_receipt_hash` | 32 B hex or `null` | `link_hash` of this signer's previous claim; `null` for its first. | | `sig` | 64 B hex | Ed25519 signature, below. | @@ -378,7 +393,10 @@ Rules: - A `claim` carries the authority of its signer and no more. `class` says what is asserted, never that it is true. A claim signature is not evidence about delivery, and a delivery signature is not - evidence about a claim. + evidence about a claim. What that authority is — which web origin + stands behind the key, and whether it gave the key this class — is + §7's to establish. The *signed by* column above names the party each + class expects; §7.5 is how a verifier checks it. - A `claim` that repeats a figure a delivery receipt carries for the same subject **MUST** agree with it; the `energy` class does this deliberately, to bind its labels to the figure. Where the delivery @@ -555,10 +573,14 @@ names (§3.1 step 6): the declaration **MUST** itself pass §3.1 steps (`context_mismatch`); a decision **MUST** then name it by `link_hash`, share its `subject`, `subject_type` and `vocabulary`, and decide on a purpose the declaration lists. That check does not establish that the -declaration was the service operator's: a verified delivery receipt -supplies a publisher's key (§3.2), but this extension names no source -for an operator's, so the check establishes only that the decision -matches a declaration, signed by whoever signed it. +declaration was the service operator's. A verified delivery receipt +supplies a publisher's key (§3.2); an operator's is confirmed under the +operator's web origin by §7, with the authority +`lawful-basis/declaration`, and only under a pinned anchor (§7.5). +Without that confirmation the check establishes only that the decision +matches a declaration, signed by whoever signed it; with it, that the +declaration was made under that origin, and, where the verifier expects +the service's origin (§7.5), under the service's. What this class does not do. It records what was declared and what was decided. It is not a consent mechanism: a declaration is agnostic @@ -600,6 +622,10 @@ the subject as logged. This claim's signature never stands in for the log's, and [transparency](transparency.md) §6 applies: registration is a witness, not a verification. +Whose key attached the log's evidence can be confirmed under §7, with +the authority `log-inclusion`. That confirmation is not a check of +`log_receipt`. + #### 2.4.8. `class: "measurement"` [energy-measurement](energy-measurement.md) §6 requires a published figure @@ -1096,7 +1122,10 @@ match the algorithm style of WAI SPEC.md §4 (numbered, terminating). `origin` are bound with them. Where it is not, `joules_micro` and `origin` are the publisher's unsigned statements. The verifier MAY now persist the group receipt as the new chain head for `signer_pubkey` - and dispatch the WAI envelope per WAI SPEC.md §4. + and dispatch the WAI envelope per WAI SPEC.md §4. Whose key + `signer_pubkey` is, is §7's to confirm; a group receipt needs the + authority `delivery`, and the energy authority of each class its + labelled leaves and its labelled total carry (§7.5). A verifier MUST treat any rejection in steps 1–5 as a hard failure for the object in question. A verifier MUST NOT accept a partial @@ -1259,6 +1288,9 @@ signer made this statement about this subject. A `measurement` claim is checked at step 6 always: without the receipt carrying its figure it is `figure_not_held`, not accepted (§2.4.8). §2.4.4's rules bound what that means, and §2.4.7 bounds it further for `log-inclusion`. +Who the signer is, and whether a web origin gave its key this claim's +authority, is §7's to confirm. A verifier reports it beside acceptance +and never folds it in: confirmation changes the outcome of no step here. | step | reason | raised when | |---|---|---| @@ -1466,8 +1498,9 @@ that case states: accept, or reject at the stated step with the stated reason. The cases are normative as written here. Vectors that instantiate them are generated outside this tree, by a companion implementation, and are not yet published in this repository; once a revision publishes them in the tree, a -conforming verifier MUST also verify against them. (The claim-receipt corpus of -§5.6 is published in this tree.) Each +conforming verifier MUST also verify against them. (The corpora of §5.6, §5.7 +and §5.8 — claim receipts, labelled JWP receipts and key discovery — are +published in this tree.) Each vector consists of: the WAI envelope bytes for every object in the group, the canonical group-receipt JSON, the canonical object-receipt JSON for every object, and a `result.json` declaring the expected @@ -1610,6 +1643,56 @@ verify` on every platform of the conformance matrix. joule-code's `joule-moq` pins leaves, roots, signatures and link hashes copied from the corpus. +### 5.8. Key discovery (`signer-conformance/`) + +§7 is checked by the corpus in +[`signer-conformance/`](../signer-conformance/), generated and verified +by `wai_signer_vectors` (`wai-rs`, feature `key_discovery`) and run by +the WAI conformance workflow on every platform of its matrix. Three +procedures appear. `anchor` gives a key log, how it was obtained and any +anchor already held, and declares `anchored` with the anchor's +`revision_hash` and path, `not_anchored` with a reason of §7.7, or +`replaced`. `confirm` gives an item — a claim's wire form, or the +signer, the acquisition classes and the signed time of a group receipt +or another family's receipt — with the revisions and anchors a verifier +holds and any expected origin, and declares `confirmed` with the origin, +key-id, head and anchor path and what §7.4 reports beside them, or +`unconfirmed` or `refused` at a step of §7.4 with a reason of §7.7. A +claim that fails §3.1 is declared rejected at its own step with its own +reason, and is never confirmed. `module` gives a module's bytes, a +grant, the sink's module origins, the revisions, anchors and statuses a +sink holds, the time, the capability being decoded and whether the +envelope leaves a fallback, and declares `run` with the kill latency, +or `fallback` or `inert` at a check of §7.9 with its reason; for a +pinned parameter set it may give no grant, and it adds the refusal SPEC +§3.1's check 5 reports. The corpus +covers each rule of §7.2, the head walk with revisions that are not +held, unattached, injected, forked and superseded, each move of an +anchor, each step of §7.4 including the pinned-only authorities, the +authorities a `measurement` or `operating-point` claim needs, a family +receipt whose figures carry two classes, +instants compared across offsets, fractions and a leap second, and each +check of §7.9, with grants from an origin that is not one of the sink's +module origins, a grant whose `origin` is not the one its `signer_id` +names, a digest one module origin revokes and another grants, a status +signed under an injected revision, and a pinned parameter set whose +digest is revoked while its grant fails an earlier check or no grant is +held. + +A **canonical fixture** — a revision given with its members out of order +at two levels and escapes in its text — has its canonical bytes, +signature and `revision_hash` computed by +`signer-conformance/signer_verify.py`, which takes from +`claim-conformance/claim_verify.py` only what §7 reuses of §3.1 and +shares no code with `wai-rs`, and pinned as literals that +`wai_signer_vectors` checks first. That script also reports every +vector's verdict from this section. CI runs it on the linux runner, and +requires its `verdicts` output to equal `wai_signer_vectors verdicts` +line for line. §7 changes no vector of the claim corpus (§5.6): every +signer it names writes a label (§7.1), and every vector keeps its +declared outcome, those of the `measurement` and `operating-point` +classes included. + --- ## 6. Security considerations @@ -1691,7 +1774,8 @@ multi-signer container; the v1.0 wire format remains a strict subset. (ML-DSA), or hybrid Ed25519 + post-quantum receipts is deferred to a future major revision of this extension. Implementers building for long-lived archives SHOULD plan key-rotation policies - that allow ciphersuite migration when the upgrade lands. + that allow ciphersuite migration when the upgrade lands. §7.6 gives + the rotation and revocation of a signing key. - **Confidentiality of receipts.** Receipts carry identity, joules, and origin labels in cleartext. Carriers that require receipt confidentiality (e.g. trade-secret origin labels) MUST @@ -1700,8 +1784,9 @@ multi-signer container; the v1.0 wire format remains a strict subset. is distinct from **payload** confidentiality: the [Confidentiality extension](confidentiality.md) seals the object bytes a receipt attests, and this chain deliberately commits over - that ciphertext so a relay signs delivery + energy without ever - reading the content. A `measurement` claim (§2.4.8) also carries the + that ciphertext so a relay forwards the sealed bytes and their + receipts unaltered, signs nothing in the publisher's name (§4.3), and + never reads the content. A `measurement` claim (§2.4.8) also carries the measuring machine's power levels, thread count, build and power reader in cleartext. - **Pricing semantics for joules.** `joules_micro` and @@ -1722,9 +1807,11 @@ order, under which a signature can be made without any private key, and restates RFC 8032's rule that a key has only its canonical encoding, so that verifiers following it accept the same signatures. What a signature does not settle is whether its signer was entitled -to say it: this extension defines no authority class and no key -discovery, and a claim is worth exactly as much as the verifier's -reason to trust the key that signed it. +to say it. §7 settles as much of that as a web origin's say-so can: +that an origin the verifier has anchored lists the key with the claim's +authority. A confirmed claim is worth exactly as much as that +confirmation; an unconfirmed one, as much as the verifier's own reason +to trust the key that signed it. ### 6.8. Soft binding @@ -1759,3 +1846,781 @@ reason to trust the key that signed it. recovered (§3.2 step 4; for a recovery record, `observed` ≠ `subject`), so a soft binding cannot stand in for a hard one that still verifies. + +## 7. Key discovery and signing authority + +Every signature in this extension is checked against the `signer_pubkey` +the receipt itself carries (§3 step 2, §3.1 step 3). That settles that +the holder of that key signed. It does not settle who holds it: a forger +supplies a key as easily as a signature. This section lets a signer say +where its keys are published, and lets a verifier confirm it. The signer +names a web origin in `signer_id`; the origin publishes a signed, chained +**key log** listing its keys and what each may sign; a verifier that has +**anchored** the origin checks an item it has accepted against that log. +The same log says which keys may authorise the decoder and reconstruct +modules a sink runs, revokes modules by digest, and bounds how long a +revoked module keeps running (§7.9). + +Nothing here adds a member to a receipt or claim, or changes a signed +byte. `signer_id` is inside every signature already (§2.3, §2.4.3), so no +relay can change the origin it names. A receipt verifies exactly as +before, and one whose signer does not use this section is reported +unconfirmed, as every signer was before it. + +Beside an item's own verdict, a verifier that runs this section reports +one of three levels, and **MUST NOT** report one as another: + +- **confirmed** — an origin the verifier has anchored lists the item's + key, in force, with every authority the item needs (§7.4, §7.5); +- **refused** — what the verifier holds contradicts the item's claim to + speak for the identity its `signer_id` names: a valid log of that + origin, or the `signer_id` itself (§7.1); +- **unconfirmed** — the verifier holds nothing it can confirm against, + or nothing that meets the anchor an authority requires (§7.5). It + establishes nothing about who signed. + +Confirmation never changes an item's verdict under §3 or §3.1. A refused +claim is still a statement its signer made; a confirmed one still asserts +only what §2.4.4 says its class asserts. + +### 7.1. What a `signer_id` names + +A `signer_id` — `publisher_id` in §2.3 — has one of three forms. + +- **Origin-bound.** A signer that publishes its keys under a web origin + writes `signer_id` as a `did:web` DID URL with a host, no path and a + fragment: + + ``` + signer_id = "did:web:" host [ "%3A" port ] "#" key-id + host = label 1*( "." label ) ; the last label is not all digits + label = 1-63 of a-z 0-9 "-", not beginning or ending with "-" + port = 1-65535 in decimal, no leading zero, not 443 + key-id = 1-64 of A-Z a-z 0-9 "-" "_" "." + ``` + + A host is at most 253 characters. Its **origin** is `https://` + followed by the host and, where a port is written, `:` and the port — + the ASCII serialisation of a web origin (RFC 6454 §6.2), unrelated to + the `origin` label of an `object` receipt (§2.4.2). Its **key log** is + at the origin followed by `/.well-known/wai-keys`, and nowhere else. + The suffix `wai-keys` is not yet registered in the IANA Well-Known URIs + registry; registration under RFC 8615 is to be requested. Each origin + and each key-id has one spelling: a capital letter in the host, an + escape other than `%3A`, a path, a missing fragment, the default port + written out and an IP address are not this form. A signer writes this + form only for a key its origin's log lists. +- **Ed25519 `did:key`.** The characters `did:key:z` followed by the + base58btc encoding (the Bitcoin alphabet, each leading zero byte + written as `1`) of the bytes `0xed`, `0x01` and a 32-byte key, where + decoding the text and encoding the result again gives the same text. It + names that key and nothing else: not who holds it, and not what it may + sign. +- **Label.** Any other string, including `did:key` and `did:web` strings + of neither form above. A verifier reads nothing from a label. + +A verifier **MUST NOT** report a `signer_id` as the identity of an item's +signer unless it has confirmed the item (§7.4); otherwise it reports the +`signer_id` as the signer's own label. + +A `did:web` resolver reads a DID document at `/.well-known/did.json` on +the same origin. An origin that publishes one **SHOULD** list in it +exactly the `active` keys of its latest key-log revision, each as an +Ed25519 `publicKeyMultibase` under the id +`did:web:[%3A]#`, the key's origin-bound `signer_id`. A +verifier here **MUST NOT** take a key from it: the key log is what §7.2 +signs and chains, and it governs. The `wai-keys` tool of the reference +implementation emits such a document from a log and reports every +difference between a document and a log. + +### 7.2. The key log + +A **log date-time** is an RFC 3339 date-time in UTC, written +`YYYY-MM-DDTHH:MM:SSZ` with `T` and `Z` in capitals, no fractional second +and a second of at most 59, so that two of them compare as strings. An +**authority** is one or more segments of `A-Z`, `a-z`, `0-9`, `-`, `_` and +`.`, joined by `/`, at most 128 characters in all (§7.5). + +A key log is a JSON object with exactly two members: `kind`, which is +`"wai-key-log"`, and `revisions`, a non-empty array of revisions whose +first has `seq` 0 and each of which is the parent of the next. A revision +is a JSON object with exactly these members, under §2.4.3's encodings: + +| member | type | meaning | +|---|---|---| +| `kind` | string | `"wai-key-set"`. | +| `origin` | string | The origin the log is published under, as §7.1 derives it. | +| `seq` | integer | `0` for the first revision; otherwise one more than its parent's. | +| `prev` | 32 B hex or `null` | Its parent's `revision_hash`; `null` exactly when `seq` is `0`. | +| `issued_at` | log date-time | When the origin issued it. Self-asserted; not earlier than its parent's. | +| `root` | 32 B hex | The Ed25519 key that signs this revision. | +| `next_root` | 32 B hex | The commitment (below) to the key that may sign as `root` after this one; never the commitment to `root`. | +| `keys` | array | The key entries, below. | +| `revoked_modules` | array of 32 B hex | The SHA-256 digests of the modules and pinned parameter sets the origin has revoked (§7.9), none twice. | +| `grant_max_s` | integer | The longest module grant, in seconds, the origin issues: at least 1 and at most §7.9's ceiling. | +| `status_max_age_s` | integer | How long, in seconds, a key status stays fresh: at least 1 and at most §7.9's ceiling. | +| `sig` | 64 B hex | `root`'s signature, below. | + +A key entry has exactly these members: + +| member | type | meaning | +|---|---|---| +| `id` | string | A key-id of §7.1. | +| `key` | 32 B hex | An Ed25519 verifying key that §3.1 step 3 accepts. | +| `may_sign` | array of authorities | The authorities (§7.5) the origin gives this key: at least one, none twice. | +| `status` | string | `"active"`, `"retired"` or `"revoked"`. | +| `until` | log date-time or `null` | For a retired key, the time after which the origin signs nothing with it; `null` for a key never retired. | +| `compromised_since` | log date-time or `null` | For a revoked key, the time from which the origin attests that another party may have held it; `null` otherwise. | + +An `active` entry has both times `null`; a `retired` entry has `until` and +no `compromised_since`; a `revoked` entry has `compromised_since`, and +`until` if it was retired first. No two entries share an `id` or a `key`, +no entry's `key` is `root`, and none has `next_root` as its commitment. + +``` +sig = Ed25519(root, "wai:key-set\x01" || JCS(revision without "sig")) +revision_hash = BLAKE3("wai:key-set-link\x01" || JCS(revision)) +commitment(K) = BLAKE3("wai:key-set-next\x01" || K) +``` + +JCS is §2.4.3's canonical form. Of the revisions a verifier holds for an +origin, a revision's **parent** is the one whose `revision_hash` is its +`prev`. A revision whose `prev` names no revision the verifier holds is +**unattached**: it is ignored until its parent is held. Each rule below +has a reason (§7.7). Rules 1–3 concern a revision alone, rules 4–6 +compare it with its parent, and rule 7 compares revisions that share a +parent: + +1. It is a JSON text that §3.1 step 1 would read, with exactly the + members above, each of the type and encoding given, and it meets the + constraints stated with the tables (`key_log_malformed`). +2. Its `origin` is the origin being checked (`origin_mismatch`). +3. Its `sig` verifies under its `root` by §3.1 step 3's rule, which also + refuses a `root` that does not decode or is of small order + (`key_log_signature_invalid`). +4. Its `seq` is one more than its parent's, and its `issued_at` is not + earlier than its parent's (`key_log_chain_break`). Its `prev` is its + parent's `revision_hash` by the definition of a parent; between + consecutive revisions of a log being anchored (§7.3), a `prev` that is + not the previous revision's `revision_hash` is also + `key_log_chain_break`. +5. Either its `root` and `next_root` are its parent's, or its `root` + differs from its parent's and has its parent's `next_root` as its + commitment (`key_log_root_not_committed`). A revision of the second + kind is a **rotation**: signing it proves possession of the committed + key. Only a rotation changes the commitment. +6. It erases and rewrites nothing: its `keys` begin with its parent's + entries, in the same order, each with the same `id`, `key` and + `may_sign`; a `status` stays, or moves from `active` to `retired` or + `revoked`, or from `retired` to `revoked`; a time, once set, does not + change; and its `revoked_modules` begin with its parent's + (`key_log_rewrites_history`). A revocation therefore stays in every + later revision, and the latest revision lists every key and every + revoked module the origin has ever listed. +7. It has one history. Of the revisions a verifier holds that share a + parent and are signed by a key that parent **authorises** — its + `root`, or the key its `next_root` commits to — exactly one is taken: + the only one, or, where there are several, the only rotation among + them, which **supersedes** the others and every revision descended + from them. Any other case is `key_log_fork`. + +A verifier **holds** a revision for an origin only if the revision passes +rules 1–3 for that origin. A revision it does not hold, an unattached +revision, and a revision signed by a key its parent does not authorise +are no child of anything, and cannot stop a confirmation. Only a key the +log itself authorises can: a root that signs two histories, or a history +that breaks rules 4–6. + +### 7.3. Anchors + +A verifier confirms against an origin only once it has **anchored** it: +it holds a key log of that origin that passed the checks below, whose +latest revision is its **anchor**, and a record of how it came to hold +it. + +- **Pinned** — the log is provisioned out of band by the relying party, + as a sink holds a trust anchor + ([unidirectional-delivery](unidirectional-delivery.md) §3). +- **Fetched** — the log is retrieved from the origin's key-log location + over HTTPS, under a certificate the verifier's TLS trust store accepts + for the host (RFC 9110 §4.3.4), without following a redirect, and the + verifier records when it retrieved it (`acquired_at`). The fetch + establishes only that the origin served that log then, and only to the + verifier that fetched it: whoever relies on a confirmation built on it + relies on that verifier's record. + +Anchoring checks the log in this order, reports the first failure, and +anchors nothing on a failure: the document has exactly the members +`kind` and `revisions`, with the values above (`key_log_malformed`); +each revision, in order, passes rules 1–3 (that rule's reason); the first +revision has `seq` 0 (`key_log_chain_break`); and each later revision +passes rules 4–6 against the one before it (that rule's reason). + +The anchor, and every revision it descends from that the verifier holds, +is **trusted**. Later revisions may come from anywhere — the origin, a +cache, a mirror, a transparency log — because each is checked against its +parent; a revision obtained other than by pinning or from the origin's +own location is never an anchor. An anchor moves in these ways only: + +- **Forward**, when the verifier anchors the origin again from its + location and the log it retrieves contains the anchor: the log's latest + revision is then the anchor. The record keeps how the origin was first + anchored — a pinned record stays pinned, since the new anchor descends + from the pinned one along the chain its root signed — and a fetched + record takes the new `acquired_at`. A pinned record also keeps the + `revision_hash` of the revision that was pinned (its **pinned + revision**), which no move changes: the hardware-class and + lawful-basis authorities are confirmed under the moved record, and a + pre-emption authority only from the pinned revision itself (§7.4 + step 6a). +- **To a rotation** that supersedes (rule 7) the anchor or a trusted + revision it descends from, where the trusted revision's own child is + not a rotation. A confirmation reports such a move (§7.4 step 3). A + retrieved log whose revisions after the last one it shares with the + trusted line begin with such a rotation moves the anchor to its latest + revision, and the record is then fetched: the rotation reached the + verifier other than by pinning. +- **Anew**, by pinning, whatever the verifier held. + +Otherwise an anchor never moves. A verifier that has anchored an origin +and retrieves from it a log whose latest revision is a trusted revision +other than the anchor keeps its anchor and reports `key_log_rolled_back`: +the log is an older copy, or the origin's server was rolled back. One +that retrieves a log that does none of the above keeps its anchor, +reports `key_log_replaced`, and anchors that origin anew only by pinning. +A name that has changed owner and a server held by an intruder look the +same from here. + +Retrieval is the only step that touches the network, and it comes before +confirmation, never within it. Confirmation (§7.4) and the module checks +(§7.9) are functions of what they are given — the item, the revisions +the verifier holds, its anchor records, and for §7.9 the statuses it +holds and the time — so verifiers holding the same bytes reach the same +verdict, offline, on every platform. A verifier that must re-check a +confirmation later keeps those revisions and its anchor record. + +### 7.4. Confirmation + +A verifier confirms only an item it has accepted: a `group` receipt that +passed §3 steps 1–2, whose `object` receipts share its confirmation; a +`claim` accepted under §3.1; a receipt of another WAI family whose signed +bytes include its `signer_pubkey` and `signer_id` and whose own +verification passed; or a module grant whose signature verified (§7.9). +Its inputs are the item's `signer_pubkey`, `signer_id`, the authorities +it needs (§7.5) and its signed time where it has one — a claim's +`issued_at`, a group receipt's `group_end`, a grant's `not_before` — the +revisions the verifier holds, its anchor records, and, where it has one, +an expected origin (§7.5). It runs these steps in order and reports the +first that fails, as a level and a reason (§7.7): + +1. `signer_id` is origin-bound (§7.1). A `did:key` naming a key other + than `signer_pubkey` is refused (`signer_id_names_another_key`); any + other form is unconfirmed (`signer_id_not_origin_bound`). +2. The verifier has anchored the origin and holds the anchor + (`origin_not_anchored`). +3. The **head**. The verifier first looks along the trusted line, from + its oldest held revision to the anchor, for a trusted revision whose + trusted child is not a rotation and which has a held child that is a + rotation: the first such revision's rotation supersedes the rest of + the line, and the walk starts there, checked against its parent under + rules 4–6 (the anchor has **moved**). Otherwise it starts at the + anchor. From the start it takes, repeatedly, the child rule 7 selects, + checking it against its parent under rules 4–6, until it reaches a + revision with no child it holds. That revision is the head. The + result depends on which revisions are held, never on the order in + which they arrived. A failure is unconfirmed with the rule's reason + (`key_log_chain_break`, `key_log_root_not_committed`, + `key_log_rewrites_history`, `key_log_fork`). +4. The head lists an entry whose `id` is the `signer_id`'s key-id + (`key_not_listed`) and whose `key` is `signer_pubkey` + (`key_mismatch`). +5. The entry is not `revoked` (`key_revoked`), whatever time the item + carries: whoever holds a compromised key can write any time into what + it signs. A `retired` entry fails (`key_retired`) when the item's + signed time is later than `until`, and passes when it is not or the + item has none. A signed time that is not an RFC 3339 date-time is + later than every `until`. +6. The entry's `may_sign` holds every authority the item needs + (`authority_not_granted`). + - 6a. Where any of those authorities is **pinned-only** (§7.5), the + anchor record is pinned, with its pinned revision, and the anchor + has not moved in step 3 (`anchor_not_pinned`). For an authority + registered for pre-emption, the entry is also listed, with that + authority, in the record's pinned revision itself — the revision + the relying party provisioned, not a later one a forward move + brought ([unidirectional-delivery](unidirectional-delivery.md) §3: + a trust anchor delivered over the bearer it authorises is not + one). +7. Where the verifier expects an origin (§7.5), or one of a set of + origins (§7.9's module origins), the origin is that one, or one of + that set (`origin_not_expected`). An empty set expects no origin the + item can have. + +Steps 1–3 and 6a fail as unconfirmed and steps 4–7 as refused, except +step 1's `signer_id_names_another_key`, which is refused. Step 6a is +unconfirmed, not refused: nothing contradicts the item; the verifier +lacks the anchor the authority requires. + +Two times are compared as instants: each is brought to UTC by applying +its offset, carrying into the minute, hour and date as the calendar +requires and leaving the second as written, and the results are compared +as (date, hour, minute, second, fraction), the fraction as a decimal +fraction. A second of 60 thereby falls after second 59 of its minute and +before the next minute. + +A confirmed item is reported with its origin, key-id and authorities; the +head's `revision_hash`; the anchor record — pinned or fetched, the +anchor's `revision_hash`, `acquired_at` if fetched, and the pinned +revision if pinned — and the rotation +it moved to, if it moved; where the entry is retired, `until`, and that +the item is untimed if it has no signed time; where an origin was +expected, where the expectation came from; and, where the item needs a +hardware-class authority (§7.5), whether its key also holds `delivery` +(`also_delivers`). A `key_revoked` refusal is reported with +`compromised_since`, and with the item's own time if it has one. + +- A verifier **MUST NOT** present an item it has not confirmed as signed + by the party its `signer_id` names. +- A verifier **MUST NOT** report a refusal as unconfirmed. +- A confirmation establishes that the origin, as anchored, lists this key + with these authorities. It does not establish that the statement is + true (§2.4.4), that the key was not misused within its authority, or + that an energy figure is what its class says + ([energy-measurement](energy-measurement.md) §2.2). It is the origin's + say-so, made checkable. +- A confirmation describes the log as the verifier holds it. A + revocation the verifier has not retrieved is not in it, and the head's + `revision_hash` in the report shows how current the log was. + +### 7.5. Authorities: who may sign which class + +The authorities an item needs are fixed by the item, never by its signer. +A confirmation needs every one of them: + +| item | authorities | +|---|---| +| a `group` receipt, and the `object` receipts under it | `delivery`; and `energy/` for each acquisition class an `object` receipt under it labels, where its leaf binds a label (§2.2), and for the class a labelled group payload signs for the group's total (§2.3) | +| a `claim` | its class's authority: `energy/` followed by its `energy_provenance` for `energy`, for example `energy/OnChipCounter`; `lawful-basis/declaration` or `lawful-basis/decision` by its `record`; its class name for every other class — `log-inclusion`, `soft-binding`, `binding-recovery`, `measurement` and `operating-point`. And `energy/` followed by its `energy_provenance` whenever its body carries that member, whatever its class | +| a receipt of another WAI family that signs its own `signer_pubkey` and `signer_id` | `receipt`; and `energy/` for each class its figures carry a label for | +| a module grant (§7.9) | `module/decoder` or `module/reconstruct`, by its `form` | + +A class registered in §2.4.4 registers its authority here with it, and a +family that needs a narrower authority than `receipt` registers one here. +An authority a verifier does not know matches nothing. Because an energy +authority is read from the `energy_provenance` member wherever it +appears, a class that restates a figure with its acquisition class needs +that class's authority without this table naming the class. + +**Pinned-only authorities.** The hardware-class energy authorities — +`energy/CalibratedInstrument`, `energy/OnChipCounter` and the legacy +`energy/HwShunt` — the lawful-basis authorities — +`lawful-basis/declaration` and `lawful-basis/decision` — and any +authority registered for pre-emption are confirmed only under a pinned +anchor that has not moved (§7.4 step 6a). Every other authority is +confirmed under a pinned or a fetched anchor, and every confirmation +reports which. + +**Expected origin.** A verifier **MAY** expect an origin for an item: one +its relying party names — the service a sink used, the publisher it +subscribed to — or the **subject's origin**: the origin under which it +has confirmed, with the authority `delivery`, the group receipt that +carries a claim's subject (for a claim whose `subject_type` is `object`), +or under which it has confirmed the receipt a claim's subject names +(`subject_type` `receipt`). It reports which. §7.9 expects one of the +sink's module origins. + +§2.4.4's *signed by* column names the party each class expects. Which of +its keys is that party is the origin's decision, and its log records it: + +- **Delivery.** A group receipt confirmed under an origin was signed with + a key that origin gives `delivery`. +- **Energy.** `energy/` lets a key state figures of that class and + no other, so an origin can confine what a metering key may claim. No + authority raises a figure's class or grade, or adds to what a class + establishes ([energy-measurement](energy-measurement.md) §2, §2.2). An + origin **SHOULD** give the hardware-class authorities only to a key + held by the component that signs the reading, and not to a key it also + gives `delivery`. Beside a confirmed item that needs a hardware-class + authority, a verifier reports whether its key also holds `delivery` + (`also_delivers`). A group receipt whose objects' leaves bind a + hardware-class label, or whose labelled total carries one, is signed by + one key for delivery and for the figures, so `also_delivers` is true + there by construction, and is reported as such. An `energy` claim + confirmed under the subject's origin was made under the publisher's + authority, by its own key or by a metering key its log lists. +- **Lawful basis.** A declaration confirmed with + `lawful-basis/declaration` is its origin's; §2.4.6's check of a + decision against it then establishes that the decision matches a + declaration made under that origin. A verifier **MUST NOT** report a + decision as matching a service operator's declaration unless it has + confirmed the declaration, and reports the declaration's level beside + the decision's. +- **Measurement and operating point.** A `measurement` claim needs + `measurement` and the energy authority of the class it restates; an + `operating-point` claim needs `operating-point` and that energy + authority too. A report of a hardware-class figure is therefore + confirmed only under a pinned anchor, and is reported with + `also_delivers`. §2.4.8's check 5 already requires a `measurement` + claim's key to be the key that signed the figure; confirmation adds + that the figure's origin gave that key the authority to report on it. + An origin can give `operating-point` to the party that records + requests and commitments without letting it state figures of its own. +- **Log inclusion.** A confirmation says under whose authority the log's + evidence was attached. It does not check `log_receipt`: §2.4.7 is + unchanged. +- **Soft binding.** A declaration confirmed with `soft-binding` is its + origin's; confirmed under the subject's origin, it is the publisher's. + §3.2 step 3 and §2.6.3 check 4 are unchanged: a recovery still needs + the declaration's key to be the key of a verified delivery receipt. +- **Modules.** `module/decoder` lets a key grant a decoder module, or a + parameter set pinned by an envelope, for the capabilities the grant + names; `module/reconstruct` lets it grant a reconstruct module (§7.9). + Neither is pinned-only; §7.8 says what a module authorised under a + fetched anchor is. +- **Pre-emption** ([unidirectional-delivery](unidirectional-delivery.md) + §3), relay attestation (§4.3) and reconciliation have no class and so + no authority. When one is registered for pre-emption it is pinned-only, + and the pinned revision itself must list the key with it (§7.4 + step 6a): a fetched anchor is authority taken on first use, which that + section forbids for an instruction that displaces what a person chose. + +### 7.6. Rotation and revocation + +- **A signing key** is rotated by listing its successor under a new `id` + before the successor signs, and retiring the old key with an `until` + no earlier than the last item it signed. Items the old key signed stay + confirmable. A claim chain (§2.4.3) is per key: the successor starts + its own, and §3.1 step 5 is unchanged; the log, not the chain, connects + the two keys. An entry's authorities never change (rule 6): an origin + that gives a key another authority lists the key's successor with it. +- **A compromised key** is revoked with `compromised_since`, and every + item under it is refused from then on (§7.4 step 5): every receipt, + claim and module grant it signed, whatever time the item carries. An + item's own time cannot restore it, and this section defines no + independent time that could. +- **A module or a pinned parameter set** is revoked by adding its digest + to `revoked_modules` (§7.9). A revocation, like a key's, stays in every + later revision. +- **The root** is pre-committed: each revision commits to the next root, + and only a rotation changes the commitment. Whoever steals the root key + in use can sign revisions that add keys, but can neither rotate nor + change the commitment; the owner's rotation, signed with the committed + key, supersedes what the thief signed from the point where the two + histories fork (§7.2 rule 7). The committed key is the recovery key and + **SHOULD** be held apart from the root in use. An origin that loses + control of the committed key loses its log: verifiers anchored on it + confirm nothing new, and anchoring again means pinning. +- **Registration.** An origin **SHOULD** register each revision with a + Transparency Service, as it registers group receipts + ([transparency](transparency.md) §3), so that a fork is visible to any + Relying Party that checks the log. Registration anchors nothing + ([transparency](transparency.md) §6). + +### 7.7. Reasons + +| step | level | reason | raised when | +|---|---|---|---| +| 1 | unconfirmed | `signer_id_not_origin_bound` | `signer_id` is not of §7.1's origin-bound form | +| 1 | refused | `signer_id_names_another_key` | `signer_id` is a `did:key` of a key other than `signer_pubkey` | +| 2 | unconfirmed | `origin_not_anchored` | the verifier has no anchor for the origin, or does not hold it | +| 3 | unconfirmed | `key_log_chain_break` | a child's `seq` or `issued_at` does not follow its parent's | +| 3 | unconfirmed | `key_log_root_not_committed` | a child signed by its parent's `root` has another `next_root` | +| 3 | unconfirmed | `key_log_rewrites_history` | a child drops or reorders an entry, changes its key or authorities, moves a status back, changes a time once set, or drops a revoked module | +| 3 | unconfirmed | `key_log_fork` | a parent has several authorised children and not exactly one rotation among them, or a trusted revision has several superseding rotations | +| 4 | refused | `key_not_listed` | the head lists no entry with the key-id | +| 4 | refused | `key_mismatch` | the entry's key is not `signer_pubkey` | +| 5 | refused | `key_revoked` | the entry is revoked | +| 5 | refused | `key_retired` | the entry is retired and the item's signed time is later than `until` | +| 6 | refused | `authority_not_granted` | the entry's `may_sign` does not hold every authority the item needs | +| 6a | unconfirmed | `anchor_not_pinned` | the item needs a pinned-only authority, and the anchor is fetched, has moved, or has no pinned revision, or, for pre-emption, the pinned revision does not list the key with it | +| 7 | refused | `origin_not_expected` | the origin is not the one the verifier expects, or not one of the set it expects | +| §7.3 | — | `key_log_malformed` | a log or a revision is not of §7.2's form | +| §7.3 | — | `origin_mismatch` | a revision names another origin | +| §7.3 | — | `key_log_signature_invalid` | a revision's signature does not verify under its `root` | +| §7.3 | — | `key_log_chain_break` | the first revision's `seq` is not 0, or a revision does not follow the one before it | +| §7.3 | — | `key_log_root_not_committed` | a revision's `root` is neither the previous one's nor committed to by it, or its `next_root` changes without a rotation | +| §7.3 | — | `key_log_rewrites_history` | as at step 3, between consecutive revisions of the log being anchored | +| §7.3 | — | `key_log_rolled_back` | a log retrieved from an anchored origin ends at a trusted revision older than the anchor; the anchor is kept | +| §7.3 | — | `key_log_replaced` | a log retrieved from an anchored origin neither contains the anchor nor supersedes a trusted revision by a rotation; the anchor is kept | + +The reasons of §7.9's checks are listed there. + +### 7.8. Security considerations + +- **First use.** A fetched anchor is as good as the first retrieval: the + domain name system, the certificate accepted, and the origin's server + at that moment. Whoever controls any of them then can serve a log of + its own to a verifier anchoring for the first time. Once anchored, a + verifier is not exposed to that again, since every later revision must + descend from its anchor (§7.3). A relationship that cannot bear the + risk pins its anchor; the authorities §7.5 makes pinned-only are those + for which WAI does not leave that choice open. +- **Modules on first use.** A module authorised under a fetched anchor is + code accepted on first use: whoever served the first retrieval chose + which keys may grant it. A sink that cannot bear that pins the anchors + of its module origins. Every module outcome reports the anchor's path + (§7.9). +- **Which origin grants a module.** Anchoring an origin to confirm its + receipts does not make it a source of modules: a grant is held only + from one of the sink's module origins (§7.9), so an origin anchored on + first use for some other publisher's receipts authorises nothing a sink + runs on this envelope. +- **Revocation from any anchored origin.** A digest revoked in the head + of any origin a sink has anchored is not held, whichever origin grants + it (§7.9 check 5), so a publisher's revocation cannot be undone by + another origin's grant. Any anchored origin can therefore make a + module fall back; it cannot make one run. +- **Withheld revisions.** An origin, or anyone between it and a verifier, + can withhold its latest revisions and with them a revocation. How + often a verifier retrieves the log is its own policy; the head named in + a report shows what it rested on, and registration (§7.6) makes a + withheld revision's existence checkable. For modules, §7.9's status + bounds how long a withheld revocation can go unnoticed. +- **A rolled-back server.** A retrieval that returns an older log is + reported (`key_log_rolled_back`), never taken as the anchor; a status + that names a revision older than the head the sink holds is not + current (§7.9). +- **A stolen root.** Until the owner's rotation reaches a verifier, a + verifier holding only the thief's revisions confirms what the thief + adds. When the rotation arrives it supersedes them, and every key they + added is no longer listed. +- **Injected revisions.** A revision that fails rules 1–3, is unattached, + or is signed by a key its parent does not authorise is no child of + anything and cannot stop a confirmation; only the origin's own root and + committed key can. A status names a revision on the head's line or is + not held (§7.9), so one signed under an injected revision changes + neither a module outcome nor its reason. +- **Retired keys and untimed items.** Many receipt families carry no + signed time. An item without one, under a retired key, stays + confirmable for as long as the key is not revoked, and is reported as + untimed with the key's `until`. An origin that cannot vouch for a + retired key's past use revokes it. +- **Clocks.** §7.9's freshness and grant windows are measured on the + sink's clock. A sink whose clock can be set back runs a module for as + long as the clock is set back; a sink with no clock it trusts treats + every status as stale and runs no module. The kill latency of §7.9 is + a bound on a trusted clock and on nothing else. +- **Published private keys.** A key whose private half has been + published attests nothing, and **MUST NOT** be listed in a key log. Every + key WAI's own corpora, pins and reference meters sign with has a + published seed, and every such seed is one byte repeated 32 times. The + reference implementation exposes the public keys of all such seeds + (`key_log::published_test_keys`); its `wai-keys` tool refuses to put one + in a log unless told the log is a test log, and reports one it finds + in a log it verifies. Every vector of §5.8 is a test log, and every + anchored verdict there carries that report. +- **A name with a new owner.** Whoever controls a domain next can publish + a new log under its origin. It does not descend from an existing + anchor, so an anchored verifier keeps its anchor and reports + `key_log_replaced`; a verifier anchoring for the first time sees only + the new log, in which items signed under the old one are not listed. A + verifier that must keep confirming them keeps the revisions it + confirmed them against (§7.3). +- **Retrieval reveals interest.** Retrieving a log tells the origin that + someone is checking its signatures. A verifier retrieves the log, not a + key per item, and may take later revisions from any source. +- **Separation.** A root signs revisions and statuses and nothing else: + no entry may be a root or the committed key (§7.2), and a revision's, + a status's and a grant's signing domains are distinct from every + receipt's and claim's. + +### 7.9. Decoder and reconstruct modules + +A **module** is code a sink obtains apart from an envelope and runs to +decode a capability (a **decoder** module) or to rebuild a +reconstruction from what a decoder produced (a **reconstruct** module) — +for example a WebAssembly module. A parameter set an envelope pins by +its digest — the prior a neural capability decodes against +([prior-carriage](prior-carriage.md)) — determines a decode the way code +does. It is revoked the same way, by its digest, at every sink that holds +a key log, and authorised the same way, as a decoder module, by a sink +that holds pinned parameter sets to this section. Running a module +requires this section; holding pinned sets to it is the relying party's +choice per capability ([SPEC](../SPEC.md) §4). A sink that holds a +capability's pinned sets to this section runs the checks below as +[SPEC](../SPEC.md) §3.1's check 5, before it looks for the set among +what it holds; a sink that holds a key log but not the capability's sets +reads only check 5's revocation lists (*Pinned sets and several grants*, +below). + +**Identity.** A module's **digest** is the SHA-256 of its bytes exactly as +the sink instantiates them, and a pinned parameter set's is its pin's +`sha256`, the digest of the set in the pin's `form` ([SPEC](../SPEC.md) +§3.1); both are written as 64 lowercase hexadecimal digits. For a pinned +set the pin's digest stands for the bytes' at check 2, and SPEC §3.1's +checks 6–8 then require the bytes the decode reads to have it. A sink that +instantiates other bytes than it hashed runs code nothing authorised. + +**Grant.** An origin authorises a module by a signed **grant**, a JSON +object with exactly these members, under §2.4.3's encodings: + +| member | type | meaning | +|---|---|---| +| `kind` | string | `"wai-module-grant"`. | +| `origin` | string | The origin under whose key log the grant is checked: the one its `signer_id` names (§7.1). | +| `module` | 32 B hex | The module's digest. | +| `form` | string | `"decoder"` or `"reconstruct"`. | +| `capabilities` | array of terms | The capabilities the module may decode or reconstruct: at least one, none twice. | +| `not_before` | log date-time | When the grant takes effect. | +| `not_after` | log date-time | When it lapses; not earlier than `not_before`. | +| `signer_pubkey` | 32 B hex | The granting key. | +| `signer_id` | string | The granting key's `signer_id` (§7.1). | +| `sig` | 64 B hex | `Ed25519(signer, "wai:module-grant\x01" \|\| JCS(grant without "sig"))`. | + +**Status.** A key status is a cacheable signed pointer to an origin's +head, re-issued on each heartbeat, so the key log does not grow with +every heartbeat. It is a JSON object with exactly the members `kind` +(`"wai-key-status"`), `origin`, `head` (32 B hex, a `revision_hash`), +`seq` (that revision's `seq`), `issued_at` (a log date-time) and `sig`, +`Ed25519(root, "wai:key-status\x01" || JCS(status without "sig"))`, +where `root` is the `root` of the revision `head` names. A sink **holds** +a status only if it has that form, names the grant's origin, and names a +revision on the **head's line** — the head §7.4 step 3 reached, or a +revision the sink holds that the head descends from by `prev` — with its +`seq`, under whose `root` the signature verifies. A status that is not +held is ignored, as an injected revision is (§7.2). + +**Module origins.** A sink holds a module only from an origin it accepts +module grants from for the envelope and capability at hand — its +**module origins**, each recorded with where it came from: + +- **relying-party** — an origin the sink's relying party names as a + source of modules for that capability; +- **subject** — the origin under which the sink confirmed, with the + authority `delivery`, the group receipt that carries the envelope: its + publisher. + +A sink with no module origin for an envelope holds no module for it. An +origin the sink anchored for any other purpose — to confirm some other +publisher's receipts, say — is not thereby one of them. + +**Ceilings.** WAI sets two normative ceilings: `grant_max_s` is at most +**86400** seconds and `status_max_age_s` at most **3600** seconds. A key +log declaring more is malformed (§7.2 rule 1). + +**The checks.** A sink runs a module, or, where it holds pinned +parameter sets to this section, decodes against one, only if every +check below passes. It runs them in this +order, before it instantiates the module, and reports the first that +fails: + +1. The grant has the form above, its `signer_id` is origin-bound and + names the grant's `origin`, and its `sig` verifies under its + `signer_pubkey` by §3.1 step 3's rule (`module_grant_invalid`). +2. The bytes' digest is the grant's `module` (`module_digest_mismatch`). +3. The grant passes §7.4 as an item needing `module/`, with its + `not_before` as its signed time and the sink's module origins + expected: step 7 refuses an origin that is not one of them + (`origin_not_expected`). A failure is reported with §7.4's own + reason. A revoked granting key therefore fails every grant it signed, + whatever the grant's times. +4. The sink holds a status for that origin naming the head §7.4 step 3 + reached; of those, the latest is **fresh**: its `issued_at` is not + later than `now` and at most that head's `status_max_age_s` seconds + earlier (`module_status_stale`). A sink that holds statuses but none + naming that head — each names an older revision of the head's line — + fails with `module_status_not_descended`. +5. The grant's `module` is not in the `revoked_modules` of that head, + nor of the head §7.4 step 3 reaches from any other anchor the sink + holds — or, where none can be reached, of that anchor revision + (`module_revoked`). +6. `not_before` ≤ `now` (`module_grant_not_yet_valid`) and `now` ≤ + `not_after` (`module_grant_expired`). +7. `not_after` − `not_before` is at most the head's `grant_max_s` + (`module_grant_too_long`). +8. The capability being decoded is one of the grant's `capabilities` + (`module_capability_not_granted`). + +`now` is the sink's trusted clock, as a log date-time. The checks are a +function of the module's bytes, the grant, the sink's module origins, +the revisions, anchors and statuses the sink holds, and `now`, and read +no clock and no network of their own. A sink runs them before it instantiates a module, and again +before it keeps using an instance once the status or the grant it relied +on is no longer current; an instance that fails is discarded. + +**Fail closed.** Every failure — no network, no status, a stale status, a +clock the sink does not trust, an expired grant — means the module is +**not held**. A sink with no trusted clock has no `now`: no status is +fresh, and every grant is treated as expired. A grant that lapses without +renewal is not held from the moment it lapses. + +**Fallback, never inert.** A module or pinned parameter set that is not +held makes its capability unsupported for that envelope at that sink, and +dispatch continues to the declared fallback or a sibling rendition +([SPEC](../SPEC.md) §4). A sink **MUST NOT** report a file inert because +of module authorisation while a fallback or sibling it can decode without +that module remains. An encoder emitting a capability whose decode needs +a module **MUST** declare such a fallback. A pinned parameter set this +section does not hold is refused at SPEC §3.1's check 5: `prior_revoked` +where its digest is revoked (below), and `prior_not_authorised` for every +other failure. + +**Pinned sets and several grants.** For a pinned set, a sink runs the +checks with each grant of form `decoder` it holds; a grant of form +`reconstruct` names no set. The set is held when one grant passes every +check. When none does, the sink reports `module_revoked` if any grant +reached check 5 and failed it, or, failing that, if the set's digest is in +the `revoked_modules` of the head the sink reaches from any anchor it +holds — or, where none can be reached, of that anchor revision — with +that anchor's origin. A revocation is of the digest, whichever grant +names it, whichever check a grant fails first, and whether or not the +sink holds a grant at all. Otherwise the sink reports the check and +reason of the grant that passed the most checks; a sink that holds no +such grant reports that. + +A sink that holds a key log but does not hold the capability's pinned +sets to this section reads only those revocation lists: it refuses a set +whose digest they hold (`module_revoked`, and SPEC §3.1's +`prior_revoked`), and runs none of checks 1–8 otherwise. Both apply as +well to the set a sink decodes an envelope that pins none against (SPEC +§4), identified by each digest a pin over it could name: it is refused +when any of them is revoked, and, for a capability held to this section, +unless one of them is held. + +**Kill latency.** The longest a module whose grant is no longer renewed, +or whose digest or granting key is revoked, keeps running at a sink that +follows this section is + +``` +K = grant_max_s + status_max_age_s +``` + +with both values from the head the sink holds, measured on the sink's +clock and bounded by the ceilings. `K` is a normative parameter of the +origin's log: an origin states it by the two members it declares, and a +sink reports it with every module it runs. `K` bounds what the granting +origin does; a revocation in another anchored origin's head (check 5) +takes effect once the sink holds that head. + +**Reporting.** A module that runs is reported with the granting key's +origin and key-id, the head's `revision_hash`, the anchor record's path, +where the module origin came from, and `K`. One that does not is +reported with the check and the reason — for a digest another origin +revokes, that origin — and with what dispatch did instead: fell back, or +left the file inert because no fallback was declared. + +`key_log::module_check` in `wai-rs` implements these checks. The +reference dispatcher runs SPEC §3.1's check 5 when the sink is +configured with a key log (`key_log::SinkKeyLog`, given to +`pin::verify_with` or to `neural::ModelRegistry::with_key_log`, which +also runs it before an unpinned decode): the revocation lists for every +capability a pin applies to, and these checks for the capabilities a sink +holds to this section. A sink configured with no key log runs neither. The reference dispatcher runs no module obtained +apart from an envelope, and `wai-web` and the C ABI take no key log +([SPEC](../SPEC.md) §4). + +Sub-delegation, where one grant authorises another, is out of scope. A +module is revoked by its digest or through the key that granted it. + +| check | reason | raised when | +|---|---|---| +| 1 | `module_grant_invalid` | the grant is not of the form above, its `signer_id` does not name its `origin`, or its signature does not verify | +| 2 | `module_digest_mismatch` | the bytes' SHA-256 is not the grant's `module` | +| 3 | §7.4's reason | the granting key is not confirmed with `module/` under one of the sink's module origins (`origin_not_expected` where its origin is not one) | +| 4 | `module_status_stale` | no status is held, or the latest naming the head is older than `status_max_age_s` or later than `now`, or there is no trusted clock | +| 4 | `module_status_not_descended` | statuses are held, and none names the head the sink reached | +| 5 | `module_revoked` | the head's `revoked_modules`, or another anchored origin's, holds the digest | +| 6 | `module_grant_not_yet_valid` | `now` is earlier than `not_before` | +| 6 | `module_grant_expired` | `now` is later than `not_after` | +| 7 | `module_grant_too_long` | the grant's window exceeds the head's `grant_max_s` | +| 8 | `module_capability_not_granted` | the capability is not one the grant names | diff --git a/wai/extensions/prior-carriage.md b/wai/extensions/prior-carriage.md index 100f5c0d7..a89f0d009 100644 --- a/wai/extensions/prior-carriage.md +++ b/wai/extensions/prior-carriage.md @@ -243,8 +243,11 @@ WAI's binding sits beside the standard's, never in place of it. signature. - **Stripping is not repackaging.** Removing signature units changes the bytes, so the result no longer matches its pin. It is a different artifact. -- **Keys.** WAI has no key-discovery mechanism of its own. A key location found - in-band is recorded, not trusted. +- **Keys.** WAI's key discovery ([jwp-receipts](jwp-receipts.md) §7) covers the + signers of WAI's own receipts and claims, and who may authorise a pinned + parameter set (jwp-receipts §7.9). A key location found in-band in a prior + artifact is outside it: it is recorded, not trusted, and never an anchor + (jwp-receipts §7.3). ## 7. Reference implementation @@ -266,12 +269,17 @@ WAI's binding sits beside the standard's, never in place of it. `pin::write_listing` and `pin::listing_of_dir` read and write a listing. `pin::READABLE_FORMS` holds `wai.prior.set`: the reference sink reads no coded form. +- `pin::verify_bytes_with` and `pin::verify_with` run SPEC §3.1's check 5 with a + `pin::PinAuthority`, before any held file is read. `key_log::SinkKeyLog` + (feature `key_discovery`) is the one in this crate: built from the key log a + sink holds, it runs jwp-receipts §7.9 over the pinned sets of the capabilities + the sink's relying party holds to it. Without one, check 5 is skipped. - `neural::ModelRegistry` holds, for each capability, its registered path and any further paths given to `hold`. For a form-less pin it hashes each held file — for `wai.neural.int_synth`, the `int_synth.json` beside it. For a `wai.prior.set` pin it takes every `*.sha256` file directly inside each held path's directory (the path itself if it is a directory) as a listing rooted - there. + there. `ModelRegistry::with_key_log` gives it a key log for check 5. - `container::Wai::dispatch_verified` applies SPEC §4 steps 3–6 and `container::WaiMulti::pick` SPEC §7.1 steps 2 and 5, each with a pin verifier the caller supplies. `neural::decode_envelope` and `neural::decode_multi` diff --git a/wai/extensions/transparency.md b/wai/extensions/transparency.md index 8563a9446..32ce7cdb4 100644 --- a/wai/extensions/transparency.md +++ b/wai/extensions/transparency.md @@ -61,6 +61,11 @@ forcing them together would put a registration round-trip in the per-object path receipt's link hash, and its body carries the Transparency Service's receipt verbatim with the profile it was registered against. Accepting that claim is not verifying the inclusion (§6). +- An origin that publishes a key log ([`jwp-receipts`](jwp-receipts.md) §7) + **SHOULD** register each revision as it registers group receipts. A fork in + the log is then visible to every Relying Party that checks the log, not only + to one that happened to hold both branches. The registration witnesses a + revision; it does not anchor the origin (§6). The checking party is the **Relying Party** in the terminology of the SCITT architecture; this extension uses that term rather than coining another. @@ -111,6 +116,8 @@ cannot check a proof without knowing the log's rules. witnesses that a receipt existed at a time; it does not attest that the receipt is true, and an implementation **MUST NOT** report registration as verification. -- It does **not** solve key discovery. That is [`jwp-receipts`](jwp-receipts.md) - §7, and a log entry naming a key the Relying Party cannot resolve is still - unresolvable. +- It does **not** solve key discovery. [`jwp-receipts`](jwp-receipts.md) §7 + does, for a signer that names its web origin and publishes its keys there. A + log entry naming a key the Relying Party cannot confirm under that section is + still unconfirmed, and registering a key-log revision witnesses it without + anchoring the origin. diff --git a/wai/extensions/unidirectional-delivery.md b/wai/extensions/unidirectional-delivery.md index 88cd7a626..e09b5da85 100644 --- a/wai/extensions/unidirectional-delivery.md +++ b/wai/extensions/unidirectional-delivery.md @@ -61,6 +61,11 @@ an object either has no such field or has one anyone may set. signed, never that they were entitled to. - Authority **MUST NOT** be inferred from position in the delivery chain. The entity that transmits is not thereby entitled to interrupt. +- A key-log anchor a sink fetched from an origin on first use + ([`jwp-receipts`](jwp-receipts.md) §7.3) is not a trust anchor here. When a + class is registered for pre-emption, its authority is confirmed only under a + pinned anchor, and only for a key the pinned revision itself lists with it + (jwp-receipts §7.4, §7.5). ## 4. Detached receipts diff --git a/wai/pin-conformance/README.md b/wai/pin-conformance/README.md index 068bac99d..8eb9a9b09 100644 --- a/wai/pin-conformance/README.md +++ b/wai/pin-conformance/README.md @@ -10,7 +10,10 @@ for every case: SPEC §3.1 names: `pin_malformed`, `form_not_registered`, `form_unreadable`, `form_required`, `prior_not_held`, `digest_superseded`, `set_malformed`, `part_mismatch`. A held artifact is a `file`, or a `listing` together with the - `root` its paths are read below. Paths are relative to this directory. + `root` its paths are read below. Paths are relative to this directory. The cases + model a sink that holds no key log, so check 5 (`prior_revoked`, + `prior_not_authorised`) does not run; it is covered by the parameter-set vectors of + [`../signer-conformance/`](../signer-conformance/) and the `key_log` library tests. - `selection.json` — given a `WAI2` manifest, its payloads, the capabilities the sink advertises and the one-file parameter sets it holds, the rendition SPEC §7.1 picks and the reason for each entry it skips (`not_selectable`, diff --git a/wai/research/2026-09-global-sweep.md b/wai/research/2026-09-global-sweep.md index 2f39bdf60..8c6a8daac 100644 --- a/wai/research/2026-09-global-sweep.md +++ b/wai/research/2026-09-global-sweep.md @@ -689,7 +689,7 @@ review. | G3 | **SyfAI's AI update-framework thread, opened at MPEG 155 jointly with WG 7 (S ⚠), covers parameter formats, manifests, repositories, integrity checking and bit exactness for NN decoders. The artifact a WAI `Prior` names has no normative wire form.** `Prior { id, sha256, determinism }` pins identity: `sha256` hashes the decoder artifact the sink runs. No wire format for that artifact is specified (2030 §6.4). | `wai-rs/src/container.rs` `struct Prior`; 2030 landscape §6.4; no SyfAI or Exploration 52 mention in the tree (grep) | **Addressed at link 05 (PR #106), with limits.** [`prior-carriage.md`](../extensions/prior-carriage.md) §2: a `Prior` MAY declare `form`; when it does, the digest covers the coded bitstream, and a sink that cannot read the form MUST refuse the prior. §3 registers `wai.prior.nnc` (ISO/IEC 15938-17:2024) as the one form, optional and not recommended, because its royalty status and cross-decoder exactness are not established. `Prior::is_cross_hardware_exact` requires a declared form to be established exact, none is, so a `replicate` resting on a declared form is refused (§5). The reference sink reads no form (`READABLE_PRIOR_FORMS` is empty), and `wai-rs` has no NNC reader or writer. SPEC's opening section now says a model's identity MAY be pinned by digest (a `Prior`). [`syfai-input.md`](syfai-input.md), committed with this record, maps WAI onto the update framework; its G1 records what SPEC still does not define | | G4 | **WAI's `HwShunt` takes in IOReport, a reading the monorepo classes five ways; and neither WAI nor the family's receipt ladder separates platform-relayed counters from independently rooted instruments. Only `sandbox-tee` does.** Three layers. **(a) The family's receipt ladder.** Every standard that lists interfaces under `HwShunt` puts on-die counters and dedicated shunts in it together (JCR-1 §2.3, JCP §5.1, Sandbox §6.3, `mesh-schema`, `proof-schema`), and IOReport is classed inconsistently (§5.3). **(b) `sandbox-tee`'s `CounterClass`** reserves `HwShunt` for a dedicated shunt and gives RAPL, NVML, IOReport and Redfish classes of their own; its `AttestTrust` ladder is derived from evidence, and `efficiency/1` commits to the attestation (§5.1). **(c) WAI** has `HwShunt` cover "RAPL, NVML, SMC, IOReport, or an external shunt", and `wai_video_meter` stamps IOReport readings `HwShunt`. That places WAI with Joule Code's text and the Sandbox meter, and against JCR-1 §2.3. WAI's texts present the ladder as the family's own without noting the split: `video_receipt.rs` says it is "reused verbatim from `mesh-schema` / `proof-schema`", which list no IOReport; `energy_binding.rs` calls it "the family's ladder"; `energy-measurement.md` §2 says WAI reuses "the same three names the receipt families elsewhere in the ecosystem already use". At every layer but (b), a verifier cannot tell a platform-relayed counter, which may be deliberately filtered (§5.1), from an independently rooted instrument. | §5.3 (the per-standard survey); `wai/extensions/energy-measurement.md` §2; `wai-rs/src/energy_binding.rs`:30, :47–51 (`Acquisition`); `wai-rs/src/video_receipt.rs` `Provenance` doc comment; `wai-rs/src/bin/wai_video_meter.rs`:184–190; `jouleclaw/jouleclaw-rs/crates/jouleclaw-jcr1/docs/JCR-1-spec.md` §2.3; `jcp/SPEC.md` §5.1; `sandbox/SPEC.md` §6.3–§6.4; `mesh/mesh-rs/crates/mesh-schema/src/lib.rs` and `proof/proof-rs/crates/proof-schema/src/lib.rs` (`Provenance`); `sandbox/sandbox-rs/crates/sandbox-tee/src/energy.rs` (`AttestTrust`, `CounterClass`, `is_independently_rooted`); `jcp/jcp-rs/crates/jcp-efficiency/MAPPING.md` §4 and `src/training.rs` (`energy_attestation`). No mention of `sandbox-tee`, `CounterClass`, `AttestTrust`, `energy_attestation`, `PhysicallyRooted` or `IntegrityAttested` under `wai/` (grep) | **WAI's side closed at link 08 (PR #109); the family's side open.** energy-measurement §2 separates `CalibratedInstrument` (an external instrument with a current calibration) from `OnChipCounter` (a counter the measured silicon keeps, or any instrument whose uncertainty is not traceable). Each carries a declared uncertainty (§2.3); the counter carries its filtering state (§2.4), the instrument a calibration reference (§2.5). `HwShunt` is legacy, accepted and never emitted (§2.6), and IOReport is `OnChipCounter` (§2.9). `energy_class.rs` is the one definition, and the reference meters seal `OnChipCounter`. The "reused verbatim" sentence is gone; `video_receipt.rs` and `energy_binding.rs` still call the ladder the family's, and §2 says the three-name vocabulary is extended, not replaced, with both refinements mapping to `HwShunt`. §2.9's list of differing documents is partial (§5.3). No WAI text references `sandbox-tee`'s ladder (grep). The family's IOReport split and whether its receipt-level ladder should separate the two kinds of hardware evidence are unassigned (below) | | G5 | **The codec registry has no AV2, VVC, HEVC, LCEVC, MPEG-H Audio, AVS3 or Audio Vivid capability.** Brazil mandates three of these (§3.3). Two are Chinese standards: AVS3 (a group standard, with its national standard pending) and Audio Vivid (GB/T 46271-2025). 2030 §6.1 (*Honest scope*) records that WAI registers no VSEI-family codec. **The tree's codec survey is a second surface to reconcile.** `wai/glossary/codecs.json`, which `wai/README.md` describes as the "Reference catalog of media codecs surveyed for capability registration", has entries for HEVC / H.265 and VVC / H.266 (both `niche`), LCEVC (`niche`) and ECM (`academic`, JVET's exploration model beyond VVC), and none for AV2, MPEG-H Audio, AVS3 or Audio Vivid. `glossary/SCHEMA.md` defines `success` as "carries non-trivial production traffic today" and `niche` as "alive in specific contexts". §4 reports HEVC in production use at 65% in a vendor survey (**S** ⚠), §3.3 reports VVC as a national broadcast system's main codec (**R** ⚠), and the HEVC entry's own description calls it universal in broadcast UHD. The grades are descriptive and the contrary evidence is unverified, so this is a reconciliation item, not a correction. | capability strings in `wai-rs/src/codecs/*.rs`: none for these codecs. Other mentions, from a grep over `*.rs`, `*.md`, `*.json`, `*.toml`, `*.yml`, `*.yaml`, `*.html`, `*.ts` and `*.js` under `wai/`: the `glossary/codecs.json` entries above; the H.264/H.265 royalty sentence in `SPEC.md` and `CHANGELOG.md`; the BD-rate note that names VVC in `wai-rs/src/codecs/mod.rs` and `wai-rs/src/neural/mlicpp.rs`; `HEVC` as an example name in `glossary/SCHEMA.md`; the 2030 landscape. Royalty class per codec **not established by this sweep**: only AV2's royalty-free patent policy was seen at the issuing body (the AOMedia press release, §2.4) | **Closed in the registry at link 04 (PR #105); the glossary is not reconciled.** SPEC §5 *Broadcast and streaming codecs* registers `wai.video.{av2,vvc,hevc,avc,vp9,evc,evc.baseline,avs3,lcevc}` and `wai.audio.{mpegh,avs3,aac}`, each with one payload framing and a licence class whose basis is recorded in [`media-codec-registry.md`](media-codec-registry.md). `wai.audio.avs3` names AVS3 audio / Audio Vivid (GB/T 46271-2025). All are sink-supplied, none is a default, and `wai.video.lcevc` is companion-only. SPEC §7 classes AVC, HEVC, VVC and VP9 `StandardDefined` on the conformance clauses read, AV2 `StandardTolerance`, and EVC, AVS3, LCEVC, MPEG-H 3D Audio and AAC `StandardUnclassified`. The licence classes rest on the registry's recorded bases, not on this sweep. `glossary/codecs.json` is unchanged, so its grades still stand against §3.3 and §4 | -| G6 | **No WAI receipt says who can check the energy figure, and the base JWP receipt does not carry even the acquisition class that `energy-measurement.md` requires.** The precedents in §5.1 differ exactly on who can check: instrument-signed (OCMF), peer-reviewed but unsigned, log-registered (SCITT), TEE-attested with a derived trust level (the family's `sandbox-tee` `AttestTrust`), or producer-asserted. **The base JWP receipt** has no acquisition-class field; its group signature covers `total_joules_micro` and no class, and the per-object `joules_micro` is in neither the Merkle leaf nor the signature. `energy-measurement.md` §2 requires a class inside the signature for any non-zero figure, while its header says it adds "no new wire format". **The video receipt** signs its class. **The energy binding** carries its class in a C2PA companion assertion signed as part of whatever manifest carries it. **The OER receipt** carries an evidential grade and a ledger identity, and no class. 2030 §8 item 4 queues typed receipt classes. | `extensions/energy-measurement.md` header, §1, §2 and §5; `extensions/jwp-receipts.md` §2.1–§2.4; `joule-code/joule-code-rs/crates/joule-moq/src/receipt.rs` (`ObjectReceipt`, `GroupReceipt::signing_payload`: no class); `wai-rs/src/energy_binding.rs`, `wai-rs/src/video_receipt.rs`; `extensions/oer-receipts.md` §2–§3, `wai-rs/src/oer.rs`; no reference to the family ladder under `wai/` (grep, as in G4) | **Partly closed at link 10 (PR #112); the rest open.** Every receipt within energy-measurement's scope that `wai-rs` and `wai-quantum` seal can carry the class (link 08, PR #109). jwp-receipts §2.4.3–§2.4.7 add a `claim` kind with typed classes. An `energy` claim (§2.4.5) restates a JWP object's figure beside its class, the class's evidence, a coverage statement and a grade; a verifier holding the object receipt rejects the pair if the figures differ, and one holding the OER receipt the claim names checks that grade and figure agree. `claim_receipt.rs` (feature `receipt_claims`) implements it, with vectors in `claim-conformance/`. Open, as energy-measurement §1 records: the JWP object receipt has no member of its own for the class; the group total has none, and nothing carries one for it; the `WAI_RECEIPT` property's class and evidence sit outside any signature; an OER/2 receipt has no member for the class. On who can check: a claim carries its signer's authority and no more (§2.4.4); key discovery and an authority class for who may sign which class are not done (CHANGELOG, v0.4.0 entry for typed receipt classes); no WAI receipt references `sandbox-tee`'s ladder or `efficiency/1`'s attestation (grep) *Status after the v0.4.0 change "Energy reports, labelled object leaves, and what the signatures cover":* the object's class rides in its labelled leaf, a group total can carry a signed class of its own, and a `WAI_RECEIPT` property's class counts only where a signed statement restates it; joule-moq verifies both labelled forms. Who can check remains open. | +| G6 | **No WAI receipt says who can check the energy figure, and the base JWP receipt does not carry even the acquisition class that `energy-measurement.md` requires.** The precedents in §5.1 differ exactly on who can check: instrument-signed (OCMF), peer-reviewed but unsigned, log-registered (SCITT), TEE-attested with a derived trust level (the family's `sandbox-tee` `AttestTrust`), or producer-asserted. **The base JWP receipt** has no acquisition-class field; its group signature covers `total_joules_micro` and no class, and the per-object `joules_micro` is in neither the Merkle leaf nor the signature. `energy-measurement.md` §2 requires a class inside the signature for any non-zero figure, while its header says it adds "no new wire format". **The video receipt** signs its class. **The energy binding** carries its class in a C2PA companion assertion signed as part of whatever manifest carries it. **The OER receipt** carries an evidential grade and a ledger identity, and no class. 2030 §8 item 4 queues typed receipt classes. | `extensions/energy-measurement.md` header, §1, §2 and §5; `extensions/jwp-receipts.md` §2.1–§2.4; `joule-code/joule-code-rs/crates/joule-moq/src/receipt.rs` (`ObjectReceipt`, `GroupReceipt::signing_payload`: no class); `wai-rs/src/energy_binding.rs`, `wai-rs/src/video_receipt.rs`; `extensions/oer-receipts.md` §2–§3, `wai-rs/src/oer.rs`; no reference to the family ladder under `wai/` (grep, as in G4) | **Partly closed at link 10 (PR #112); the rest open.** Every receipt within energy-measurement's scope that `wai-rs` and `wai-quantum` seal can carry the class (link 08, PR #109). jwp-receipts §2.4.3–§2.4.7 add a `claim` kind with typed classes. An `energy` claim (§2.4.5) restates a JWP object's figure beside its class, the class's evidence, a coverage statement and a grade; a verifier holding the object receipt rejects the pair if the figures differ, and one holding the OER receipt the claim names checks that grade and figure agree. `claim_receipt.rs` (feature `receipt_claims`) implements it, with vectors in `claim-conformance/`. Open, as energy-measurement §1 records: the JWP object receipt has no member of its own for the class; the group total has none, and nothing carries one for it; the `WAI_RECEIPT` property's class and evidence sit outside any signature; an OER/2 receipt has no member for the class. On who can check: a claim carries its signer's authority and no more (§2.4.4); key discovery and an authority class for who may sign which class are not done (CHANGELOG, v0.4.0 entry for typed receipt classes); no WAI receipt references `sandbox-tee`'s ladder or `efficiency/1`'s attestation (grep). *Status after the v0.4.0 change "Energy reports, labelled object leaves, and what the signatures cover":* the object's class rides in its labelled leaf, a group total can carry a signed class of its own, and a `WAI_RECEIPT` property's class counts only where a signed statement restates it; joule-moq verifies both labelled forms. *Status after the v0.4.0 change "Key discovery, signing authority and module revocation":* key discovery and an authority class for who may sign which class are jwp-receipts §7 (`wai-rs` `key_log`, feature `key_discovery`; `signer-conformance/`); a hardware-class energy authority is confirmed only under a pinned anchor, and a verifier reports whether the key also holds `delivery` | | G7 | **Explicit parameter transmission appears in `determinism-tiers.md` only as a generic route. Its first published cross-NPU instance (arXiv 2606.28027) and its cost are not recorded.** Also unrecorded: the mixed-precision entropy-synchronisation line (arXiv 2608.00483: FP16/FP32 across GPUs, no bit-exactness claim); the quantised learned-coding line (arXiv 2007.04684, 2111.09348); and IEEE 1857.11's use of *reproducibility cross-checks* rather than bit-exact decode (**R** ⚠), a reproducibility notion the tier vocabulary does not name. Both 2606.28027 and 2608.00483 target a synchronised entropy stage, so at most `entropy-consistency`, and 2608.00483 only over the GPUs it tested. On `main`, whether such a decode may back `replicate` depends on which of G10's conditions is read. | `extensions/determinism-tiers.md` §1–§3; `SPEC.md` §7 (read) | **Partly closed at link 02 (PR #103).** determinism-tiers §1 names arXiv 2606.28027 as the explicit-parameter instance and says the side information costs bitrate (no magnitude), and names a quantised decoder, arXiv 2312.11209, as the route to a reproducible reconstruction. Such a decode reaches `entropy-consistency` and does not back `replicate` (G10). Not in the tree at link 10 (grep): arXiv 2608.00483, the quantised line 2007.04684 / 2111.09348, and IEEE 1857.11's reproducibility cross-checks; the last is described in [`syfai-input.md`](syfai-input.md) §5, committed with this record | | G8 | **Standards inputs not tracked in the tree:** SyfAI; 3GPP TR 26.942; DVB S100 and CM-EE; the beyond-VVC CfP; Gaussian Splat Coding Exploration 45; IEEE 1857.11 / P1857.11.1; UWA 042; India D2M; the JPEG AI core experiments on energy consumption and bit-exact reconstruction. | a case-insensitive, word-bounded `git grep -P` over tracked `*.md`, `*.rs`, `*.json`, `*.toml`, `*.yml`, `*.yaml`, `*.html`, `*.ts` and `*.js` files finds none of these designations outside this record. The one other hit for `uwa` is the Hausa word in `aac/aac-rs/crates/aac-core/src/translations_data.json` (`"ha":"uwa"`), which is not the organisation. MPEG's splat work *is* tracked, though not Exploration 45 by name: `media-codec-registry.md` and `codecs/mod.rs` register `wai.splat.sog` against "MPEG WG4" work in progress, `3d-neural-coding-landscape.md` records splat coding under standardisation, and 2030 §6.4 records an NNC amendment covering Gaussian splats (G11) | **Partly closed by the two notes committed with this record.** [`syfai-input.md`](syfai-input.md) tracks SyfAI, IEEE 1857.11 and UWA 042; [`energy-exposure-mapping.md`](energy-exposure-mapping.md) tracks TR 26.942, DVB S100 and CM-EE. The same grep over the tree at link 10 finds the beyond-VVC CfP, Exploration 45, India D2M and the JPEG AI core experiments in no document but this record. Those four stay open | | G9 | **WAI's texts and code class `wai.image.jpegai` in ways that contradict one another, and SPEC contradicts itself on it.** SPEC §5, the capability's registration (`SPEC.md`:197), describes the decode as "integer entropy front end (mANS) + integer dequant/value path + fixed-order synthesis and post-filters" and declares "**decode-equivalence**: one pinned reconstruction … reproducible on every machine (a portable decoded-pixel hash)". `reconstruction-binding.md` §1 (:30–31) says the decode to RGB is "byte-identical on every machine", and §2 (:72) sets the binding's criterion at `decode-equivalence`; `determinism-tiers.md` §2–§3 list the capability there. SPEC §7 lets a neural capability back `replicate` only "with no float operation anywhere" (:445–448). `capability_replicate_class` returns `StandardDefined` for it through the default arm, and SPEC §7 defines that class as "reconstruction is fixed by the named codec/standard … the neural float hazard does not apply"; neither half holds for JPEG AI (§2.2; its decode runs the model). The strict `replicate` check reads only the envelope's prior (`neural/mod.rs`:282–292). **Loose ends:** (i) `jpegai_synthesis.rs` and `jpegai_mans.rs` say they back `wai.neural.jpeg_ai`, a string registered nowhere; (ii) the decode has f32 steps between its integer stages — its dequantisation among them, although SPEC §5 calls it integer; (iii) the byte-for-byte claim is made normatively, in SPEC §5 and `reconstruction-binding.md` §1, and rests on an `exp()` from the platform maths library and on no measurement on a second platform; (iv) the reference sink's dispatch (`neural/mod.rs`:205–216) uses the exact decoder only when the integer weight bundle is present, and otherwise falls back to a float ONNX Runtime decode. | `SPEC.md` §5 (:197) and §7 (:445–448, the `StandardDefined` bullet at :476–478, :492–495); `extensions/reconstruction-binding.md` §1 (:21–35) and §2 (:72); `extensions/determinism-tiers.md` §2–§3; `wai-rs/src/codecs/mod.rs` (the default arm and the test asserting `StandardDefined`); `wai-rs/src/jpegai_decode.rs`, `jpegai_synthesis.rs`, `jpegai_mans.rs`, `jpegai_pipeline.rs`; `wai-rs/src/neural/mod.rs`:205–216, :282–292; `git grep -n decode-equivalence` over `wai/` | **Closed at link 02 (PR #103), with the maths library removed at link 03 (PR #104).** SPEC §5's row registers `entropy-consistency` (registry class `NeuralEntropyExact`), describes dequantisation, latent refinement, post-filters and colour conversion as IEEE-754 float with a fixed evaluation order, and says "**Downgraded in v0.4.0:** this row previously claimed decode-equivalence". `reconstruction-binding.md` §1–§2 make the pinned reconstruction a property of the exact decoder, reproducible under that decoder's build and floating-point conditions. SPEC §7 states the new class and the reason. Loose ends: (i) fixed — the module docs name `wai.image.jpegai`, and `wai.neural.jpeg_ai` appears nowhere else in the tree (grep); (ii) the float steps remain and are now stated as float; (iii) link 03 builds `exp` and rounding from IEEE-754 basic operations (`float_repro`), guards the decode path with clippy's `disallowed_methods` and a linked-symbol check (`tests/jpegai_decode_symbols.rs`), and pins nine hashes for one codestream; determinism-tiers §6 records them identical on macOS arm64, wasm32-wasip2, Linux arm64 and Linux x86_64 under user-mode emulation, and not measured on Windows, macOS x86_64 or x86_64 hardware; (iv) the float fallback remains, and SPEC §5, determinism-tiers §5 and reconstruction-binding §1 now state it, and that the result does not record which path ran. Still true: a `norm.json` with a missing or mistyped key panics in the loaders instead of falling back, as the `try_jpegai_exact` doc comment says | @@ -736,8 +736,10 @@ review. total can carry its own class under a labelled group payload that also signs the parent link (§2.3, §2.4.1); a `WAI_RECEIPT` property's class reads as signed only where it equals the leaf's label or an `energy` claim - (moq-streaming-format §5). Open: key discovery for claim signers, an OER/2 - receipt's class, and who, other than the signer, stands behind a figure. + (moq-streaming-format §5). + *Status after the v0.4.0 change "Key discovery, signing authority and module + revocation":* key discovery for claim signers is jwp-receipts §7. Open: an + OER/2 receipt's class, and who, other than the signer, stands behind a figure. - **G7's other lines** (arXiv 2608.00483, the quantised learned-coding line, IEEE 1857.11's reproducibility cross-checks) and **G8's other inputs** (the beyond-VVC CfP, Exploration 45, India D2M, the JPEG AI core experiments) are @@ -775,6 +777,7 @@ when that document is next revised. | §4.4 | `draft-ietf-moq-transport` rev 18, approaching WG Last Call | now `-21` (2026-09-08), state WG Document; transport and C4M to IESG targeted Dec 2026 | P✓ | | §5.1 | `draft-ietf-avtcore-rtcp-green-metadata-16`, IESG Evaluation | now `-17` (2026-09-09); IESG state **RFC Ed Queue** | P✓ | | §5.1 | 23001-11 4th ed. at CD (N01742) | dated on the part page: WD 2026-03-02 (MPEG 153), CD text 2026-09-21 (MPEG 155). The CD text is WG 3 N1864 ([`energy-exposure-mapping.md`](energy-exposure-mapping.md) §1) | P✓ / tree | +| §4.1 (grid-carbon paragraph) | "`jwp-receipts.md` §7 states that conversion of joules to currency, carbon or grid mix is performed by downstream aggregators" | That statement is jwp-receipts §6.6, *Out of scope* ("Pricing semantics for joules"). When the landscape was written jwp-receipts had no §7 in the tree; its §7 is now *Key discovery and signing authority*, so the reference names the wrong section | tree | | §9 item 1 | three legs (23001-11 4th ed., Part 6 WD, a GREEN WG document); "check at each MPEG meeting and at each GREEN WG revision" | GREEN leg checked and not triggered; Part 6 leg not applicable yet; 23001-11 leg not triggered at CD on energy-exposure-mapping §4's search (§6.1) | P✓ / tree | --- diff --git a/wai/research/energy-exposure-mapping.md b/wai/research/energy-exposure-mapping.md index 3fa57ffa3..cf204ce89 100644 --- a/wai/research/energy-exposure-mapping.md +++ b/wai/research/energy-exposure-mapping.md @@ -422,6 +422,13 @@ claim checkable, not true (§2.5 there). Still open: a rule on which key signs the publisher's, or one held by the metering component — and a mapping onto GREEN's accuracy identities. +*Status after the v0.4.0 change "Key discovery, signing authority and module +revocation":* jwp-receipts §7.5 recommends (SHOULD) that a hardware-class figure be +signed by a key held by the component that signs the reading and distinct from the +delivery key, has a verifier report whether the key also holds `delivery`, and +confirms a hardware-class energy authority only under a pinned anchor. The mapping +onto GREEN's accuracy identities is still open. + **L3: Declared tolerances.** energy-measurement §2.1 requires an implementation to state how far its class lets the figure be leaned on. On `main` no tolerance figure appears in the extensions or in `energy_binding.rs` / `video_receipt.rs`. @@ -454,6 +461,14 @@ its sections end at §6.8. OCMF leaves this out of band too: public keys reach t verifier "by means other than this protocol (out-of-band) … e.g. via a central register". WAI names no register or other mechanism. +*Status after the v0.4.0 change "Key discovery, signing authority and module +revocation":* jwp-receipts §7 now exists, and transparency §6 points to it. A +signer names its web origin in `signer_id`; the origin publishes a signed, chained +key log at `/.well-known/wai-keys`; a verifier anchors the origin by pinning or by +one recorded fetch and confirms offline. The anchor reaches the verifier out of +band or on first use, and the hardware-class energy authorities are confirmed only +under a pinned one. + **L8: Not re-measurable, by construction.** A third party reproduces the output and the work unit, never the joules (energy-measurement §5). The same holds for every field that describes the measurement: the acquisition class and its diff --git a/wai/signer-conformance/.gitattributes b/wai/signer-conformance/.gitattributes new file mode 100644 index 000000000..165556bc9 --- /dev/null +++ b/wai/signer-conformance/.gitattributes @@ -0,0 +1,3 @@ +# The pinned corpus is compared byte-for-byte with what the generator writes (LF), on +# every platform of the CI matrix. Keep a CRLF checkout from turning that into a failure. +* text eol=lf diff --git a/wai/signer-conformance/README.md b/wai/signer-conformance/README.md new file mode 100644 index 000000000..692784132 --- /dev/null +++ b/wai/signer-conformance/README.md @@ -0,0 +1,79 @@ +# signer-conformance + +Conformance for [key discovery, signing authority and module authorisation](../extensions/jwp-receipts.md) +(§7): the forms of a `signer_id`, an origin's key log, anchoring, the confirmation a +verifier reports beside a receipt's or claim's verdict, and the checks a sink runs +before it runs a decoder or reconstruct module. + +Each vector declares **one verdict**, set from the text in the generator and never +copied from a verifier. A verifier that reaches another verdict — another level, step, +reason, head or anchor path — does not conform. + +## Files + +| file | what it is | +|---|---| +| `vectors.json` | The corpus. Regenerate with `wai_signer_vectors gen .`; `wai_signer_vectors verify .` checks the canonical fixture, a byte-identical regeneration and every verdict. | +| `signer_verify.py` | A second verifier, written from §7, that shares no code with `wai-rs`. It takes from `../claim-conformance/claim_verify.py` only what §7 reuses of §3.1: the JSON reading, JCS, BLAKE3, the strict Ed25519 rule, the RFC 3339 syntax and claim verification. | + +## Procedures + +**`confirm`** (§7.4) runs on an item and reports `confirmed` with the origin, key-id, +head `revision_hash` and anchor path — and, where they apply, `moved_to`, +`retired_until`, `untimed=true`, `expected=` and `also_delivers=` — or `unconfirmed` or +`refused` at a §7.4 step (`1`–`7`, or `6a`) with a reason of §7.7. The item is either +`receipt`, a claim's wire form, which is first verified under §3.1 (a claim that fails +is `reject step= reason=