open-standards/wai/signer-conformance
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Transaction Science 28de796b42 wai: a key log's revocation refuses a set at every sink that holds one
SPEC §3.1's check 5 now has two parts. Revocation runs at every sink
that holds a key log, for every capability a pin applies to: a set whose
digest is in the revoked_modules of the head the sink reaches from any
anchor it holds is refused with prior_revoked. It needs no grant, status
or clock, so it no longer waits on the relying party holding the
capability to jwp-receipts §7.9, and it is reported as prior_revoked
whichever check of §7.9 a grant fails first, offline or with no grant
included. Authorisation is still the relying party's choice per
capability. SPEC §3.1 and §4, jwp-receipts §7.9 and the CHANGELOG say so.

An envelope that pins no set no longer escapes check 5. The neural
registry, given a key log, checks the set an unpinned decode reads under
each digest a pin over it could name (the file's SHA-256 for a one-file
set, and each listing beside it), and dispatch falls back when it is
refused. PinAuthority gains check_unpinned for this.

A family receipt now needs energy/<class> for each class its figures
carry, as Item::from_family already derived: the §7.5 row, the corpus
README, signer_verify.py and the corpus reader now agree. Item::for_group
and Item::for_family are crate-internal, the corpus's builders, and the
docs say that from_family takes signer_id and time from its caller.

signer-conformance gains vectors for a revoked set whose grant fails at
check 4, a revoked set with no grant, a set with no grant, and a family
receipt with two classes, regenerated by wai_signer_vectors gen; both
verifiers reproduce every verdict, and every earlier vector keeps its
declared outcome. The CI library tests cover the revocation part at a
sink that does not hold the capability to §7.9, and the neural key-log
gate covers the unpinned decode.

Docs say that a key log naming the subject's origin holds for one
envelope. ModelRegistry's key_log field is private, which the CHANGELOG
records. The pin corpus README says its cases model a sink with no key
log. The parameter-set entry's refusal list names check 5's two reasons,
and the did:web label test is described by the files it reads.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-01 09:36:49 -04:00
..
.gitattributes wai: key discovery, signing authority and module revocation 2026-10-01 07:12:20 -04:00
README.md wai: a key log's revocation refuses a set at every sink that holds one 2026-10-01 09:36:49 -04:00
signer_verify.py wai: a key log's revocation refuses a set at every sink that holds one 2026-10-01 09:36:49 -04:00
vectors.json wai: a key log's revocation refuses a set at every sink that holds one 2026-10-01 09:36:49 -04:00

signer-conformance

Conformance for key discovery, signing authority and module authorisation (§7): the forms of a signer_id, an origin's key log, anchoring, the confirmation a verifier reports beside a receipt's or claim's verdict, and the checks a sink runs before it runs a decoder or reconstruct module.

Each vector declares one verdict, set from the text in the generator and never copied from a verifier. A verifier that reaches another verdict — another level, step, reason, head or anchor path — does not conform.

Files

file what it is
vectors.json The corpus. Regenerate with wai_signer_vectors gen .; wai_signer_vectors verify . checks the canonical fixture, a byte-identical regeneration and every verdict.
signer_verify.py A second verifier, written from §7, that shares no code with wai-rs. It takes from ../claim-conformance/claim_verify.py only what §7 reuses of §3.1: the JSON reading, JCS, BLAKE3, the strict Ed25519 rule, the RFC 3339 syntax and claim verification.

Procedures

confirm (§7.4) runs on an item and reports confirmed with the origin, key-id, head revision_hash and anchor path — and, where they apply, moved_to, retired_until, untimed=true, expected= and also_delivers= — or unconfirmed or refused at a §7.4 step (1–7, or 6a) with a reason of §7.7. The item is either receipt, a claim's wire form, which is first verified under §3.1 (a claim that fails is reject step=<n> reason=<label>, and is never confirmed), or context.item: the kind (group or family), signer_pubkey, signer_id, classes (for a group, the acquisition classes its objects' labelled leaves and its labelled total carry; for a family receipt, the acquisition classes its figures' labels carry) and its signed time (or null). The authorities an item needs are derived from it by §7.5's table. The context also holds revisions (revision texts the verifier holds, in any order; one that fails rule 1 is not held), anchors (origin, revision, path, acquired_at, and pinned_revision for a pinned record, which a forward move keeps) and, optionally, expected_origin (origin and source, relying-party or subject).

anchor (§7.3) runs on a key-log document (log) retrieved for origin by path (pinned or fetched, with acquired_at), and optionally current: an anchor already held, with the revisions held with it. It reports anchored revision=<hash> anchor=<path> — with pinned_revision=<hash> where a pinned record moved forward — not_anchored reason=<label> or replaced reason=key_log_replaced.

module (§7.9) runs on a module's bytes (module, hex), whether they are a module or a pinned parameter set (object), a grant, the capability being decoded, the sink's module_origins (each an origin and source), the revisions, anchors and statuses the sink holds, its trusted clock (now, or null for none) and whether the envelope leaves a fallback. It reports run with the granting key's origin, key-id, head, anchor path, where its module origin came from (expected=) and kill_latency_s, or fallback or inert at a §7.9 check with its reason; a digest another anchored origin revokes adds revoked_by=<origin>. A parameter set that is not held adds the refusal SPEC §3.1's check 5 reports: refusal=prior_revoked where its digest is revoked, refusal=prior_not_authorised for any other check. A parameter set whose digest the head of an anchor the sink holds revokes is reported at check 5 with revoked_by=<that origin> and prior_revoked whichever check its grant failed first, unless the grant reached check 5 itself. For a parameter set grant may be null, a sink that holds no grant: the set is then refused as revoked where its digest is, and otherwise reported as no_grant refusal=prior_not_authorised.

Keys, origins and times

Everything is synthetic. Keys are Ed25519 from published one-byte seeds and attest nothing: 0x61 the root in use, 0x62 the key it commits to, 0x68 the key that one commits to, 0x63 and 0x66 delivery keys (0x66 also meters), 0x64 and 0x69 metering keys, 0x65 and 0x6e lawful-basis keys, 0x6b and 0x6c module-granting keys, 0x60 a key added later, 0x67, 0x6a and 0x6f keys an attacker holds, 0x70, 0x71 and 0x72 the root, committed key and module-granting key of a second origin, 0x73 and 0x74 a measurement key and an operating-point key, and 0x75 a key that signs family receipts with a ModelBased figure. jwp-receipts §7.8 says such a key must never be listed in a real key log: every anchored verdict here carries note=published_test_key, which is what a verifier reports on finding one. Origins are reserved example names (publisher.example, with and without port 8443, service.example, other.example). Times are in September 2026 and invented.

signer_verify.py needs Python 3 with the cryptography package. CI runs it on the linux runner (independent-verifiers in wai-conformance.yml), and requires its verdicts output to equal wai_signer_vectors verdicts line for line. By hand:

python3 signer_verify.py            # every vector, and the fixture against wai-rs
python3 signer_verify.py fixture    # the fixture's bytes, signature and hash
python3 signer_verify.py verdicts vectors.json