open-standards/wai/extensions/forensic-watermark.md
2026-10-20 08:44:50 -04:00

742 lines
44 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: Forensic Watermark Sessions
> Status: Draft. Registers the record `wai.live.watermark`: a signed record of each session in
> which an edge chose forensic A/B variants for a viewer of a room, as ETSI TS 104 002 has an
> edge choose them. The edge signs a **session record**, `WWS1`, when its part in the session
> ends: the room and the content, the span, the content keys the variants were encrypted
> under, the watermark token by its digest, the positions of the pattern it embedded with the
> variant at each, and a salted commitment to the session's grant and principal that only the
> room authority can open. Anyone who recovers a variant sequence from a leaked copy states it
> in a signed **recovered sequence**, `WWQ1`; a deterministic rule says which session records
> it agrees with. For one matched session, the room authority signs a **disclosure**, `WWD1`,
> that opens the commitment: a third party checks it against that session record and the grant
> it names, and learns nothing of other sessions. The record covers Lane A delivery alone
> (SPEC §6.5): a room that is end-to-end encrypted has no edge that can mark it, and a record
> for one is refused. No new transport and no new token format: the token is TS 104 002's.
> The reference implementation is the `live_watermark` module of `wai-rs` (feature
> `live_watermark`); the corpus is [`live-conformance/watermark/`](../live-conformance/watermark/),
> and its second implementation is `live-conformance/live_watermark_verify.py`.
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
>
> Texts read for this revision: ETSI TS 104 002 V1.1.1 (2023-08), "the TS", downloaded from the
> ETSI deliverables server, whose terms allow reading it and refer reproduction to ETSI: this
> text cites its clauses and reproduces none of it; RFC 8392 (CWT), which the TS requires the
> token to be; RFC 8949 §4.2 (deterministically encoded CBOR) and RFC 8610 (CDDL); RFC 9052
> (COSE structures, which obsoletes the RFC 8152 that RFC 8392 cites, with the same structures)
> and RFC 9053 (COSE algorithms), which the TS cites; and RFC 4648 §5. A clause or section
> number of one of those texts is that revision's.
## 1. Scope
In scope: where the record applies, and why only Lane A (§3); how a WAI verifier reads the
TS's watermark token and what it takes from it (§4); the session record, its positions and
bits, the commitment to the session's grant, its signature, and who may sign it (§5); what
the edge keeps, where, for how long, and who reads what (§6); the recovered sequence and the
rule that matches it against session records (§7); the disclosure, when the authority signs
one, and how a third party checks it (§8); and verification (§9).
Out of scope: a transport; the TS's content preparation, its WMPaceInfo carriage and its
origin endpoints, which this extension reads and does not change; how a vendor's detector
recovers a pattern from a copy, which the TS leaves out of its own scope (§5.8 there) and
which this extension does not define either (§7.5); the revocation of a token, also out of the
TS's scope; client-driven and client-side watermarking, which the TS leaves out (§4 there);
and how a recovered sequence or a disclosure reaches anyone.
Relationship to the core spec: `wai.live.watermark` 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. It is one of the records SPEC §6.5 names for a Lane A title.
Relationship to [live-rooms](live-rooms.md): a session record is bound to the grant its
session ran under (§4 there), through a commitment (§5.3), and an edge is admitted to sign one
through the authority's key log (§5 there; §5.6 here). The room's region pin and retention
bound (§3.7 there) bound what the edge keeps (§6).
Relationship to [confidentiality](confidentiality.md): a room with a confidentiality record is
Lane B and has no session record (§3.2).
Relationship to [live-metering](live-metering.md): an edge that signs session records is a hop
of the room's sessions and signs its per-hop record there as well; neither record names the
other. A per-hop record names grants in its ledger, which the deployment gives only to the
parties that verify the receipt (live-metering §11); a session record names none.
## 2. Terms
- **Room**, **room id**, **room authority**, **clock**, **room key**, **principal**, **grant**:
as in live-rooms §2.
- **Viewer**: a principal that subscribes to a room's tracks under a grant.
- **Edge**: the server that chooses, for each segment of a session, which variant it serves,
and signs the session record: the TS's edge (§5.1 there), here the hop that serves the
viewer's session (§3.3).
- **Segment**, **variant**, **WM pattern**, **sequencing**: as in the TS's §3.1. A variant is
numbered as the TS's `variantId` numbers it, from 0: variant 0 is A, variant 1 is B.
- **Token**: the TS's WM token (§5.4 there), as the bytes base64url (RFC 4648 §5) decodes it
to: one CBOR data item.
- **Pattern bit** *p*: bit *p* of the WM pattern, *p* from 0, addressed big-endian as the TS's
§5.4 requires: bit 7 − (*p* mod 8), the most significant first, of byte ⌊*p* / 8⌋.
- **Session**: what an edge serves one viewer under one grant and one token: the subscriber
sessions (MOQT sessions, or the HTTP requests of a playback) in which that token is presented
under that grant (§3.3).
- **Claimant**: whoever states a recovered sequence and signs it.
- *u64*, *u32*, *u16*, *u8*: little-endian unsigned integers of that many bits. A *short*
field is a u8 length and that many bytes. A digest is SHA-256 (FIPS 180-4), 32 bytes.
Ed25519 is RFC 8032's, verified strictly (SPEC §7 *Signatures*): a key that fails step 1 or
2 is `bad_key`, and a signature that fails a later step is `bad_signature`.
- A **bitmap** of a pattern of `patlen` bits is ⌈`patlen` / 8⌉ bytes, bit *p* addressed as a
pattern bit is, and its bits from `patlen` on, in its last byte, are 0.
## 3. Lane A only
### 3.1 Where the record applies
The record applies to a session whose media the edge delivered as Lane A content (SPEC §6.5):
segments encrypted under content keys that a platform's protected media path decrypts, with
the decoded frames out of the application's reach. That is the delivery the TS is written for:
its functional architecture has the content encrypted and protected by a DRM system, with the
A and B variants of a representation encrypted under one content key, so that one licence opens
whichever variant the edge serves (§5.2, §5.6.1 there).
A session record names the key identifier (KID) of every content key its variants were
encrypted under, the KID a release decision of `wai.video.keys` names (SPEC §5), so the
session is tied to the release decisions of those keys without the record naming the licence
or the viewer. A record that names none is refused `not_protected`. A deployment that marks
content delivered in the clear is refused by nothing in WAI, and this record does not cover
it: a plaintext object of a room is already named by its content hash and its receipt, and an
edge's choice between two plaintext variants is not a Lane A delivery.
### 3.2 Rooms that are end-to-end encrypted
A room with a confidentiality record ([confidentiality](confidentiality.md) §3) is Lane B, and
has no edge that can mark it:
- Each object of its media tracks is sealed by its publisher, and the AEAD authenticates the
object's Full Track Name, Group ID and Object ID (confidentiality §4.1, §4.7). An edge that
served an object of a variant track as an object of the viewer's track would have it refused
by the receiver, `bad_tag`; and MOQT holds one object at one location (confidentiality §6
step 8), so one track cannot carry one object to one subscriber and another to the next.
- The only parties that could vary what one member receives are its publisher, which holds no
per-viewer choice in this model, and the member's own device, which is client-side
watermarking, outside the TS (§4 there).
- Lane B does not keep the decoded output from the member's application (SPEC §6.5), so the
premise of the TS's server-driven model, that the device only conveys the token and cannot
see or choose its variants, does not hold for it.
A verifier given a room's confidentiality record refuses every session record of the room
`e2ee_room` (§9.1 step 2). An edge **MUST NOT** sign a session record for such a room.
### 3.3 The edge
The edge is the server of the TS's server-driven architecture (§5.1, §5.2 there): it validates
the viewer's token, which accompanies every request for a watermarked segment (§5.7.5.1
there), reads each segment's WMPaceInfo (§5.5, §5.7.5.2 there), and serves the variant the
pattern selects. In a room, it is the hop that serves a subscriber session one of the room's
tracks:
- **Over HTTP**, a rendition of the room's tracks (live-rooms §10.3) served as the TS's
§5.7 serves a manifest's segments, the token carried as its §5.7.3 carries it.
- **Over MOQT**, a relay serving a track of the room (live-rooms §6.1): a segment is one group
of the track; the variants of the track are two upstream tracks whose names are the
deployment's (this revision names none); the subscriber receives the track under the name
live-rooms §6.1 gives it, as the TS keeps the manifests a device receives neutral (§5.6.4
there). How a MOQT subscriber presents the token is not defined in this revision; an edge
that receives it by the deployment's means signs the same record.
The edge admits the session under the viewer's grant, with proof of possession of its key
(live-rooms §10.5), so it knows the grant and the principal. A session admitted without a grant
(live-rooms §10.4) names no principal, and has no session record.
A session record covers one session. An edge binds a token to the grant under which it is
first presented, and refuses it under any other grant, so one token is one session's (§9.1
step 3.8); a viewer that reconnects under its grant with its token continues its session. An
edge that is given a new token ends the record of the old one and starts another, and the
records of one viewer's sessions have spans that do not overlap.
## 4. The watermark token
### 4.1 Its bytes and its digest
The token travels as the TS's §5.4 has it: a CWT (RFC 8392) whose claims set has integer keys
and is deterministically encoded (RFC 8949 §4.2), signed as RFC 8392 §7 has it, and
base64url-encoded. Its **bytes** are what the base64url text decodes to, and its **digest** is
SHA-256 over those bytes. A session record names its token by the digest alone (§5.1).
The record does not carry the token, for two reasons. The token may carry what identifies a
viewer: RFC 8392's `sub`, and in the TS's indirect mode a `wmid` that its vendor derives the
pattern from, which the TS leaves to the vendor and which may be an account's identifier (§5.4
there); a record that carried it would put that in the hands of everyone who matches recovered
sequences. And until its `exp`, a token can be presented again to obtain segments marked with
its pattern. The token travels beside the record to the parties that check it (§5.7, §6.3).
### 4.2 Reading a token
A reader refuses a token at the first of these steps that fails, with the code it names:
`token_malformed` where the token is not what the TS and the RFCs it cites require, and
`token_unsupported` where it is a form they allow that this revision does not read.
1. **CBOR.** The bytes are one data item (RFC 8949 §3), with no byte after it, read by these
rules, item by item from the start, each refused at the first rule it breaks:
- an item nested more than 16 deep (the token itself at depth 1, an item inside an array,
a map or a tag one deeper than it) is `token_unsupported`, checked before the item is
read;
- a head whose additional information is 28, 29, 30 or 31 (a reserved value, or an
indefinite length or a break, which deterministic encoding forbids) is
`token_malformed`; so is an argument not written in its shortest form (RFC 8949
§4.2.1);
- in major type 7, a simple value written in one extra byte below 32 is `token_malformed`
(RFC 8949 §3.3); a floating-point value, or any simple value other than false, true and
null, is `token_unsupported`;
- a byte or text string longer than the bytes left, an array or a map that runs past them,
and a text string that is not UTF-8 (RFC 3629) are `token_malformed`;
- a map whose keys, each read when it is reached, are not in strictly ascending bytewise
order of their encodings (RFC 8949 §4.2.1, which also rules out a key repeated) is
`token_malformed`.
2. **The CWT tag.** When the item is tag 61 (RFC 8392 §6), the item it holds is read on, and
is a COSE tag (16, 17, 18, 96, 97 or 98, RFC 9052 §2): otherwise `token_malformed` (RFC 8392
§7.2 step 2).
3. **The COSE tag.** The item is tag 17, COSE_Mac0, or tag 18, COSE_Sign1: the two structures
of the TS's algorithms (§4.3). Another COSE message, or an untagged one, whose type RFC 8392
§7.2 step 3 leaves to an application context the TS does not fix, is `token_unsupported`.
4. **The structure.** The tag holds an array of four: a byte string (the protected header), a
map (the unprotected header), a byte string or null (the payload), and a byte string (the
tag, or the signature). Otherwise `token_malformed`. A null payload, detached, is
`token_unsupported`.
5. **The headers.** The protected header is empty, or holds one data item, read as step 1
reads, that is a map (`token_malformed`). Every label of the two maps is 1 (`alg`) or 4
(`kid`): another is `token_unsupported` (RFC 8392 §7.2 step 4). A label in both maps is
`token_malformed` (RFC 9052 §3). `alg` is in the protected map (`token_malformed`), and is 5
or −7 (`token_unsupported`); 5 needs tag 17 and a tag of 32 bytes, −7 tag 18 and a signature
of 64 bytes (`token_malformed`). A `kid` is a byte string (`token_malformed`).
6. **The payload** holds one data item, read as step 1 reads. A tag 61 or a COSE tag there is
a nested CWT (RFC 8392 §7.2 step 6), `token_unsupported`. Otherwise it is a map
(`token_malformed`) whose every key is an integer (`token_malformed`, the TS's §5.4): the
claims set.
7. **The claims**, all `token_malformed` but where another code is named:
1. `exp` (4) and `iat` (6) are present, as the TS requires, and are unsigned integers;
`nbf` (5), when present, is an unsigned integer;
2. `iss` (1), `sub` (2) and `aud` (3), when present, are text strings, and `cti` (7) a
byte string;
3. `wmver` (300) is present and an unsigned integer, and is 1 (`token_unsupported`: the TS
describes version 1);
4. `wmvnd` (301) is present and an unsigned integer of at most 255, and `wmpatlen` (302)
one of at most 65 535;
5. `wmsegduration` (303), when present, is an array of two unsigned integers; `wmid` (305),
when present, a text string; `wmopid` (306) and `wmkeyver` (307), when present, unsigned
integers;
6. with `wmpattern` (304) present, the token is in **direct** mode, and the claim is a byte
string of exactly ⌈`wmpatlen` / 8⌉ bytes, the pattern **in the clear**; or an array of
three items, a byte string, a map, and a byte string or null (COSE_Encrypt0, RFC 9052
§5.2), or of four whose fourth is an array of at least one item (COSE_Encrypt, §5.1),
the pattern **encrypted**. Without it, the token is in **indirect** mode, its pattern
**derived** by the vendor's core (Annex A there), and `wmid`, `wmopid` and `wmkeyver`
are present.
Other claims are not read: the TS's claims set admits any integer-keyed extension.
### 4.3 What a token gives
A token that reads gives: its `alg`; its **vendor**, `wmvnd`; its **pattern length**,
`wmpatlen`; its **source**, 1 for a pattern in the clear, 2 for an encrypted one, 3 for a
derived one; the pattern itself where it is in the clear; and its **window**: it is good at the
instant *u*, in microseconds since 1970-01-01T00:00:00Z, when
max(`iat`, `nbf`) × 10^6 ≤ *u* < `exp` × 10^6, `nbf` counting as 0 when absent.
### 4.4 What a WAI verifier does not check
A WAI verifier does not check the token's MAC or signature. HMAC 256/256 is keyed by a secret
the token's issuer shares with the edge, and ES256 by the issuer's key, neither of which a
WAI verifier holds; the edge checks it (the TS's §5.7.5.1), and an edge **MUST NOT** sign a
session record for a session whose token did not validate. The edge's signature on the record
is the edge's statement that it did. Nor does a WAI verifier open an encrypted pattern or derive
an indirect one: the record states the pattern the edge sequenced by, and §5.7 checks it against
the token where the token holds it in the clear.
## 5. The session record
### 5.1 Layout
| field | size | meaning |
|---|---|---|
| `magic` | 4 | `WWS1` (ASCII) |
| `version` | 1 | 1 in this revision |
| `room` | 32 | the room id |
| `edge` | 32 | the edge's Ed25519 key, which signs the record |
| `owner` | short | the content: the owner field of its Track Namespace (live-rooms §6.1), 64 lowercase hexadecimal digits of its publisher's key or `_` and a slot's name |
| `media` | 1 | the content's media kind: 1 audio, 2 video, 3 screen |
| `kids` | 1 + 16 × *k* | a u8 count (0 to 8), then the KID of each content key the variants were encrypted under (§3.1), strictly ascending |
| `start`, `end` | 8 each | u64: the session's span on the room clock, in microseconds: from `start`, up to and not including `end` |
| `token` | 32 | the digest of the session's token (§4.1) |
| `vendor` | 1 | the token's `wmvnd` |
| `source` | 1 | how the edge has the pattern: 1 in the clear, 2 decrypted, 3 derived (§4.3) |
| `patlen` | 2 | u16: the token's `wmpatlen` |
| `served` | ⌈`patlen` / 8⌉ | a bitmap: the positions at which the edge served the session a sequenced segment (§5.2) |
| `bits` | ⌈`patlen` / 8⌉ | a bitmap: at each position of `served`, the variant it served there (0 or 1); 0 elsewhere |
| `segments` | 4 | u32: the sequenced segments it served the session |
| `unmarked` | 4 | u32: the segments it served the session that were not sequenced (§5.2) |
| `binding` | 32 | the commitment to the session's grant and principal (§5.3) |
| `region` | 2 | where the edge keeps the record: an ISO 3166-1 alpha-2 code, two capitals (§6.1) |
| `expires` | 8 | u64: seconds since 1970-01-01T00:00:00Z, the instant from which the record, its token and its opening are kept no more (§6.1) |
| `sig` | 64 | §5.4 |
### 5.2 Positions and bits
Each segment the edge serves carries a WMPaceInfo `position` (the TS's §5.5.2). The edge
**sequences** a segment when sequencing is enabled at it (§5.3 there) and the position is at
least 0 and below `patlen`: it serves the variant the pattern bit at that position selects,
variant 0 for 0 and variant 1 for 1 (§10.3), sets the position in `served` and the variant in
`bits`, and counts the segment in `segments`.
Every other segment it serves is **unmarked**, served from variant A's endpoint as the TS's
§5.3 and §5.7.5.1 have it, counted in `unmarked`, and in neither bitmap: a segment of position
−1 (not watermarked, or a contingency of §5.7.5.1 there, which that clause keeps out of
detection); one served while sequencing was disabled at the edge, when every viewer receives
variant A; and one whose position is at or beyond `patlen`, which has no bit in the pattern,
since the TS addresses bits within the pattern and defines no wrap. An edge that serves an ad
from another edge that does not sequence (§5.7.2 there) serves nothing for it.
One position may be sequenced many times in a session, as positions repeat: each time the same
bit selects the same variant. So `bits` is the pattern at the positions of `served`, and 0
elsewhere: what the edge embedded in the session, and nothing it did not.
### 5.3 The binding
The **binding** is SHA-256 over the 27 bytes `wai:live-watermark-binding` (ASCII) and 0x01,
followed by `salt` (32), `grant` (32) and `principal` (32):
- `salt`: 32 bytes the edge draws for the session from a cryptographically secure generator;
- `grant`: the digest of the grant the session ran under (live-rooms §4.2);
- `principal`: that grant's `principal`.
The **opening** of a record is its digest (§5.4), `salt`, `grant` and `principal`. The edge
gives the opening to the room authority when it signs the record (§6.2). Without the salt, the
binding reveals nothing of the grant or the principal: a verifier holding every grant of the
room cannot test them against it.
Why a commitment, and not the grant's digest in the clear: every participant of a room holds
its grants (live-rooms §10.5), and a grant names its principal (live-rooms §13), so a grant's
digest names a viewer to anyone who holds the room's grants. The record goes to the parties
that match recovered sequences (§6.3), and names no viewer to them.
### 5.4 Digest and signature
The **digest** of a record is SHA-256 over its bytes before `sig`. The **signature** is
Ed25519 by `edge` over the 27 bytes `wai:live-watermark-session` (ASCII) and 0x01, followed by
the bytes the digest covers.
### 5.5 Reading and rules
A reader reads exactly the bytes it is given as one record, in the order of §5.1, and refuses
at the first of these that holds:
1. Fewer than 4 bytes: `truncated`. A magic other than `WWS1`: `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 refuses as `record_invalid` a record that breaks any of these rules:
- `owner` is 64 lowercase hexadecimal digits, or `_` followed by a slot name (live-rooms §2);
`media` is 1, 2 or 3;
- `kids` has at most 8 entries, strictly ascending, none of them 16 zero bytes;
- `start` is below `end`; `source` is 1, 2 or 3; `patlen` is at least 1;
- `served` and `bits` are bitmaps (their bits from `patlen` on are 0), and `bits` sets no bit
`served` does not;
- `segments` is at least the number of positions `served` sets;
- `region` is two capitals `A`–`Z`.
### 5.6 Who may sign
Where the room names an origin, the edge's key has an entry in the head given for the room
(live-rooms §5) whose `may_sign` holds the authority `live/watermark`
([jwp-receipts](jwp-receipts.md) §7.5): otherwise `edge_not_authorised`. An entry whose
`status` is `revoked` is `edge_revoked`. Where the room names no origin, the edge is a room key
(live-rooms §2): otherwise `edge_not_authorised`.
`live/watermark` is an authority of its own, and not `live/relay`: a relay's key signs what it
carried, and a session record is a forensic statement about one viewer's session that an
origin gives to the edges it trusts with it.
### 5.7 The token beside the record
A verifier given the token with a record checks, in this order:
1. its digest is the record's `token`: otherwise `token_mismatch`;
2. it reads (§4.2): `token_malformed`, `token_unsupported`;
3. its vendor, pattern length and source are the record's `vendor`, `patlen` and `source`;
and, for a pattern in the clear, the record's `bits` are the pattern's bits at the positions
of `served`: otherwise `token_mismatch`;
4. the session's span is within the token's window (§4.3): the token is good at `t0` +
`start` and at `t0` + `end` − 1, `t0` the descriptor's (live-rooms §3.1). Otherwise
`outside_token_window`.
A record given without its token is verified all the same, and the verifier states that its
token was not given; the vendor, pattern length and bits are then the edge's statement alone.
## 6. Retention and privacy
### 6.1 Where, and for how long
An edge keeps each session record, its token and, until it is delivered (§6.2), its opening,
and keeps them within the room's region pin (live-rooms §3.7). The pin covers what a relay
keeps of a room beyond delivery, and a record of a session of the room is such; this extension
holds it to the pin as capture-binding holds a take's master and live-recording a package.
`region` states where the record is kept, and a verifier refuses a region outside a pin that is
not empty: `region_not_pinned`.
`expires` is the instant from which none of them is kept, by the edge or by the room authority.
It is at or after the session's end, `expires` × 10^6 ≥ `t0` + `end` (otherwise
`record_invalid`), and, when the descriptor's `retention_days` is not 0, at most that many days
after it: `expires` × 10^6 ≤ `t0` + `end` + `retention_days` × 86 400 × 10^6, otherwise
`retention_exceeded`. The room id commits to the descriptor, so every viewer that joined the
room joined under the bound on what is kept of its sessions.
### 6.2 The opening
The edge gives each record's opening (§5.3) to the room authority when it signs the record, by
the deployment's means, and **MUST NOT** keep it once the authority holds it. The authority
keeps the openings within the pin until each record's `expires`, and deletes them then. Nothing
but an opening links a record to a viewer: the edge admitted the session under its grant, and
keeps no other record of which grant a record's session ran under once the opening is
delivered.
### 6.3 Who reads what
- **A session record** goes to the parties the deployment names to match recovered sequences
(a rights holder, and its agents), and to a third party checking a disclosure. It names no
grant, principal or viewer: its span, its content, its token's digest and its pattern bits
identify a session, and only an opening links a session to a principal.
- **A token** goes to the room authority and to a third party checking a disclosure (§8.4). A
matching party needs only the records.
- **An opening** stays with the room authority, and is disclosed one session at a time (§8).
A pattern the token's issuer draws once per session links nothing across sessions. A pattern
drawn per device or per account (the TS allows either, §4 there) is the same in every session
of that device or account, so the records of its sessions share their bits; a deployment that
needs a viewer's sessions unlinkable to a matching party draws a pattern per session.
## 7. Matching a recovered sequence
### 7.1 The recovered sequence
A claimant that recovers a variant sequence from a copy states it in a recovered sequence,
`WWQ1`:
| field | size | meaning |
|---|---|---|
| `magic` | 4 | `WWQ1` (ASCII) |
| `version` | 1 | 1 in this revision |
| `room` | 32 | the room id |
| `owner` | short | the content, as in §5.1 |
| `media` | 1 | the content's media kind |
| `vendor` | 1 | the `wmvnd` of the vendor whose detector recovered it |
| `patlen` | 2 | u16: the pattern length it was recovered at |
| `start`, `end` | 8 each | u64: the span of the room clock the copy was delivered in, as the claimant states it; 0 and 2^64 − 1 when it does not know |
| `known` | ⌈`patlen` / 8⌉ | a bitmap: the positions recovered |
| `bits` | ⌈`patlen` / 8⌉ | a bitmap: at each position of `known`, the variant recovered; 0 elsewhere |
| `copy` | 32 | the digest of the copy it was recovered from, as the claimant holds it; 32 zero bytes when not stated |
| `claimant` | 32 | the Ed25519 key that signs it |
| `sig` | 64 | Ed25519 by `claimant` over the 28 bytes `wai:live-watermark-sequence` (ASCII) and 0x01, followed by the bytes before `sig` |
Its **digest** is SHA-256 over its bytes before `sig`.
### 7.2 Reading and rules
It is read as §5.5 steps 1 to 3 read a record, with `WWQ1` for `WWS1`, and refused as
`sequence_invalid` when: `owner` and `media` break §5.5's rules for them; `patlen` is 0;
`start` is not below `end`; `known` or `bits` is not a bitmap; `bits` sets a bit `known` does
not; or `known` sets none. Then its `room` is the room id (`wrong_room`), and its signature
verifies (`bad_key`, `bad_signature`).
### 7.3 Candidates and agreement
A session record *S* is a **candidate** for a recovered sequence *Q* when *S*'s `owner`,
`media`, `vendor` and `patlen` are *Q*'s, and their spans meet: *S*'s `start` is below *Q*'s
`end` and *Q*'s `start` below *S*'s `end`.
A candidate **agrees** with *Q* when, at every position *Q*'s `known` sets, *S*'s `served`
sets it and *S*'s `bits` holds *Q*'s bit. Otherwise its **first disagreement** is the lowest
such position at which one of the two fails: **unserved** where *S* did not sequence it,
**differs** where it did and embedded the other variant.
### 7.4 The outcome
Over the session records it is given, each verified (§9.1), a verifier states each candidate in
the order given, with whether it agrees and, where it does not, its first disagreement and how;
and the number of positions *Q* states. Then:
- **matched**: exactly one candidate agrees; the outcome names its digest;
- **unmatched**, `no_match`: no candidate agrees, or there is no candidate;
- **unmatched**, `ambiguous_match`: more than one candidate agrees; the outcome names each.
### 7.5 What the rule leaves to detection
The TS leaves detection, and the statistics of its decoder, out of its scope (§5.8 there): a
vendor's detector extracts the identifier from a copy of several minutes, and the TS sets no
error model, no tolerance and no threshold. This rule therefore binds what was embedded, and
decides nothing about how it was recovered:
- A position the detector could not read with confidence is left out of `known`: an
**erasure**. A stated bit is compared exactly; this rule has no tolerance for a stated bit
that disagrees, since the TS gives none.
- How many positions tell sessions apart depends on how the token's issuer drew the patterns
(their length and their uniqueness, which the TS leaves to it); the outcome states the number
of positions stated, and sets no minimum.
- A copy assembled from several sessions' segments (collusion) is outside the TS's model and
this rule's: it agrees with none of them at every stated position unless their bits coincide
there.
- A segment served unmarked (§5.2) is in no record's `served`; a bit recovered from one makes
a true session disagree (unserved), which is why a claimant states only positions recovered
from the span in which the content was sequenced.
## 8. Disclosure
### 8.1 Layout
| field | size | meaning |
|---|---|---|
| `magic` | 4 | `WWD1` (ASCII) |
| `version` | 1 | 1 in this revision |
| `room` | 32 | the room id |
| `session` | 32 | the digest of the session record it opens (§5.4) |
| `sequence` | 32 | the digest of the recovered sequence it answers (§7.1) |
| `grant` | 32 | the opening's grant digest |
| `principal` | 32 | the opening's principal |
| `salt` | 32 | the opening's salt |
| `t` | 8 | u64: seconds since 1970-01-01T00:00:00Z, when the authority signed it |
| `sig` | 64 | §8.2 |
### 8.2 Digest and signature
The **digest** is SHA-256 over the 205 bytes before `sig`. The **signature** is Ed25519 by the
descriptor's `authority` over the 30 bytes `wai:live-watermark-disclosure` (ASCII) and 0x01,
followed by the bytes the digest covers. A disclosure is read as §5.5 steps 1 to 3 read a
record, with `WWD1` for `WWS1`; it has no other field rule.
### 8.3 When the authority discloses
The room authority signs a disclosure only:
- for a recovered sequence signed by its claimant, that reads and verifies (§7.2);
- whose match over every session record of that content the authority holds for the
sequence's span is **matched** (§7.4), and for that one session: a third party given one
record cannot check this, and the authority, which holds the openings, answers for it;
- before the record's `expires` (§6.1).
It discloses one session per disclosure, and never an opening for a sequence that does not
agree with its record. Whether a claimant's request is lawful, and to whom the authority
answers it, is the deployment's and the law's; a disclosure records that the authority answered
this sequence with this session's opening.
### 8.4 Checking a disclosure
A third party is given the room (a descriptor, its grants, the head of the authority's key log
or none, and the room's confidentiality record if it has one), one session record and, if it
holds it, its token, one recovered sequence, and the disclosure. It runs §9.3, and from an
accepted disclosure learns: the session's grant and principal, and that the edge embedded in
that session, at every position the claimant states, the variant the claimant recovered. It
learns nothing of another session: it holds no other record, and the disclosure names one.
## 9. Verification
### 9.1 Session records
A verifier is given the room (a descriptor, its grants, the head of the authority's key log or
none, and the room's confidentiality record or none) and a list of session records, each with
its token or none. It runs these steps and stops at the first refusal, which it reports with
the reason and where:
1. **The room**: the descriptor reads and verifies (live-rooms §3.4, §3.2), at `descriptor`;
the key log is held (live-rooms §5 steps 1 to 4), at `key_log`; and the grants are checked
(live-rooms §4.3), at `grant` and the grant's index; each with that section's reasons.
2. **Confidentiality**, at `confidentiality`, when a confidentiality record is given: it reads
(confidentiality §3.3 step 1: `truncated`, `malformed`, `unknown_version`), names the room
(`wrong_room`), and its signature verifies under the descriptor's `authority`
(`bad_key`, `bad_signature`). A record that passes states that the room is end-to-end
encrypted: `e2ee_room` (§3.2). Its suites are confidentiality's to check, not this
extension's.
3. **Each session record**, in the list's order, at `session` and its index:
1. it reads (§5.5): `truncated`, `malformed`, `unknown_version`, `record_invalid`;
2. its `room` is the room id: `wrong_room`;
3. its signature verifies (§5.4): `bad_key`, `bad_signature`;
4. its edge may sign (§5.6): `edge_not_authorised`, `edge_revoked`;
5. its content is the room's: an `owner` of the form `_` and a slot's name names a slot of
the descriptor, of the record's `media`. Otherwise `content_unknown`. A publisher's key
as `owner` is not checked here;
6. it names a content key (§3.1): `not_protected`;
7. its region and its `expires` (§6.1): `region_not_pinned`, `record_invalid`,
`retention_exceeded`;
8. its `token` is no earlier record's in the list: `token_reused`. One token is one
session's (§3.3), and two records that shared one would share its pattern;
9. with its token given, §5.7: `token_mismatch`, `token_malformed`, `token_unsupported`,
`outside_token_window`.
The records are **accepted** when every one passes. The verifier states, for each, its digest,
its edge, its content, its span, its KIDs, its vendor, source and pattern length, the number of
positions it served, its `segments` and `unmarked`, its region and `expires`, and whether its
token was given, with what the token gives (§4.3) when it was.
### 9.2 A match
A verifier is given what §9.1 is given and a recovered sequence. It runs §9.1, then reads and
verifies the sequence (§7.2), at `sequence`, then states the outcome of §7.4.
### 9.3 A disclosure
A verifier is given what §8.4 lists. It runs these steps and stops at the first refusal:
1. §9.1 over the one session record, with its token if given.
2. The sequence (§7.2), at `sequence`.
3. The disclosure, at `disclosure`: it reads (§8.2: `truncated`, `malformed`,
`unknown_version`); its `room` is the room id (`wrong_room`); its signature verifies under
the descriptor's `authority` (`bad_key`, `bad_signature`).
4. Its `session` is the record's digest and its `sequence` the sequence's: otherwise
`disclosure_mismatch`.
5. The record is a candidate for the sequence and agrees with it (§7.3): otherwise
`sequence_disagrees`.
6. Its opening opens the record's `binding` (§5.3): otherwise `opening_mismatch`.
7. A grant given has the digest `grant` (`grant_unknown`), and its `principal` is the
disclosure's (`grant_wrong_principal`).
8. That grant was good over the session's span: `nbf` × 10^6 ≤ `t0` + `start` and
`t0` + `end` ≤ `exp` × 10^6. Otherwise `outside_grant_window`.
9. Its `t` is at or before the record's `expires`: otherwise `disclosure_expired`.
A disclosure that passes is **accepted**, and the verifier states its digest, the session's and
the sequence's digests, the grant's digest, the principal, the number of positions the sequence
states, and whether the session's token was given.
### 9.4 Outcomes
- **refuse**: the reason and where.
- **accept**, for session records and a disclosure, with what §9.1 and §9.3 state.
- **matched** or **unmatched**, for a match (§7.4), with what it states.
## 10. Reading the documents together
### 10.1 Algorithm identifiers
The TS's §5.4 names the algorithms a recipient supports as "HMAC 256/256 (kty number 5)" and
"ES256 (kty number −7)", and its pattern encryption as "ECDH-SS+A128KW (key type −32)". RFC
9053 registers 5 (§3.1), −7 (§2.1) and −32 (§6.4) as `alg` values, not key types; this
extension reads them as the `alg` of the token's protected header (§4.2 step 5), and HMAC
256/256 as a COSE_Mac0 and ES256 as a COSE_Sign1, the structures RFC 9052 gives a MAC and a
signature with an implicit key.
### 10.2 COSE
RFC 8392 cites RFC 8152 for the COSE structures; RFC 9052 obsoletes it and keeps the
structures, their tags and their headers, and this extension reads them from RFC 9052. The
TS's CDDL gives `wmpattern` as `COSE_Encrypt0 // COSE_Encrypt // bytes`, which in RFC 9052's
CDDL are the untagged arrays; this extension reads them so (§4.2 step 7.6).
### 10.3 From a bit to a variant
The TS's §4 has each of two variants carry a watermark that encodes '0' or '1', and its §5.4
addresses one bit of the pattern per position; it does not spell out which variant a bit
selects. This extension reads a bit *b* as selecting the variant numbered *b*: 0 is A, the
variant every viewer receives when nothing is sequenced (§5.3, §5.7.5.1 there), and 1 is B.
A third variant has no bit to select it, and this revision records A/B sequencing alone.
### 10.4 Positions beyond the pattern
The TS bounds a position by its WMPaceInfo carriage (a 15-bit field, or a 2-byte integer, §5.5
there) and a pattern by `wmpatlen`, and does not say what an edge serves for a position at or
beyond `wmpatlen`. This extension serves such a segment unsequenced (§5.2), so that no record
states a bit the pattern does not hold.
## 11. Reasons
The codes this extension adds, each with where it is raised:
| reason | raised at |
|---|---|
| `record_invalid` | §5.5, §6.1, §9.1 step 3 |
| `not_protected` | §3.1, §9.1 step 3 |
| `e2ee_room` | §3.2, §9.1 step 2 |
| `edge_not_authorised` | §5.6, §9.1 step 3 |
| `edge_revoked` | §5.6, §9.1 step 3 |
| `content_unknown` | §9.1 step 3 |
| `region_not_pinned` | §6.1, §9.1 step 3 |
| `retention_exceeded` | §6.1, §9.1 step 3 |
| `token_reused` | §9.1 step 3 |
| `token_malformed` | §4.2, §5.7 |
| `token_unsupported` | §4.2, §5.7 |
| `token_mismatch` | §5.7 |
| `outside_token_window` | §5.7 |
| `sequence_invalid` | §7.2 |
| `no_match` | §7.4 |
| `ambiguous_match` | §7.4 |
| `disclosure_mismatch` | §9.3 step 4 |
| `sequence_disagrees` | §9.3 step 5 |
| `opening_mismatch` | §9.3 step 6 |
| `disclosure_expired` | §8.3, §9.3 step 9 |
A verifier of this extension also raises `truncated`, `malformed` and `unknown_version` for a
record, a sequence, a disclosure or a confidentiality record that does not read; `wrong_room`,
`bad_key` and `bad_signature` for one of another room or whose signature fails; every code a
descriptor, a key log or a grant raises (live-rooms §11 and the live-events codes it lists), at
the descriptor, the key log or the grant; and `grant_unknown`, `grant_wrong_principal` and `outside_grant_window`, which
live-rooms §11 lists, for a disclosure's grant (§9.3 steps 7 and 8).
## 12. Conformance
[`live-conformance/watermark/`](../live-conformance/watermark/) holds the corpus, in one file,
`vectors.json`, written from fixed seeds by `wai_live_watermark_vectors gen`. It has:
- **Rooms**: a broadcast room whose origin's key log lists two edges with `live/watermark`, a
relay without it and an edge revoked; a room with no origin, whose clock is its edge; and an
end-to-end encrypted room with its confidentiality record.
- **Tokens**, each read by §4.2 with its outcome and what it gives: COSE_Mac0 tokens under HMAC
256/256 in each mode (a pattern in the clear, encrypted as COSE_Encrypt0 and as
COSE_Encrypt, derived), one in the CWT tag, and one refused for each rule of §4.2 a corpus
token breaks.
- **Sessions**: two edges' records of four sessions of the broadcast room, each mode of token
among them, with their tokens and without; the room with no origin; and one refused list for
each reason of §11 a record or its token raises, each a control of one defect.
- **Matches**: a sequence recovered from one session's copy with erasures, matched; one that
agrees with two sessions; one whose bits disagree, one whose span meets no session, and one
of another vendor, each unmatched; and sequences refused.
- **Disclosures**: the authority's disclosure of the matched session, accepted, and one refused
for each reason of §9.3, among them a disclosure of another session, an opening of another
session, and a disclosure for a sequence that does not agree with its session.
The tokens' MACs are HMAC-SHA-256 under a fixed test key, which the corpus states; no verifier
checks them (§4.4).
A conforming verifier gives every vector the outcome, the reason, the place and the facts the
corpus states. `wai_live_watermark_vectors verify` and the test `tests/live_watermark.rs` check
that the file regenerates byte for byte, that `wai-rs` gives every vector its outcome, that
every reason of §11 appears, and that §11 lists exactly the reference's codes.
`live-conformance/live_watermark_verify.py` is the second implementation, written from this
text: it shares no code with `wai-rs`; it reads descriptors, grants and key logs with the
live-rooms corpus's second implementation (`live_rooms_verify.py`), verifies every signature
with WAI's strict rule (`claim-conformance/_ed25519.py`), reads every token with a CBOR reader
of its own written from RFC 8949, and requires every mutated copy of the corpus to fail.
## 13. Security and privacy considerations
- **What the record proves.** An edge's signature on a record is its statement that it served
that session those variants at those positions under that token, and that it admitted the
session under the grant its binding commits to. Nothing in the record proves the media were
delivered as stated; an edge that lies about its own session is caught only where another
record contradicts it. A recovered sequence that agrees with a record is evidence the copy
came from that session to the extent the stated positions tell sessions apart (§7.5).
- **Framing.** A token is a capability to obtain segments marked with its pattern, and a copy
of them would match its viewer's session. The edge binds a token to the grant it was first
presented under, admitted with proof of possession of the principal's key (live-rooms
§10.5), and refuses it under any other (§3.3), so a copied token marks no other grant's
session; two records that name one token are refused, `token_reused`. Records do not carry
tokens (§4.1).
- **Who learns a viewer.** Only the room authority, through an opening, and only for the
sessions it discloses. A party matching sequences holds records that name no grant; a third
party checking a disclosure learns one session's principal.
- **The authority's duty.** The authority answers for the uniqueness of a match (§8.3), which a
third party cannot check. A disclosure is signed, and names the sequence it answers and the
claimant's key through it, so every disclosure the authority makes is attributable to it and
to a request.
- **Retention.** Records, tokens and openings are kept within the room's region pin and until
`expires`, bounded by the room's retention. A disclosure after `expires` is refused, so an
authority that keeps openings past the bound cannot present what it discloses as conforming.
The `t` of a disclosure is the authority's own statement.
- **Linkability.** Per-device or per-account patterns link a viewer's sessions to anyone who
holds their records (§6.3).
- **Lane B.** An end-to-end encrypted room offers no edge a choice to make, and its members'
applications hold the decoded output (§3.2, SPEC §6.5). Marking it would need the member's
device to mark what it decodes, which neither the TS nor this extension defines.