742 lines
44 KiB
Markdown
742 lines
44 KiB
Markdown
# 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.
|