open-standards/wai/extensions/live-rooms.md
Transaction Science 00866d4bd4
Some checks failed
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 23s
wai-app and Create / wai-app · does this change touch it (push) Successful in 23s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 25s
joule-moq-http live HTTP egress / joule-moq-http · does this change touch it (push) Successful in 29s
wai-sink-android native sink library / does this change touch it (push) Successful in 18s
WAI Studio / does this change touch it (push) Successful in 22s
wai-sink-swift native sink package / does this change touch it (push) Successful in 24s
wai-ledger service / does this change touch it (push) Successful in 1m27s
wai-audit collector and player integration / does this change touch it (push) Successful in 1m37s
wai-quantum for JavaScript / does this change touch it (push) Successful in 1m32s
wai-fleet service / does this change touch it (push) Successful in 1m33s
wai-pack / wai-pack · does this change touch it (push) Successful in 1m31s
WAI operation catalogue / does this change touch it (push) Successful in 1m34s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 1m48s
wai-quantum studio and desktop / does this change touch it (push) Successful in 1m32s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Has been cancelled
WAI Studio / Studio in Chromium and Firefox · macos (push) Has been cancelled
WAI Studio / Studio in WebKit · linux (push) Has been cancelled
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Has been cancelled
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Has been cancelled
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Has been cancelled
wai-quantum studio and desktop / studio · browser (push) Has been cancelled
wai-quantum studio and desktop / desktop app · macos (push) Has been cancelled
wai-quantum studio and desktop / desktop app · linux (push) Has been cancelled
wai-quantum studio and desktop / desktop app · windows (push) Has been cancelled
wai-quantum for JavaScript / javascript · linux (push) Has been cancelled
wai-pack / wai-pack · linux (push) Has been cancelled
wai-pack / wai-pack · linux-arm64 (push) Has been cancelled
wai-pack / wai-pack · macos (push) Has been cancelled
wai-pack / wai-pack · independent verifier (Python) (push) Has been cancelled
wai-pack / wai-pack · the floor through a media prober (push) Has been cancelled
wai-pack / wai-pack · dependency licences (push) Has been cancelled
wai-ledger service / service · linux (push) Has been cancelled
wai-ledger service / service · macos (push) Has been cancelled
wai-fleet service / service · linux (push) Has been cancelled
wai-fleet service / service · macos (push) Has been cancelled
WAI examples / WAI examples · does this change touch them (push) Has been cancelled
WAI examples / WAI examples · portable · linux (push) Has been cancelled
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (push) Has been cancelled
WAI examples / WAI examples · android-app · linux-arm64 (push) Has been cancelled
WAI operation catalogue / the catalogue and every door · linux (push) Has been cancelled
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Has been cancelled
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Has been cancelled
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Has been cancelled
wai-audit collector and player integration / collector · linux (push) Has been cancelled
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Has been cancelled
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Has been cancelled
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Has been cancelled
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (push) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Has been cancelled
joule-moq-http live HTTP egress / joule-moq-http, every target (push) Has been cancelled
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Has been cancelled
joule-code conformance gate / joule-moq-relay, every target (push) Has been cancelled
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Has been cancelled
joule-code conformance gate / joule-moq-ingest, every target (push) Has been cancelled
live-rooms: the roles an AI participant may hold, and the media its anchors name
The ai power (bit 5) says a role may be held by an AI participant; where a descriptor's role
holds it, a grant carrying ai is of such a role (§4.3 step 9, ai_role_not_permitted). An AI
participant's chat, reaction or caption naming another model is ai_model_mismatch apart from
ai_unmarked (§9.2 step 6), and its anchored events name a track its grant gives it: one it
publishes, or a layer within its max_layer of a track the room has published (§9.2 step 7,
ai_anchor_not_granted). A relay forwards no event either step refuses. The corpus gains an AI
captioner's room and its five refusals; live_rooms_verify.py checks the three rules.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 05:32:23 -05:00

1262 lines
81 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

