open-standards/wai/live-conformance/age/README.md
Transaction Science 298eaa70f5 age × co: the room's own service is not held to the serving rule
A room with both `[age]` and `[co]` broke: co-performance's clock follows and
logs every performer's objects over a relay session of its own, which holds no
grant and no token, so age-assurance §5.5 served it nothing rated.

- age-assurance §5.5: a session that proves possession of a room key (the
  authority's or the clock's) is the room's own service, served every object of
  the room. The proof is a GRANT_PROOF naming no grant (32 bytes of zero) over
  the session's challenge, checked as live-rooms §10.5.4 checks a proof; one
  that does not pass refuses the session with that section's reason; one that
  passes by a key that is no room key gives nothing. Co-performance §8 points
  to it; §11 and §12 name the reasons and the new vectors.
- joule-moq: `grant_proof::NO_GRANT`, `add_room_key_proof`, `room_key_proofs`.
- joule-moq-relay: SETUP checks room-key proofs; a session proving a key the
  relay holds as a room key (an authority's, for its rooms; `[floor]` or `[dvr]`
  room keys, for every room) has `SessionCtx::service`, and its threshold there
  is past every age. `ClientOptions::room_keys` presents such proofs.
  tests/age_room_service.rs: a room rated 18 with the floor held; the clock's
  and the authority's proofs served every group, the clock's token alone and a
  non-room key refused `below_threshold`, a forged clock proof and one over
  another challenge closed UNAUTHORIZED.
- joule-moq-room: the clock's session proves both room keys it holds.
- wai-rs `age_assurance::room_service`, the corpus case `room_service`, and
  `age_assurance_verify.py` taught the rule, with three new checks and four
  mutations, all caught.
- e2e: scenario `ageco`, a room with both `[age]` and `[co]`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 07:08:25 -04:00

79 lines
5.6 KiB
Markdown

# live-conformance/age — age assurance
The corpus of [`extensions/age-assurance.md`](../../extensions/age-assurance.md) §12, in one
file, `vectors.json`:
- `principals`: each Ed25519 key, by name (informative);
- `issuer_keys`: the three issuer keys of the policies, each its `source`, `issuer`,
`threshold` and `key` (RFC 9578 §6.5's SubjectPublicKeyInfo, hexadecimal), informative;
- `rfc9578_a2_vector1`: RFC 9578 Appendix A.2's first test vector, as published: `pkI`,
`token_challenge`, the client's `nonce`, its `blind` *r* and its PSS `salt`, the
`token_request` and `token_response` of its issuance, and the `token`, in hexadecimal. A
conforming verifier finds that the token's `challenge_digest` is the SHA-256 of
`token_challenge`, its `token_key_id` the SHA-256 of `pkI`, and its authenticator *s* a valid
RSASSA-PSS signature under `pkI` (RFC 9578 §6.4); and that the issuance holds (§6.1 to §6.3
there, RFC 9474 §4.2 to §4.4): the request's blinded message is (*s*·*r*)^e mod *n*, and the
response *s*·*r* mod *n*. A client of the issuance protocol reproduces `token_request` and
`token` from the others;
- `admission_logs`: admission logs by name, each a list of admissions, `t` (room-clock
microseconds), `challenges` (the TokenChallenges the relay sent that session, hexadecimal)
and `token` (hexadecimal);
- `cases`: each a `name`, an `about`, a `kind` (one of §9.1's), its inputs, and `expect`, what
a verifier gives it.
Every record is a JSON text (§2). A room's records are hexadecimal: a `descriptor`, `grants`
and `chains` (each chain a list of `WEV1` records), and `key_log` (`null` throughout), as the
live-rooms corpus gives them.
**Cases.** By `kind`:
- `ratings`: `claims`, rating claims. `expect`: `accept` with `claims`, each its `link`,
`signer`, `subject` (the subject's `type`), `age` and `derived` (1 for a claim with inputs, 0
otherwise); or `refuse` at `["claim", i]`.
- `admission`: a room `descriptor`, an age `policy`, the session's `challenges`, the nonces
`redeemed`, a `token`, the age `required` and the binding's `origin`: `null` over MOQT, where
a challenge's `origin_info` is the room's scope of live-rooms §10.4, or the service's server
name over HTTP (§5.6), where it is RFC 9577's list of server names. `expect`: `accept` with
the `issuer`, the `threshold` and the `key_id` the token admits under; or `refuse` at
`["policy"]` or `["token"]`.
- `serving`: a room `descriptor`, a `track` of it (`namespace`, a list of its fields, and
`name`), `claims`, and `requests`, each a `threshold`, a `group` and `held`, the indexes of
the claims the relay holds when it serves the request, and, for a session that presented a
proof of a room key (§5.5), its `service`: the `proof` (hexadecimal) and the `challenge` and
`context` the relay holds for the session. `expect`: `accept` with the `track`'s digest and,
for each request, the group's `age` and its `outcome`, `serve` (with `service` `true` for the
room's own service) or `refuse` with the `reason`; or `refuse` at `["claim", i]`.
- `predicate`: a sink's `anchor` (each key's `issuer`, `threshold` and `key`), its `credentials`
(tokens, hexadecimal), the `claims` it holds, and `objects`, each a position: a
`content_hash`, a `track` digest and a `group`, any of them `null`. `expect`: `accept` with the
sink's `state` and, for each object, its `age` and `verdict`, `present` or `withhold`.
- `receipt`: a room `descriptor`, an age `policy`, a `receipt`, and `admissions`: the name of an
admission log, or `null` for none. `expect`: `accept` with the receipt's `link` and its
`cells` (each `window`, `issuer`, `threshold`, `admitted` and `suppressed`); or `refuse` at
`["policy"]`, `["admission", i]` or `["receipt"]`.
- `room`: a room. `expect`: `accept` with the room's own outcome (`room_outcome`, `accept` or
`gap`), `meets_profile` (1 or 0), each grant's `thresholds` in the order given, and the
`minor_grants` that took effect, each a grant digest in the merged order; or `refuse` at
`["room", …]` with the room verifier's own place (`["room", "record", c, i]` for record `i` of
chain `c`), `["grant", i]` or `["record", c, i]`.
A verifier that reaches another outcome, reason, place or statement does not conform.
**Provenance.** Generated here: `wai_age_assurance_vectors gen` writes `vectors.json` from
records authored in code, from fixed seeds. Every Ed25519 key is SHA-256 of a fixed label; every
time is a constant (the rooms' `t0` is 2026-10-21T00:00:00Z); every content hash, token nonce,
redemption context and RSASSA-PSS salt is SHA-256 output of a fixed label; and Ed25519 signing is
deterministic, so the file is the same bytes on every machine. The issuer keys are published test
keys whose private keys are published with them: RFC 7517 Appendix A.2's RSA key (threshold 13),
RFC 9500 §2.1's `testRSA2048` (16) and RFC 9578 Appendix A.2's issuer key (18), each written as
RFC 9578 §6.5's SubjectPublicKeyInfo; the generator signs each token's input with the key
directly, the signature the blind protocol would also yield. `rfc9578_a2_vector1` is that RFC's
vector as printed. The challenges over MOQT carry the room's scope of live-rooms §10.4, and the
refused ones the scopes of the revision before it, or a scope broken one way; those over HTTP
carry `room.example`, the corpus's service. The rating schemes, their values and the issuers are `.example` names (RFC
2606) and a `tag:` URI written for the corpus: no scheme here is any classification body's, and
nothing in the corpus states what any content deserves. No recorded data.
```
python3 -I ../age_assurance_verify.py .
```