Outside the integer path, a sink whose key log only revokes ran check 5 over the registered file's digest and any listing found beside it, then decoded from the path. With no listing beside them, a revoked several- file set decoded, and a file changed after the check was what the decode read. At a registry with a key log, the neural sink now reads once what the decode can read: the registered file of a one-file set; otherwise every file below the directory the decoder reads from (the registered directory, or the registered file's), as pin::listing_of_dir lists it (pin::read_dir_set, which listing_of_dir now uses). Check 5 runs over every digest a pin over it could name: the file's, or the directory's derived listing and each member's, and each listing beside them that matches what was read. The decode then reads only those bytes (neural::ParamSource::Checked, which replaces Authorised and records the registered file within a set), never the path; where the key log holds the capability's sets to jwp-receipts §7.9, only the part §7.9 authorised. The decoders outside the integer path read files by name from that directory, some chosen by the payload, so the set is the directory rather than a fixed list. With no key log the decode reads the path, as before. SPEC §4 (Module authorisation) and jwp-receipts §7.9 state the reference behaviour. Test: neural_native_decode::a_revoke_only_key_log_checks_and_reads_the_set_once (a revoked set beside its listing; with no listing, the decoder and the directory registered; a member's own digest revoked; the tables swapped on disk after the check; no key log). Co-Authored-By: Claude Opus 5.5 <[email protected]>
151 KiB
WAI Extension: JWP Receipt Chain
Status: Draft. Sub-spec of WAI v1.0. Canonical text:
wai/extensions/jwp-receipts.mdin 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 §7signer_idandkey_log(featurekey_discovery).
Keywords MUST, MUST NOT, SHOULD, MAY, REQUIRED, OPTIONAL are RFC 2119 / RFC 8174.
1. Scope and model
This extension defines a JWP-shaped receipt chain that attests per-object delivery of WAI envelope objects through a chain of relays. A receipt names the bytes a sink received, the publisher that produced them, the energy spent producing them, and the chain of relays the bytes traversed — all under a single Ed25519 signature per group.
In scope:
- A per-object content commitment (BLAKE3-256) over WAI envelope bytes.
- A per-group Merkle root binding every object in a group under one signature.
- A per-group Ed25519 signature over canonical group metadata.
- A typed receipt envelope (JSON or equivalent CBOR) with explicit parent-receipt linkage for cross-hop preservation.
- Verifier behaviour, relay preservation requirements, and conformance test vectors.
- Typed receipt classes (§2.4): every receipt belongs to exactly one
class.
groupandobjectreceipts are the delivery class; aclaimreceipt (§2.4.3) carries one of the others —energy,lawful-basis,log-inclusion,measurement,operating-point(§2.4.5–§2.4.9) — on one envelope, one canonical form and one signature rule, so a statement other than delivery does not need a field set of its own. - An optional acquisition-class label on the object receipt, bound with the object's figure and origin into its Merkle leaf (§2.2, §2.4.2); an optional class for the group total, signed under a labelled group payload with its coverage and the parent link (§2.3, §2.4.1); and the group total read from its objects (§3).
- A second binding class (§2.6): a soft binding, declared by the publisher with a registry-named algorithm and a resolution endpoint, 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_idthat 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:
- A transport. This extension does not define how receipts move on the wire; receipts ride alongside WAI envelopes in the carrier protocol (e.g. a MoQT object header field, an HTTP trailer, a sidecar file).
- A codec. The receipt chain is opaque to the WAI capability dispatch in WAI SPEC.md §4. A receipt attests bytes; it does not interpret them.
- An envelope. WAI SPEC.md §2 defines the envelope; this extension
sits above it. A receipt's content hash is computed over the
full WAI envelope (
WAI1magic + manifest + payload), not over the payload alone. - 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.
Relationship to WAI SPEC.md:
- WAI SPEC.md §2 (container) — receipts hash the container bytes end-to-end; a sink that mutates the envelope (re-orders manifest keys, re-frames the payload) breaks the content hash and MUST be rejected by §3 step 4 below.
- WAI SPEC.md §4 (capability dispatch) — receipts attest what was delivered to the dispatcher, not what the dispatcher produced. A sink that successfully verifies a receipt then dispatches the envelope per §4 unchanged.
2. Wire format
2.1. Per-object content hash
For each WAI envelope object the publisher emits, the publisher MUST compute a 32-byte BLAKE3-256 content hash:
content_hash = BLAKE3(domain || envelope_bytes)
domain = "moq-jwp:object\x01"
envelope_bytes is the full WAI envelope per WAI SPEC.md §2 (the
WAI1 magic, the manifest-length prefix, the manifest, the payload-
length prefix, the payload — concatenated, with no insertion or
reframing). The domain string is appended as a domain separator to
prevent cross-protocol hash collisions.
2.2. Per-group Merkle root
A group is an ordered list of one or more objects under a single publisher key. The publisher MUST build a BLAKE3 binary Merkle tree over the object content hashes in their group order. Leaves and internal nodes use distinct domain separators:
leaf(content_hash) = BLAKE3("moq-jwp:merkle-leaf\x01" || content_hash)
leaf(content_hash, joules_micro, class, origin)
= BLAKE3("moq-jwp:merkle-leaf\x02" || content_hash
|| u64_be(joules_micro) || class
|| u32_be(len(origin)) || origin_utf8)
node(left, right) = BLAKE3("moq-jwp:merkle-node\x01" || left || right)
An object's leaf takes the first form when its receipt carries no class
label (§2.4.2) and the second when it does. class is the label's signed
encoding: the tag byte and the evidence that follows it
(energy-measurement §2.7), tag 0 marking an
object explicitly unmetered. The encoding is self-delimiting and origin is
length-prefixed, so no two inputs share a leaf. The version byte keeps the two
forms disjoint, so a label, or the origin, can be neither added, stripped nor
altered on a delivered object without changing the root the group signature
covers. A group may mix the forms. A verifier that predates the labelled form
computes the first form for every object: it refuses a labelled object at §3
step 5 and verifies an unlabelled one as before.
When the leaf count at any level is odd, the publisher MUST duplicate
the final leaf to obtain an even pair. The root is the single hash at
the top of the tree. The empty-group root is the 32-byte all-zero
value 0x00..00.
2.3. Per-group Ed25519 signature
The publisher MUST sign the canonical signing payload with an Ed25519 key per RFC 8032 (Pure Ed25519, no prehash). The signing payload is the big-endian concatenation:
signing_payload =
"moq-jwp:group-sign\x01" // domain
|| u64_be(track_alias) // optional carrier-scoped id (0 if unused)
|| u64_be(group_id) // monotonic per-track group counter
|| merkle_root[32] // §2.2
|| u64_be(leaf_count) // number of objects in the group
|| u64_be(total_joules_micro) // sum of microjoules across the group
|| u32_be(len(group_start)) || group_start_utf8 // RFC 3339 start timestamp
|| u32_be(len(group_end)) || group_end_utf8 // RFC 3339 end timestamp
|| publisher_key[32] // Ed25519 verifying key (RFC 8032)
|| u32_be(len(publisher_id)) || publisher_id_utf8 // bound identity string
The signature is exactly 64 bytes. Implementations MUST reject any
group receipt whose signature length is not 64.
A group whose total carries a class of its own (§2.4.1) is signed over the
labelled payload instead: the fields above, in that order, under the
domain "moq-jwp:group-sign\x02", followed by
|| class // energy-measurement §2.7: tag, evidence
|| u8(coverage) // 0 Whole, 1 Partial
|| u8(has_parent) || parent[32] // 1 and parent_receipt_hash, or 0 and 32 zero bytes
class is tag 0 exactly when total_joules_micro is zero, and then
coverage is 0. The labelled payload signs parent_receipt_hash, which the
legacy payload does not (§6.1). The two domains keep the payloads disjoint. A
verifier or relay that predates the labelled payload computes the legacy one
and refuses a labelled group at §3 step 2; every group signed over the legacy
payload verifies as before.
2.4. Receipt envelope (typed)
Three receipt kinds are defined: group (one per group), object
(one per object) and claim (§2.4.3). Every receipt belongs to
exactly one class (§2.4.4). group and object are the
delivery class; for them the class is implied by the kind and is
not carried, so their wire form is exactly as below. A claim names
its class in a class member. All are typed JSON; equivalent CBOR is
permitted when the carrier mandates it, provided the field set and
semantics are identical (for a claim, see §2.4.3 on which bytes are
signed).
2.4.1. kind: "group"
Required fields:
| field | type | meaning |
|---|---|---|
kind |
string | MUST equal "group". |
track_alias |
u64 | Carrier-scoped track identifier; 0 if unused. |
group_id |
u64 | Monotonic group counter under publisher_key. |
root_hash |
32 B hex | §2.2 Merkle root over object content hashes. |
leaf_count |
u64 | Number of objects covered by root_hash. |
joules_total |
u64 | Sum of microjoules across the group's objects; 0 is the unmetered marker (energy-measurement §2). Its class, where it carries one, is in the labelled members below; a verifier holding every object checks it, or, for a group without one, reads the total from the objects' labels (§3, The group total). |
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). 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. |
The total's class (labelled form). A group whose total carries a class of its own adds these members and is signed over the labelled payload (§2.3):
| field | type | meaning |
|---|---|---|
sign_version |
integer | 2. Absent (or null), the group is in the legacy form; any other value is refused (group_label_invalid). |
energy_provenance, energy_uncertainty, energy_counter_filtering, energy_calibration_ref |
as energy-measurement §2.7 | The total's class and its evidence; all absent for the unmetered marker beside a zero total. |
energy_coverage |
string | Whole or Partial; absent with the unmetered marker. |
A legacy group carrying any of these members, a sign_version other than 2,
or a labelled group whose class cannot be read, is refused
(group_label_invalid), never read as the legacy form. The class MUST be the one the group's objects compose to, by this
rule over the objects whose figure F_i is not zero, in integers:
- if any such object is unlabelled, the group MUST NOT carry a class (it is signed in the legacy form);
- if there is none, the class is the unmetered marker and the coverage
Whole; - otherwise the class is the weakest of the objects' classes, in
energy-measurement §2's order, and the coverage is
Partialexactly when some object's figure is zero; - for a hardware class,
relative_ppmis at least⌈Σ F_i·p_i / Σ F_i⌉andwindow_usis the longestwindow_us_i. The bound is linear in the parts, so it assumes their errors fully correlated, and it is conservative; - an
OnChipCountertotal's filtering state is the objects' common one, orUnknown; aCalibratedInstrumenttotal needs one calibration reference shared by every object, or the group carries no class.
The resolution floor then holds for the total. With the group alone a verifier
checks that the class reads, is not HwShunt, is the unmetered marker exactly
beside a zero total, and reaches the resolution floor (§3 step 2); with every
object it checks the composition (§3, The group total).
2.4.2. kind: "object"
Required fields:
| field | type | meaning |
|---|---|---|
kind |
string | MUST equal "object". |
track_alias |
u64 | Mirrors the parent group receipt. |
group_id |
u64 | Mirrors the parent group receipt. |
object_id |
u64 | Monotonic from 0 within the group. |
content_hash |
32 B hex | §2.1 BLAKE3 over the WAI envelope bytes. |
joules_micro |
u64 | Microjoules consumed producing this object; 0 is the unmetered marker. This field fixes the unit and the scope of attribution, not how the number is obtained — see energy-measurement, which requires an acquisition class beside it and forbids publishing a figure from a measurement that failed. Its class rides as an optional label, below, bound into the object's leaf with the figure and the origin (§2.2). |
origin |
string | Datacenter / origin label. |
merkle_proof |
object | Inclusion proof: { "siblings": [32 B hex, …], "direction_bits": u64, "leaf_index": u64 }. direction_bits bit i is 1 iff the current node at level i is the right child. |
Optional class label. An object receipt MAY carry its figure's
acquisition class as an optional label
(energy-measurement §2.8): "leaf_version": 2, and
the class in energy-measurement §2.7's JSON fields — energy_provenance, and
for a refined hardware class energy_uncertainty with
energy_counter_filtering or energy_calibration_ref. With
"leaf_version": 2 and no class the object is explicitly unmetered (tag 0),
which is valid only beside a zero figure. Absent, the receipt is exactly as
above and its leaf takes the legacy form of §2.2. Present, its leaf takes the
labelled form, and the group signature covers the figure, the class, the
class's evidence and the origin through the root. A label MUST NOT be the
legacy HwShunt and MUST NOT sit beside a zero figure; a refined class
MUST carry its evidence and reach the resolution floor (energy-measurement
§2.3); class members without "leaf_version": 2, evidence without a class, or
a class that cannot be read, are refused rather than dropped
(energy-measurement §2.7). A verifier refuses an object that breaks any of
these at §3 step 5 (object_label_invalid). An energy claim (§2.4.5) remains
the way for a party other than the publisher to sign a class for the figure.
2.4.3. kind: "claim"
group and object answer one question — what was delivered, by
whom, at what energy cost. Other statements are made about the same
objects: how an energy figure was acquired and how much weight it
bears, on what basis what a sink observes may be processed, that a
receipt was entered in a log, that an object carries a binding that
survives re-encoding. None is a delivery statement, and several are
signed by someone other than the publisher. A claim receipt carries
one such statement — a class-scoped body under a class
discriminator — on one envelope.
Required members. A claim has no optional members: a nullable member
is present and null, so one claim has one canonical form.
| field | type | meaning |
|---|---|---|
kind |
string | MUST equal "claim". |
class |
string | A class of §2.4.4 other than delivery. |
subject_type |
string | "object" or "receipt"; which a class admits is fixed by §2.4.4. |
subject |
32 B hex | For object: the §2.1 content hash of the object the claim is about. For receipt: the link hash of a receipt — §3 step 3's for a group receipt, link_hash below for a claim — or, for a class that names a figure profile (§2.4.8–§2.4.9), the identity that profile defines (energy-measurement §6.1). |
seq |
integer | Strictly increasing across the claims of one signer_pubkey. |
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, 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. |
Encodings. Byte strings are lowercase hex — one byte string, one
signed encoding. Integers are unsigned and at most 2^53 − 1, written in
plain decimal: a number written with a sign, a fraction or an exponent
— -0, 1.0, 1e0 — is not an integer here, whatever its value. A
claim carries no other number, and no boolean. A term is a compact
IRI (dpv:Consent) or an IRI: a non-empty string of at most 2048 bytes
in UTF-8 holding no character with the Unicode White_Space property and
no control character (general category Cc). An absolute URI is a
term with a scheme (RFC 3986 §3.1), a colon, and a non-empty
remainder. A date-time is RFC 3339 §5.6's date-time with ASCII
digits, T and Z in either case: a month of 1–12 and a day its month
has in its year (§5.7, in the proleptic Gregorian calendar of RFC 3339
Appendix C — 2026-02-29 is not a date, 2028-02-29 is); an hour of at
most 23, a minute of at most 59 and a second of at most 60; any number
of fractional-second digits, at least one if the point is written; and
an offset of Z or ±hh:mm with hh at most 23 and mm at most 59.
A second of 60 is accepted at any time of day: whether a leap second
was inserted then is a fact about the calendar, not the syntax, and
issued_at is self-asserted.
Canonical form, signature, identity. A claim's canonical form is
its JSON Canonicalization Scheme serialisation (JCS, RFC 8785). On the
values a claim may carry — strings, unsigned integers no larger than
2^53 − 1, null, arrays and objects — JCS reduces to: member names
sorted by their UTF-16 code units; no insignificant whitespace;
strings escaped with \" \\ \b \f \n \r \t and \u00xx
(lowercase hex) for the remaining characters below U+0020, every other
character written as itself in UTF-8; integers in plain decimal. No
number formatting is involved.
sig = Ed25519(signer_key, "moq-jwp:claim\x01" || JCS(claim without "sig"))
link_hash = BLAKE3("moq-jwp:claim-link\x01" || JCS(claim))
A verifier canonicalises before it verifies: whitespace and member order on the wire are not signed. Duplicate member names are rejected, since RFC 8785 is defined over I-JSON (RFC 7493), which forbids them, and two parsers resolving a duplicate differently would canonicalise one claim two ways. The signed bytes are this JSON form whatever the carriage: a carrier that moves claims as CBOR MUST reproduce it to verify. A claim SHOULD be sent in canonical form, and relays forward it byte-for-byte (§4.1).
Chain. A signer's claims form one chain through
parent_receipt_hash and seq, separate from the group chain: a
claim's parent is never a group receipt and a group receipt's parent
is never a claim, so every existing group-chain verifier is unaffected
by claims. A publisher that issues both keeps two chains under one
key.
2.4.4. Receipt classes
class |
carried by | subject_type |
body | signed by |
|---|---|---|---|---|
delivery |
group, object (implied, not carried) |
— | §2.4.1, §2.4.2 | the publisher |
energy |
claim |
object |
§2.4.5 | whoever acquired the figure |
lawful-basis |
claim |
object or receipt |
§2.4.6 | the service operator (a declaration); the party recording it (a decision) |
log-inclusion |
claim |
receipt |
§2.4.7 | whoever attaches the log's evidence |
soft-binding |
claim |
object |
§2.6.2 | the object's publisher |
binding-recovery |
claim |
object (the ancestor) |
§2.6.3 | the party that performed the recovery |
measurement |
claim |
object or receipt, as its figure profile fixes |
§2.4.8 | the key that signed the figure: the group's publisher for moq-jwp:object, the receipt's signer for every other profile |
operating-point |
claim |
object or receipt, as its figure profile fixes |
§2.4.9 | the party that records the request, the commitment and the delivery |
Rules:
- A verifier MUST reject a
claimwhoseclassit does not know, and MUST report the rejection rather than skip the claim. Silently discarding a class a signer thought load-bearing is the failure this rule exists to prevent. The rejection is of that claim only: a delivery receipt for the same subject verifies or fails on its own. deliveryis reserved togroupandobject. Aclaimnaming it MUST be rejected; delivery is stated only in the §2.3 signed payload.- The class is read from
classand MUST NOT be inferred from the body's shape. A body MUST carry exactly its class's members; a missing or additional member is a rejection, not an extension point. A new class, or a new member, is a new registration here. - A
claimcarries the authority of its signer and no more.classsays 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. 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
claimthat repeats a figure a delivery receipt carries for the same subject MUST agree with it; theenergyclass does this deliberately, to bind its labels to the figure. Where the delivery receipt labels its figure (§2.4.2), the claim MUST also agree with that label's class and evidence. Where they disagree a verifier MUST reject rather than choose. - A class that is about one figure —
measurementoroperating-point— names it by a figure profile (energy-measurement §6.1) and the identity that profile defines, restates it with its class, and is checked against the receipt that carries it at §3.1 step 6. It never changes that receipt, and never supplies a class that receipt does not carry.
2.4.5. class: "energy"
§2.4.2's joules_micro fixes the unit and the scope of attribution;
energy-measurement §2 requires an acquisition
class beside any non-zero figure, with the class's evidence inside the
signature, and the object receipt has no member for either. An
energy claim supplies them without changing the signed object
format: it restates the figure and binds to it, under the claim's
signature, the labels other extensions define. It defines none of those
vocabularies. The class and its evidence are written as
energy-measurement §2.7 writes them in a JSON receipt, under the same
member names; a class that carries no evidence has those members
present and null.
| member | type | meaning |
|---|---|---|
joules_micro |
integer | The figure. Equal to the object receipt's joules_micro for the same subject. A zero is an integer here and is refused by rule 1, at step 4. |
energy_provenance |
string | An acquisition class of energy-measurement §2, verbatim. |
energy_uncertainty |
object or null |
The declared uncertainty (energy-measurement §2.3): { "relative_ppm": integer, "window_us": integer }, exactly those two members. |
energy_counter_filtering |
string or null |
An OnChipCounter's filtering state (energy-measurement §2.4). |
energy_calibration_ref |
string or null |
A CalibratedInstrument's calibration reference (energy-measurement §2.5). |
coverage |
string | Whole or Partial, per energy-binding §4. |
grade |
string | An evidential grade of oer-receipts §2, verbatim. |
method |
absolute URI | The published protocol the figure was obtained under. |
work_units |
integer | The recomputable work the figure is expressed against (energy-measurement §5). A zero is refused by rule 6, at step 4. |
work_unit |
term | What one work unit is (e.g. subpixel). |
oer_receipt |
string or null |
The receipt_id (sha256:<hex> or blake3:<hex>, lowercase) of an OER receipt recording the same figure, or null. |
§3.1 step 2 reads these members in the table's order, and
energy_uncertainty's two members, where it is an object, as it is
reached; it checks their types and encodings, and nothing more. Which
class, evidence and figure they may hold together, and whether the
figure or the work count is zero, is step 4's.
Rules, checked at §3.1 step 4 in this order:
joules_microMUST NOT be zero. A zero is a failed measurement, and an unmetered object gets noenergyclaim at all (energy-measurement §2).- The acquisition class, in this order:
energy_provenanceMUST name a class of energy-measurement §2 (energy_unknown_acquisition);- it MUST NOT be
HwShunt(energy_legacy_acquisition). energy-measurement §2.6 forbids the legacy label in a new receipt, and no claim predates the refinement, so a verifier can hold every claim to it — as energy-measurement §2.8 holds the optional labels; - the evidence MUST be the class's, whole
(
energy_acquisition_evidence).CalibratedInstrumentandOnChipCountercarry a declared uncertainty whose two parts are non-zero and whoserelative_ppmis at most 2³² − 1, the width of its signed encoding (energy-measurement §2.7), and which reaches the resolution floor,joules_micro × relative_ppm ≥ 288 676(energy-measurement §2.3).OnChipCountercarries a filtering state ofOn,OfforUnknownand no calibration reference; a filtering state ofnullis notUnknownhere, so that one class has one spelling.CalibratedInstrumentcarries a calibration reference that is not empty once Unicode White_Space is trimmed from both ends, and no filtering state.ModelBasedandEstimatorcarry none of the three.
coverageMUST beWholeorPartial.gradeMUST name a grade of oer-receipts §2.gradeMUST NOT rank above the grade the class supports. The correspondence is oer-receipts §2's —CalibratedInstrumentandOnChipCountersupport up tomeasured,ModelBasedup toderived,Estimatorup toprojected— with grades ranked as oer-receipts lists them (projected<simulated<derived<measured). Grading a figure below what its class supports is permitted. The grade does not say which hardware class is behind ameasuredfigure; the class and its declared uncertainty do.work_unitsMUST NOT be zero.
A relying party holding an energy claim with coverage: "Partial"
for a subject MUST NOT report that subject's joules_micro as the
whole cost of producing it.
Against the delivery receipt (§3.1 step 6): the claim's
joules_micro MUST equal the object receipt's for the same subject
(energy_disagrees_with_delivery). Where that receipt carries a class label
(§2.4.2), the claim's class and its evidence MUST also equal the label's
(energy_class_disagrees_with_delivery). Each is read as energy-measurement
§2.7 reads it, so an OnChipCounter label with no filtering state reads as
Unknown.
Against the OER receipt it names (§3.1 step 6): a verifier holding that receipt checks, in this order. The receipt is read as oer-receipts §4 reads a document: a text it refuses whole is no receipt, so a verifier holding only such a text holds none.
- that it is the one named — its
receipt_idequalsoer_receipt(context_mismatch); - that it verifies under oer-receipts with no
reason (
energy_oer_invalid); - that it is about the claim's subject — its
subject.outputequals the claim'ssubjectexactly as the claim writes it, 64 lowercase hex digits (context_mismatch). oer-receipts does not fix whatsubject.outputholds; an OER receipt anenergyclaim can name holds there the object's §2.1 content hash in this form. Without this check a claim about one object could name a valid OER receipt about another that happens to carry the same figure and grade; - that its
provenance.howequalsgrade, and that itsenergy.joules_total, rounded to the nearest microjoule, equalsjoules_micro(a figure exactly half-way between two microjoules admits either) (energy_disagrees_with_oer). Check 2 has established that the figure is a quantity oer-receipts §4 reads — a decimal string of at most 34 significand digits, or an integer within I-JSON's range — since a receipt whose figure is anything else, or whose body holds a fraction,-0or an integer beyond that range anywhere, does not verify. The figure is compared exactly, as oer-receipts §6 compares figures:0.0120005is half-way and admits 12,000 and 12,001 µJ, and0.0120004followed by twenty-six nines is below half-way, although rounding it to 28 significant digits would make it a tie.
This checks that two signed statements of one figure about one object
agree. It does not check that the OER receipt's energy.boundary is
the scope §2.4.2 attributes to joules_micro; that remains the claim
signer's assertion.
The claim does not make the figure verifiable — re-execution reproduces the work, never the joules (energy-measurement §5). It makes the figure's labels signed, and a contradiction between them detectable.
2.4.6. class: "lawful-basis"
The afferent dual of a delivery statement: on what basis what a sink
observes may be processed, and the record that a decision was taken
against that basis. A body is one of two records, discriminated by
record, which §3.1 step 2 reads before anything else in the body:
record absent is missing_required_field, and a record other than
"declaration" or "decision" is field_type. The body is then
checked against the table of the record it names.
A declaration (record: "declaration"), signed by the service
operator:
| member | type | meaning |
|---|---|---|
record |
string | "declaration". |
vocabulary |
absolute URI | The vocabulary the terms are drawn from (for example the W3C Data Privacy Vocabulary). |
fragment |
32 B hex or null |
§2.1 content hash of the envelope carrying the declaration fragment beside the manifest, when one is carried. |
purposes |
array | One entry per purpose: { "purpose": term, "basis": term or "", "data": [term, …] }, data possibly empty. The array may be empty and a basis may be "" as far as step 2 is concerned: both are refused by the rules below, at step 4, each with its own reason. |
A decision (record: "decision"), signed by the party recording
it:
| member | type | meaning |
|---|---|---|
record |
string | "decision". |
vocabulary |
absolute URI | As the declaration's. |
declaration |
32 B hex | link_hash of the declaration claim the decision is expressed against. |
purpose |
term | The purpose decided on. |
outcome |
term | The decision, as a vocabulary term. |
Rules (§3.1 step 4): a declaration MUST list at least one purpose;
purposes are then checked in order, and each MUST carry its own
non-empty basis — a basis is per purpose, never per service — and
MUST NOT repeat an earlier purpose. Against the declaration it
names (§3.1 step 6): the declaration MUST itself pass §3.1 steps
1–4 (context_invalid) and be a lawful-basis declaration
(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); 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
across bases, and each purpose carries its own. It makes no statement
that a basis is sufficient for any purpose under any regime, and a
verifier here does not check terms against the vocabulary. The shape
— purposes, each with its own basis and data categories, from a
declared vocabulary — is the one ABNT NBR 25608:2025 uses for its
broadcast privacy declaration, as described in that standard's public
operational guidelines (see the 2030 landscape survey
§7.2), whose records follow ISO/IEC TS 27560. A lawful-basis claim
is signed, like every claim.
2.4.7. class: "log-inclusion"
A statement that a receipt was entered in an append-only log, carrying the log's own evidence. Its subject is always a receipt: a log registers receipts, never objects, and per transparency §3 per-object receipts are not registered individually — the subject is a group receipt's link hash, or a claim's.
| member | type | meaning |
|---|---|---|
log |
absolute URI | The log. |
profile |
absolute URI | The registration profile the log's evidence is checked under (for example urn:ietf:rfc:9942, COSE Receipts). |
entry |
term | The log's identifier for the entry. |
log_receipt |
hex | The log's own receipt for the entry, verbatim. |
Rule (§3.1 step 4): log_receipt MUST NOT be empty; an inclusion
claim without the log's evidence is only its signer's say-so.
Accepting a log-inclusion claim establishes that its signer
asserted inclusion and carried the log's evidence. It does not
establish inclusion. Inclusion is established only by checking
log_receipt under profile against the log, which this extension
does not specify; a verifier that has not done so MUST NOT report
the subject as logged. This claim's signature never stands in for the
log's, and transparency §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 §6 requires a published figure
to be accompanied by how it was measured: for a figure obtained under that
extension's §3, the values its bracket gave, and for any figure, the build
profile. A measurement claim carries them for one figure. It restates the
figure and its acquisition class, so that it can be checked on its own, and
leaves the receipt that seals the figure unchanged. It names that receipt by a
figure profile (energy-measurement §6.1) and the receipt's identity under it.
Its subject_type and subject are the ones the profile gives: object and
the object's §2.1 content hash for moq-jwp:object, and receipt and the
receipt's identity for every other profile.
| member | type | meaning |
|---|---|---|
figure_profile |
term | A figure profile of energy-measurement §6.1. |
figure_index |
integer or null |
Which figure, from 0, in a profile that carries several — for moq-jwp:object, the object's leaf index; null in one that carries one. |
group |
32 B hex or null |
For moq-jwp:object, the link hash of the group receipt the object was delivered under (§3 step 3); null for every other profile. |
joules_micro |
integer | The figure. |
energy_provenance |
string | Its acquisition class, as §2.4.5 writes it. |
energy_uncertainty |
object or null |
As §2.4.5. |
energy_counter_filtering |
string or null |
As §2.4.5. |
energy_calibration_ref |
string or null |
As §2.4.5. |
bracket |
object or null |
For a figure obtained under energy-measurement §3: { "idle": summary, "busy": summary, "window_us": integer, "completions": integer, "in_flight": integer, "opening": "from-rest" or "running" }, a summary being { "n": integer, "mean_uw": integer, "sd_uw": integer, "live_intervals": integer, "longest_live_run": integer } as energy-measurement §6.2 defines them. null otherwise. |
build |
object | { "profile": "release" or "debug", "toolchain": string or null, "target": term or null }: the build of the code measured (energy-measurement §5, §8). release is a build optimised as shipped; any other build is debug. A finer vocabulary is a new registration. |
power_reader |
string or null |
The reader of the power interface (energy-measurement §8). Required under a hardware class (rule 4). |
A report carries none of what follows from its values: neither the §4 ratio nor
its verdict, which every reader derives (energy-measurement §6.2). A bracket
carrying either has a member its table does not define (unknown_member).
A null bracket says the figure was not obtained under energy-measurement §3 —
under a hardware class, for example, a counter read through the span of the
work. Such a report carries no values to check, so the figure's liveness
(energy-measurement §3.1) rests on its producer's statement alone, and no reader
derives a §4 verdict for it.
Rules, checked at §3.1 step 4 in this order:
joules_microMUST NOT be zero (energy_zero_figure): a zero is the unmetered marker, or a failed measurement, and gets no report.figure_profileMUST name a profile of energy-measurement §6.1 whose subject type is the claim'ssubject_type(figure_unknown_profile);figure_indexMUST be an integer exactly when that profile carries several figures (figure_index_mismatch); andgroupMUST be a hash exactly when the profile ismoq-jwp:object(figure_group_mismatch).- The acquisition class, as §2.4.5's rule 2 (
energy_unknown_acquisition,energy_legacy_acquisition,energy_acquisition_evidence). - Under
OnChipCounterorCalibratedInstrument,power_readerMUST NOT benull(measurement_no_power_reader): a hardware figure is a reading, and its report names what read it, whether or not it carries a bracket. - Where
bracketis notnull, in this order, withMits busy mean less its idle mean:- each
nMUST be at least 2 (measurement_too_few_samples); - under
OnChipCounterorCalibratedInstrument, each bracket's rail MUST have been live (energy-measurement §3.1):sd_uwnot zero (a bracket whose samples are all equal advanced once at most),longest_live_runat least 2,live_intervalsat least 5 and at least four fifths ofn(live_intervals · 5 ≥ n · 4), andlongest_live_run ≤ live_intervals ≤ n(measurement_rail_not_live); window_us,completionsandin_flightMUST NOT be zero (measurement_empty_burst);MMUST be positive (measurement_no_marginal);- under
OnChipCounterorCalibratedInstrument,MMUST be at least the idlesd_uw. A ratio below 1 is a failure to measure (energy-measurement §4) (measurement_below_attribution_floor); joules_microMUST be the figureFthe bracket gives (energy-measurement §6.2) (measurement_figure_mismatch);- under
OnChipCounterorCalibratedInstrument, the declaredwindow_usMUST be the bracket's (measurement_window_mismatch), and the declaredrelative_ppmMUST be at least the leastpthe bracket supports (energy-measurement §6.2) (measurement_declaration_below_bracket).
- each
Against the figure (§3.1 step 6). The context is the receipt the claim
names, verified under its own profile's rules; for moq-jwp:object, that is
the object receipt accepted under §3 against the group receipt group names.
In this order:
- the receipt verifies (
context_invalid); it is offigure_profile, its identity under that profile issubject, it is within the groupgroupnames, and it carries a figure atfigure_index(context_mismatch); - that figure is
joules_micro(figure_disagrees); - that figure carries an acquisition class (
figure_unclassified). An unlabelled figure has none, and neither has the unmetered marker; a claim does not supply one; - that class and its evidence are the claim's, each read as energy-measurement
§2.7 reads it (
figure_class_disagrees); - an Ed25519 key signs the figure (
figure_signer_unknown) — every profile butwai:energy-binding-idhas one — and it issigner_pubkey(figure_signer_mismatch). Formoq-jwp:objectit is the group receipt'ssigner_pubkey. Only the key that signed a figure signs its report.
Only check 5 shows that a measurement claim's signer is the figure's, so a
measurement claim is accepted only once step 6 has run against the receipt
it names. A verifier that does not hold that receipt reports
figure_not_held at step 6 — recoverable by fetching the receipt — and
MUST NOT present the claim as the figure signer's report, nor derive a §4
verdict for the figure from its values: without the context, any key can sign
any values about any figure.
An accepted measurement claim establishes that the figure's signer reported
these values for this figure, and that the figure and its declared uncertainty
are what the values give. It does not establish that the values were measured:
they cannot be re-measured (energy-measurement §5). A reader derives the §4
verdict from an accepted claim's bracket (energy-measurement §6.2).
2.4.9. class: "operating-point"
energy-binding §7 makes the operating point a figure was
spent at part of the claim about it: what was requested, what was committed,
what was delivered, and what a declared descriptor predicted. An
operating-point claim records these for one figure, named and restated as
§2.4.8 names and restates it, without changing the receipt that seals the
figure. It is signed by the party that records them.
| member | type | meaning |
|---|---|---|
figure_profile, figure_index, group, joules_micro, energy_provenance, energy_uncertainty, energy_counter_filtering, energy_calibration_ref |
As §2.4.8. | |
vocabulary |
absolute URI | The vocabulary the operating points' parameters, and the prediction's descriptor, are drawn from. WAI defines none. |
op_requested |
point or null |
What the requesting party asked for; null when nothing was requested. |
op_committed |
point or null |
What the acting party said it would use. |
op_delivered |
point or null |
What was used. |
prediction |
object or null |
{ "predictor": absolute URI, "descriptor_hash": 32 B hex, "predicted_joules_micro": integer }: the model that turned the declared descriptor into energy, BLAKE3("wai:energy-descriptor\x01" ‖ descriptor) over the descriptor as vocabulary serialises it, and the energy it implies for the work the figure covers. |
A point is an object whose member names are terms and whose values are integers of §2.4.3. JCS orders its members and §3.1 step 1 refuses a repeated one, so two points are equal exactly when their canonical forms are.
Rules, checked at §3.1 step 4 in this order:
- §2.4.8's rules 1–3. A claim restating
HwShuntis refused there, so no operating point is recorded against a figure labelled with the legacy class. - A point that is present MUST NOT be empty (
operating_point_empty_point). op_deliveredMUST NOT be present withoutop_committed(operating_point_delivered_alone).op_requestedandop_committedMUST NOT be present withoutop_delivered(operating_point_incomplete).- A claim MUST carry
op_deliveredorprediction(operating_point_empty).
Against the figure (§3.1 step 6): §2.4.8's checks 1–4. The claim may be
signed by a party other than the figure's signer, so check 5 does not apply,
and it may name a figure no Ed25519 key signs. An operating point recorded
against a figure that carries the unmetered marker or no class is refused here
(figure_disagrees, figure_unclassified).
From an accepted claim a reader derives, and no claim states:
- whether the commitment was met: it was when
op_deliveredequalsop_committed, and there was none whenop_committedisnull; - the residual,
joules_micro − predicted_joules_micro, a signed integer, wheneverpredictionis present.
A reader MUST NOT report the commitment as met unless it derives that it
was. An accepted claim establishes what its signer recorded about the request,
the commitment, the delivery and the prediction. An op_requested in a claim
the acting party signed is that party's account of the request.
2.5. Worked example
Manifest JSON (group receipt, formatted for readability — wire form serializes with no insignificant whitespace and preserves key insertion order):
{
"kind": "group",
"track_alias": 7,
"group_id": 3,
"root_hash": "5d2c7b1f9a04e2a16c83b3d774a09f12c44b65a3e8d11f88c2901a47b50d2c61",
"leaf_count": 2,
"joules_total": 250,
"group_start": "2026-04-25T12:00:00Z",
"group_end": "2026-04-25T12:00:01Z",
"signer_pubkey": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
"signer_id": "did:joule:publisher-1",
"sig": "9f3a4d6b1c0e8742f1b6b94e7d1c2a85f5103e4c8b2d61a93f0e4b8f2c70a1d61f6a2b71d4eecbb0d7a2e90c4b1bf2c8a0e4d63b9101fa0e2c4d83b1f70e9a103",
"parent_receipt_hash": null
}
Object receipt for the second object in that group:
{
"kind": "object",
"track_alias": 7,
"group_id": 3,
"object_id": 1,
"content_hash": "1c3f4a2e88b0d77192aabd0c66f12345e89c7b41ab2cd35f7e80b912ad6c9012",
"joules_micro": 150,
"origin": "joule-cloud-us-west-2",
"merkle_proof": {
"siblings": [
"a07b4d2c91f5e3110687f4b210ff8e1133acdb0741e3920c19acf8b3110d57e2"
],
"direction_bits": 1,
"leaf_index": 1
}
}
The signature hex shown is illustrative; conformance is checked against the test vectors in §5.
An energy claim (corpus vector energy_accept, §5.6), formatted for
readability — its wire form is its JCS serialisation. This signature
is real, under the corpus's published test seed, which attests
nothing; the figure is invented:
{
"body": {
"coverage": "Whole",
"energy_calibration_ref": null,
"energy_counter_filtering": null,
"energy_provenance": "ModelBased",
"energy_uncertainty": null,
"grade": "derived",
"joules_micro": 12000,
"method": "https://transaction.science/wai/extensions/energy-measurement",
"oer_receipt": "sha256:970afc417e8a310929f3cdcd65ac989a477f240b5a7f27435139261da73288ea",
"work_unit": "subpixel",
"work_units": 49152
},
"class": "energy",
"issued_at": "2026-09-27T12:00:00Z",
"kind": "claim",
"parent_receipt_hash": null,
"seq": 0,
"sig": "58ab60212898afda4315a8f864769aa3378f8b6afd3ddaa77ea4d0061e64a447f298f935aaa26e878e94a0a50fa4f5e76b83cb48376e081c4f761596d94c4f0a",
"signer_id": "did:example:meter",
"signer_pubkey": "a09aa5f47a6759802ff955f8dc2d2a14a5c99d23be97f864127ff9383455a4f0",
"subject": "5bb20a10b980be8c6d95915394f6e89cd49c16d0f5e94cd87c0ec6ce37bcfc5e",
"subject_type": "object"
}
Its link_hash is
797a6d60712bb66270be3683546ed9d92c7134d3a3cfa34f304e6fdad886046f.
A measurement claim (corpus vector measurement_accept, §5.6) reporting
on an energy claim's figure, by the key that signed it: ten idle and
fourteen busy samples, every one live, ten workers released from rest and
sixty completions over 3 s. Its figure and its declared uncertainty are the
least its bracket's integers give (energy-measurement §6.2). Formatted for
readability; the signature is real, under the corpus's published test seed,
and the values are invented:
{
"body": {
"bracket": {
"busy": {
"live_intervals": 14,
"longest_live_run": 14,
"mean_uw": 30000000,
"n": 14,
"sd_uw": 115471
},
"completions": 60,
"idle": {
"live_intervals": 10,
"longest_live_run": 10,
"mean_uw": 20000000,
"n": 10,
"sd_uw": 115471
},
"in_flight": 10,
"opening": "from-rest",
"window_us": 3000000
},
"build": {
"profile": "release",
"target": "x86_64-unknown-linux-gnu",
"toolchain": "rustc 1.98.0 (example)"
},
"energy_calibration_ref": null,
"energy_counter_filtering": "Unknown",
"energy_provenance": "OnChipCounter",
"energy_uncertainty": {
"relative_ppm": 44669,
"window_us": 3000000
},
"figure_index": null,
"figure_profile": "moq-jwp:claim-link",
"group": null,
"joules_micro": 461538,
"power_reader": "reader:example"
},
"class": "measurement",
"issued_at": "2026-09-27T12:00:00Z",
"kind": "claim",
"parent_receipt_hash": null,
"seq": 1,
"sig": "1e3f79ad3571073e64b4ff97733c472c030d241377367a511e2a7be4f5975ea0fb34f2612fc78271941281fa185c3ba1252e8b8c96e3b3c61ee7854580e56e0f",
"signer_id": "did:example:meter",
"signer_pubkey": "a09aa5f47a6759802ff955f8dc2d2a14a5c99d23be97f864127ff9383455a4f0",
"subject": "c857bdf859edf4041541d2507611c4205435be54f47ec38fc6ab4350feaf6b4c",
"subject_type": "receipt"
}
Its link_hash is
31cfac4ec8c104a7db59c4156cd7968f539c690c7b5df0c69e34cf2c48529db9.
2.6. Binding classes
A receipt binds to an object in one of two ways.
2.6.1. Hard binding
§2.1: BLAKE3 over the envelope bytes. It holds or it fails, with no tolerance, and it is destroyed by any change to those bytes — a transcode, a remux, a platform re-encode. Those are not attacks; they are what normal distribution does. Reconstruct-class objects have no transcode hop by construction, and a transcoder that participates is a lineage step (provenance §2). What the hard binding cannot survive is a re-encode by a party that does not participate: the receipt stays intact, the object no longer matches it, and with one binding class nothing connects them.
2.6.2. Soft binding (class: "soft-binding")
A second, deliberately weaker class. A soft binding is a value
computed from the samples rather than the bytes — a watermark decoded
back out of them, or a fingerprint computed over them — by a
registered algorithm, and it survives re-encodes the hard binding does
not. The publisher declares it with a claim of class soft-binding
whose subject is the object (its §2.1 content hash):
| member | type | meaning |
|---|---|---|
alg |
term | The algorithm identifier, verbatim from the registry registry names, in that registry's reverse-DNS form (a rule below, at step 4). |
registry |
absolute URI | The public algorithm registry alg is listed in. |
binding_type |
string | "watermark" or "fingerprint", as the registry types the algorithm. |
value |
hex | The value the algorithm produced for the object. |
resolution_endpoint |
absolute URI | Where this declaration — and through it the publisher's receipts for the object — may be recovered from value. That it is an https URI with a host and no userinfo is a rule below, at step 4. |
Rules (§3.1 step 4): alg MUST be in reverse-DNS registry form
(two or more dot-separated labels of ASCII letters, digits, _, +
and -) and so MUST NOT be a bare name; value MUST NOT be
empty; resolution_endpoint MUST be an https URI with a host
and no userinfo. That is: it begins with the characters https://,
and its authority — what follows, up to the first /, ? or # —
holds no @ and is a host, optionally followed by : and a port of
ASCII digits (RFC 3986 §3.2.2–§3.2.3; the port may be empty). The host
is the text before the first :, or, for an IP literal, the text
between [ and ], and it MUST NOT be empty. Nothing further
about the host is checked.
The fields align with the C2PA soft-binding algorithm list — a public,
schema-validated registry whose entries carry an algorithm identifier,
its type (watermark | fingerprint) and optional resolution APIs. A
declaration naming that list in registry names its algorithm by the
list's identifier in alg. WAI defines no soft-binding algorithm, no
resolution service and no transport for one: resolution_endpoint is
a locator, referenced the way the carrier protocol is, and a verifier
here checks that alg has registry form, not that it is listed. A
soft binding here covers the whole object; a binding scoped to a
region or a time range is not expressible.
2.6.3. Recovery record (class: "binding-recovery")
A recovery (§3.2) MAY be recorded as a claim of class
binding-recovery, signed by the party that performed it. Its subject
is the ancestor — the declaration's subject — never the object in
hand.
| member | type | meaning |
|---|---|---|
alg |
term | As the declaration's. |
binding_type |
string | As the declaration's. |
value |
hex | The value computed from the object in hand. |
declaration |
32 B hex | link_hash of the soft-binding claim the value matched. |
resolution_endpoint |
absolute URI | The endpoint the recovery was made through; an https URI with a host and no userinfo, as §2.6.2's rule requires at step 4. |
observed |
32 B hex or null |
§2.1 content hash of the object in hand, when it is still a WAI envelope. |
attests |
string | MUST be "ancestor" — the ancestor rule, stated in-band. Any string passes step 2; another value is refused at step 4 (recovery_not_ancestor). |
Rules (§3.1 step 4): §2.6.2's rules on alg, value and
resolution_endpoint; attests MUST be "ancestor"; observed
MUST NOT equal subject — an object whose hard binding holds is
verified by §3, not recovered, and recording it as a recovery would
downgrade a hard binding to a soft one.
Against the declaration it names (§3.1 step 6). The context is the declaration together with the publisher key P of a delivery receipt for the declaration's subject that the verifier has verified under §3 — P taken from that receipt, never from the declaration or from the record, as in §3.2 step 3. A verifier that holds the declaration but no such receipt holds no context for the record, and MUST NOT report the record as checked against its declaration. The checks, in order:
- the declaration passes §3.1 steps 1–4 (
context_invalid). Its members,signer_pubkeyamong them, are its signer's only once its signature is checked: a declaration that was only parsed can carry the publisher's key over anyone's signature; - the declaration is a
soft-bindingclaim (context_mismatch); declarationis itslink_hash, andsubject,algandbinding_typeequal the declaration's (recovery_declaration_mismatch);- the declaration's
signer_pubkeyis P (recovery_signer_mismatch) — §3.2 step 3's check, without which a record could carry a recovery that §3.2 itself refuses; valuematches the declaration'svalueunder the algorithm's own rule (recovery_no_matcher,recovery_value_mismatch).
What an accepted record establishes. Checked against that context: that its signer recovered through a declaration made by the ancestor's publisher, with a value that matches it under the algorithm — and no more, since §3.2's ancestor rule and its bound on what a match is worth apply unchanged. Accepted without that context (steps 1–5 only): that its signer reports a recovery, and nothing about who made the declaration it names or whether the value matches it.
3. Verifier algorithm
A conforming verifier MUST execute the following six steps in order for every group receipt and every object receipt it accepts. Steps match the algorithm style of WAI SPEC.md §4 (numbered, terminating).
-
Parse and shape-check. Read the group receipt. If
kind != "group", reject (wrong_kind). Ifsigis not 64 bytes, if any required field from §2.4.1 is absent, or if a member §2.4.1 does not define is present, reject (malformed). Labelled members on a group without"sign_version": 2, asign_versionother than2, or a class that cannot be read, reject (group_label_invalid). Ifsigner_pubkeydoes not decode under RFC 8032 §5.1.3 or is of small order — §3.1 step 3's key rule — reject (bad_signer_key). -
Verify the Ed25519 group signature. Construct the signing payload per §2.3 from the receipt's fields: the labelled payload for a group with
"sign_version": 2, the legacy payload otherwise. Verifysigagainstsigner_pubkeyunder §3.1 step 3's rule —S< L,Rnot of small order, the cofactorless equation — the same rule every claim signature is held to, so that every conforming verifier accepts the same group signatures. If verification fails, reject (signature_verify_failed). For a labelled group, check its class with the group alone (§2.4.1) and reject one that breaks it (group_label_invalid). After this step, the verifier holds a verified root: the receipt'sroot_hashfield, now bound tosigner_pubkeyunder signature, and for a labelled group the total's class. -
Check parent linkage. If the verifier maintains receipt-chain state for
signer_pubkey, computeBLAKE3("moq-jwp:receipt-link\x01" || canonical_bytes(previous_group_receipt))and compare toparent_receipt_hash. If they differ, reject (chain_break). If no previous receipt is known andparent_receipt_hashisnull, accept as the chain head. A non-nullparent_receipt_hashwithout a corresponding previously-verified receipt is a recoverable condition (parent_not_held): the verifier MAY request the missing receipt before accepting; it MUST NOT silently accept.canonical_bytes(G)is G's §2.3 signing payload, in the form G was signed in, followed by its 64-bytesig. The resulting hash is G's link hash: the identity aclaimnames G by (§2.4.3), and aWAI_RECEIPTproperty'sgroup_receipt(moq-streaming-format §5). For a group in the legacy form it does not coverparent_receipt_hash, which that payload does not sign either; the labelled payload signs it. -
Verify the object's content hash. For each accompanying
kind: "object"receipt: recomputecontent_hashper §2.1 over the received WAI envelope bytes and compare to the receipt'scontent_hashfield. If they differ, reject the object (content_hash_mismatch: the envelope was mutated in transit). An object receipt missing a member of §2.4.2, or carrying one neither §2.4.2 nor its label defines, ismalformed. -
Verify the Merkle inclusion proof. In this order:
- If the object receipt carries a class label (§2.4.2), check it as
§2.4.2 requires and reject the object if it fails
(
object_label_invalid). - Check the object's position:
leaf_indexis below the group'sleaf_count;merkle_proof.siblingsholds exactly ⌈log₂leaf_count⌉ hashes (none for one leaf);direction_bitsequalsleaf_index, so it holds the index's bits and no higher ones;object_idequalsleaf_index; andtrack_aliasandgroup_idare the group's. Otherwise reject (proof_position_mismatch). Without this, the last leaf of an odd level, duplicated, walks two positions, and an object can be presented at another object's index. - Compute the object's leaf in the labelled form of §2.2 where it
carries a label, and in the legacy form otherwise. Walk the
merkle_proof.siblingslist from level 0 upward; at each leveli, combine the current value with the sibling using thedirection_bitsbiti(1 = current is right child, 0 = current is left child) and themoq-jwp:merkle-node\x01domain. After walking all siblings, compare the resulting hash to the verified root from step 2. If they differ, reject (inclusion_proof_failed).
- If the object receipt carries a class label (§2.4.2), check it as
§2.4.2 requires and reject the object if it fails
(
-
Accept. The object's bytes are bound to
signer_pubkeyat(track_alias, group_id, object_id). Where the object's leaf is labelled, itsjoules_micro, its class, the class's evidence and itsoriginare bound with them. Where it is not,joules_microandoriginare the publisher's unsigned statements. The verifier MAY now persist the group receipt as the new chain head forsigner_pubkeyand dispatch the WAI envelope per WAI SPEC.md §4. Whose keysigner_pubkeyis, is §7's to confirm; a group receipt needs the authoritydelivery, 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
verification (e.g. valid Merkle proof against an unverified root): a
reading of an object's figure, a group total and a WAI_RECEIPT
property are taken only from objects accepted under steps 1–6.
The group total. A verifier that holds, for every leaf_index below
the group's leaf_count, exactly one object receipt it has accepted under
steps 1–6 MUST check that their joules_micro sum to joules_total;
if they do not, it reports group_total_mismatch, and the objects' own
verdicts stand. If they do:
- for a group in the legacy form, it reads the total from the objects' labels as energy-measurement §2.8 reads a total: unmetered, unlabelled, or the weakest class as a class name only;
- for a labelled group, it checks that the group's class is the one the
objects compose to (§2.4.1) — the class and its evidence other than the
declared uncertainty (
group_class_disagrees), the coverage (group_coverage_disagrees), and arelative_ppmat least the composed one over the composed window (group_uncertainty_below_parts) — and reads the total at the group's signed class.
A verifier that does not hold exactly one accepted object for every index reads a legacy group's total as unlabelled, and a labelled group's total at its own signed class, which it has not checked against the parts.
3.1. Claim receipts
A verifier checks a claim in six steps and reports the first check
that fails, as a step and a reason. Within a step, checks run in the
order listed.
- Envelope. A value that is not JSON (RFC 8259), is not an
object, or has a duplicate member name is
malformed. A text that begins with a byte-order mark is not JSON here: RFC 8259 §8.1 lets a parser ignore one, and this step does not. So ismalformeda value holding a\uescape that leaves a surrogate unpaired — RFC 8259's grammar admits it, but it denotes no Unicode text, and parsers differ on what they make of it (RFC 8259 §8.2) — or arrays and objects nested more than 32 deep, the claim itself being the first level. A number is nevermalformed: every JSON number is JSON, whatever its size, and one a member may not hold (1e400,-0, an integer of four hundred digits) isfield_typebelow.kindabsent ismissing_required_field;kindother than"claim"iswrong_kind. Any §2.4.3 member absent ismissing_required_field; any other member present isunknown_member.classthat is not a string isfield_type; equal todeliveryit isclass_reserved, and a string naming no class of §2.4.4 isunknown_class. Then, in table order withclassdone andbodyleft to step 2, a member of the wrong type or encoding —subject_typeoutside its enumeration, a hash that is not 64 lowercase hex digits,seqnot an integer of §2.4.3,issued_atnot a date-time of §2.4.3,signot 128 lowercase hex digits — isfield_type. Asubject_typethe class does not admit issubject_type_not_admitted. - Body. A
bodythat is not an object isfield_type. Otherwise it is checked against the class's member table (for a lawful-basis body, the table of the record thatrecord, read first, names; see §2.4.6): a member absent ismissing_required_field, an additional member isunknown_member, and then, in the table's order, a member that is not of the type the table gives it isfield_type. That is a wrong JSON type; an integer that is not one of §2.4.3; a byte string that is not lowercase hex, two digits a byte, or a hash that is not 64 of them; a value outside an enumeration this extension defines here (record,binding_type, a bracket'sopening, a build'sprofile); a URI that is not absolute; a term that is not one of §2.4.3 (empty, longer than 2048 bytes, or holding a White_Space or control character); or a malformedoer_receipt; or an operating point whose member names are not terms or whose values are not integers of §2.4.3. A member's type and encoding are checked together, before the next member is read. An object a class fixes the members of — a lawful-basis purpose, anenergy_uncertainty— is checked the same way, its members and then their types, where the table reaches it, and the entries of an array in the array's order. Of a member's value, this step checks the type column and nothing else. What a class's rules require beyond it is step 4's, each with its own reason, and is neverfield_typehere: labels and evidence from vocabularies another extension defines (acquisition class and its evidence, coverage, grade); ajoules_microorwork_unitsof zero; an emptypurposes; an emptybasis, which is why its type isterm or ""; an emptylog_receiptorvalue; analgthat is a term but not in registry form; aresolution_endpointthat is an absolute URI but not anhttpsURI with a host and no userinfo; anattestsother than"ancestor"; a figure profile, an index or a group that does not fit the profile; a hardware figure's report that names no power reader; a bracket with fewer than two samples, liveness facts that do not show a live rail, a zero window, completion or thread count, or a busy mean not above its idle mean; the figure and least uncertainty a bracket gives; an empty point; and which points and predictions are present together. - Signature. Ed25519 (RFC 8032). This step restates RFC 8032's
own decoding and range rules, and fixes the two choices on which
RFC 8032 verifiers differ — whether a key or
Rof small order is refused (RFC 8032's procedure does not refuse them; this step does), and which verification equation is checked — so that every conforming verifier accepts the same signatures.signer_pubkeyisbad_signer_keyif it does not decode under RFC 8032 §5.1.3 — which refuses aycoordinate ≥ p, andx= 0 with the sign bit set, so every point has exactly one encoding — or if it decodes to a point of small order (one whose order divides 8).sigisR‖S, and it issignature_verify_failedunlessS< L (the order of the base point B, as RFC 8032 §5.1.7 requires),Ris not the encoding of a point of small order, and the encoding of [S]B − [k]A equalsRbyte for byte, where A is the key's point and k = SHA-512(R‖signer_pubkey‖ §2.4.3's signing input) mod L. That is the cofactorless equation. RFC 8032 §5.1.7 states the cofactored one and allows the cofactorless one; they disagree on some signatures whose points have a small-order component, and this step uses the cofactorless one only. The key checks run first. Under a key of small order the equation can hold for a signature made without any private key — under the identity,R= [s]B withS= s verifies for every message — so such a key is refused before its signature is examined. - Class rules, per §2.4.5–§2.4.9 and §2.6, each with its own reason (below). They run after the signature because a rule about what a signer said only means something once it is established who said it.
- Chain. Against the previous claim the verifier holds for
signer_pubkey. A claim is held only if it passes steps 1–4 itself. One that fails steps 1–3 is not known to be its signer's, and one that fails step 4 breaks its class's rules; neither is held, so an altered copy can neither satisfy a link nor break one in the signer's name. A non-nullparent_receipt_hashwith no previous claim held isparent_not_held— recoverable by fetching that claim, if it verifies, and MUST NOT be accepted silently. A parent that is not the held claim'slink_hash, aseqthat does not increase, or anullparent while a previous claim is held, ischain_break. - Context: checks against other receipts the verifier holds,
each verified first — an
energyclaim against theobjectreceipt for its subject, verified under §3, including the class label its leaf carries (§2.4.5), and against the OER receipt it names, verified under oer-receipts as §2.4.5's check 2 (energy_oer_invalid); ameasurementoroperating-pointclaim against the receipt that carries its figure, verified under that receipt's own profile (formoq-jwp:object, the object receipt accepted under §3 against the group receiptgroupnames) (§2.4.8); a decision against its declaration (§2.4.6); a recovery record against its declaration together with the publisher key of a verified delivery receipt for the declaration's subject (§2.6.3), and never against the declaration alone. A declaration is verified here under steps 1–4, and one that fails them — or is not JSON — iscontext_invalid: its members,signer_pubkeyamong them, are its signer's only once its signature is checked. For a decision or a recovery record the order is: the claim is of a class that names a declaration (context_mismatch), the declaration passes steps 1–4 (context_invalid), it is of the class the claim names (context_mismatch), then the class's own checks. A context receipt of another class iscontext_mismatch. For anenergyclaim, anobjectreceipt about another subject, and an OER receipt other than the one named or about another subject, arecontext_mismatch, in §2.4.5's order. For a decision or a recovery record, a declaration other than the one named or about another subject is the class's own reason,decision_declaration_mismatchorrecovery_declaration_mismatch; for a recovery record, a declaration signed by anyone but that publisher is thenrecovery_signer_mismatch, in §2.6.3's order.
A claim that passes every step it was checked at is accepted: its
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 |
|---|---|---|
| 4 | energy_zero_figure |
joules_micro is 0 in an energy, measurement or operating-point claim |
| 4 | energy_unknown_acquisition |
energy_provenance names no class of energy-measurement §2 |
| 4 | energy_legacy_acquisition |
energy_provenance is the legacy HwShunt |
| 4 | energy_acquisition_evidence |
the class's evidence is missing, carried by a class that has none, or below the resolution floor (§2.4.5 rule 2) |
| 4 | energy_unknown_coverage |
coverage is not Whole or Partial |
| 4 | energy_unknown_grade |
grade names no grade of oer-receipts §2 |
| 4 | energy_grade_exceeds_acquisition |
grade ranks above what the acquisition class supports |
| 4 | energy_zero_work |
work_units is 0 |
| 4 | lawful_basis_no_purposes |
a declaration lists no purpose |
| 4 | lawful_basis_purpose_without_basis |
a purpose has an empty basis |
| 4 | lawful_basis_duplicate_purpose |
a purpose repeats an earlier one |
| 4 | log_inclusion_no_evidence |
log_receipt is empty |
| 4 | soft_binding_alg_not_registry_form |
alg is not in registry form |
| 4 | soft_binding_empty_value |
value is empty |
| 4 | soft_binding_endpoint_not_https |
resolution_endpoint is not an https URI with a host and no userinfo |
| 4 | recovery_not_ancestor |
attests is not "ancestor" |
| 4 | recovery_of_hard_bound_object |
observed equals subject |
| 4 | figure_unknown_profile |
figure_profile names no profile of energy-measurement §6.1 for the claim's subject_type |
| 4 | figure_index_mismatch |
figure_index is an integer under a profile that carries one figure, or null under one that carries several |
| 4 | figure_group_mismatch |
group is a hash under a profile other than moq-jwp:object, or null under it |
| 4 | measurement_no_power_reader |
under a hardware class, power_reader is null |
| 4 | measurement_too_few_samples |
a bracket's n is below 2 |
| 4 | measurement_rail_not_live |
under a hardware class, a bracket's liveness facts do not show a live rail, or its samples are all equal |
| 4 | measurement_empty_burst |
a bracket's window_us, completions or in_flight is 0 |
| 4 | measurement_no_marginal |
a bracket's busy mean is not above its idle mean |
| 4 | measurement_below_attribution_floor |
under a hardware class, the marginal draw is below the idle standard deviation |
| 4 | measurement_figure_mismatch |
joules_micro is not the figure the bracket gives |
| 4 | measurement_window_mismatch |
under a hardware class, the declared window is not the bracket's |
| 4 | measurement_declaration_below_bracket |
under a hardware class, relative_ppm is below the least the bracket supports |
| 4 | operating_point_empty_point |
a point is present with no member |
| 4 | operating_point_delivered_alone |
op_delivered is present without op_committed |
| 4 | operating_point_incomplete |
op_requested or op_committed is present without op_delivered |
| 4 | operating_point_empty |
neither op_delivered nor prediction is present |
| 6 | energy_disagrees_with_delivery |
the figure differs from the object receipt's |
| 6 | energy_class_disagrees_with_delivery |
the object's leaf is labelled, and the energy claim's class or evidence differs from the label |
| 6 | figure_disagrees |
the receipt a measurement or operating-point claim names carries another figure at figure_index |
| 6 | figure_unclassified |
that figure carries no acquisition class |
| 6 | figure_class_disagrees |
its class or evidence differs from the claim's |
| 6 | figure_signer_unknown |
a measurement claim names a figure no Ed25519 key signs |
| 6 | figure_signer_mismatch |
a measurement claim's figure is signed by an Ed25519 key other than signer_pubkey |
| 6 | figure_not_held |
a measurement claim is checked without the receipt that carries its figure (§2.4.8) |
| 6 | energy_oer_invalid |
the named OER receipt reports any reason under oer-receipts |
| 6 | energy_disagrees_with_oer |
the named OER receipt states another grade or figure |
| 6 | context_invalid |
a receipt a claim is checked against — a declaration a decision or a recovery record names, or the receipt that carries a figure — does not verify |
| 6 | decision_declaration_mismatch |
a decision does not match the declaration it names |
| 6 | decision_purpose_not_declared |
a decision is about a purpose its declaration does not list |
| 6 | recovery_declaration_mismatch |
a recovery record does not match the declaration it names |
| 6 | recovery_signer_mismatch |
the declaration a recovery record names (§2.6.3), or a recovery runs through (§3.2), is not signed by the publisher of a verified delivery receipt for its subject |
| 6 | recovery_value_mismatch |
the value does not match under the algorithm's rule |
| 6 | recovery_no_matcher |
the verifier has no matcher for the algorithm |
3.2. Recovery through a soft binding, and the ancestor rule
For an object in hand that fails §3 step 4 — or is no longer a WAI
envelope at all — a verifier MAY attempt recovery. It needs: a
soft-binding claim D, typically returned by a resolution endpoint;
the publisher key P of a delivery receipt for D's subject that it
has verified under §3; the value V the registered algorithm computes
from the object in hand; the object in hand's §2.1 content hash H,
if it is still a WAI envelope; and a matcher for D's algorithm.
- Verify D under §3.1 steps 1–4. A rejection of D is the recovery's rejection, reported unchanged.
- D MUST be a
soft-bindingclaim (context_mismatch). - D's
signer_pubkeyMUST equal P (recovery_signer_mismatch). P comes from the verified delivery receipt, never from D: a soft binding declared by anyone but the publisher links the object in hand to that party's statement, not to the publisher's receipts. The resolution endpoint is a locator, not an authority; what it returns counts only if it passes this step. - If H equals D's subject the hard binding holds and there is
nothing to recover (
recovery_of_hard_bound_object): verify under §3 instead. - Evaluate V against D's
valueunder the algorithm's own rule. A verifier with no matcher for the algorithm MUST NOT report a recovery (recovery_no_matcher); no match isrecovery_value_mismatch.
Steps 2–5 report at step 6. On success the result is a recovery, and it attests D's subject: the ancestor.
The ancestor rule (REQUIRED). A receipt reached through a soft binding attests the ancestor the object in hand was derived from, not the object in hand. A verifier MUST report a recovery as such, naming the ancestor, and MUST NOT report the object in hand as verified, as bound, or as the subject of the ancestor's receipts. The energy consequence is part of the rule: the ancestor's figures cover producing the ancestor; the transformation from the ancestor to the object in hand is unaccounted, which is not the same as zero, and a verifier MUST NOT report the ancestor's figures as the energy of the object in hand. An implementation that cannot express this distinction MUST NOT implement recovery: a recovery presented as a hard binding is worse than no recovery, because it launders a transformation nobody attested.
What a match is worth. A soft-binding match is evidence at the algorithm's own error rates, which WAI does not state. A watermark can be copied onto other content, and unrelated content can share a fingerprint. A recovery says the object in hand matched the publisher's declared binding under the named algorithm — nothing about how it was derived, by whom, or with what changes.
A recovery MAY be recorded as a binding-recovery claim (§2.6.3).
The reference implementation (wai-rs, feature receipt_claims,
claim_receipt::recover) returns a value whose only attestation is the
ancestor; there is no path from it to a statement about the object in
hand.
3.3. Delivery reasons
| check | reason | raised when |
|---|---|---|
| §3 step 1 | wrong_kind |
kind is not the receipt's kind |
| §3 step 1, 4 | malformed |
a group receipt (step 1) or object receipt (step 4) lacks a member, carries one its kind does not define, or holds one of the wrong type or encoding |
| §3 step 1 | bad_signer_key |
signer_pubkey does not decode under RFC 8032 §5.1.3, or is of small order |
| §3 step 1, 2 | group_label_invalid |
a group's labelled members appear without "sign_version": 2, its sign_version is not 2, or its labelled members cannot be read (step 1), or its class breaks §2.4.1 with the group alone (step 2) |
| §3 step 2 | signature_verify_failed |
the group signature fails §3.1 step 3's rule over the payload of the group's form |
| §3 step 3 | chain_break, parent_not_held |
as §3.1 step 5, for groups |
| §3 step 4 | content_hash_mismatch |
the envelope does not hash to content_hash |
| §3 step 5 | object_label_invalid |
an object receipt's class label cannot be read or breaks §2.4.2: legacy HwShunt, a class beside a zero figure, the unmetered marker beside a non-zero one, a refined class without its evidence or below the resolution floor, or evidence without a class |
| §3 step 5 | proof_position_mismatch |
the object is not at the position its proof walks: leaf_index outside the group, a proof of the wrong length, direction bits other than the index's, an object_id other than the index, or another group's coordinates |
| §3 step 5 | inclusion_proof_failed |
the object's leaf, walked up its proof, does not reach the verified root — among other causes, because the leaf was computed in the other form of §2.2 |
| the group total | group_total_mismatch |
the accepted object receipts, one for every leaf, do not sum to joules_total |
| the group total | group_class_disagrees |
a labelled group's class, or its evidence other than the declared uncertainty, is not the one its objects compose to, or its objects compose to none |
| the group total | group_coverage_disagrees |
a labelled group's coverage is not the one its objects give |
| the group total | group_uncertainty_below_parts |
a labelled group declares less than the composed relative_ppm, or another window |
WAI_RECEIPT |
property_invalid, property_disagrees |
as moq-streaming-format §5 |
4. Relay preservation requirements
A conforming relay forwards WAI envelopes from one or more publishers to one or more subscribers. To preserve the receipt chain across hops, a conforming relay:
4.1. MUST
- Forward every group receipt and every object receipt unaltered to every subscriber that requested receipts for the corresponding track. Byte-for-byte equality of receipt bytes between ingress and egress is REQUIRED.
- Verify the group receipt's Ed25519 signature per §3 step 2 before the first object of the group is fanned out. A relay that cannot verify the signature MUST drop the group and emit a structured error to upstream operators. A relay that predates the labelled group payload (§2.3) cannot verify a labelled group and drops it, so a publisher labels a group only once the relays on its path verify both payloads.
- Forward the
parent_receipt_hashfield as received. A relay that has previously forwarded the chain MAY additionally check the link per §3 step 3 and refuse to fan out a broken chain. - Preserve the byte-exact WAI envelope (
WAI1magic, manifest bytes, payload bytes) end-to-end. Any rewriting at the envelope layer invalidates §2.1 content hashes. - Forward
claimreceipts (§2.4.3) the same way — unaltered, whatever their class. A relay MUST NOT drop a claim because it does not know its class: rejecting an unknown class is a verifier's rule (§2.4.4), and a relay that filters claims decides for every downstream verifier what it may see.
4.2. MAY
- Batch multiple
kind: "object"receipts into a carrier-level frame to amortize per-receipt overhead, provided every receipt's bytes are preserved (no field elision, no canonicalization rewrite). - Emit relay-scoped telemetry alongside the forwarded receipts:
cumulative
joules_totalacross observed groups, per-publisher delivery counts, fan-out fan-in ratios. Such telemetry is local to the relay and MUST NOT be conflated with publisher-issued receipts. - Reorder objects within a group on egress when the carrier protocol
permits — Merkle inclusion proofs are position-bound by
leaf_index, so reordering does not invalidate verification at the subscriber. The relay MUST NOT renumberobject_id.
4.3. MUST NOT
- Modify the WAI envelope payload bytes in any way (no transcoding, no manifest rewrite, no key reordering, no whitespace insertion). Envelope mutation is detected at §3 step 4.
- Re-issue the Merkle root or re-compute a new tree over a subset of objects. The publisher is the sole authority for the group root.
- Re-sign the group receipt under a relay-owned key. There is one
signer per group, and it is the publisher named in
signer_pubkey. Relay attestation, when desired, MUST be carried as a separate, parallel receipt and MUST NOT replace the publisher signature. - Strip the
parent_receipt_hashfield, even when its value isnull. The field is required by §2.4.1 and its absence is a shape-check failure at §3 step 1. - Truncate the receipt to fewer fields than §2.4 requires, even when the carrier framing has size pressure.
5. Test vectors
A conforming verifier MUST give, for each case §5.1–§5.5 defines, the outcome
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 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
verifier outcome (accept or one of the named rejection reasons).
5.1. Single-object group (vector v1_single_object)
One WAI envelope (wai.text.zstd, payload "hello" zstd-encoded).
Group receipt with leaf_count = 1, parent_receipt_hash = null.
Object receipt with object_id = 0, merkle_proof.siblings = [],
direction_bits = 0, leaf_index = 0. Expected outcome: accept.
5.2. Multi-object group with proof verification (vector v2_multi_object)
Five WAI envelopes (wai.audio.opus, 20 ms frames). Group receipt
with leaf_count = 5 (the publisher duplicates the trailing leaf
internally when building the tree). Five object receipts, each with
a non-empty merkle_proof. Expected outcome: accept for each
object receipt.
5.3. Cross-hop preservation through 2 relays (vector v3_cross_hop)
Three publisher-issued group receipts in chain order G0, G1, G2 with
parent_receipt_hash chaining each to its predecessor. Two relay
hops are simulated: ingress bytes → relay A egress bytes → relay B
egress bytes are emitted as three byte-equal copies. The verifier
walks the chain in order; expected outcome: accept at every hop,
and the chain head after G2 equals the BLAKE3 of G2's canonical
bytes per §3 step 3.
5.4. Invalid signature rejection (vector v4_bad_sig)
A well-formed group receipt with a single bit flipped in the sig
field. Expected outcome: reject at §3 step 2 with reason
signature_verify_failed.
5.5. Missing parent_receipt_hash rejection (vector v5_missing_parent)
Two group receipts G0, G1. G1 omits the parent_receipt_hash field
entirely (rather than setting it to null or a hash). Expected
outcome: reject at §3 step 1 with reason missing_required_field.
The generator writes the vectors as one directory per case. A conforming
verifier MUST accept the
accept cases bit-exactly and MUST reject the named cases at the
declared step with the declared reason. The companion implementation's verifier
exercises all five vectors in its own test suite.
5.6. Claim receipts and recovery (claim-conformance/)
The typed classes of §2.4.3–§2.4.9, the binding classes of §2.6 and
the procedures of §3.1–§3.2 are checked by the corpus in
claim-conformance/, generated and verified
by wai_claim_vectors (wai-rs, feature receipt_claims) and run by
the WAI conformance workflow on every platform of its matrix. Each
vector declares one outcome: accept — with the claim's link_hash,
which pins the canonical form, or for a recovery the ancestor it
attests — or reject at a named step with a named reason. A verifier
that rejects for another reason, or at another step, does not
conform. The corpus exercises every reason of §3.1; the ancestor rule;
each acquisition class with its evidence, each way the evidence can be
missing, misplaced or below the resolution floor, and the legacy
HwShunt; OER receipts sealed with and checked by the OER module,
including figures at the 34 digits a decimal string may hold and
figures and numbers oer-receipts §4 does not admit in a receipt body;
signatures that satisfy the verification equation and that step 3
still refuses, by RFC 8032's own decoding and range rules
(non-canonical encodings of a key, S ≥ L) or by the choices step 3
fixes (keys of small order with signatures made without a private key,
an R of small order); a non-canonical encoding of a key of large
order, which pins the reason step 3 gives; a recovery record naming a
declaration that the ancestor's publisher did not sign, and one naming
a declaration whose signer_pubkey was rewritten to the publisher's
after another party signed it; a previous claim that does not itself
verify; JSON that parsers read differently — a byte-order mark, an
unpaired surrogate escape, deep nesting, numbers out of every range —
and dates that do not exist; the boundary between a member's type
(step 2) and what a class's rules add to it (step 4), from both sides,
and the order step 2 reads a body in, where a body has two defects;
and a claim whose canonical form spans three BLAKE3 chunks. The corpus
also covers the measurement and operating-point classes: every reason
they add, the arithmetic of energy-measurement §6.2 at its boundaries — a
declaration at the least value a bracket supports and one below it, a
figure at a half microjoule, the ratio thresholds, a flat idle bracket, and
brackets whose products exceed 128 bits — the liveness rule of
energy-measurement §3.1 (a frozen rail refreshed once, a refresh published
in two steps, facts that describe no sequence), and the declaration a
sealing path computes from unrounded samples, which the rule refuses. That
arithmetic is pinned against values claim_verify.py computes with
Python's integers. It covers an energy claim checked against an object
whose leaf is labelled.
The generator and the verifier in wai-rs are one implementation, so
two checks are made against code that shares nothing with it. A
canonical fixture — a claim given with its members out of order at
three levels and escapes in the input text — has its canonical bytes,
signature and link_hash computed by
claim-conformance/claim_verify.py (Python's JSON serialiser with
UTF-16 member order, an Ed25519 implementation other than the one
wai-rs uses, and BLAKE3 written from its specification), and pinned
as literals that wai_claim_vectors checks first. And that script, a
second verifier written from this section, reports every vector's
declared outcome; it reads and verifies OER receipts with the OER
reference verifier, as wai-rs does with its own OER module. The
corpus pins one outcome per vector, which shows the two verifiers agree
on those vectors and nothing more, so
claim-conformance/differential.py also generates inputs the corpus
does not hold — JSON at the edges of what parsers accept, calendar
dates, URI authorities, acquisition classes with every combination of
evidence, figures near a half-microjoule and at the edges of
oer-receipts §4 and §6, OER receipts holding numbers §4 forbids in a
body, files §4 does not read at all, context receipts that do not
verify, bodies of every class with one defect and with two at
different positions, measurement brackets and operating points around
every rule — runs both verifiers' verdicts mode on them, and reports
every input on which their verdicts differ. Agreement shows that the two
read alike, not that either reads §3 as written; only the vectors'
declared outcomes, set from the text, speak to that, and only for the
inputs they hold. The fixture and the bracket arithmetic run in CI on every
platform of the matrix; the script and the differential, which need Python
with an Ed25519 library, run in CI on the linux runner with a fixed seed.
5.7. Labelled leaves, labelled groups and the group total (jwp-conformance/)
wai_jwp_vectors gen in wai-rs writes jwp-conformance/vectors.json,
and verify requires a byte-identical regeneration and every declared
outcome. Every vector is synthetic: keys come from published fixed seeds
and attest nothing, and envelopes, figures and origins are invented. The
corpus holds leaf vectors in both forms of §2.2; a group in the legacy
payload whose objects mix the forms, and a group in the labelled payload
chained to it, each with its signing payload, signature and link hash;
each object's outcome (accept, or a rejection at its §3 step with its
§3.3 reason), including objects presented at another object's position;
group signatures the strict rule refuses; the group total's reading or its
reason; and WAI_RECEIPT properties with the reading
moq-streaming-format §5 gives them.
jwp-conformance/jwp_verify.py, written from this text, reports the same
outcomes; CI runs it on the linux runner, and runs wai_jwp_vectors 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/, 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
6.1. Replay
A receipt without sequencing information is replayable: an attacker who captures group receipt G can re-present it to a downstream verifier and bind unrelated bytes to the publisher's identity at a later time. This extension mitigates replay with two mechanisms:
parent_receipt_hash(§2.4.1) links each group receipt to its predecessor under the samesigner_pubkey, so a chain replay requires forging every intermediate hash. The labelled payload (§2.3) signs it; the legacy payload does not, so in a legacy group the link is the publisher's unsigned statement and a relay can rewrite it.group_idis monotonic per publisher key. Verifiers SHOULD track the highestgroup_idseen persigner_pubkeyand reject any receipt with a non-increasinggroup_id. This binds the chain to forward progress.group_end(§2.3) carries a signed RFC 3339 timestamp. A verifier MAY additionally bound it against its own clock for the group it treats as the current tip — rejecting a receipt whosegroup_endis older than a deployment freshness window (a relay holding content back) or implausibly far in the future (skew). This wall-clock check is OPTIONAL and MUST NOT be applied to historical receipts replayed for late-joiner resync, whose timestamps are legitimately old — apply it only to the live edge. The reference verifier exposes both the monotonicgroup_idguard (per track) and this freshness bound as opt-in checks, disabled by default so durable re-verification of an old receipt stays a feature.
6.2. Forgery
The signing payload (§2.3) is bound under Ed25519 (RFC 8032). At classical security levels, Ed25519 provides ~128-bit unforgeability under chosen-message attack (EUF-CMA). Forging a group receipt is equivalent to recovering the publisher's private key or breaking Ed25519. Implementations MUST source signing keys from a CSPRNG and MUST NOT reuse keys across distinct publisher identities.
6.3. Cross-group confusion
The signing payload binds track_alias, group_id, and
signer_pubkey under the same signature. An attacker who attempts
to re-present a group receipt under a different (track_alias, group_id) tuple cannot do so without re-signing — the receipt's
fields are signature-covered, and any field mutation breaks
verification at §3 step 2. The Merkle proof is similarly bound:
the verifier checks that the proof walks the position leaf_index
names (§3 step 5), so an object's proof cannot be transplanted to a
different position within the same group, and the duplicated last
leaf of an odd level cannot be presented at the position past the
group's end.
6.4. Envelope mutation in transit
Any mutation of the WAI envelope bytes (manifest rewrite, payload re-frame, whitespace insertion) changes the §2.1 content hash and is detected at §3 step 4. Relays that re-canonicalize the manifest SHOULD be treated as broken: they break content-hash equality even when the semantic meaning is unchanged. WAI SPEC.md §2 already requires that the manifest be serialized with no insignificant whitespace and that encoders preserve key insertion order; receipt verification depends on relays honouring that requirement.
6.5. Quorum and multi-signer receipts
v1.0 of this extension is single-signer: every group receipt is signed by exactly one Ed25519 key. Multi-signer / quorum receipts (threshold Ed25519, BLS aggregate signatures) are explicitly out of scope for v1.0. A future minor revision MAY extend §2.3 with a multi-signer container; the v1.0 wire format remains a strict subset.
6.6. Out of scope
- Post-quantum group-signing ciphersuites. Migration to hash-based signatures (SLH-DSA), lattice-based signatures (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. §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
layer their own encryption above this extension; v1.0 receipts
are integrity-protected, not confidentiality-protected. Note this
is distinct from payload confidentiality: the
Confidentiality extension seals the object
bytes a receipt attests, and this chain deliberately commits over
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
measurementclaim (§2.4.8) also carries the measuring machine's power levels, thread count, build and power reader in cleartext. - Pricing semantics for joules.
joules_microandjoules_totalare unit-of-energy fields, not unit-of-account fields. Conversion to currency, carbon, or grid-mix CO₂ is performed by downstream aggregators and is not normative here.
6.7. Claims
A claim is a signed statement, and §6.1–§6.3 apply to it as to a
group receipt: seq and parent_receipt_hash bound replay per
signer, and every member — class, subject, the whole body — is
inside the signature, so a label cannot be promoted in transit (an
energy claim whose energy_provenance is rewritten from ModelBased
to OnChipCounter, or whose declared uncertainty is rewritten smaller,
fails at §3.1 step 3). Step 3 also refuses keys of small
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. §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
- A copied watermark. A watermark decoded from an object shows that the object carries the payload, not that it descends from the ancestor: a payload can be embedded into unrelated content. The ancestor rule (§3.2) bounds the damage — a recovery attests the ancestor's receipts, never the object in hand — and a relying party MUST NOT read a recovery as the provenance of the object in hand.
- Fingerprint collisions. Unrelated objects can share a
fingerprint value. How often is a property of the algorithm: WAI
states no rate, a
soft-bindingdeclaration carries none, and a verifier here does not assume one. A relying party that needs a rate takes it from the algorithm's own specification, where one is published; a match is evidence at that rate, never proof of derivation. - A hostile resolution endpoint. An endpoint can return any declaration it likes. What it returns counts only if it passes two checks, made wherever a declaration is relied on — §3.2 steps 1 and 3 when a recovery is performed, and §2.6.3's checks 1 and 4 when a recorded one is verified: the declaration verifies under §3.1 steps 1–4, and its signer is the publisher of a delivery receipt for its subject that the verifier has already verified. Neither suffices alone. Without the first, the signer check compares a key the declaration's author wrote in, over a signature that may be anyone's; without the second, a valid declaration binds the object in hand to whoever made it. A recovery record accepted without that context establishes nothing about who made the declaration it names (§2.6.3). What passing both establishes is bounded by the ancestor rule and by the algorithm's own error rates (§3.2).
- Downgrade. An object whose hard binding holds is never
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_idnames: a valid log of that origin, or thesigner_iditself (§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_idas adid:webDID 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 theoriginlabel of anobjectreceipt (§2.4.2). Its key log is at the origin followed by/.well-known/wai-keys, and nowhere else. The suffixwai-keysis 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 charactersdid:key:zfollowed by the base58btc encoding (the Bitcoin alphabet, each leading zero byte written as1) of the bytes0xed,0x01and 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:keyanddid:webstrings 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:<host>[%3A<port>]#<key-id>, 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:
- 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). - Its
originis the origin being checked (origin_mismatch). - Its
sigverifies under itsrootby §3.1 step 3's rule, which also refuses arootthat does not decode or is of small order (key_log_signature_invalid). - Its
seqis one more than its parent's, and itsissued_atis not earlier than its parent's (key_log_chain_break). Itsprevis its parent'srevision_hashby the definition of a parent; between consecutive revisions of a log being anchored (§7.3), aprevthat is not the previous revision'srevision_hashis alsokey_log_chain_break. - Either its
rootandnext_rootare its parent's, or itsrootdiffers from its parent's and has its parent'snext_rootas 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. - It erases and rewrites nothing: its
keysbegin with its parent's entries, in the same order, each with the sameid,keyandmay_sign; astatusstays, or moves fromactivetoretiredorrevoked, or fromretiredtorevoked; a time, once set, does not change; and itsrevoked_modulesbegin 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. - 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 itsnext_rootcommits 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 iskey_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 §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 therevision_hashof 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):
signer_idis origin-bound (§7.1). Adid:keynaming a key other thansigner_pubkeyis refused (signer_id_names_another_key); any other form is unconfirmed (signer_id_not_origin_bound).- The verifier has anchored the origin and holds the anchor
(
origin_not_anchored). - 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). - The head lists an entry whose
idis thesigner_id's key-id (key_not_listed) and whosekeyissigner_pubkey(key_mismatch). - 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. Aretiredentry fails (key_retired) when the item's signed time is later thanuntil, 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 everyuntil. - The entry's
may_signholds 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 §3: a trust anchor delivered over the bearer it authorises is not one).
- 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 (
- 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_idnames. - 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 §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_hashin 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/<class> 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/<class> 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/<class>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 §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 givesdelivery. Beside a confirmed item that needs a hardware-class authority, a verifier reports whether its key also holdsdelivery(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, soalso_deliversis true there by construction, and is reported as such. Anenergyclaim 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/declarationis 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
measurementclaim needsmeasurementand the energy authority of the class it restates; anoperating-pointclaim needsoperating-pointand that energy authority too. A report of a hardware-class figure is therefore confirmed only under a pinned anchor, and is reported withalso_delivers. §2.4.8's check 5 already requires ameasurementclaim'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 giveoperating-pointto 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-bindingis 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/decoderlets a key grant a decoder module, or a parameter set pinned by an envelope, for the capabilities the grant names;module/reconstructlets 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 §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
idbefore the successor signs, and retiring the old key with anuntilno 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 §3), so that a fork is visible to any Relying Party that checks the log. Registration anchors nothing (transparency §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); itswai-keystool 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) — 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 §4). A sink that holds a capability's pinned sets to this section runs the checks below as SPEC §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
§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:
- The grant has the form above, its
signer_idis origin-bound and names the grant'sorigin, and itssigverifies under itssigner_pubkeyby §3.1 step 3's rule (module_grant_invalid). - The bytes' digest is the grant's
module(module_digest_mismatch). - The grant passes §7.4 as an item needing
module/<form>, with itsnot_beforeas 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. - The sink holds a status for that origin naming the head §7.4 step 3
reached; of those, the latest is fresh: its
issued_atis not later thannowand at most that head'sstatus_max_age_sseconds 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 withmodule_status_not_descended. - The grant's
moduleis not in therevoked_modulesof 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). not_before≤now(module_grant_not_yet_valid) andnow≤not_after(module_grant_expired).not_after−not_beforeis at most the head'sgrant_max_s(module_grant_too_long).- 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 §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, to int_payload::resolve, which runs it for a pinned
decode and over the files an unpinned integer decode reads, or to
neural::ModelRegistry::with_key_log, which also runs it before an
unpinned decode, over the bytes that decode then reads, read once, by
every digest a pin over them could name): 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 §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/<form> 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 |