Skip to main content

WAI Extension: Age Assurance

Mirrored from the canonical text at commit b1681667 ().

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/, 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 §5): a signed record a WAI tool emits and verifies. It names no payload format and is not dispatchable.

Relationship to live-rooms: 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 and deriving-edge: 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 §5) is named by the claim’s record, as an origin claim names it.

Relationship to unidirectional-delivery: 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 and capture-binding: 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 §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 §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:

membertypemeaning
kindstring"wai-rating"
subjectobjectwhat 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)
ratingsarrayone 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
inputsarrayfor 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
record32 B hex or nullthe identity of a record that derived the subject (deriving-edge §5.2), where its deriver signs one; null otherwise
issued_atdate-timewhen 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:

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 §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:

membertypemeaning
kindstring"wai-age-policy"
room32 B hexthe room id
issuersarray1 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_sintegerthe length, in seconds, of the windows an admission receipt counts in (§7), 60 to 86 400
issued_atdate-timewhen 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 §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:

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:

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), 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":

membertypemeaning
kindstring"wai-age-admissions"
room32 B hexthe room id
policy32 B hexthe link hash of the age policy the admissions were made under
start, endintegerthe span, room-clock microseconds, from start up to but not including end; start below end
window_sintegerthe policy’s window_s
cellsarrayone cell for each window, issuer and threshold with at least one admission in the span, each {"window", "issuer", "threshold", "admitted", "suppressed"}
issued_atdate-timewhen 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 §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:

  1. 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 §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

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:

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.

instrumentwhat it asks forwhat 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 persona 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 thresholdblind 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 applicationseveral 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 46double-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 sharedthe 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 feasibleoutside WAI’s records (§8.3)
Act No. 127, 2024, sections 63D, 63DA and 63DBreasonable 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 purposea 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:

reasonraised 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/ 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:

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