open-standards/wai/extensions/age-assurance.md
Transaction Science 94444cfbf6
Some checks failed
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-relay, every target (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Has been cancelled
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Has been cancelled
standards workspaces build + test / which workspaces this change touches (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
WAI conformance / does this change touch it (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Has been cancelled
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
WAI energy receipts / does this change touch it (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / does this change touch it (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Has been cancelled
wai-pack / wai-pack · does this change touch it (pull_request) Has been cancelled
wai-pack / wai-pack · linux (pull_request) Has been cancelled
wai-pack / wai-pack · linux-arm64 (pull_request) Has been cancelled
wai-pack / wai-pack · macos (pull_request) Has been cancelled
wai-pack / wai-pack · independent verifier (Python) (pull_request) Has been cancelled
wai-pack / wai-pack · the floor through a media prober (pull_request) Has been cancelled
wai-pack / wai-pack · dependency licences (pull_request) Has been cancelled
wai-quantum studio and desktop / does this change touch it (pull_request) Has been cancelled
wai-quantum studio and desktop / studio · browser (pull_request) Has been cancelled
wai-quantum studio and desktop / desktop app · macos (pull_request) Has been cancelled
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Has been cancelled
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Has been cancelled
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Has been cancelled
wai-web wasm32 / does this change touch it (pull_request) Has been cancelled
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Has been cancelled
age_serving: groups after the claim's object are sent once the relay holds it (each group is read by a task of its own); the corpus paragraph of §12 states each scope case's reason
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 19:46:46 -04:00

881 lines
61 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# WAI Extension: Age Assurance
> Status: Draft. Registers the record `wai.age.assurance`: content ratings, admission by an age
> threshold without identity, and a profile for rooms in which a minor publishes. A **rating
> claim** (`wai-rating`) says, of an object, a track, a span of a track or a room, what the
> content is rated under one or more rating schemes, each named by its vocabulary URI and each
> stating the minimum age its value means; several national schemes sit side by side, and WAI
> adds none of its own. A claim is carried forward through every derivation: a derived object's
> claim names its inputs' claims, and a scheme an input is rated under is never dropped or
> lowered. A rating that rises for a span holds from the span's first group, whenever the claim
> arrives. A room's **age policy** (`wai-age-policy`), signed by the room authority, names the
> Privacy Pass issuers it admits by (RFC 9576, RFC 9577, RFC 9578), each with **one key per
> threshold**, so a token says "at or over *T*" and nothing else. A relay admits a viewer to a
> rated track by a token of type 0x0002 whose key's threshold is at least the track's minimum
> age, and learns only that threshold and the issuer; tokens are unlinkable to one another and to
> their issuance. A room's service may redeem the token itself over HTTP (RFC 9577) and state the
> threshold in the grant it issues, and a relay is given ratings as a property of the tracks and
> objects it carries. On a bearer with no return path, a rating is a predicate the sink evaluates
> against an age credential it holds, and nothing leaves the device. An **admission receipt**
> (`wai-age-admissions`) counts admissions per threshold and issuer per window, with every count
> under 50 suppressed, so a service shows that it gated without a log of who. A room in which a
> minor publishes meets the **minors' profile**: its roles hold neither the record nor the seek
> power, a role that publishes nothing sends no `chat` and no `reaction`, it is never recorded,
> and a relay's DVR window keeps nothing of it. No new transport and no new cryptography. The
> reference implementation is the `age_assurance` module of `wai-rs` (feature
> `age_assurance`); the corpus is [`live-conformance/age/`](../live-conformance/age/), and its
> second implementation is `live-conformance/age_assurance_verify.py`.
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
>
> Texts read for this revision, each pinned to the version named, and marked where this
> revision verified the text itself and where it relies on a summary:
>
> - RFC 9576 (§3.3, §3.5.1, §4.4, §6.1), RFC 9577 (§2.1.1 the TokenChallenge, §2.1.1.2 its
> redemption context, §2.2.1 the token) and RFC 9578 (§6 the publicly
> verifiable issuance protocol, §6.4 token verification, §6.5 the issuer's key, and Appendix
> A.2, whose key and first vector the corpus carries); RFC 9474; RFC 8017 §8.1.2 and
> Appendix B.2.1; RFC 9500 §2.1 and RFC 7517 Appendix A.2, whose published 2048-bit RSA test
> keys the corpus carries; `draft-ietf-moq-privacy-pass-auth-03` §3.2 to §3.2.4 (the
> scope's grammar), §3.3.1, §3.3.2, §3.4.1, §3.5.1, §3.5.5.2 and §3.5.5.3, an
> Internet-Draft not past Working Group Last Call, and the latest revision the IETF
> datatracker listed on 2026-10-21. Verified.
> - RFC 9577 §2.1.1.1 to §2.1.3 and §2.2.2 (the `PrivateToken` HTTP authentication scheme,
> its server names and its redemption contexts), RFC 9578 §4 (the issuer's directory) and
> §6.1 to §6.3 (issuance) with Appendix A.2's first issuance vector, RFC 9474 §4.2 to §4.4,
> RFC 2104 §2 and RFC 4648 §5, for the binding over HTTP (§5.6). Verified.
> - Commission Recommendation (EU) 2026/1035 of 29 April 2026 on establishing a common
> framework for EU wide Age Verification technologies (OJ L, 2026/1035, 8.5.2026), recitals
> (12) and (13) and points (4) and (5), as the Publications Office serves it. Verified.
> - Communication from the Commission, Guidelines on measures to ensure a high level of
> privacy, safety and security for minors online, pursuant to Article 28(4) of Regulation (EU)
> 2022/2065 (C/2025/5519, OJ C, 10.10.2025), paragraphs 46 and 57. Verified.
> - The consultation *Additional Safety Measures* under the United Kingdom's Online Safety Act
> 2023 (published 30 June 2025, closed 20 October 2025), its chapter 6 and proposed measure
> ICU F3, read from the consultation document. Verified. Whether a statement has since adopted
> the measure was not checked. The Protection of Children Codes of Practice in force under that
> Act from 25 July 2025 were not read for this revision; they are cited from summaries only.
> - Online Safety Amendment (Social Media Minimum Age) Act 2024 (Cth), No. 127, 2024, inserting
> sections 63C to 63F into the Online Safety Act 2021, read as made. Verified. The day section
> 63D took effect, 10 December 2025, is the day of the Minister's instrument under section 63E
> as summaries give it; the instrument itself was not read.
> - ETSI TS 102 796 V1.8.1 (2026-09) Annex A.2.28, for how a scheme fixes the ages its values
> mean. Verified.
>
> A section or paragraph number of one of these texts is that version's. Nothing in this
> extension states that any service meets any of these instruments; §10 maps what each asks for
> onto what WAI's records do.
## 1. Scope
In scope: rating claims, what they cover, how they are carried through a derivation and how a
rating that rises for a span holds (§3); the age policy (§4); admission by a threshold token,
what a relay learns, grants that state a threshold, the rule a relay serves a rated track by,
admission by a room's service over HTTP, and ratings carried over MOQT (§5); the predicate a sink evaluates with no return path, and what a player owes (§6); the
admission receipt and its suppressed counts (§7); the minors' profile, and what a recording and
a DVR window do in such a room (§8); verification (§9); and the mapping onto the instruments
above (§10).
Out of scope: operating an issuer or an attester, and how an attester establishes a person's
age (RFC 9576 §3.5.1 leaves it to the attester, and the instruments above to their schemes); any
identifier of a person; which rating a piece of content deserves; a transport; and a viewer's
device capturing what it displays by means outside the service (§8.3).
Relationship to the core spec: `wai.age.assurance` is a **record** ([SPEC](../SPEC.md) §5): a
signed record a WAI tool emits and verifies. It names no payload format and is not
dispatchable.
Relationship to [live-rooms](live-rooms.md): a room admits a viewer that need not be identified
by a Privacy Pass token (§10.4 there); this extension fixes the issuers and keys of such a token
for a rated room, the rule a relay serves a rated track by, and the grant attribute `age` (§5.4)
that live-rooms §4.4 lists. It reads the attribute `minor` (§4.4 there) for the minors' profile,
which is a condition on a descriptor's role and event bitmaps (§3.3 there) and adds no field.
Relationship to [synthesis](synthesis.md) and [deriving-edge](deriving-edge.md): a rating is
carried forward the way an origin claim is (synthesis §3.4): the derived object's claim names
its inputs' claims by link hash, and the verifier refuses one that drops what they rate. A
derivation record ([deriving-edge](deriving-edge.md) §5) is named by the claim's `record`, as an
origin claim names it.
Relationship to [unidirectional-delivery](unidirectional-delivery.md): the sink's predicate is a
sink-evaluated predicate of §2 there, and the sink's anchors for issuer keys are provisioned as
§3 there has a trust anchor provisioned.
Relationship to [live-recording](live-recording.md) and [capture-binding](capture-binding.md): a
room whose recording policy has `mode` 0 holds no take (capture-binding §3.3) and no recording
package (live-recording §4.4 step 5), and a relay keeps no DVR window of a minors' room
(live-recording §8.2).
## 2. Records
Every record of this extension is a JSON object (RFC 8259) under the encodings of
[jwp-receipts](jwp-receipts.md) §2.4.3: byte strings in lowercase hexadecimal; integers unsigned,
at most 2^53 − 1, in plain decimal; no other number; no boolean; a **term**, an **absolute URI**
and a **date-time** as defined there. A record is read as jwp-receipts §3.1 step 1 reads a
claim: RFC 8259's grammar, no member named twice in one object, at most 32 levels of nesting.
Each record has exactly the members its section lists, no member missing and none added, at any
level, and a `kind` naming it; one that does not is `record_malformed`, except that a member
added to an admission receipt is `receipt_identifies` (§7.3). A record is read whole, by these
rules and its section's, before its signature is verified.
Every record carries `signer_pubkey` (32 bytes, the Ed25519 key of RFC 8032 that signs it) and
`sig` (64 bytes):
```
sig = Ed25519(signer_key, "wai:age-assurance\x01" || JCS(record without "sig"))
link_hash = BLAKE3("wai:age-assurance-link\x01" || JCS(record))
```
JCS is RFC 8785, which on these values reduces to what jwp-receipts §2.4.3 states. The signature
is verified strictly ([SPEC](../SPEC.md) §7 *Signatures*): a key that fails step 1 or 2 is
`bad_key`, and a signature that fails a later step `bad_signature`. A record is named by its
link hash wherever another names it. `issued_at`, where a record carries it, is a date-time, the
instant its signer signed it, self-asserted.
An object's **content hash** is jwp-receipts §2.1's. A track's **digest** is live-rooms §6.2's:
SHA-256 (FIPS 180-4) over the MOQT §8.8 rendering of its namespace, one zero byte, and its name.
An **age** is an integer from 0 to 21, in whole years; 0 means no minimum.
## 3. Rating claims
### 3.1 The claim
A rating claim, `kind` `"wai-rating"`, has these members:
| member | type | meaning |
|---|---|---|
| `kind` | string | `"wai-rating"` |
| `subject` | object | what it rates (§3.3): `{"type": "object", "content_hash"}`, one object by its content hash; `{"type": "track", "track", "from_group"}`, a track by its digest from the group `from_group` on; `{"type": "span", "track", "from_group", "to_group"}`, the groups `from_group` to `to_group` of a track, both included; or `{"type": "room", "room"}`, every track of a room, by its room id (live-rooms §3.2) |
| `ratings` | array | one entry for each scheme the subject is rated under, 1 to 8, each `{"scheme", "value", "min_age"}`: the scheme's vocabulary URI (an absolute URI), the value it gives (1 to 64 bytes of `!` to `~`), and the age that value means under the scheme |
| `inputs` | array | for a derived subject (§3.4), the objects it was derived from, 0 to 16, each `{"content_hash", "rating"}`: the input's content hash, and the link hash of the rating claim it was carried with, or `null` where it carried none; empty otherwise |
| `record` | 32 B hex or `null` | the identity of a record that derived the subject ([deriving-edge](deriving-edge.md) §5.2), where its deriver signs one; `null` otherwise |
| `issued_at` | date-time | when the signer signed it |
| `signer_pubkey`, `sig` | | §2 |
Who signs: the party that rates the content: its publisher, its distributor, a room authority,
a classification body. A claim carries its signer's authority and no more; §3.6 says whose
claims a room honours.
### 3.2 Reading
A claim is `record_malformed` when, besides §2: `subject` is not one of the four forms, a digest
or an id in it is not 32 bytes, or a span's `to_group` is below its `from_group`; `ratings`
holds fewer than 1 or more than 8 entries, or entries not in strictly ascending byte order of
`scheme` (so no scheme twice); a `value` is empty, longer than 64 bytes, or holds a byte outside
`!` to `~`; a `min_age` is above 21; or `inputs` holds more than 16 entries, or one content hash
twice.
The **age** of a claim is the greatest `min_age` among its ratings.
A scheme fixes the ages its values mean, and the claim restates them: under the parental rating
of ETSI EN 300 468, a rating value *r* from 1 to 15 means a minimum age of *r* + 3 years (ETSI
TS 102 796 Annex A.2.28, Table A.4). A verifier holds the claims given to it to one statement per
value: two claims that give one scheme's one value two different ages are refused
`rating_inconsistent`, at the later one.
### 3.3 What a claim covers, and an object's age
A claim **covers** an object *o* that a party holds at position (*X*, *g*), the group *g* of the
track whose digest is *X* (or an object outside any track, at no position), when:
- `object`: its `content_hash` is *o*'s;
- `track`: its `track` is *X* and `from_group` ≤ *g*;
- `span`: its `track` is *X* and `from_group` ≤ *g* ≤ `to_group`;
- `room`: its `room` is the room of *X*, the first field of *X*'s namespace (live-rooms §6.1).
The **age** of *o* under a set of claims is the greatest age of the claims of the set that cover
it, and 0 when none does. Under each scheme, its **label** is the `value` of the claim that
covers it and gives that scheme the greatest age; a party that shows a label shows one of these.
Several schemes sit side by side on one claim and one object, each with its own value, and the
age of an object is the greatest any of them gives. A party that gates by age gates by that
number; WAI picks no scheme over another, and a sink **MAY** show the label of the scheme its
settings name.
### 3.4 Through every derivation
A party that derives an object from others and publishes it signs a rating claim for it whose
`inputs` name each input, with the claim it was carried with. Every derivation of
[synthesis](synthesis.md) §3.4's table carries a rating: an encode, a transcode, a rung of a
ladder, a crop, a composition, an enhancement, a generated frame over its inputs, and a clip of a
recording. A relay that forwards an object derives nothing, and the object's claim holds for it.
A derived claim **carries** its inputs' ratings when, for each input:
1. its `rating`, where it is not `null`, is the link hash of a claim the verifier holds; where
that claim's subject is an object, it is this input's content hash. Otherwise
`rating_unknown`;
2. its `rating` is not `null` where the verifier holds a claim that covers the input as an
object (its subject is the input's content hash): a derivation that names no claim for a
rated input drops it from the chain. Otherwise `rating_dropped`;
3. every claim the verifier holds that covers the input as an object, and the claim its `rating`
names, is **carried**: for each of that claim's ratings, the derived claim rates the same
scheme with a `min_age` at least as great. Otherwise `rating_dropped`.
So a rating can rise along a chain of derivations and never fall, and no scheme an input is rated
under drops out: a composition of a 12-rated and an 18-rated input is rated at least 18 under
each scheme either carries, and a transcode of an 18-rated object that names `null` for its
input, or a lower value, is refused. A derived claim names its inputs' claims by link hash, so a
verifier that holds the chain checks it hop by hop, as synthesis §3.4 has an origin claim
checked.
### 3.5 A rating that rises
A rating can rise partway through a live track: a claim of type `span`, or of type `track` from
a later group, signed while the track runs. Such a claim holds **from its first group**, whenever
it was issued and whenever a party receives it. A party that holds it withholds every group it
covers from every session, sink or presentation its rule does not admit (§5.5, §6.2), from the
instant it holds it: a group of the span delivered live before the claim arrived is not delivered
again below the age, by a FETCH, a join's fill, a DVR window, a recording or a clip. What was
delivered before the claim arrived cannot be recalled, and this extension does not pretend
otherwise; a party that rates a span before its first group is published leaves no such window.
A claim lowers nothing: an object's age is the greatest of the claims that cover it, so a later
claim with a lower age changes nothing, and a span's rise ends at its `to_group`. A track whose
base rating is to fall later is rated by spans.
### 3.6 Who may rate a room's tracks
A relay of a room honours a claim about the room, a track of it, a span of such a track, or an
object served on one, only when it is signed by a room key (the descriptor's authority or
clock, live-rooms §2), or, for a track whose owner is a principal (live-rooms §6.1), by that
principal. A `room` claim is checked first for its room, then for its signer: one naming another
room is refused `wrong_room`, and a claim about the room or a track of it that another key signs
is refused `rating_not_authorised`. A principal therefore raises the rating of its own tracks and
of no one else's; the authority rates the room and any track in it. A relay attributes a `track`
or `span` claim by its track digest and an `object` claim by the object it carries: a claim it
cannot attribute to the room, a track it serves or an object it carries is not about the room,
and it neither honours nor refuses it; such a claim covers nothing the relay serves.
## 4. The age policy
### 4.1 Members
A room that admits by age states which issuers it admits by in an age policy, `kind`
`"wai-age-policy"`, signed by the room authority:
| member | type | meaning |
|---|---|---|
| `kind` | string | `"wai-age-policy"` |
| `room` | 32 B hex | the room id |
| `issuers` | array | 1 to 16 issuers, each `{"name", "token_type", "keys"}`: the issuer name a TokenChallenge carries (RFC 9577 §2.1.1), which a policy holds to 1 to 255 bytes of `!` to `~`, the token type, 2 (0x0002, RFC 9578 §6), and its keys, 1 to 8, each `{"threshold", "key"}`: an age from 1 to 21, and the issuer's public key for it, the DER SubjectPublicKeyInfo of RFC 9578 §6.5 |
| `window_s` | integer | the length, in seconds, of the windows an admission receipt counts in (§7), 60 to 86 400 |
| `issued_at` | date-time | when the authority signed it |
| `signer_pubkey`, `sig` | | §2 |
### 4.2 Rules
A policy is `record_malformed` when, besides §2: the issuers are not in strictly ascending byte
order of `name`; a name breaks §4.1; a `token_type` is not 2; an issuer's keys are not in
strictly ascending order of `threshold`, or a threshold is outside 1 to 21; a key appears twice
in the policy, under one issuer or two; a key does not read as RFC 9578 §6.5 has it: the DER
SubjectPublicKeyInfo whose algorithm is id-RSASSA-PSS with the parameters RFC 9578 Appendix A.2's
key carries (SHA-384, MGF1 with SHA-384, a salt of 48 bytes), byte for byte, and whose key is an
RSAPublicKey of a 2048-bit modulus and a positive exponent; or `window_s` is outside its range.
Then its `room` is the room id (`wrong_room`), its signer is the descriptor's `authority`
(`signer_not_permitted`), and its signature verifies (`bad_key`, `bad_signature`).
**One key per threshold.** An issuer signs a token for threshold *T* only under its key for *T*,
and only after its attester has established that the person it issues to is at least *T* years
old (RFC 9576 §3.5.1: the attester; RFC 9578 §6: the issuance). A key is the threshold it stands
for: no key appears under two thresholds or two issuers, so a token's `token_key_id`, the
SHA-256 of the key (RFC 9578 §6.5), names exactly one issuer and one threshold. An issuer that
offers thresholds 13, 16 and 18 holds three keys, and a person 17 years old can be issued tokens
under the first two.
Several issuers sit in one policy, as several Member States' solutions sit beside one another
under Recommendation (EU) 2026/1035; each issuer's thresholds are its own.
A room has one policy in this revision. A later revision may chain policies, as
[live-moderation](live-moderation.md) §3.4 chains review policies.
## 5. Admission
### 5.1 The challenge
A relay that admits a viewer of a room by age over MOQT sends, in its SETUP, one AUTH
CHALLENGE (`draft-ietf-moq-privacy-pass-auth-03` §3.4.1) for each issuer of the room's
policy, its Challenge a TokenChallenge of RFC 9577 §2.1.1:
- `token_type` 0x0002;
- `issuer_name` the issuer's `name`;
- `redemption_context` the same for each issuer of the session, and one of three:
- **per session**: 32 bytes the relay draws for the session from a cryptographically
secure generator (`draft-ietf-moq-privacy-pass-auth-03` §3.5.5.3);
- **per window**: 32 bytes fixed for the room and a window of time, RFC 9577 §2.1.1.2's
context bound to a time window. With *W* the challenger's window in seconds and *w* =
⌊*u* / *W*⌋ for the Unix time *u* in seconds, the value is the same for every challenge
of the room in window *w*, and cannot be known before *w* begins: the reference
computes HMAC-SHA-256 (RFC 2104) under a 32-byte key it draws at its start from a
cryptographically secure generator and never discloses, over `wai:age-window` and
0x01, the room id (32 bytes) and *w* as 8 bytes big-endian. The challenger accepts the
challenges of windows *w* and *w* − 1, so a token obtained just before a window ends is
good through the next. A client may obtain tokens for the window ahead of the request it
presents them with (RFC 9577 §2.1.4);
- **empty**: the relay accepts a token issued for any session and relies on its record of
redeemed nonces alone;
- `origin_info` the room's scope of live-rooms §10.4: one MoQAuthorizationInfo scope, the
actions SUBSCRIBE_NAMESPACE, SUBSCRIBE, FETCH and TRACK_STATUS on `MATCH_PREFIX` of the
tuple of the room id and on every track name, 78 bytes. The relay holds each track to §5.5.
The challenge names no threshold. The room's policy and its ratings are public, so a client
computes the age a track asks for (§3.3) and presents a token under a key of that threshold or
above (§5.3). The client presents it in its SETUP's AUTHORIZATION TOKEN
(`draft-ietf-moq-privacy-pass-auth-03` §3.5.1).
### 5.2 Checking a token
A relay is given the room's policy, the challenges it sent the session, the nonces of the
tokens it has redeemed under the policy, the token presented, and the age the request asks for.
It refuses at the first of these that fails:
1. The token reads as RFC 9577 §2.2.1 writes one of type 0x0002 (RFC 9578 §6): a `token_type` of
0x0002, then a `nonce`, a `challenge_digest` and a `token_key_id` of 32 bytes each, and an
`authenticator` of 256 bytes; 354 bytes in all (`token_malformed`).
2. Its `challenge_digest` is the SHA-256 of a challenge the relay accepts for the session
(§5.1), and that challenge has the structure of RFC 9577 §2.1.1's TokenChallenge, every field
read as bytes and no byte after `origin_info`, with `token_type` 0x0002, an `issuer_name` of
at least one byte and a `redemption_context` of 0 or 32 bytes (`token_wrong_challenge`). Its
`origin_info` is then the one its binding writes. Over MOQT (§5.1) it is live-rooms §10.4's
scope for the policy's `room`, read as that section reads it: one that does not read as a
MoQAuthorizationInfo is `scope_malformed`, and one that reads and is not that scope,
byte for byte, `scope_mismatch`. Over HTTP (§5.6) it is RFC 9577 §2.1.1's list of server
names: one or more names joined by `,`, each of `!` to `~` other than `,` and `@` (no
userinfo, §2.1.1.1 there), or it is `scope_malformed`; and a name of the list is the
challenger's own, compared without regard to ASCII case (§2.1.3 there), or it is
`scope_mismatch`.
3. Its `token_key_id` is the SHA-256 of a key of the policy's issuer whose `name` is that
challenge's `issuer_name` (`issuer_unknown`).
4. Its `authenticator` verifies under that key: RSASSA-PSS-VERIFY (RFC 8017 §8.1.2) with
SHA-384, MGF1 with SHA-384 and a salt of 48 bytes, over the token's first 98 bytes, as RFC
9578 §6.4 has it (`token_bad_signature`).
5. Its `nonce` is not among the nonces redeemed (`token_replayed`). The relay adds it.
6. The key's threshold is at least the age asked for (`below_threshold`).
A token that passes **admits** the session under that issuer and that threshold. A token
refused at step 6 has been spent all the same (step 5 recorded it): a client presents a token
of the threshold the track asks for.
### 5.3 What a relay learns
From a token that passes, a relay learns the issuer and the threshold of the key that signed it:
"at or over *T*, by issuer *K*". It learns no age but that bound, and nothing that names the
person:
- the issuer signed the token blindly (RFC 9474; RFC 9578 §6.1 to §6.3), so the issuer cannot
tell which of its issuances a redeemed token came from, and the relay cannot ask it;
- the `nonce` is drawn by the client for that token alone, and the `redemption_context` for that
session alone (RFC 9577 §2.2.1, §2.1.1.2), so two presentations by one person share nothing
but the key, and the key is shared by everyone the issuer issued that threshold to;
- the issuer learns no room and no relay: the challenge names the relay's session only by a
random context, and the attester, the issuer and the origin each see only their part (RFC
9576 §3.3, §4.4).
A client that presents a token of a higher threshold than a track asks for tells the relay that
higher bound. A client that holds tokens of several thresholds presents the one the track asks
for, the least bound that admits it.
A relay that admits by age keeps the nonces it redeemed, to refuse a replay, for as long as a
token of the challenges it accepts can be presented: for a context per session, the session's
life; per window, the two windows it accepts (§5.1), each window's nonces dropped when it stops
accepting that window, since a token of it then fails step 2; for an empty context, as long as
the policy's keys are in use. It bounds the nonces it keeps per window or session, and once the
bound is reached it redeems no further token of that window or session until it may drop them:
it never forgets a nonce it must keep, since a nonce forgotten is a token that can be spent
again. The reference keeps at most 65 536 a window: two windows of nonces, 32 bytes each. The
nonces are random values that name no one; the counts of §7 are all a relay needs beyond
them.
### 5.4 Grants that state a threshold
A principal that holds a grant (live-rooms §4) is admitted by its grant, not by a token. Its
authority states the threshold it admitted the principal under in the grant attribute `age`
(live-rooms §4.4): a decimal integer from 1 to 21 with no leading zero, the threshold of a token
the authority redeemed under this extension's §5.2 before it issued the grant. A grant without
`age` admits at 0. A grant whose `age` does not read, or whose `minor` is `true` and whose `age`
is 18 or more, is refused `age_attr_invalid`. A principal's threshold at room-clock *t* is the
`age` of its grant in effect at *t*.
### 5.5 Serving a rated track
A relay serves an object of a room's track to a session only when the session's threshold (§5.2,
§5.4; 0 for a session admitted by neither) is at least the object's age (§3.3) under every claim
the relay holds for the room that §3.6 lets it honour. The rule holds for every way an object
reaches a session: a live subscription, the fill of a SUBSCRIBE, a FETCH, the cache, the DVR
window (live-recording §8). For a request whose start is at or past an object its rule does not
admit, the relay refuses with `REQUEST_ERROR` `UNAUTHORIZED` (MOQT §12.3) and the reason phrase
`below_threshold`. For a live subscription, from the instant it holds a claim that raises a
group's age above the session's threshold, the relay forwards none of that group's objects to
the session, and none of a later group the claim covers.
The rule is a function of the session's threshold, the group and the claims the relay holds when
it serves the object (§3.5): with *T* the threshold and *A* the age of group *g* under them, it
serves when *T* ≥ *A*, and refuses `below_threshold` otherwise. A session's threshold for a room
is the greatest of its admissions there: a token's (§5.2) and the `age` of each grant of the
room it proved (§5.4).
### 5.6 Admission by the room's service (HTTP)
A room's service that issues the room's grants (live-rooms §4) over HTTP may admit by age
itself, as a Privacy Pass origin of RFC 9577, and state the threshold it admitted under in the
grant (§5.4). A relay then reads the threshold from the grant it honours beside its proof
(live-rooms §10.5).
1. **The challenge.** To a request for a grant that carries no token, in a room with an age
policy, the service answers `401 Unauthorized` with one `WWW-Authenticate`
challenge of the scheme `PrivateToken` (RFC 9577 §2.1.2) for each key of the policy, in
ascending order of threshold: `challenge` the TokenChallenge of the key's issuer and
`token-key` the key, its SubjectPublicKeyInfo (§4.1), each in base64url with padding
(RFC 4648 §5), and `max-age` the seconds for which the service accepts the challenge. The
TokenChallenge is §5.1's with a window context, which names the room, and an `origin_info`
of the service's own server name (RFC 9577 §2.1.1.1), as the client addressed it: over
HTTP `origin_info` is RFC 9577's list of origins, so the room is bound by the context.
2. **The token.** The client presents one token, `Authorization: PrivateToken token="…"`
(RFC 9577 §2.2.2), on the request it repeats. The service checks it by §5.2 against the
room's policy, the challenges it accepts at that instant, the nonces it keeps (§5.3) and
the room's age: the greatest age of the claims it holds whose subject is the room. A token
that passes admits the request: the service issues the grant with `age` the threshold of
the key that signed the token. A token that does not is answered `403 Forbidden` with its
reason, and the challenges again.
3. **What it counts.** Each admission, at its instant on the room clock, under its issuer and
threshold, for the receipts of §7, and nothing else of it.
A client chooses, among the challenges, the least threshold at or above the room's age it can
obtain (§5.3), checks the challenge as RFC 9577 §2.1.3 has it, and obtains the token from the
issuer by RFC 9578 §6, at the request URL of the issuer's directory (RFC 9578 §4). A
publisher's grant is requested with a token as a viewer's is: every track of the room is held
to its rating (§3.3), the room's events and the other publishers' tracks among them, and a
holder admitted at no threshold would be served none of them.
What the relay learns is what a token would tell it: the grant names a key the client made for
the visit (live-rooms §13) and states "at or over *T*"; the service learns the threshold and
the issuer, and the address any HTTP server sees.
### 5.7 Ratings over MOQT
A rating claim rides MOQT as the property `WAI_RATING`, type `0x3A4D`, in the range MOQT §8.4
leaves to applications and odd, so that its value is bytes: the claim's text, as §2 writes a
record. It is a Track Property of a PUBLISH or a SUBSCRIBE_OK (MOQT §10), and an Object
Property of any object, and may appear more than once. A relay reads each `WAI_RATING` of a
track it carries and of each object it forwards, and from that instant holds each claim that
reads, verifies and §3.6 lets it honour; one it cannot attribute it passes over (§3.6). It
forwards the property as it forwards any other, and neither strips nor adds one.
A room's service gives the room's claims, signed by a room key, to each principal it grants,
and each publisher carries them on every track it publishes, so a relay holds them from the
PUBLISH that opens the track; the room's clock carries them on its events track (live-events
§8.1) too. A principal carries its claims about its own tracks on those tracks, and a claim
that rises during a track rides an object of it from then on. A relay that holds no claim of a
room serves its objects at age 0.
## 6. No return path
On a bearer with no return path ([unidirectional-delivery](unidirectional-delivery.md)), no
relay admits anyone: every sink receives every byte. A rating is then a **sink-evaluated
predicate** (unidirectional-delivery §2) of the object and the sink's **age state**.
### 6.1 The age state
A sink's **anchor** is a list of issuer keys provisioned on the device out of band, as a trust
anchor is (unidirectional-delivery §3), each `{issuer name, threshold, key}`. A sink's
**credentials** are tokens of type 0x0002 that issuers signed for the device. A credential is
issued for the **device challenge** of its issuer: the TokenChallenge of RFC 9577 §2.1.1 with
`token_type` 0x0002, the issuer's `issuer_name`, an empty `redemption_context` and an empty
`origin_info`, so that it names no origin and no session. A credential is never presented: the
device holds it and checks it, and spends nothing.
A credential **holds** under the anchor when it reads (§5.2 step 1), its `challenge_digest` is
the SHA-256 of the device challenge of an issuer the anchor names, its `token_key_id` is the
SHA-256 of a key the anchor lists for that issuer, and its `authenticator` verifies under that
key (§5.2 step 4). The sink's **age state** is the greatest threshold of a credential that holds,
and 0 when none holds. It is the predicate's state class (unidirectional-delivery §2).
### 6.2 The predicate
For an object *o* the sink holds, at its position: **present** when the sink's age state is at
least *o*'s age under every rating claim the sink holds that reads and verifies (§2, §3.2) and
covers *o* (§3.3); **withhold** otherwise. A claim can only raise an age, so a sink honours every
claim that verifies, whoever signed it, and passes over one that does not read or verify. A track
of a bearer that is not a room's track has no room, and a `room` claim covers nothing on it. Two
conforming sinks holding the same claims and the same credentials reach the same verdict on the
same object.
### 6.3 What a player owes
A player that presents media of a bearer with no return path:
1. evaluates the predicate for each object before it presents it, and presents none whose verdict
is withhold;
2. evaluates it again for every object it still holds (in a buffer, a time-shift window or a
recording on the device) each time it receives a rating claim, so that a span is withheld from
its first group (§3.5);
3. **MUST NOT** cause a request in evaluating it, and sends its verdict, its age state and its
credentials nowhere (unidirectional-delivery §2);
4. presents, with no credential that holds, only objects whose age is 0.
The predicate is deterministic and auditable, and enforces nothing against a sink that lies about
its state (unidirectional-delivery §2). What it holds to is narrow: the source learns nothing, the
device's age state never leaves the device, and two sinks with the same state agree.
## 7. Evidence without identity
### 7.1 The admission receipt
A room key (live-rooms §2) signs, for a span of the room clock, an admission receipt, `kind`
`"wai-age-admissions"`:
| member | type | meaning |
|---|---|---|
| `kind` | string | `"wai-age-admissions"` |
| `room` | 32 B hex | the room id |
| `policy` | 32 B hex | the link hash of the age policy the admissions were made under |
| `start`, `end` | integer | the span, room-clock microseconds, from `start` up to but not including `end`; `start` below `end` |
| `window_s` | integer | the policy's `window_s` |
| `cells` | array | one cell for each window, issuer and threshold with at least one admission in the span, each `{"window", "issuer", "threshold", "admitted", "suppressed"}` |
| `issued_at` | date-time | when it was signed |
| `signer_pubkey`, `sig` | | §2 |
A **window** *w* is the room-clock microseconds from *w* × `window_s` × 10^6 up to but not
including (*w* + 1) × `window_s` × 10^6. An admission at room-clock *t* is in window ⌊*t* /
(`window_s` × 10^6)⌋.
### 7.2 Cells and suppression
A cell counts, in its `admitted`, the sessions admitted by a token (§5.2) in its window under its
issuer and its threshold. A cell of fewer than 50 admissions is **suppressed**: its `suppressed`
is 1 and its `admitted` 0, as live-metering §4.7 suppresses a QoE cell under
the rule of delivery-telemetry §8 (its paragraph 8.4), suppressed and not rounded. Otherwise
`suppressed` is 0 and `admitted` the count.
A receipt holds no entry for a session, no token, no nonce, no key of a principal and no address:
its members are the room, the policy, the span and the cells, and a cell is a count. A
suppressed cell says that between 1 and 49 sessions were admitted at that threshold by that
issuer in that window, and no more.
### 7.3 Reading and checking
A receipt is read as §2 reads a record, with one change: once its text reads as JSON under §2's
grammar (RFC 8259, no member named twice, at most 32 levels), a receipt that holds a member §7.1
does not list, at its top or in a cell that is an object, is refused `receipt_identifies`, before
its other members are read. A member beyond the table is a place a session's identity could
ride, and a verifier cannot tell an identifier from any other value, so it refuses the place. It
is then `record_malformed` when, besides §2: `start` is not below `end`; `window_s` is 0; the
cells are not in strictly ascending order of `window`, then `issuer` by bytes, then `threshold`; a
cell's window does not overlap the span; a `suppressed` is not 0 or 1; or a suppressed cell's
`admitted` is not 0.
Then: its `room` is the room id (`wrong_room`); its signer is a room key
(`signer_not_permitted`); its signature verifies (`bad_key`, `bad_signature`); its `policy` is
the link hash of the policy the verifier is given, its `window_s` is that policy's, and each
cell's issuer and threshold are a key of it (`receipt_mismatch`); and no cell that is not
suppressed has fewer than 50 admissions (`count_not_suppressed`).
A verifier that is given the admissions too (an auditor holding a relay's redemptions for a
window, or the corpus), each an instant, the challenges the relay sent that session and the token
that admitted it (a token refused at §5.2 step 6 admitted no one, and is not among them), checks
each by §5.2 steps 1 to 5 in the order given, the nonces of the earlier ones redeemed (its
refusal, at that admission); counts each admission whose instant is in the span in its cell, and
none other; and, after every check above, requires the receipt's cells to be exactly the cells
§7.2 writes from those counts (`count_mismatch`).
### 7.4 What a receipt shows
A receipt shows, under the room key's signature, that the room admitted by age under the policy
it names, and how many sessions it admitted at each threshold by each issuer in each window, above
the floor. With the room's ratings (§3) and its policy, it shows which ages each track asked for
and which thresholds were presented for them. It does not show who was admitted, and nothing a
service keeps under this extension does: a token names no one (§5.3), and a receipt counts.
Where a regime asks the operator of a service for the lawful basis on which it processes the
age-related attributes it receives, the operator states it in a `lawful-basis` declaration
([jwp-receipts](jwp-receipts.md) §2.4.6) whose subject is the receipt's link hash; this extension
registers no basis of its own.
## 8. The minors' profile
### 8.1 The profile
A room descriptor (live-rooms §3) **meets the minors' profile**, `minors`, when:
1. its recording policy's `mode` is 0: the room is never recorded (live-rooms §3.7);
2. no role's `powers` sets bit 3 (record) or bit 4 (seek) (live-rooms §3.3);
3. no role whose `publish` is 0 permits the event kind `chat` (`0x03`) or `reaction` (`0x04`)
in its `events` (live-events §4.4, §4.5).
and its grants meet it when:
4. no grant whose `publish` is 0 permits `chat` or `reaction` in its `policy`.
The profile adds no field and no bit: the role bitmaps, the grants' capability sets and policies
and the recording policy already express it, and this section names it. Every grant is within its
role (live-rooms §4.3 step 7), so in a room that meets rules 1 to 3 no grant holds the record or
the seek power. Rule 4 is a rule of grants and not only of roles: a grant may narrow a publishing
role's `publish` to 0 and keep the role's `chat` (live-rooms §4.3 step 7 lets it), and that grant's
principal publishes nothing and could still comment. A viewer admitted by a token holds no grant,
sends no event and does not seek back (live-rooms §10.4, §10.6).
### 8.2 When a room meets it
A grant whose `publish` is not 0 and whose `attrs` hold `minor` with the value `true` is a
**minor's publishing grant**: the room authority marks its principal a minor (live-rooms §4.4)
and lets it publish. A room in which a minor's publishing grant takes effect, by a `join` naming
it or a `grant_exchange` to it (live-rooms §7.2), **MUST** meet the profile, its descriptor and
every one of its grants: a room verifier under this extension, at the first such `join` or
`grant_exchange` in the merged order, refuses it `minor_profile_unmet`, at its record, when the
descriptor breaks rule 1, 2 or 3, and otherwise refuses the first grant, in the order given, that
breaks rule 4 `minor_profile_unmet`, at that grant. The grants a verifier is given are the room's
own, so a grant the room holds is held to rule 4 whenever it takes effect.
The descriptor is fixed when the room is made, so a room service that issues a minor's publishing
grant issues it only in a room whose descriptor meets the profile. A room that does not meet it
holds no minor publisher under this extension. A room that records a minor's take under a
guardian's consent ([capture-binding](capture-binding.md) §9.4) has a recording policy whose
`mode` is not 0, so it does not meet the profile; capture-binding's verifier is unchanged, and
such a room is outside what this extension admits.
### 8.3 What the profile removes
- **Comments and reactions.** A subscriber that publishes nothing sends no `chat` and no
`reaction`: its grant does not permit them (§8.1 rules 3 and 4), and a relay forwards no event a
room verifier refuses (`kind_not_permitted`, live-rooms §9.2 step 4).
- **Gifts.** WAI defines none: no event kind and no power of live-rooms moves funds from one
principal to another, so there is nothing to remove. A gift a deployment carries outside WAI's
records is outside this profile.
- **Recording and clips.** No role holds the record power, so no `record_start` is accepted
(live-rooms §9.2 step 5); the recording policy's `mode` is 0, so no take is permitted
(capture-binding §3.3) and no recording package is accepted (live-recording §4.4 step 5). A clip
is cut from a recording or fetched from a DVR window (live-recording §5, §8.1); there is no
recording, no role seeks back below a live edge (live-rooms §10.6), and the DVR keeps nothing
(§8.4), so there is nothing to cut one from.
- **Capture outside the service.** A device that shows a stream can capture what it shows by
means outside the service. WAI's records do not reach them.
### 8.4 The DVR window
A relay keeps nothing of a minors' room beyond delivery. It knows such a room by a minor's
publishing grant: a session whose SETUP proved possession (live-rooms §10.5) of a grant signed by
an authority the relay trusts, whose `publish` is not 0 and whose `minor` is `true`. From then on
it keeps nothing of the room's tracks in its DVR window, deletes what it kept of them, and
remembers the room across a restart, exactly as live-recording §8.2 has it do for a room whose
objects are sealed. It still delivers the room live, and still honours the moderation decisions
it reads for what it delivers. The rule is the relay's: a room service cannot keep a relay from
storing what it carries, and the relay already reads every grant presented to it.
## 9. Verification
### 9.1 Inputs and outcomes
A verifier under this extension checks one case at a time. Each case is one of these, and gives
**accept** with what the case states, or **refuse** with a reason and where:
- **ratings** (§3): rating claims. Each claim reads and verifies (§2, §3.2), in the order given;
then the claims are consistent (§3.2), the first that gives a value a second age refused; then
each claim with inputs, in the order given, carries them (§3.4), input by input and, for each
input, rule by rule, against every claim of the case. It states each claim's link hash, signer,
subject type, age and whether it is derived. At `["claim", i]`.
- **admission** (§4, §5.2): a room descriptor, an age policy, the challenges, the nonces
redeemed, a token, the age asked for and the binding: `origin` is `null` over MOQT (§5.1),
or the challenger's server name over HTTP (§5.6). The policy reads (§4.2), at
`["policy"]`; the token is checked (§5.2), at `["token"]`. It states the issuer, the
threshold and the key id admitted.
- **serving** (§3.5, §3.6, §5.5): a room descriptor, a track of the room by its namespace and
name, rating claims, and requests, each a threshold, a group and the claims the relay holds when
it serves it. Each claim, in the order given, reads, verifies and is not refused by §3.6, at
`["claim", i]`; then each request is served or refused `below_threshold`, and is stated with the
age of its group. It states the track's digest.
- **predicate** (§6): an anchor, credentials, rating claims and objects, each at a position. It
states the sink's age state and, for each object, its age and its verdict, `present` or
`withhold`. Nothing in it is refused: a credential that does not hold and a claim that does not
read or verify are passed over (§6.1, §6.2).
- **receipt** (§7): a room descriptor, an age policy, a receipt, and the admissions or none. The
policy reads, at `["policy"]`; then each admission is checked, at `["admission", i]`; then the
receipt is read and checked by §7.3 in its order, the recount last, at `["receipt"]`. It states
its link hash and its cells.
- **room** (§5.4, §8): a room as live-rooms §9.1 gives one. The room verifier's refusal is the
case's, at `["room", …]` with its own place, whatever record it refuses; then each grant's `age`
reads (§5.4), in the order given, at `["grant", i]`; then the profile is held (§8.2), at
`["record", c, i]` or `["grant", i]`. It states the room verifier's own outcome, whether the
descriptor meets rules 1 to 3 of the profile, each grant's threshold, and each minor's
publishing grant each time one takes effect, in the merged order.
### 9.2 What a verifier does not decide
A verifier does not decide whether a rating is right for its content, whether an issuer's
attester established an age, or whether a service meets an instrument. It decides that the
records are what they say, that a derivation carried its inputs' ratings, that a token admits
what its key stands for, that a relay's rule serves only what it admits, that a receipt counts and
suppresses as §7 has it, and that a room in which a minor publishes is held to the profile.
## 10. Mapping onto the instruments
Informative. What each text asks for, and the part of this extension that answers it.
| instrument | what it asks for | what WAI does |
|---|---|---|
| Recommendation (EU) 2026/1035, recital (12) | selective disclosure, by default a true or false answer to the age-related question the relying party asks, with no more about the person | a key per threshold (§4.2): a token answers "at or over *T*" and nothing else (§5.3) |
| Recommendation (EU) 2026/1035, recital (13); point (4)(e) | confirm only that a user is over a certain age, without revealing other information to the proof provider or the service; age verification determines whether a user meets a given age threshold | blind issuance and a fresh nonce per token (§5.3); the issuer learns no room, the relay no person |
| Recommendation (EU) 2026/1035, point (5) | Member States make a solution available by 31 December 2026, in a European Digital Identity Wallet or as a stand-alone application | several issuers in one policy (§4.2); how a wallet or an application presents a proof of age to an issuer's attester is outside WAI (§1) |
| Guidelines C/2025/5519, paragraph 46 | double-blind age verification: the platform learns only whether the threshold is met, and the age verification provider learns nothing of the services it is used for | §5.3 |
| Guidelines C/2025/5519, paragraph 57(b)(i) and (ii) | by default, interaction with a minor only by accounts the minor accepted; no account can download or capture content a minor shared | the minors' profile: no `chat` or `reaction` from principals that publish nothing, no record or seek power, no recording, no DVR window (§8) |
| Consultation *Additional Safety Measures*, measure ICU F3 (a) to (d) | for a one-to-many livestream by a child: no comments, no gifts, no reactions, no in-service capture or recording | §8.3, item for item: `chat`, none in WAI, `reaction`, and record, seek, recording, clips and the DVR |
| Consultation *Additional Safety Measures*, measure ICU F3 (e) | capture by tools outside the service, where technically feasible | outside WAI's records (§8.3) |
| Act No. 127, 2024, sections 63D, 63DA and 63DB | reasonable steps to keep users under 16 from holding accounts; no information of a kind the legislative rules specify, and no government-issued identification material unless an alternative is offered, collected for that purpose | a threshold of 16 (§4.2); the service receives a token, which carries no identification material, and learns a threshold and an issuer (§5.3); a receipt counts (§7) |
## 11. Reasons
The codes this extension adds, each with where it is raised:
| reason | raised at |
|---|---|
| `record_malformed` | §2, §3.2, §4.2, §7.3 |
| `signer_not_permitted` | §4.2, §7.3 |
| `rating_inconsistent` | §3.2 |
| `rating_unknown` | §3.4 rule 1 |
| `rating_dropped` | §3.4 rules 2 and 3 |
| `rating_not_authorised` | §3.6 |
| `token_malformed` | §5.2 step 1 |
| `token_wrong_challenge` | §5.2 step 2 |
| `scope_malformed` | §5.2 step 2; live-rooms §10.4 |
| `scope_mismatch` | §5.2 step 2; live-rooms §10.4 |
| `issuer_unknown` | §5.2 step 3 |
| `token_bad_signature` | §5.2 step 4 |
| `token_replayed` | §5.2 step 5 |
| `below_threshold` | §5.2 step 6, §5.5 |
| `age_attr_invalid` | §5.4 |
| `receipt_identifies` | §2, §7.3 |
| `receipt_mismatch` | §7.3 |
| `count_not_suppressed` | §7.3 |
| `count_mismatch` | §7.3 |
| `minor_profile_unmet` | §8.2 |
A verifier under this extension also raises `bad_key`, `bad_signature` and `wrong_room` (live-events
§9) for a record's signature and its room, and every code a room verifier raises (live-rooms §11
and the live-events codes it lists), at the room. A recording verifier raises capture-binding's
`recording_not_permitted` for a package of a room never recorded (live-recording §4.4 step 5).
## 12. Conformance
[`live-conformance/age/`](../live-conformance/age/) holds the corpus, in one file,
`vectors.json`, written from fixed seeds by `wai_age_assurance_vectors gen`. Its issuer keys are
published test keys: RFC 9578 Appendix A.2's, RFC 9500 §2.1's `testRSA2048` and RFC 7517
Appendix A.2's RSA key, each written as RFC 9578 §6.5's SubjectPublicKeyInfo; with their private
keys published, they are test keys and nothing else. It holds RFC 9578 Appendix A.2's first token
vector, which verifies under its key, so a verifier checks its RSASSA-PSS against the published
bytes, and that vector's issuance: the client's nonce, blind and salt, its TokenRequest and the
issuer's TokenResponse, which a verifier checks against the token (with *s* its authenticator and
*r* the blind, the blinded message is (*s*·*r*)^e mod *n* and the response *s*·*r* mod *n*) and
a client of RFC 9578 §6 reproduces. It has:
- **Ratings**: a programme rated under two schemes; a transcode of it, and a composition of the
transcode with a 12-rated trailer, each carrying both schemes at the programme's ages; and a
clip of the composition rated higher, under a third scheme too. Refused: a transcode that names
no claim for its rated input, one that lowers a scheme's age and one that drops a scheme, a
composition that drops a scheme, a transcode that carries the claim it names and drops a scheme
of a second claim the verifier holds about its input (`rating_dropped`); a transcode naming a
claim the verifier does not hold and one naming another object's claim (`rating_unknown`); two
claims that give one value two ages (`rating_inconsistent`); claims that do not read: schemes
out of order, an age above 21, a span that ends before it starts, a value holding a space, a
scheme that is not an absolute URI, a member added, a member named twice, a boolean, a date
that does not exist, a content hash of 31 bytes, no rating, an input named twice and the kind of
another record (`record_malformed`); a claim signed under the
identity point (`bad_key`); and a claim whose signature fails.
- **Admissions**: under a policy of two issuers, one holding keys for 13 and 16 and one a key for
18: a token of each key admitted where its threshold is asked, a token of 18 admitted where 16
is asked, and a token of 13 on a track of age 0. Refused: a token from the 13 key presented
where 16 is asked and one from the 16 key where 18 is asked (`below_threshold`); a token
presented twice; a token issued for another session's challenge, and one whose challenge names
type 0x0001; challenges whose `origin_info` the binding does not admit: over MOQT, the first
three scopes of the revision before this one, the scope of another room and an empty
`origin_info` (`scope_mismatch`), and all four of those scopes, their length past 255 written
in its one byte, an action outside the draft's table, a byte after the scope and a list of
server names (`scope_malformed`); over HTTP, a token admitted under the service's own name and
under a list holding it in another case, and refused under another origin's name and an empty
list (`scope_mismatch`) and under the MOQT scope (`scope_malformed`); a token under a key the
policy does not hold, and one named by one issuer's challenge under the other's key
(`issuer_unknown`); a token whose authenticator was altered; tokens that do not read (one byte short, of type 0x0001); and policies that break §4.1 or §4.2:
no issuer, a window of 30 seconds, a threshold of 22, a key of 1024 bits (RFC 9500's `testRSA1024`), issuers or
thresholds out of order, one key under two issuers, a key whose algorithm is not
id-RSASSA-PSS, a token type of 1, a policy signed by the clock, one of another room and one
whose signature fails.
- **Serving**: a room rated 13 whose performer's track rises to 18 for groups 40 to 59, a span
the authority rates after group 40 went out, and from group 80 on to 16, as the performer rates
its own track; requests within, before and after the span and the rise, at thresholds 0, 13, 16
and 18, before and after the relay holds each claim; and claims the relay cannot attribute to
the room, a viewer's about a track of another room and a stranger's about an object it does
not carry, which cover nothing. Refused: a claim about the room signed by a
viewer and one signed by the performer, a span of the performer's track rated by a viewer and
one rated by the director (`rating_not_authorised`), and a room claim naming another room.
- **Predicates**: over a broadcast track rated 13 with a span rated 18, a feature rated 16 and an
unrated object: a sink holding credentials of 13 and 16, one of 18, one with none, one whose
only credential is under a key its anchor does not list, one whose 16 credential was issued for
a relay session's challenge, one whose 16 credential's authenticator was altered, one whose
anchor lists the 16 key under the other issuer, and the first sink before the span's claim
arrives.
- **Receipts**: a receipt over two windows, 50 admissions at 16 counted and 7 at 13 and 3 at 18
suppressed, recomputed from the admissions; the same checked without them; the same signed by
the clock; and a receipt over the first window alone, the admissions outside its span not
counted. Refused: a cell carrying a session's nonce and a receipt naming a principal at its
top (`receipt_identifies`); a cell of 7 admissions stated, not suppressed; a cell stated as 51
and a receipt that leaves a cell out (`count_mismatch`); a receipt naming another policy and a
cell of an issuer the policy does not hold and a receipt counting in windows other than the
policy's (`receipt_mismatch`); a receipt of another room; a receipt signed by a viewer; one whose
signature fails; receipts that do not read: a suppressed cell that states a count, a
`suppressed` of 2, cells out of
order, a cell outside the span, a span that ends where it starts, and one that ends where it
starts with no cell; and admissions in which one token is redeemed twice.
- **Rooms**: a minors' room in which a minor performs and two viewers, one with the threshold 13,
watch and send nothing; a room that does not meet the profile and holds no minor. Refused: a
viewer of the minors' room sending a chat its grant does not permit (`kind_not_permitted`, at the
room); a minor publishing in a room whose director holds the record power, whose viewers hold the
seek power (a clip), whose viewers may chat, whose viewers may react, and that is always
recorded, a viewer exchanged to a minor's publishing grant in a room whose viewers chat, and a
minor publishing beside a grant narrowed to publish nothing that keeps its role's `chat`, and
one beside such a grant that keeps `reaction` (`minor_profile_unmet`); a grant whose `age` is written with a leading zero and a minor's grant
stating 18 (`age_attr_invalid`).
A conforming verifier gives every case the outcome, the reason, the place and everything else the
corpus states. `wai_age_assurance_vectors verify` and the test `tests/age_assurance.rs` check that
the file regenerates byte for byte, that `wai-rs` gives every case its outcome, that every reason
of §11 refuses a case, and that §11 lists exactly the reference's codes.
`live-conformance/age_assurance_verify.py` is the second implementation, written from this text:
it shares no code with `wai-rs`; it verifies each room with the live-rooms corpus's second
implementation (`live_rooms_verify.py`), every Ed25519 signature with WAI's strict rule
(`claim-conformance/_ed25519.py`), every link hash with BLAKE3 written from its specification
(`claim-conformance/_blake3.py`), and every token's RSASSA-PSS in its own arithmetic; and it
mutates copies of the corpus and requires every copy to fail.
## 13. Security and privacy considerations
- **What a token reveals.** A token's key is its threshold, so a relay learns "at or over *T*"
from it and a bound from nothing else; the client chooses which bound to show (§5.3). Where few
people present one threshold in one window, the threshold together with a time and an address
the relay sees anyway narrows who they are; a relay that logs addresses beside admissions undoes
§5.3, and the receipt's suppression (§7.2) is there so that what is published does not do the
same.
- **What an issuer reveals.** An issuer's attester establishes a person's age and sees the
person; the issuer signs blindly and sees neither the token nor the room. Which attester and
which proof of age an issuer accepts are the issuer's, and the instruments' schemes (§10), and
this extension names none.
- **Issuer keys.** A policy pins each issuer's keys, so a relay verifies a token with no request
to the issuer: type 0x0002 is publicly verifiable (RFC 9578 §6). An issuer that rotates a key
rotates the policy; a room's policy is replaced, not amended (§4.2). A key that signs for two
thresholds would let a token stand for either, which is why no key appears twice.
- **Double spending.** A nonce is redeemed once under a policy (§5.2 step 5); with a fresh
`redemption_context` per session, a token is good for one session alone, and a stolen token
admits one session at its threshold. With a context per window a token is good at one
challenger, for its room, in two windows at most, and is spent at the first presentation;
challengers that share a window's challenges share its nonces (RFC 9577 §2.1.1.2), which the
reference avoids by keying its contexts with a key of its own. A challenger whose nonce store
is full refuses tokens rather than forget a nonce (§5.3).
- **Ratings.** A claim raises and never lowers, so a forged claim can withhold content but never
admit it. Within a room only room keys and a track's owner are honoured (§3.6), so a viewer
cannot withhold another's track.
- **What was delivered stays delivered.** A span rated after its first group went out holds from
then on (§3.5); nothing recalls bytes already presented, and a party that must not show a span
rates it before publishing it.
- **The sink's state.** On a bearer with no return path a sink can lie about its age state, and
nothing here prevents it (unidirectional-delivery §2). Its credentials and its verdicts never
leave the device (§6.3).
- **Minors' rooms.** A relay knows a minors' room only once a minor's publishing grant is
presented to it (§8.4); a room service issues such a grant only in a room that meets the profile
(§8.2), so the room's own records keep the profile from its first instant, and the DVR keeps
nothing from the relay's.