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]>
911 lines
63 KiB
Markdown
911 lines
63 KiB
Markdown
# 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).
|
||
|
||
**The room's own service.** A session that proves possession of a room key (live-rooms §2: the
|
||
authority's key or the clock's) is the room's own service: its clock, which follows each
|
||
participant's tracks to sign the lifecycle and, with co-performance, logs every performer's
|
||
objects and signs its marks (co-performance §8). The rule does not hold such a session in that
|
||
room: a relay serves it every object of the room's tracks, whatever their age. The proof is the
|
||
only way a session becomes the room's service; a name, a namespace, a token or a flag the client
|
||
sets never does.
|
||
|
||
1. **The proof.** The session's SETUP carries a `GRANT_PROOF` (live-rooms §10.5.2) whose `grant`
|
||
is 32 bytes of zero and whose `key` is the room key, over the session's challenge with the
|
||
verb `SETUP`. A service that holds both room keys may present a proof by each.
|
||
2. **The check.** The relay checks it as live-rooms §10.5.4 checks a proof, with 32 bytes of
|
||
zero for the grant's digest and the proof's own `key` for the principal. A proof that does
|
||
not pass is refused with that section's reason (`pop_malformed`, `pop_wrong_grant`,
|
||
`pop_bad_signature`, `pop_wrong_challenge`, `pop_wrong_context`), and the session with it,
|
||
as a grant's proof that does not pass refuses it (`UNAUTHORIZED`, MOQT §12.2).
|
||
3. **The key.** A proof that passes makes the session the room's service only where its `key`
|
||
is a room key of the room as the relay holds it: an authority it honours for the room, or a
|
||
room key it is configured with. A proof that passes by any other key gives the session
|
||
nothing: it is served by its threshold.
|
||
|
||
The exemption is the room's, not a viewer's: a room key is held by the room's service, never
|
||
issued to a principal, and a session that holds one already signs the room's own records.
|
||
|
||
### 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; and live-rooms §10.5.4's reasons for a session's
|
||
proof of a room key (§5.5). 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. The room's own service: sessions proving the clock's key and
|
||
the authority's, served every group at threshold 0; a session with no proof, and one proving
|
||
the performer's key, served by their thresholds; and proofs refused, at any threshold, with
|
||
live-rooms §10.5.4's reasons: the clock's key named under the director's signature, a proof
|
||
over another session's challenge, one for another relay, one naming a grant and one cut short.
|
||
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.
|