# WAI Extension: Live Rooms
> Status: Draft. Registers the record `wai.live.room`: the signed **room descriptor**
> that fixes a room, its id, its clock and what each role may do; and the signed **room
> grant** under which one principal takes part, carrying the event policy
> [live-events](live-events.md) §5 holds it to. A grant is exchanged for another at a tick
> of the room clock, and revoked through the room authority's key log
> ([jwp-receipts](jwp-receipts.md) §7). Tracks are named in the MoQ namespace tuple; a
> slot lets a publisher of higher precedence take a virtual track over, and a standby
> take it on failover; the publish profiles fix codecs, layers and limits as data. A
> room verifier replays every principal's events under the grant in effect at each
> instant of the room clock. No new transport and no new token format: the grant maps
> onto existing token formats (§10). The reference implementation is the `live_room`
> module of `wai-rs` (feature `live_room`); the corpus is
> [`live-conformance/rooms/`](../live-conformance/rooms/), and its second implementation
> is `live-conformance/live_rooms_verify.py`.
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
>
> Drafts read for this revision: `draft-ietf-moq-transport-22` (MOQT),
> `draft-ietf-moq-msf-01` (MSF), `draft-ietf-moq-c4m-02` and
> `draft-ietf-moq-privacy-pass-auth-04`, all Internet-Drafts, none past Working Group
> Last Call; with RFC 9246, RFC 8392, and CTA-5007-B as the IANA "CBOR Web Token (CWT)
> Claims" registry lists its claims (keys 308 to 323). A section number of one of those
> drafts in this text is that revision's.
## 1. Scope
In scope: the room descriptor, its digest (the room id) and signature (§3); the room
grant, its checks, its attributes, how a receipt names it, and its exchange (§4);
revocation through the key log (§5); track naming (§6); precedence and redundancy over
slots, and the `detail` of every lifecycle kind of live-events §4.13 (§7); the publish
profiles (§8); room verification (§9); and the bindings onto token formats (§10), with the
proof of possession every binding requires (§10.5) and seeking back below a track's live
edge (§10.6).
Out of scope: a transport; how a principal obtains a grant; composition, which reads the
grant's attributes ([live-compose](live-compose.md)); the review workflow, of which this extension
fixes only the policy's fields and their meaning (§3.6), and which
[live-moderation](live-moderation.md) defines; and what a recording holds,
which the recording policy constrains (§3.7) and later extensions define: a device's
master recording is [capture-binding](capture-binding.md)'s take.
Relationship to the core spec: `wai.live.room` 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.
## 2. Terms
- **Room authority**: the Ed25519 key (RFC 8032) that signs a room's descriptor and its
grants.
- **Clock**: the key, named by the descriptor, that stamps the room clock and authors the
room's lifecycle events for the authority. It **MAY** be the authority's key.
- **Room key**: the authority's key or the clock's.
- **Room clock**, **principal**, **event**, **chain**: as in live-events §2.
- **Grant in effect**: for a present principal, the grant its latest `join` or
`grant_exchange` named (§9.3).
- **Slot**: a virtual track the descriptor declares, which one live publisher stands
behind at a time (§7.1).
- **Name**: 1 to 32 bytes of `a`–`z`, `0`–`9`, `_` and `-`. A **slot name** is 1 to 32
bytes of `a`–`z`, `0`–`9` and `_`. A **token** is 1 or more bytes of `!` to `~`
(0x21–0x7E).
- **Kinds bitmap**: 32 bytes in which kind *k* is set when bit *k* mod 8 of byte *k* div
8 is set, as live-events §5 writes `kinds`.
## 3. The room descriptor
### 3.1 Layout
Integers are little-endian. A *short* field is a u8 length and that many bytes; a *long*
field is a u16 length and that many bytes.
| field | size | meaning |
|---|---|---|
| `magic` | 4 | `WRD1` (ASCII) |
| `version` | 1 | 1 in this revision |
| `authority` | 32 | the room authority's Ed25519 key |
| `clock` | 32 | the clock's Ed25519 key |
| `t0` | 8 | u64: the room's `t0`, in microseconds since 1970-01-01T00:00:00Z |
| `tick` | 8 | u64: the room-clock tick, in microseconds, that a grant exchange takes effect on (§4.6) |
| `origin` | long | the origin whose key log lists the authority (§5); empty for none |
| `max_present` | 4 | u32: the most principals present at once (§3.5) |
| `max_publishers` | 2 | u16: the most principals publishing at once (§7.3) |
| `reval` | 4 | u32: the re-validation interval, in seconds, a token of the room carries (§10.2); 0 for none |
| `ceiling` | 65 | a `WEP1` record (live-events §5): the event ceiling no grant exceeds (§3.5) |
| `roles` | 1 + … | a u8 count, then each role: `name` (short), `publish` (u8), `subscribe` (u8), `max_layer` (u8), `powers` (u8), `events` (32, a kinds bitmap) (§3.3) |
| `slots` | 1 + … | a u8 count, then each slot: `name` (short), `media` (u8), `redundancy` (u8), `failover_ms` (u32) (§7.1) |
| `profiles` | 1 + … | a u8 count, then each publish profile (§8.1) |
| `review` | 37 | `kinds` (32, a kinds bitmap), `deadline_ms` (u32), `fallback` (u8) (§3.6) |
| `recording` | 12 + … | `mode` (u8), `retention_days` (u16), `commit_interval_ms` (u32), `jitter_ms` (u32), then a u8 count and that many 2-byte `regions` (§3.7) |
| `sig` | 64 | §3.2 |
### 3.2 Digest, room id and signature
The **digest** of a descriptor is SHA-256 (FIPS 180-4) over its record from `magic`
through `recording`: every byte but `sig`. It is the **room id**, the 32 bytes every event
of the room names (live-events §3.1).
The **signature** is Ed25519 by `authority` over the 14 bytes `wai:live-room` (ASCII) and
0x01, followed by the bytes the digest covers. It is verified strictly, as
[SPEC](../SPEC.md) §7 *Signatures* requires: a key that fails step 1 or 2 is `bad_key`,
and a signature that fails a later step is `bad_signature`.
### 3.3 Roles and capability sets
A **role** maps a name to a **capability set**:
- `publish`: bit *m* − 1 for each media kind *m* the role may publish: 1 `audio`, 2
`video`, 3 `screen` (§6.1).
- `subscribe` (0 or 1) and `max_layer`: whether the role may subscribe, and the highest
layer (§6.3) it may subscribe to; 0 when it may not subscribe.
- `events`: the event kinds it may send, a kinds bitmap within the ceiling's `kinds`.
- `powers`: bit 0 **moderate** (its `decision` events, live-events §4.11, are a
moderator's), bit 1 **direct** (it may send `control` commands other than recording),
bit 2 **compose** (it may sign the room's composition descriptor and publish a composed
track, [live-compose](live-compose.md) §3.5, §7), bit 3
**record** (it may send `record_start` and `record_stop`), bit 4 **seek** (it may fetch
a track of the room below its live edge, §10.6), bit 5 **ai** (an AI participant may hold
the role, §4.4).
A role whose `events` permit `decision` has the moderate power; one whose `events` permit
`control` has direct or record. A grant narrows its role's set and never widens it
(§4.3).
### 3.4 Reading and field rules
A reader reads exactly the bytes it is given as one descriptor, in the order of §3.1, and
refuses at the first of these that holds:
1. Fewer than 4 bytes: `truncated`. A magic other than `WRD1`: `malformed`. No byte after
the magic: `truncated`. A version other than 1: `unknown_version`.
2. The bytes end before a field the layout requires: `truncated`.
3. Bytes after `sig`: `malformed`.
It then holds the descriptor to these rules, and refuses one that breaks any of them as
`descriptor_invalid`:
- `tick` is above 0; `origin` is empty or a token of at most 255 bytes; `max_present` and
`max_publishers` are above 0.
- `ceiling` is a policy live-events §5 reads, and it permits no lifecycle kind (`0x10` to
`0x1F`).
- There are 1 to 16 roles, each named by a name no other role has, with `subscribe` 0 or
1, `publish` setting no bit above bit 2, `powers` none above bit 5, `max_layer` 0 when
`subscribe` is 0, `events` within the ceiling's `kinds`, and the powers §3.3 requires
of its `events`.
- There are at most 16 slots, each named by a slot name no other slot has, of a media
kind of §6.1, with `redundancy` 1 and `failover_ms` 0, or `redundancy` 2 and
`failover_ms` above 0.
- There are at most 16 profiles, each meeting §8.1 and named by a name no other profile
has. A profile whose name §8.2 registers is that registered profile, field for field.
- The review policy and the recording policy meet §3.6 and §3.7.
Then the signature (§3.2): `bad_key` or `bad_signature`.
### 3.5 Limits
- `max_present`: a `join` that would make more principals present than this is refused
`participant_limit` (§7.2).
- `max_publishers`: §7.3.
- `ceiling`: no grant's policy exceeds it (§4.3 step 8): its `max_payload`, `max_text_cp`,
`max_events` and `max_reactions` are at most the ceiling's, and its `window_us` and
`reaction_window_us` at least the ceiling's, so every window of the ceiling's length
holds no more events than the ceiling allows. The ceiling's `kinds` bound the roles'.
### 3.6 The review policy
`kinds` names the event kinds held for review; all zero, nothing is reviewed. An event of
a reviewed kind is shown by a sink only once a principal with the moderate power has sent
a `decision` on it (live-events §4.11), or once `deadline_ms` milliseconds of room clock
have passed after its `t` with none, when the `fallback` verdict applies: 1 allow, 2 deny.
With no reviewed kind, `deadline_ms` and `fallback` are 0; with one, the kinds are within
the ceiling's, `deadline_ms` is above 0, and `fallback` is 1 or 2. This revision fixes the
policy's fields and their meaning; the review workflow and its verification are
[live-moderation](live-moderation.md)'s, whose review policy keeps this field (§3.5 there).
### 3.7 The recording policy
- `mode`: 0 the room is never recorded; 1 it is recorded from a `record_start` command
(live-events §4.9) of a principal with the record power; 2 it is always recorded.
- `retention_days`: the most days a recording is kept; 0 states no bound.
- `commit_interval_ms` and `jitter_ms`: how often a device capturing a take commits to it
in the room's events, and the jitter its timing is allowed
([capture-binding](capture-binding.md) §5, §7). With `mode` 0 both are 0. Otherwise
`commit_interval_ms` is above 0, 2 000 (two seconds) by default, and `jitter_ms` is at
most `commit_interval_ms`, 500 by default.
- `regions`: the **region pin**, for data residency: ISO 3166-1 alpha-2 codes, each two
capitals `A`–`Z`, in ascending order, at most 16. A recording of the room, and the
media and events a relay keeps of it beyond delivery, **MUST** be stored only within
those regions; none means no pin. A take's upload receipts state where its master is
stored ([capture-binding](capture-binding.md) §6.3), and a recording's manifest where the
package is kept ([live-recording](live-recording.md) §4.4); each is checked against the pin.
`mode` is at most 2, and `commit_interval_ms` and `jitter_ms` keep the rule above.
### 3.8 AI usage preferences
The descriptor's layout holds no AI usage preference. A room states one in a room usage
record, `WAU1` ([ai-usage](ai-usage.md) §4.2): a usage statement the authority signs, naming
the room id, chained so that a preference can change at an instant of the room clock and never
for what was published before it. An object of the room is governed by the record in effect at
its time, and by its own statement where its manifest carries one (ai-usage §5.1). A grant
states the terms its principal is granted in the attribute `ai_usage` (§4.4), and a principal
whose grant carries terms is an agent principal, which a relay MAY require consumption claims
of before it serves it (ai-usage §7).
## 4. The room grant
### 4.1 Layout
| field | size | meaning |
|---|---|---|
| `magic` | 4 | `WRG1` (ASCII) |
| `version` | 1 | 1 in this revision |
| `room` | 32 | the room id |
| `principal` | 32 | the Ed25519 key the grant is for |
| `prev` | 32 | the digest of the grant this one is exchanged for (§4.6); 32 zero bytes for a first grant |
| `effective` | 8 | u64: for an exchanged grant, the room-clock tick it takes effect at; 0 for a first grant |
| `nbf`, `exp` | 8 each | u64: seconds since 1970-01-01T00:00:00Z, as RFC 8392's NumericDate: the grant is good from `nbf` until before `exp` |
| `role` | 1 | the index of its role in the descriptor |
| `publish`, `subscribe`, `max_layer`, `powers` | 1 each | its capability set (§3.3), within its role's |
| `precedence` | 1 | its precedence on a slot (§7.1) |
| `once` | 16 | a single-use nonce: when it is not zero, no two `join`s name grants with this `once` (§7.2); zero, the grant may be used again within its window |
| `viewer` | 16 | the viewer id revocation names (§5); zero for none |
| `policy` | 65 | a `WEP1` record (live-events §5): the event kinds it may send, its payload and text bounds, and its rate windows |
| `joules_ceiling` | 8 | u64: the energy its use may be attributed, in microjoules (live-metering §4.6 attributes a hop's figure by the bytes each grant moved, an accounting convention); 2^64 − 1 for no ceiling |
| `funds_ceiling` | 8 | u64: the funds its use may spend, in minor units of `currency`; 2^64 − 1 for no ceiling |
| `currency` | 3 | ISO 4217 alphabetic code; three zero bytes when there is no funds ceiling |
| `profiles` | 1 + *n* | a u8 count, then the index in the descriptor of each publish profile it publishes with |
| `origins` | 1 + … | a u8 count, then each delivery origin it is good for (short): a URI's scheme and authority, such as `https://relay.example` |
| `attrs` | long | its attributes (§4.4) |
| `sig` | 64 | §4.2 |
### 4.2 Digest and signature
The **digest** of a grant is SHA-256 over its record from `magic` through `attrs`. The
**signature** is Ed25519 by the descriptor's `authority` over the 20 bytes
`wai:live-room-grant` (ASCII) and 0x01, followed by the bytes the digest covers, verified
strictly (SPEC §7 *Signatures*).
### 4.3 Checking the grants
A verifier is given a room's grants in a list. It checks each in the list's order and
refuses at the first check that fails, naming the grant by its index:
1. The record reads, as §3.4 steps 1 to 3 read a descriptor with `WRG1` for `WRD1`:
`truncated`, `malformed` or `unknown_version`.
2. Its signature verifies under the descriptor's `authority` (§4.2): `bad_signature`.
3. When `prev` is not zero, it is the digest of a grant earlier in the list
(`exchange_unlinked`), and that grant's `room` and `principal` are this one's
(`exchange_mismatch`).
4. Its `room` is the room id: `wrong_room`.
5. Its fields keep these rules, and `grant_invalid` refuses it otherwise:
- `exp` is above `nbf`;
- a first grant's `effective` is 0, and an exchanged grant's a multiple of the
descriptor's `tick`;
- `role` names a role; `subscribe` is 0 or 1; `publish` sets no bit above bit 2,
`powers` none above bit 5; `max_layer` is 0 when `subscribe` is 0;
- `policy` is a policy live-events §5 reads, permits no lifecycle kind, and the powers
§3.3 requires of its kinds are among `powers`;
- `funds_ceiling` is 2^64 − 1 exactly when `currency` is three zero bytes, and
`currency` is otherwise three capitals `A`–`Z`;
- each entry of `profiles` names a profile of the descriptor, of a media kind the
grant may publish, and no two name profiles of one media kind;
- there are at most 8 `origins`, each a token;
- `attrs` meets §4.4;
- its digest is no earlier grant's.
6. `exp` − `nbf` is at most 1 209 600 seconds, fourteen days: otherwise `grant_too_long`.
7. It is within its role (§3.3): its `publish` and `powers` set no bit its role's do not,
it subscribes only where its role does, its `max_layer` is at most its role's, and its
policy's kinds are within its role's `events`. Otherwise `caps_exceed_role`.
8. Its policy is within the room's ceiling (§3.5): otherwise `grant_exceeds_room`.
9. Where a role of the descriptor holds the ai power (§3.3), a grant that carries `ai` (§4.4)
is of a role that holds it: otherwise `ai_role_not_permitted`.
A grant is **good at** room-clock `t` when `nbf` ≤ ⌊(`t0` + `t`) / 10^6⌋ < `exp`.
### 4.4 Attributes
`attrs` is a sequence of entries, each a key (a short field holding a slot name) and a
value (a long field holding UTF-8, RFC 3629). Keys are in strictly ascending byte order,
and the entries take at most 1024 bytes, not counting the field's own length. An entry
that runs past the field's end breaks the rule.
Attributes are for composition: [live-compose](live-compose.md) lays a room out from
them. It reads `featured` (`true` or `false`), `order` (a decimal integer) and `pip`
(`true` or `false`), and `gain` and `pan` (decimal integers) for the mix, under the keys a
composition descriptor names, and gives each its syntax and its meaning (live-compose
§4.3); this revision checks only the form of the entries. Capture binding
([capture-binding](capture-binding.md) §3) reads two more: `subject`, the principal a
device captures for, as 64 lowercase hexadecimal digits of its key; and `minor` (`true`
or `false`), whether the authority marks that principal a minor, which
[age-assurance](age-assurance.md) §8 also reads: a room in which a minor's grant that may publish
takes effect meets its minors' profile. Age assurance (age-assurance §5.4) reads `age`: the
threshold, in years, the authority admitted the principal under, a decimal integer from 1 to 21.
AI usage
([ai-usage](ai-usage.md) §4.3) reads `ai_usage`: the canonical form of a usage statement, the
terms the authority grants the principal, which override the room's preference for the uses
they name. Live worlds ([live-worlds](live-worlds.md) §5.1) reads `world`: `act` when the
authority lets the principal act in the room's shared world. Shoppable events
([shoppable-events](shoppable-events.md) §5.5) reads `offer`: `publish` when the authority admits
the principal to present offers in the room. Attributes are signed with the
grant, so a layout computed from them, and the terms, are what the authority stated.
One more attribute marks an **AI participant**: `ai`, the digest naming the model that acts
for the principal (a prior's digest, SPEC §3.1), as 64 lowercase hexadecimal digits. A grant
whose `ai` is not that form breaks this section's rule (§4.3 step 5, `grant_invalid`). The
authority states it, as it states `minor`; a principal is an AI participant from the instant
a grant carrying it is its grant in effect until one without it is. A room says which of its
roles an AI participant may hold by the ai power (§3.3): once a role of its descriptor holds
it, the authority grants `ai` under those roles alone (§4.3 step 9); a descriptor none of whose
roles holds it admits AI participants under any role, as rooms of the revisions before the
power was defined do. An AI participant's `chat`, `reaction` and `caption` events state that
a machine produced them, with that model (§9.2 step 6), and the tracks it publishes are
synthesized media ([synthesis](synthesis.md) §8). An event of an AI participant anchored to an
object (live-events §3.1) states the media it worked on, a caption the audio it was
transcribed from, so its anchor names a track its grant gives it (§9.2 step 7). A relay that
forwards a room's events **MUST NOT** forward one that §9.2 step 6 or 7 refuses. A sink
**MUST** show, with every presence, voice, avatar and
message of an AI participant, that it is an AI system: the information Regulation (EU)
2024/1689 Article 50(1) asks a system that interacts with people to give. The sink's showing
it is an engineering requirement of this extension, and is not a statement about the
Regulation.
### 4.5 How a receipt names a grant
A receipt that names the grant it was carried under does so with the grant reference
WAI's receipts already carry, the members `grant_hash`, `capability`,
`joule_ceiling_micro` and `funds_ceiling` that a package receipt
([packaging](packaging.md) §9) and a playlist receipt (http-delivery, Appendix H) write:
`grant_hash` is the grant's digest (§4.2), `capability` is `wai.live.room`,
`joule_ceiling_micro` is `joules_ceiling` (2^64 − 1 for no ceiling, as there), and
`funds_ceiling` is `funds_ceiling`, or none when that is 2^64 − 1. The reference carries
no currency; the grant it names does. Every object's receipt so names the grant it was
carried under, and the grant states the ceilings a meter holds that use to.
### 4.6 Grant exchange
A principal's grant is **exchanged** for a new grant that is chained to it: the new
grant's `prev` is the old grant's digest, and its `room` and `principal` are the old
one's (§4.3 step 3). The exchange takes effect at the new grant's `effective`, a tick of
the room clock, and is recorded as the clock's `grant_exchange` lifecycle event at that
`t` (§7.2). An exchange can widen what a principal may do (a viewer promoted to publish)
or narrow it, and only the authority can make one, since only it signs grants.
An exchange **MUST NOT** leave the principal publishing what its new grant does not let
it publish: an authority that narrows a publisher's grant unpublishes first (§7.2).
## 5. Revocation through the key log
A room whose descriptor names an `origin` is revocable. Its authority's key is an entry
of that origin's key log ([jwp-receipts](jwp-receipts.md) §7.2) whose `may_sign` holds
the authority `live/room` (jwp-receipts §7.5). Revocation then goes through that log:
- **A grant** is revoked by adding its digest to the head's `revoked_modules`.
- **A viewer** is revoked by adding its **tag**, SHA-256 over the 17 bytes
`wai:live-viewer` (ASCII) and 0x01 followed by the 16-byte `viewer`, to the same list.
Every grant naming that viewer is revoked with it, in every room whose descriptor names
that origin.
- **The authority** is revoked as any key is (jwp-receipts §7.6): with the key, every
descriptor and grant it signed is refused.
The list is append-only (jwp-receipts §7.2 rule 6), so a revocation stays. A relay checks
a grant against the key status it holds ([http-delivery](http-delivery.md) §8.4) at
admission and at each re-validation (§10.2), so a grant revoked stops being honoured
within `reval` plus the head's `status_max_age_s` seconds.
A room verifier is given the **head** the caller reached for the descriptor's origin
(jwp-receipts §7.4 step 3): anchoring the origin and walking its log to the head are the
caller's, and the verifier is a function of the head it is given. When `origin` is
empty, no key log is read, a head given is not read, and nothing is revoked. Otherwise,
in this order:
1. A head is given: otherwise `key_log_missing`.
2. It is held for `origin`: jwp-receipts §7.2 rules 1 to 3 hold for it. Otherwise
`key_log_not_held`.
3. It has an entry whose `key` is the descriptor's `authority` and whose `may_sign` holds
`live/room`: otherwise `authority_not_listed`.
4. That entry's `status` is not `revoked`: otherwise `authority_revoked`.
A grant whose digest, or whose viewer's tag, the head's `revoked_modules` lists is
**revoked**. A revocation refuses every use of the grant (§7.2): like a key's, it is not
bounded by a time, since a revoked grant may have been in another party's hands. An
authority that only wants a principal gone from a room removes it with `leave` (§7.2).
## 6. Track naming
### 6.1 The Full Track Name
A track of a room is named by a MOQT Full Track Name (MOQT §2.4.1):
- its **Track Namespace** is three fields:
1. the room id, as 64 lowercase hexadecimal digits;
2. the **owner**: the publishing principal's key as 64 lowercase hexadecimal digits,
or, for a slot (§7.1), `_` followed by the slot's name;
3. the media kind: `audio` (1), `video` (2) or `screen` (3); or `events` for the
principal's events track (live-events §8.1); or `world` for an actor's world track
([live-worlds](live-worlds.md) §4.1), which is not a media track;
- its **Track Name** is the layer (§6.3) in decimal, with no leading zero: `0`, `1`, …;
the events track's is `0`.
Two more tracks carry what a publisher's media tracks need, and are named alongside them:
- a media track's **receipts track** ([moq-streaming-format](moq-streaming-format.md)
§4.1), which carries the track's receipts, is in the media track's namespace, and its
Track Name is the media track's followed by `_receipts`: `0_receipts`, `1_receipts`, …
A media track that carries receipts has exactly this receipts track. The events track
has none: each of its records is signed (live-events §3);
- a publisher's **catalog track** (MSF §5), which carries its MSF catalog (§6.2), has a
Track Namespace of the first two fields alone, the room id and the owner, and the Track
Name `catalog`.
Every byte of these fields is one of `a`–`z`, `0`–`9` and `_`, so the bytes are their
own canonical form (MOQT §2.4.1) and their rendering in MOQT §8.8's format is themselves:
the fields joined by `-`, then `--` and the name, as
`<room>-<principal>-video--1`, `<room>-<principal>-video--1_receipts` and
`<room>-<principal>--catalog`. No field is empty and none begins with `.` (MOQT §2.4.3,
§8.7), and the whole is 64 + 64 + 6 + 12 bytes at most, within MOQT §8.7's limit.
The layer is the Track Name, and not a fourth namespace field, because
[moq-streaming-format](moq-streaming-format.md) §4.1 has the tracks of a staged set share
one namespace. Namespace prefix matching (MOQT §2.4.2) on the first field reaches every
track of a room; on the first two, every track of one publisher or slot.
### 6.2 The catalog
A publisher's MSF catalog is the object of its catalog track (§6.1). Its `version` is
`"draft-01"`, the string MSF §5.1.1 gives draft-ietf-moq-msf-01; a subscriber reads a catalog
of that version, and of the number 1 that publishers wrote before this revision, and no other
(MSF §5.1.1). Its events track's entry is live-events §8.1's. Its entry for a track
carries its `namespace` (MSF §5.2.2) as the MOQT §8.8 rendering of the namespace, and its
`name` (MSF §5.2.3) as the Track Name. A media track that carries receipts declares
`wai_receipts` true and names its receipts track (§6.1) in `wai_receipts_track`, and the
receipts track has an entry of its own, of `packaging` `"wai-receipts"`
(moq-streaming-format §4). The
codec, dimensions, frame rate, bitrate, sample rate and channels of the entry are the
track's profile layer's (§8.1). A track that carries WAI envelopes declares `packaging`
`"wai"` ([moq-streaming-format](moq-streaming-format.md) §3), and the tracks of a staged
profile carry `wai_staged` with `layer` equal to the Track Name (moq-streaming-format
§4.1). An anchor's `track` (live-events §3.1) is SHA-256 over that rendering of the
namespace, one zero byte, and the Track Name.
A track whose objects are CMAF chunks declares `packaging` `"cmaf"` and names its header by
`initRef` (`draft-ietf-moq-cmsf-01` §3.1, §3.5.1), with its receipts track as any media
track's. Such tracks are what [live-http-egress](live-http-egress.md) serves over HTTP while
the room is live, as low-latency HLS and DASH, each part beside its receipts. A publisher
that signs a group when it ends **MAY** also sign, on its events track, a running receipt for
each object as it sends it (live-http-egress §3.5).
### 6.3 Layers
A layer is an index from 0, the lowest. A profile's `layering` (§8.1) says what its
layers are:
- `single`: one track, layer 0.
- `simulcast`: layer *k* is the profile's *k*-th layer, an independent encoding on a
track of its own.
- `staged`: layer 0 is the base and layer *k* the *k*-th refinement
([staged-delivery](staged-delivery.md) §1), each on a track of its own.
- `svc`: one track, layer 0, carries every layer of the profile's scalability `mode`,
named as the W3C "Scalable Video Coding (SVC) Extension for WebRTC" names them
(`L1T3`, `S3T3`). A relay that forwards it to a subscriber whose `max_layer` is *m*
forwards the spatial layers 0 to *m* alone.
A subscriber whose grant's `max_layer` is *m* subscribes to the tracks named `0` to *m*,
their receipts tracks and the publishers' catalog tracks (§10.2). A `layer_request` command (live-events §4.9) asks a publisher for a layer; it
grants nothing.
A subscriber that takes one `simulcast` layer at a time moves between layers as
[moq-streaming-format](moq-streaming-format.md) §4.6 gives: the layers of one publisher are
a rendition set, a move is planned from the object in hand so that the layer left ends on a
Group boundary and the layer joined starts on one, and a subscriber that measures before it
moves up does so with a probe track. A move is among the tracks the grant already admits
(`0` to *m*); it asks the relay for nothing a SUBSCRIBE and a REQUEST_UPDATE do not.
## 7. Precedence and redundancy
### 7.1 Slots, takeover and failover
A **slot** is a virtual track the descriptor declares: a name, a media kind, a
`redundancy` of 1 or 2, and a `failover_ms`. Subscribers subscribe to the slot's tracks
(§6.1, owner `_<name>`), and which principal publishes behind them can change without
any subscriber subscribing again: a relay switches the source and the subscription stands.
A slot has a **live** publisher, and, when its `redundancy` is 2, a **standby**.
- **Takeover.** A principal whose grant in effect has a higher `precedence` than the live
publisher's replaces it: the clock records a `takeover`, the new publisher goes live, and
the replaced one leaves the slot. A takeover by a precedence not higher is refused
`takeover_not_higher`.
- **Redundancy.** With `redundancy` 2, two publishers are bound to the slot: the first to
publish into it is live, the second its standby. A third is refused `slot_full`, as is
a second where `redundancy` is 1.
- **Failover.** When the live publisher's media stops, the clock records a `failover` to
the standby. The rule: the standby goes live only once `failover_ms` milliseconds of
room clock have passed since the live publisher's last object reached the relay, which
the event states as `last_seen`; earlier is refused `failover_early`. A failover to a
principal that is not the standby is refused `failover_not_standby`. The failed
publisher leaves the slot, and can publish into it again as the standby.
A standby whose live publisher unpublishes, or leaves the room, goes live with no event of
its own: its going live follows from the `unpublish` or `leave`.
### 7.2 Lifecycle detail
The lifecycle events of live-events §4.13 are authored by a room key. Their payload is the
`subject`, the principal the event is about, then `detail`, which this section defines
for each kind. A *slot* field is a short field holding a slot name or, where the
table allows, nothing (the subject's own track). `media` is a media kind of §6.1.
| kind | `detail` |
|---|---|
| `join` | `grant` (32): the digest of the grant the subject takes part under |
| `leave` | `reason` (u8): 1 left, 2 removed, 3 its grant ended, 4 connection lost |
| `publish` | `slot` (empty for an own track), `media` (u8), `profile` (u8, the index of the profile in the descriptor), `layers` (u8, at least 1) |
| `unpublish` | `slot` (empty for an own track), `media` (u8) |
| `grant_exchange` | `old` (32), `new` (32): the grants' digests |
| `takeover` | `slot` (not empty), `media` (u8), `profile` (u8), `layers` (u8, at least 1), `previous` (32): the live publisher replaced |
| `failover` | `slot` (not empty), `media` (u8), `failed` (32): the live publisher that stopped, `last_seen` (u64): the room clock at its last object |
A `detail` that does not read, with bytes after its last field, or with a value the table
does not allow, is `lifecycle_malformed`. Each event is then checked against the room
state (§9.3) at its `t`, in the order listed, and applied:
- **`join`**: the grant is one the verifier holds (`grant_unknown`); its `principal` is
the subject (`grant_wrong_principal`); the subject is not present (`already_present`);
the grant is not revoked (§5, `grant_revoked`); a grant whose `once` is not zero names a
`once` no earlier `join` named (`grant_reused`); the grant is good at `t`
(`outside_grant_window`); and the room holds fewer than `max_present` principals
(`participant_limit`). The subject is present, under that grant.
- **`leave`**: the subject is present (`not_present`). It is not present any more, its
own tracks are unpublished, and it leaves every slot; a standby whose live publisher
leaves goes live.
- **`publish`**: the subject is present (`not_present`); a `slot` named is a slot of the
descriptor of that media kind (`slot_unknown`); the grant in effect may publish the
media kind (`publish_not_permitted`), and its `profiles` hold `profile`, a profile of
that media kind with at least `layers` layers (`profile_not_granted`); the subject does
not already publish that own track or stand in that slot (`already_published`); the
slot has room (§7.1, `slot_full`); and then at most `max_publishers` principals publish
(§7.3, `publisher_limit`). An own track is published with `layers` tracks; a slot's
publisher is live if the slot has none, and its standby otherwise.
- **`unpublish`**: the subject is present (`not_present`); a `slot` named is a slot of the
descriptor of that media kind (`slot_unknown`); the subject publishes that own track or
stands in that slot (`not_published`). It is unpublished; a standby whose live
publisher unpublishes goes live.
- **`grant_exchange`**: the subject is present (`not_present`); `old` is its grant in
effect (`exchange_not_current`); `new` is a grant the verifier holds (`grant_unknown`)
whose `prev` is `old` (`exchange_unlinked`); `t` is `new`'s `effective`
(`exchange_off_tick`); `new` is not revoked (`grant_revoked`) and is good at `t`
(`outside_grant_window`); and for everything the subject publishes, its own tracks by
media kind and then its places in slots in the descriptor's order, `new` may publish
the media kind (`publish_not_permitted`) and gives the profile (`profile_not_granted`).
`new` is then the grant in effect.
- **`takeover`**: the subject is present (`not_present`); the slot is the descriptor's,
of that media kind (`slot_unknown`); `previous` is its live publisher
(`not_published`); the grant in effect may publish the media kind and gives the profile
with at least `layers` layers (`publish_not_permitted`, `profile_not_granted`); the
subject is not `previous`, and its grant's `precedence` is higher than the grant in
effect of `previous` (`takeover_not_higher`); and then at most `max_publishers`
principals publish (`publisher_limit`). The subject is live, with `layers` layers;
`previous` leaves the slot, and the subject is no longer its standby.
- **`failover`**: the subject is present (`not_present`); the slot is the descriptor's,
of that media kind (`slot_unknown`); `failed` is its live publisher (`not_published`);
the subject is its standby (`failover_not_standby`); `last_seen` is at most `t`, and
`t` − `last_seen` is at least `failover_ms` × 1000 (`failover_early`). The standby is
live, and `failed` leaves the slot.
### 7.3 Publishers
A principal **publishes** while it has an own track published or stands in a slot, live
or standby. At most `max_publishers` principals publish at once; a `publish` or a
`takeover` after which more would is refused `publisher_limit`.
## 8. Publish profiles
### 8.1 The profile record
A profile fixes, as data, what a publisher sends on one media kind: its codecs, its
layers and their limits, and its audio processing.
| field | size | meaning |
|---|---|---|
| `name` | short | the profile's name |
| `media` | 1 | its media kind (§6.1) |
| `codecs` | 1 + … | a u8 count (1 to 4), then each `codecs` value (RFC 6381) as a short field holding a token of at most 64 bytes, in the order a publisher prefers them |
| `layering` | 1 | 0 `single`, 1 `simulcast`, 2 `svc`, 3 `staged` (§6.3) |
| `mode` | short | for `svc`, the scalability mode (§6.3): 1 to 16 of `A`–`Z`, `0`–`9` and `_`; otherwise empty |
| `layers` | 1 + 14 × *n* | a u8 count (1 to 8; exactly 1 for `single`), then each layer: `width`, `height`, `fps` (u16 each), `min_kbps`, `max_kbps` (u32 each) |
| `sample_rate` | 4 | u32: audio's sample rate in Hz; 0 for video |
| `channels` | 1 | audio's channel count, 1 to 8; 0 for video |
| `processing` | 1 | audio's processing: bit 0 echo cancellation, bit 1 noise suppression, bit 2 automatic gain control, bit 3 discontinuous transmission (DTX); 0 for video |
Each layer's `max_kbps` is above 0 and `min_kbps` at most `max_kbps`: `min_kbps` is a
floor a publisher **SHOULD NOT** send below while the network allows, and `max_kbps` a
ceiling it **MUST NOT** exceed. An audio layer has `width`, `height` and `fps` 0, and an
audio profile a `sample_rate` above 0, 1 to 8 `channels`, and `processing` setting no bit
above bit 3; a video or screen layer has all three above 0. A publisher publishing under a
profile uses one of its codecs, its layering and its layers, and turns on exactly the
processing it names.
### 8.2 Registered profiles
| name | media | codecs | layering | layers (width × height @ fps, kbps) | audio |
|---|---|---|---|---|---|
| `speech` | audio | `opus` | single | 16–64 | 48 000 Hz, 1 channel; echo cancellation, noise suppression, gain control and DTX on |
| `music` | audio | `opus` | single | 128–510 | 48 000 Hz, 2 channels (stereo, fullband); no echo cancellation, noise suppression, gain control or DTX |
| `screen` | screen | `av01.0.08M.08`, `avc1.640028` | svc, `L1T3` | 1920 × 1080 @ 15, 300–2500 | — |
| `camera-720` | video | `av01.0.05M.08`, `avc1.64001F` | simulcast | 640 × 360 @ 30, 150–500; 1280 × 720 @ 30, 500–1500 | — |
| `camera-1080` | video | `av01.0.08M.08`, `avc1.640028` | simulcast | 640 × 360 @ 30, 150–500; 1280 × 720 @ 30, 500–1500; 1920 × 1080 @ 30, 1500–4000 | — |
`opus` is RFC 6716 audio, `av01` AV1 and `avc1` ITU-T H.264. `music`'s 128 kbps floor
and its processing turned off keep what an instrument plays: echo cancellation, noise
suppression and gain control are built for speech and remove parts of music, and DTX
stops sending in what it takes for silence. `screen` drops frames rather than resolution
(`L1T3`, temporal layers alone), since text on a screen does not survive scaling. A room
lists the profiles it allows in its descriptor, under these names with these values or
under names of its own; `wai_live_room_vectors` writes each registered profile's record
into the corpus, so an implementation checks its table against them.
## 9. Room verification
### 9.1 Inputs and steps
A room verifier is given a descriptor, a list of grants, the head of the authority's key
log (or none), and the room's **chains**: a list of lists of `WEV1` records, each list one
principal's records in its stream order (live-events §6). It runs these steps and stops
at the first refusal, which it reports with the reason and where:
1. **The descriptor** (§3.4, §3.2): its refusal, at `descriptor`.
2. **The key log** (§5): its refusal, at `key_log`.
3. **The grants** (§4.3): the refusal, at the grant's index.
4. **The records**, in the merged order (§9.2), each by §9.2: the refusal, at the
record's chain and its index in that chain.
5. **The outcome** (§9.4).
### 9.2 The merged order and each record
The **merged order** takes the records of every chain into one sequence, keeping each
chain's own order: repeatedly, of the chains with records left, it takes the next record
of the one whose next record has the least key. A record's key is its `t`, then 0 for a
room key's record and 1 for any other, then its chain's index; a record that live-events
§3.3 does not read has the key (0, 0, its chain's index). Lifecycle events therefore take
effect before the events of other principals at the same `t`.
One live-events verifier (live-events §6) verifies every record, for the room id. Before
it is offered a record, the verifier is given the policy the record is held to. For each
record, in the merged order:
1. live-events §6.1 steps 1 to 4: the record reads, its kind is defined, its `room` is
the room id, its signature verifies. Otherwise that section's reason.
2. A record whose digest is that of a record accepted before is a duplicate: it is
offered under the policy it was accepted under, which live-events §6.2 step 1 ignores,
and it changes nothing.
3. A room key's record is offered under the room authority's policy (live-events §5),
and refused with live-events' reason. A lifecycle event then has its `detail` read and
is applied (§7.2).
4. Any other principal's record: the principal is present (`not_present`), its grant in
effect is good at the record's `t` (`outside_grant_window`), and the record is offered
under that grant's `policy` and refused with live-events' reason: a kind the grant does
not permit is `kind_not_permitted`, a payload over its bound `payload_too_large`, and an
event past its rate window `rate_exceeded`. A participant's lifecycle event is
therefore `kind_not_permitted`, as no grant permits one (§4.3 step 5).
5. A `control` event of such a principal needs a power of its grant in effect (§3.3):
`record_start` and `record_stop` the record power, any other command the direct power.
Otherwise `power_not_granted`.
6. A `chat`, `reaction` or `caption` event of a principal whose grant in effect carries
`ai` (§4.4) states origin 2 (machine) or 3 (machine-reviewed), with the model `ai`
names (live-events §4.4, §4.5, §4.10). One that states no origin, or origin 1, is
`ai_unmarked`; one that states origin 2 or 3 with another model is `ai_model_mismatch`.
7. An anchored event (live-events §3.1) of a principal whose grant in effect carries `ai`
names, in its anchor's `track`, a track its grant gives it: one the principal publishes,
or, where the grant may subscribe, a layer at most its `max_layer` of a track the room has
published at or before the event. A track the room has published is a layer of an own
track, or of a slot's track behind its live publisher, of the room state (§9.3) after some
lifecycle event at or before the record in the merged order, named by its anchor digest
(§6.2). Otherwise `ai_anchor_not_granted`.
Each principal's effective policy at `t` is so the policy of its grant in effect at `t`,
and the room keys' is the authority's. Each record is reported with the grant it was
verified under, or none for a room key's.
### 9.3 Room state
The room state is replayed from the lifecycle events alone, in the merged order:
- **present**: each present principal and its grant in effect;
- **published**: each own track, by principal and media kind, with its profile and its
number of layers;
- **slots**: each slot's live publisher and standby, and the live publisher's precedence:
the slot's current **precedence holder**;
- **active grants**: each change of a principal's grant in effect (a `join` or a
`grant_exchange`), with its `t`, in the order they took effect.
The tracks a subscriber of the room can subscribe to are named by §6.1: each own track's
layers, and each slot's live publisher's.
### 9.4 Outcomes
- **refuse**: the reason and where (§9.1). Step 5's `tally_mismatch` (live-events §7) is
at the record of its `poll_close`.
- **gap**: no record refused, and the live-events verifier does not hold every seq of
every chain (live-events §6.2): the gaps, under the code `delivery_gap`. The room state
is the replay of what it holds, and a tally it computes is unchecked.
- **accept**: no record refused, no gap, and every stated tally equal to the replay.
## 10. Bindings
A grant is WAI's own signed record. A relay, a CDN or an origin that authorises with an
existing token format is given a token that maps the grant's claims onto that format's;
WAI defines no second token format. A claim the grant has and the format does not stays
in the grant, which the relay holds beside the token (§10.1). A grant is public, so it is
honoured, under every binding, only beside a proof that the presenter holds its
principal's key (§10.5).
### 10.1 Common Access Token (CTA-5007-B)
A Common Access Token is a CWT (RFC 8392). Its claims are named here by name and by the
key the IANA CWT Claims registry gives them; the value syntax of each CAT claim is
CTA-5007-B's.
| grant claim | CWT / CAT claim | mapping |
|---|---|---|
| the authority | `iss` (1) | the authority's `signer_id` (jwp-receipts §7.1): origin-bound, with its key-log entry's id, where `origin` is set; its `did:key` otherwise |
| `principal` | `sub` (2) | the principal's `did:key` |
| `room` | `aud` (3) | the room id, 64 lowercase hexadecimal digits |
| `nbf`, `exp` | `nbf` (5), `exp` (4) | the same NumericDates |
| `once` | `cti` (7), `catreplay` (308) | a `once` not zero is the token's `cti`, with `catreplay` 1, which `draft-ietf-moq-c4m-02` §3 reads for MOQT as one connection: the token is good for any number of actions on the connection it is first accepted on, and on no other; zero, `catreplay` permits replay |
| `origins` | `catu` (312), `cath` (315) | each origin as a URI match on scheme and host; a deployment that matches on a request header carries it in `cath` |
| the token's renewal | `catr` (323) | a renewed token's `exp` is never later than the grant's; an exchange (§4.6) issues a new token |
| `principal`, as a key binding | `catdpop` (321), `cnf` (8) | proof of possession by the principal's key: `cnf` names the key, by its COSE Key Thumbprint `ckt` (RFC 9679) as `draft-ietf-moq-c4m-02` §4.1 asks, and `catdpop` requires a proof with each presentation. A relay that reads the token checks the proof §4.2 there defines with each action, a COSE_Sign1 by that key over the action, namespace and track; a relay that holds the grant checks the `WRP1` record of §10.5, over a fresh challenge, at SETUP. `cattpk` (319) binds a TLS key, and applies only where the client's TLS key is the principal's |
| client network, geography | `catnip` (311), `catgeoiso3166` (316) | no grant claim: a deployment **MAY** add them; a room's restriction by place is a sink-evaluated predicate ([unidirectional-delivery](unidirectional-delivery.md) §2) |
| caps, `max_layer`, the slots | `moqt` | §10.2 |
| `reval` (descriptor) | `moqt-reval` | §10.2 |
| `role`, `powers`, `precedence`, `policy`, `profiles`, `attrs`, `joules_ceiling`, `funds_ceiling`, `viewer` | none | WAI's alone; the relay holds the grant, and a deployment that links the two inside the token carries the grant's digest in a private-use claim (RFC 8392 §9.1) |
`catv` (310) is the version CTA-5007-B gives; `catpor` (309), `catm` (313), `catalpn`
(314), `catgeocoord` (317), `catgeoalt` (318), `catif` (322) and `catifdata` (320) have
no grant claim. This revision implements no CAT encoding: the crate has no CBOR or COSE,
and the claims above are mapped by name and key.
### 10.2 MOQT: the `moqt` claim (`draft-ietf-moq-c4m-02`)
A token for MOQT carries the `moqt` claim, a map whose key 0 holds an array of scopes, each
`[actions, namespace matches, track match]` of one to three elements
(`draft-ietf-moq-c4m-02` §2.1). A recipient refuses a token whose `moqt` map holds a key it
does not recognise (§2.1.1 there), and the claim a grant maps onto holds key 0 alone:
`{0: [scopes]}`. Every action is blocked unless a scope enables it (§2 there), so the
scopes a grant maps onto are all it can do. With the room id `R` and the principal `P` as
64 lowercase hexadecimal digits, the scopes are, in this order:
1. `[[1]]`: `SETUP`, the client's (MOQT §9.1).
2. For each media kind the grant may publish, in ascending order of its code, with *m* its
name (§6.1):
`[[2, 6], [R, P, m, nil]]` (`PUBLISH_NAMESPACE` and `PUBLISH` of its own tracks of that
kind: their layers and their receipts tracks), then, for each slot of the descriptor of
media kind *m*, in the descriptor's order, `[[6], [R, "_" + slot, m, nil]]` and
`[[6], [R, "_" + slot, nil], "catalog"]` (the slot's tracks, and its catalog track,
which its live publisher publishes).
3. When the grant may publish any media kind: `[[2, 6], [R, P, nil], "catalog"]`, its
catalog track (§6.1).
4. When the grant's policy permits any kind: `[[2, 6], [R, P, "events", nil], "0"]`.
5. When the grant may subscribe: `[[3], [R]]` (`SUBSCRIBE_NAMESPACE`), then
`[[4, 7, 8], [R], "catalog"]` (`SUBSCRIBE`, `FETCH`, `TRACK_STATUS` of every
publisher's catalog track), then for each layer *l* from 0 to `max_layer`,
`[[4, 7, 8], [R], "l"]` and `[[4, 7, 8], [R], "l_receipts"]` (any track of the room
named *l*, and its receipts track).
Each namespace or track match is a byte string, an exact match; `nil` ends a namespace
match at that length. No scope enables `SUBSCRIBE_TRACKS` (9), which -02 adds: a grant
subscribes to a room's tracks one by one. The token's `moqt-reval` is the descriptor's
`reval`: a relay re-validates the token, and with it the grant's revocation (§5), at least
that often, and stops what the grant authorised when it fails (§2.2 there). A `reval` of 0
asks for no re-validation, and then only expiry ends the token. A grant exchange (§4.6)
issues a new token at the tick. The corpus states each accepted vector's claim value per
grant, written as JSON: the map's key 0 as the member `"0"`, an exact match as a string and
`nil` as `null`.
A grant's seek power is applied by the relay, §10.6. A grant whose attribute `world` is `act`
maps onto one more scope, its world track's ([live-worlds](live-worlds.md) §5.2).
### 10.3 URI Signing (RFC 9246)
Over HTTP, a room's delivery (an LL-HLS or DASH rendition of its tracks) is authorised with
a URI Signing JWT:
| grant claim | URI Signing claim | mapping |
|---|---|---|
| the authority | `iss` (§2.1.1 there) | as §10.1 |
| `principal` | `sub` (§2.1.2) | the principal's `did:key`, as a JWE as that section requires |
| `nbf`, `exp` | `nbf` (§2.1.5), `exp` (§2.1.4) | the same, with no leeway |
| `once` | `jti` (§2.1.7) | a `once` not zero, so a CDN that keeps token ids refuses a replay |
| `origins`, `room`, `max_layer` | `cdniuc` (§2.1.11) | a `regex:` container (§2.1.15.2) matching the grant's origins, the room's path, and the renditions of layers 0 to `max_layer` |
| client network | `cdniip` (§2.1.10) | no grant claim; a deployment **MAY** add it |
| renewal | `cdniets`, `cdnistt`, `cdnistd` (§2.1.12–§2.1.14) | a renewed token's `exp` is never later than the grant's |
| the rest | none | as §10.1 |
`cdniv` is 1.
### 10.4 Privacy Pass (`draft-ietf-moq-privacy-pass-auth-04`)
A viewer that need not be identified is admitted by a Privacy Pass token instead of a
grant. The token's challenge carries the scope in its `origin_info`
(`MoQAuthorizationInfo`, §3.2.4 there), and its presentations are unlinkable to one
another (§2.2 there), though presentations on one connection can still be correlated
through that connection (§3.5.5 there).
**The scope.** The `origin_info` of a challenge for the room `R` is a MoQAuthorizationInfo
of exactly one MoQAuthScope:
- `actions`: SUBSCRIBE_NAMESPACE (3), SUBSCRIBE (4), FETCH (7) and TRACK_STATUS (8), in
that order (§3.2.2 there);
- `namespace_match`: `MATCH_PREFIX` (1) on the tuple of one element, `R` as 64 lowercase
hexadecimal digits (§3.2.3 there);
- `track_name_match`: `MATCH_PREFIX` on the empty string, a prefix of every track name
(§3.3.1 there).
Its bytes, with `R` the 64 digits: `4d` (the scopes, 77 bytes); `04 03 04 07 08` (the
actions); `01 00 42 00 40` and `R` (the namespace match: its type, the tuple's 66 bytes,
the element's 64); `01 00 00` (the track match: its type and an empty value). That is 78
bytes, the scopes taking 77 of the 255 the draft allows them.
A SUBSCRIBE_NAMESPACE carries a Track Namespace Prefix, which a scope authorises only by a
`MATCH_PREFIX` whose pattern is a prefix of it (§3.3.3 there): this scope authorises one on
`[R]` or any longer prefix. SUBSCRIBE_TRACKS (9), which -04 adds, is not among its actions,
as no scope of §10.2 enables it.
Why one scope: the draft bounds the scopes of one MoQAuthorizationInfo to 255 bytes, and
each scope restates its namespace match, 69 bytes for `[R]`, so at most three scopes over
`R` fit; the subscribing scopes of §10.2 step 5, two and then two for each layer, are four
at the least, 320 bytes, and do not. A shorter form of the room id would rename every track
of every binding (§6.1), and a digest of the room's subscription set is not a match rule the
draft defines, so no relay could evaluate it. One scope with a prefix match is within the
draft's grammar, binds the token to the room, and leaves 178 bytes whatever the room's
layers. What it does not carry is applied from the room's own records: the token admits
every track of the room, and a relay serves each track under the room's other rules, an
age rating's among them ([age-assurance](age-assurance.md) §5.5). A room that bounds a
token viewer's layers admits it by a grant instead, which its service issues once it has
redeemed the token (age-assurance §5.6).
**Reading the scope.** A verifier reads the `origin_info` of a token's challenge by the
draft's grammar (§3.2.4 there, in the TLS presentation language): the length of the scopes
from 1 to 255, then exactly that many bytes of whole scopes and nothing after them; in each
scope, actions of 1 to 255 bytes, each a MoQAction of the draft's table (0 to 9); each match
type one of the table's four (0 to 3); each tuple, tuple element and track name whole within
its length. An `origin_info` that does not read is refused `scope_malformed`. One that reads
and is not, byte for byte, the one scope above for the room is refused `scope_mismatch`; so
is an empty one, which scopes the token to no origin. The encoding the revision before this
one gave, the scopes of §10.2 step 5 each with a `MATCH_EXACT` track match, is refused by
these names and never read again as anything else: three of its scopes read, and are
`scope_mismatch`; four or more do not fit the one byte of their length, and are
`scope_malformed`. The codes are age-assurance's (its §5.2 step 2 checks a token's
challenge, and its §11 lists them).
| grant claim | Privacy Pass field | mapping |
|---|---|---|
| `room`, `subscribe` | `origin_info` | the one scope above |
| `max_layer` | none | the scope names every track of the room; a room that bounds layers admits by a grant (age-assurance §5.6) |
| `nbf`, `exp` | `redemption_context` (§3.2.1 there; RFC 9577 §2.1.1.2) | fresh for one session, or fixed for a window of time; a token is good while its challenger accepts its challenge (age-assurance §5.1) |
| `once` | the token's nonce | every token is single-use (§3.3.2 there), presented by value and never registered under an alias (§3.5.3 there); the challenger keeps each nonce it redeemed while a token of that challenge can still be presented (age-assurance §5.3) |
| `principal`, `viewer` | none | the token names no one; revocation is of the issuer's key, not of a viewer |
| `policy`, publish caps, ceilings, `attrs` | none | a Privacy Pass viewer sends no event and publishes nothing |
A viewer that is to send events holds a grant for a key of its own; a key per room keeps
its events unlinkable across rooms (live-events §11). A room that admits viewers by an age
threshold names the issuers of such a token, with one key per threshold, in an age policy, and a
relay serves each track to them only at or above its rating
([age-assurance](age-assurance.md) §4, §5).
### 10.5 Proof of possession
Every participant holds every grant of its room, to verify the room (§9), so presenting a
grant proves nothing about who presents it, and no binding takes a grant, or a token
mapped from it, as a bearer credential. A verifier — a relay admitting a MOQT session, a
server admitting an HTTP request — honours a grant only beside a **proof**: a signature by
the grant's `principal` over a fresh **challenge** the verifier chose, good for one SETUP
(MOQT) or one request (HTTP). It is what `catdpop` and `cnf` state for a CAT
(§10.1): the token names the key, and the presenter shows that it holds it. Both bindings
carry the same record.
#### 10.5.1 The proof record
| field | size | meaning |
|---|---|---|
| `magic` | 4 | `WRP1` (ASCII) |
| `version` | 1 | 1 in this revision |
| `grant` | 32 | the digest (§4.2) of the grant it is presented with |
| `key` | 32 | the Ed25519 key that signs it: the grant's `principal` |
| `challenge` | 32 | the verifier's challenge (§10.5.2, §10.5.3) |
| `context` | long | what the proof is for (below): ASCII, `!` to `~` and space |
| `sig` | 64 | Ed25519 by `key` over the 20 bytes `wai:live-room-proof` (ASCII) and 0x01, followed by the record from `magic` through `context` |
The **context** is a verb, a space, and the **target**: `//`, then the authority and the
path of the URI the client addressed, with no scheme, query or fragment.
- Over MOQT the verb is `SETUP`, and the URI is the session's: the WebTransport URL the
client opened, the URL of a WebSocket binding, or, over native QUIC, the `moqt` URI whose
AUTHORITY and PATH options it sends (MOQT §9.1.1, §9.1.2). For
`https://relay.example/moq` the context is `SETUP //relay.example/moq`.
- Over HTTP the verb is the request's method and the URI its target:
`POST //gateway.example/whip/r1`.
A verifier compares the target with the authority and the path it received: the
`:authority` and `:path` of a WebTransport CONNECT, the `Host` and path of an HTTP request
or a WebSocket upgrade, the AUTHORITY and PATH options of native QUIC. The scheme is left
out because a front that terminates TLS changes the one the verifier sees. The context
keeps a proof made for one endpoint, or for one method, from being presented at another,
so a server the client chose cannot pass its challenge on and present the client's proof
elsewhere.
#### 10.5.2 Over MOQT
- **The order.** The client sends its SETUP as soon as the session opens, and the relay
reads and processes it before it sends its own: `draft-ietf-moq-privacy-pass-auth-04`
§3.5.1 has a relay that authenticates SETUP do so, so that its SETUP can carry what the
client's calls for, and a relay of this extension follows it for every session. MOQT
lets each endpoint send its SETUP without waiting for the other's (MOQT §9.1), which a
client does and such a relay does not. A client that waits for the relay's SETUP before
it sends its own is sent nothing, and the relay closes the session when its wait for a
SETUP ends.
- **The challenge.** Every SETUP a relay sends carries the Setup Option `GRANT_CHALLENGE`
(type `0x3A47`, its value 32 bytes the relay draws from a cryptographically secure
generator), whether it admits the session or refuses it. A challenge is good for the
proofs of one later SETUP, at the relay that issued it, for a time the relay states
(the reference: 60 seconds). The relay keeps the challenges it issued until each is
consumed or too old. A relay never issues one challenge twice.
- **The proof.** A client presents a grant in its SETUP as an AUTHORIZATION TOKEN (MOQT
§9.1.4: Alias Type USE_VALUE or REGISTER, Token Type 0, the value the grant's record,
§4.1) and, beside it, the Setup Option `GRANT_PROOF` (type `0x3A49`, its value a proof
record, the verb `SETUP`) over a challenge the relay gave it: the `GRANT_CHALLENGE` of a
SETUP that relay sent it on an earlier session. `GRANT_PROOF` may appear once for each
grant the SETUP presents (MOQT §9.1 lets an option's definition allow it), each proof
naming its grant; the proofs of one SETUP may be over one challenge.
- **A client that holds no challenge** of the relay presents its grant with no proof. The
relay refuses that SETUP (`pop_missing`), and its own SETUP gives the challenge; the
client opens a second session and proves over it. So a client is admitted on its second
session to a relay it holds nothing of, and on its first where the relay's last SETUP to
it, the one that admitted it or refused it, is recent enough: after a GOAWAY that names
the same relay, or a connection lost and made again. This is the flow of
`draft-ietf-moq-privacy-pass-auth-04` §3.5.6.1 and its Figure 4, and of §10.5.3 over
HTTP, where a first request is answered `401` with a challenge.
- **The refusal.** A relay that refuses a session at SETUP as not authorised sends its
SETUP all the same, carrying the challenge and the Setup Option `GRANT_REFUSED` (type
`0x3A53`, its value the reason's code of §11 in ASCII: `pop_missing`,
`pop_wrong_challenge`, `pop_wrong_key`, and so on), serves nothing of the session, and
closes it with `UNAUTHORIZED` (MOQT §12.2, `0x2`), as the client is not authorised to
establish it, the reason in the close as well. A close does not carry what was sent
before it (`draft-ietf-moq-privacy-pass-auth-04` §3.5.6.1 notes that its challenge can be
lost so), so the relay holds the session open, serving nothing, until the client has
left or a bound of its own has passed (the reference: 2 seconds), and a client that
reads `GRANT_REFUSED` leaves at once. A client that reads `pop_missing`,
`pop_wrong_challenge` or `pop_replayed` opens one more session with a proof over the
challenge given; after any other reason, or a second refusal, it does not try again
(§3.5.6.8 there).
- **The code points.** MOQT sets aside no range of Setup Options for unregistered use
(MOQT §16.4): `GRANT_CHALLENGE`, `GRANT_PROOF` and `GRANT_REFUSED` are proposed for its
registry, and are not registered. A relay that does not know them ignores them (MOQT
§9.1), and then honours no grant whose rule asks for a proof.
- **The check.** The relay checks each proof by §10.5.4 before it sends its SETUP and
before it serves any request of the session, a request that arrived before the SETUP
among them (MOQT §6.3), and consumes the challenges of the proofs once every one has
passed. A grant presented later, in a request's AUTHORIZATION TOKEN (MOQT §9.20.2), is
honoured only where the session's SETUP proved possession of it; a request whose rights
rest on another is refused with `REQUEST_ERROR` `UNAUTHORIZED` (`0x1`).
- **Across drafts.** The order is the same at every MOQT draft a relay speaks. Where a
draft has the two SETUPs on one stream the client opens (a CLIENT_SETUP answered by a
SERVER_SETUP), it is that draft's own order. An implementation of the earlier order of
this section, in which the relay sent its SETUP first and the client waited for it,
does not interoperate with one of this order: such a client is sent no SETUP to wait
for, and such a relay holds no challenge of an earlier session. A relay and the clients
that present grants to it change together, and so do relays that peer (§10.7).
- **When a grant expires** (§10.2: with no re-validation, expiry alone ends a token), a
relay ends what the grant gave. At `draft-ietf-moq-transport-23`, whose PUBLISH_DONE
has the status `EXPIRED_AUTH_TOKEN` (`0x7`), it ends each subscription held by the
grant with that status and the session goes on, a later request by the expired grant
refused (`draft-ietf-moq-privacy-pass-auth-04` §3.5.6.6); a session that publishes by
the expired grant is closed, as no PUBLISH_DONE of the relay's ends a publication. At
the earlier drafts, whose PUBLISH_DONE has no such status, it closes the session with
`EXPIRED_AUTH_TOKEN` (MOQT §12.2).
- **Its bytes.** For a grant `G` and a relay at `https://relay.example/moq`, the client's
SETUP carries `AUTHORIZATION TOKEN` = `03 00` ‖ `G`'s record (USE_VALUE, type 0, the
value), and `GRANT_PROOF` = `WRP1` `01` ‖ digest(G) ‖ principal ‖ challenge ‖ `19 00` ‖
`SETUP //relay.example/moq` ‖ sig: 4 + 1 + 32 + 32 + 32 + 2 + 25 + 64 = 192 bytes. The
relay's `GRANT_CHALLENGE` is the 32 bytes alone, and its `GRANT_REFUSED` the code's
bytes alone, `pop_missing` being 11.
#### 10.5.3 Over HTTP
WHIP (RFC 9725) and its egress counterpart authenticate with an HTTP Bearer token (RFC
6750), as an HTTP delivery of the room's tracks may (§10.3). A grant presented over HTTP is
carried so:
- `Authorization: Bearer` and the grant's record in base64url without padding (RFC 4648
§5);
- `WAI-Room-Proof:` and a proof record in base64url without padding, the verb the request's
method and the target its own;
- the challenge, from the response header `WAI-Room-Challenge:` and 32 bytes in base64url
without padding. A server sends one on a `401 Unauthorized` to a request that carries a
grant and no proof that passes, with `WWW-Authenticate: Bearer error="invalid_token"`
(RFC 6750 §3), and on each response to a request it accepted, for the client's next
request: a WHIP or WHEP session's `PATCH` and `DELETE` (RFC 9725) each carry
a proof over the challenge the previous response gave. A first request carries none, is
answered `401` with a challenge, and is sent again with a proof over it, as a
resource server's DPoP nonce is (RFC 9449 §9).
`WAI-Room-Proof` and `WAI-Room-Challenge` are proposed for the HTTP Field Name Registry
(RFC 9110 §16.3), and are not registered.
Each challenge is good for one request: the server consumes it with the request whose
proof it accepts, and keeps the challenges it issued and has not consumed for a time it
states, past which a proof over one is refused (`pop_wrong_challenge`). A request whose
grant has no proof that passes is refused `401` with a fresh challenge.
#### 10.5.4 Checking a proof
A verifier is given the grant presented, which it has read and checked as §4.3, §5 and the
grant's window require; the challenge it holds for the request (HTTP), or, over MOQT, the
challenge the proof names where that is one the verifier issued and still holds (§10.5.2);
the challenges it consumed; and the verb and target it received. It refuses at the first of
these that fails:
1. A proof is presented beside the grant (`pop_missing`).
2. It reads: `WRP1`, version 1, each field of §10.5.1 whole, `context` of `!` to `~` and
space, and no byte after `sig` (`pop_malformed`).
3. Its `grant` is the grant's digest (`pop_wrong_grant`).
4. Its `key` is the grant's `principal` (`pop_wrong_key`).
5. `sig` verifies under `key`, strictly ([SPEC](../SPEC.md) §7 *Signatures*)
(`pop_bad_signature`).
6. Its `challenge` is one the verifier holds: the request's, or over MOQT one it issued in
a SETUP and has neither consumed nor let grow too old. A challenge it consumed before is
a proof presented again (`pop_replayed`); any other, another request's, one past its
time or none it issued, is `pop_wrong_challenge`.
7. Its `context` is the verb and the target the verifier received (`pop_wrong_context`).
It then consumes the challenge. A verifier that keeps no consumed challenges reports a
proof presented again as `pop_wrong_challenge`, and refuses it all the same.
### 10.6 Seeking back
A relay that keeps a window of a room's tracks, a DVR window
([live-recording](live-recording.md) §8), lets a viewer seek back into it. The **live
edge** of a track at a relay is the group of the Largest Object the relay knows of the
track (MOQT §3.1.4). A FETCH (MOQT §3.2) **seeks back** when its range starts in a group
below the live edge, and so does the fill of a SUBSCRIBE (MOQT §3.4); a fill that joins
the group in progress (MOQT §3.5) starts at the live edge, and does not.
A relay serves a request that seeks back on a track of the room only to a session whose
SETUP proved (§10.5) a grant that gives it the track (§10.2) and holds the seek power
(§3.3). It refuses any other with `REQUEST_ERROR` `UNAUTHORIZED` (MOQT §12.3) and the
reason phrase `seek_not_granted`. The power has no `moqt` action of its own:
`draft-ietf-moq-c4m-02` enables FETCH for a track, not for a range of it, so §10.2 step 5
enables FETCH for every grant that may subscribe, and the relay, which holds the grant
beside the token (§10.1), applies this rule. A viewer admitted by Privacy Pass (§10.4)
holds no grant, and does not seek back.
The rule is a function of the grant, the live edge and the request's start: with *L* the
live edge and (*g*, *o*) the start Location, the request seeks back when *g* < *L*, and is
then served only when the grant's `powers` has bit 4.
### 10.7 A room across relays
A room may be carried by several relays: one in each region, each serving the sessions that
reach it, and the relays connected to one another. MOQT leaves how relays interconnect out of
scope (`draft-ietf-moq-transport-22` §7) and namespace publication "is not a full-fledged
routing protocol and does not protect against loops" (§4.1 there). A session one relay holds
with another, in which each publishes to and subscribes from the other as the draft's relay
rules give (§7.4, §7.6 there), is a **hop link** ([live-metering](live-metering.md) §3.3).
This section is what a room needs of hop links:
1. **Each relay holds its own sessions to this section.** A principal presents its grant,
and proves it (§10.5), to the relay its session reaches, and that relay maps it onto
rights (§10.2), holds seeking back to it (§10.6), and holds rated tracks to the session's
threshold ([age-assurance](age-assurance.md) §5.5). A hop link runs under the token its
relay's operator issued for it (§10.1), never under a principal's grant: a relay **MUST
NOT** serve a principal's request on the rights of a hop link, and the relay at the far
end of the link applies none of the principal's grant, which it never sees.
2. **Seeking back across a hop link.** A relay that forwards a FETCH (MOQT §3.2) or a fill
(MOQT §3.4) of a track whose live edge it does not know learns the live edge from the
relay it would ask (TRACK_STATUS, MOQT §9.13) and holds the request to §10.6 against it,
refusing `seek_not_granted` before anything is forwarded; one that cannot learn it serves
no such request across the link to a session without the seek power. The relay that keeps
the DVR window ([live-recording](live-recording.md) §8) serves the hop link as its token
allows.
3. **Rated tracks.** A hop link proves no grant of the room, so its threshold in the room is
0 (age-assurance §5.5), and a relay forwards it no group whose age is above 0. A rated
room is served at the relays its rated tracks' publishers reach.
4. **No loops.** A relay **MUST NOT** forward a request (SUBSCRIBE, FETCH, TRACK_STATUS,
PUBLISH_NAMESPACE, SUBSCRIBE_NAMESPACE) back on the session it arrived on, nor to a relay
the request has already passed through, nor past a bound of its own on the relays a
request passes. It therefore carries, on every request it sends another relay, the relays
the request has passed through, itself last, by a key it names itself by: `draft-ietf-moq-
transport-22` §9.20 admits a Message Parameter "negotiated via Setup Options", and the
reference relay (`joule-moq-relay`) negotiates one so, at `draft-ietf-moq-transport-23`
and at -22, each end of a hop link declaring its relay id in its SETUP and the relay that
dialed proving its id over a challenge of the other's as §10.5.2 proves a key: its first
SETUP on a link holds no challenge, is refused and given one, and its second proves. A relay that receives a request naming itself refuses
it (`relay_loop`), as it does one past its bound (`relay_hops_exceeded`); a SUBSCRIBE that
no relay it may ask answers is refused `TIMEOUT` with `no_upstream`. These are reason
phrases of a relay's REQUEST_ERROR (MOQT §9.4), as `seek_not_granted` is, and no room
verifier raises them, so §11 does not list them.
5. **Evidence at every relay.** Every relay holds the objects it forwards to its own door
([jwp-receipts](jwp-receipts.md) §4.1), whatever the relay before it checked; signs its own
per-hop record of the room ([live-metering](live-metering.md) §3.10); and holds the floor and
the audience's inputs where [co-performance](co-performance.md) §8 places them.
## 11. Reasons
The codes this extension adds, each with where it is raised:
| reason | raised at |
|---|---|
| `descriptor_invalid` | §3.4 |
| `key_log_missing` | §5 step 1 |
| `key_log_not_held` | §5 step 2 |
| `authority_not_listed` | §5 step 3 |
| `authority_revoked` | §5 step 4 |
| `grant_invalid` | §4.3 step 5 |
| `grant_too_long` | §4.3 step 6 |
| `caps_exceed_role` | §4.3 step 7 |
| `grant_exceeds_room` | §4.3 step 8 |
| `ai_role_not_permitted` | §4.3 step 9 |
| `exchange_unlinked` | §4.3 step 3, §7.2 `grant_exchange` |
| `exchange_mismatch` | §4.3 step 3 |
| `lifecycle_malformed` | §7.2 |
| `grant_unknown` | §7.2 `join`, `grant_exchange` |
| `grant_wrong_principal` | §7.2 `join` |
| `grant_revoked` | §5, §7.2 `join`, `grant_exchange` |
| `grant_reused` | §7.2 `join` |
| `already_present` | §7.2 `join` |
| `not_present` | §7.2, §9.2 step 4 |
| `participant_limit` | §3.5, §7.2 `join` |
| `outside_grant_window` | §4.3, §7.2, §9.2 step 4 |
| `power_not_granted` | §9.2 step 5 |
| `ai_unmarked` | §4.4, §9.2 step 6 |
| `ai_model_mismatch` | §4.4, §9.2 step 6 |
| `ai_anchor_not_granted` | §4.4, §9.2 step 7 |
| `publish_not_permitted` | §7.2 `publish`, `grant_exchange`, `takeover` |
| `profile_not_granted` | §7.2 `publish`, `grant_exchange`, `takeover` |
| `already_published` | §7.2 `publish` |
| `not_published` | §7.2 `unpublish`, `takeover`, `failover` |
| `slot_unknown` | §7.2 |
| `slot_full` | §7.1, §7.2 `publish` |
| `publisher_limit` | §7.3 |
| `exchange_not_current` | §7.2 `grant_exchange` |
| `exchange_off_tick` | §4.6, §7.2 `grant_exchange` |
| `takeover_not_higher` | §7.1, §7.2 `takeover` |
| `failover_not_standby` | §7.1, §7.2 `failover` |
| `failover_early` | §7.1, §7.2 `failover` |
| `pop_missing` | §10.5.4 step 1 |
| `pop_malformed` | §10.5.4 step 2 |
| `pop_wrong_grant` | §10.5.4 step 3 |
| `pop_wrong_key` | §10.5.4 step 4 |
| `pop_bad_signature` | §10.5.4 step 5 |
| `pop_replayed` | §10.5.4 step 6 |
| `pop_wrong_challenge` | §10.5.3, §10.5.4 step 6 |
| `pop_wrong_context` | §10.5.4 step 7 |
| `seek_not_granted` | §10.6 |
Of live-events §9's codes, a room verifier also raises `truncated`, `malformed` and
`unknown_version` when a descriptor or a grant does not read (§3.4, §4.3); `bad_key` and
`bad_signature` for their signatures (§3.2, §4.2); `wrong_room` for a grant of another
room (§4.3 step 4); every code live-events §6 raises for a record (§9.2); `tally_mismatch`
(§9.4); and `delivery_gap` for a gap, which is not a refusal.
## 12. Conformance
[`live-conformance/rooms/`](../live-conformance/rooms/) holds the corpus, in one file,
`vectors.json`, written from fixed seeds by `wai_live_room_vectors gen`. It has the
records of each registered profile (§8.2), and **vectors**: a descriptor, grants, a
key-log head or none, and chains, with the outcome §9.4 gives them.
- **Accepted**: a lesson-style room (a director, a performer, an audience, and the
director's remote record control of the performer, under a key log whose head revokes
a grant and a viewer the room never admits); a broadcast room (one publisher, many
viewers, a moderator); a takeover; a redundant pair with failover; a grant exchange
that promotes a viewer to a publisher; and a broadcast room with a DVR window, whose
viewers' role holds the seek power, one viewer's grant holding it and another's
narrowing it away (§10.6). Each states the room id, every grant's digest, the merged
order with the grant each record was verified under, the room state (§9.3) with the
names of the tracks a subscriber can reach (§6.1), the tally (live-events §7), each
grant's `moqt` claim and re-validation interval (§10.2), and whether the grant holds
the seek power (§3.3).
- **Gap**: a chain with a record not delivered.
- **Refused**: each reason of §11 at least once, each vector a control of one defect, and
the codes of live-events §9 that a descriptor, a grant or a record of a room raises;
among them a grant with the seek power whose role lacks it, a grant and a role with
a power above bit 5, a grant whose `ai` attribute does not read, and an AI participant's
chat that states no origin, one that states origin 1 and one that names another model; an
AI captioner's caption that states origin 1, one that names another model, one anchored
to the audio of a principal that never published in the room and one anchored to a layer
above its `max_layer`; and a grant carrying `ai` under a role without the ai power, in a
room one of whose roles holds it.
- **AI participant**: a host whose grant names the model that acts for it, its chat and
its caption stating that model, beside a person whose chat and reaction state no origin;
and an AI captioner, of a role that holds the ai power, whose partial and final captions,
each with its annotation ([timed-text](timed-text.md) §4.2), are anchored to the host's
audio and state the model its grant names.
- **Proofs** (§10.5), in the file's `proofs`: a grant of a room presented with a proof over
MOQT (verb `SETUP`) and over HTTP (a WHIP `POST` and a `DELETE`, in their header forms),
each accepted; and, each refused with its reason, a grant presented with no proof, a
proof presented again in a later SETUP or request, a proof by a key other than the
grant's principal, one naming the principal and signed by another key, one over a
challenge the verifier does not hold, one naming another grant, one for another endpoint or method, a
MOQT proof presented over HTTP, and records that do not read. Each states the grant, the
proof (or the headers), the challenge the verifier holds, the challenges it consumed, the
verb and target it received, and the outcome.
- **Seeking back** (§10.6), in the file's `seek`: requests on a track of the DVR room, each
naming a grant of it by the vector's name and the grant's index, and stating the live
edge, the request's start Location and whether it is a FETCH or the fill of a
SUBSCRIBE. Served: a FETCH and a fill below the live edge under a grant with the seek
power; and, under a grant without it, a FETCH from the live edge's first object and
from within its group, a fill that joins the group in progress, and a FETCH from the
start of a track whose live edge is group 0. Refused `seek_not_granted`: a FETCH and a
fill below the live edge under the viewer's grant that narrows the power away, among
them one from the group just below the edge, and a FETCH below it under the
publisher's grant, whose role lacks the power. Each states the outcome, `serve` or
`refuse` with its reason.
A conforming verifier gives every vector the outcome, the reason, the place, the gaps and
the state, order, tally, claims and seek power the corpus states, every proof its
outcome, and every request of `seek` its outcome. `wai_live_room_vectors verify` and
the test `tests/live_room.rs` check that the file regenerates byte for byte, that `wai-rs`
gives every vector its outcome, and that §11 lists exactly the reference's codes.
`live-conformance/live_rooms_verify.py` is the second implementation, written from this
text: it shares no code with `wai-rs`, verifies signatures with WAI's strict rule written
from RFC 8032 (`claim-conformance/_ed25519.py`), verifies each event with the live-events
corpus's own second implementation (`live_events_verify.py`), and gives every vector,
every proof and every request of `seek` the same outcome.
## 13. Security and privacy considerations
- Every claim a principal acts under is signed by the room authority, and every event a
principal sends by the principal; a relay can drop either but alter neither, and a room
verifier replays the room from them alone.
- `t` is its author's claim (live-events §11). The clock's lifecycle events fix when a
principal was present and under which grant; a participant's `t` outside its grant's
window or outside its presence is refused. A clock that lies about time is the room's
own key lying, which its signature makes evidence of.
- A grant is public, and is a capability only together with the principal's key: every
event under it is signed by that key, so a copied grant lets no one else send, and every
binding honours it only beside a proof of that key over the verifier's fresh challenge,
good for one SETUP or one request and bound to the endpoint addressed (§10.5), so a
copied grant admits no one else either. A token mapped from it (§10) is bound to the
principal's key where the format allows (`catdpop`, `cnf`), and its window is fourteen
days at most.
- A proof is good for the one SETUP or the one request that consumes its challenge, at the
target it names and at the verifier that issued the challenge. Over MOQT a challenge is
given in one session and answered in a later one, so it is not bound to a connection: it
is bound to the relay that issued it, to a time that relay states, and to one use, and a
proof over it is bound to the principal's key and the endpoint. Whoever reads a
challenge off the wire can make no proof without the key; a relay that kept the proof
could present it to no other endpoint, and not to this one again. A verifier that issued
a challenge and did not keep it refuses a proof over it; one that keeps consumed
challenges tells a replay from a stranger's challenge, and both are refused. Relays
behind one name that do not share the challenges they issue each refuse a proof over
another's and give their own, so a front keeps a client's sessions on one relay for
that time, or the client proves on its next session wherever it lands.
- Revocation is through the key log, so it is as available as the log (jwp-receipts §7.8):
a relay with no fresh status for the authority's origin fails closed.
- A viewer's tag is a hash of an id the authority assigns; the key log publishes the tag,
not the id, and an authority **SHOULD** assign ids that are not otherwise meaningful.
- A grant names its principal, so a room's grants link that principal's participation; a
principal that needs participation unlinkable across rooms uses a key per room, and a
viewer that needs no identity is admitted by Privacy Pass (§10.4).
- The region pin (§3.7) binds where a recording is kept; checking a recording's receipt
against it is the recording extension's.