open-standards/wai/extensions/stream-audit.md
Transaction Science f2acb11dd4 wai: an unnamed acknowledgement refused, the collector's port, the stitched form
- The falsifications found the check that refuses an acknowledgement the
  record does not name guarded by no vector in either implementation:
  the corpus gains ack_not_named (audit_bundle_invalid), and the Python
  verifier can drop that check too, which the corpus now catches.
- The collector's default port moves to 9694: 9690 is the stitched
  presentation tool's.
- stream-audit §6.3 states that this revision claims every marked ad
  with WPC1, and that a stitched ad is claimed with the stitched
  presentation record where a deployment holds it, its bundle then
  carrying the manifests its claims name.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 22:05:49 -04:00

609 lines
37 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: Stream Audit
> Status: Draft. Registers the record `wai.delivery.audit`: evidence a content owner gathers from
> the players of a stream it already distributes over HTTP, with no change to the service that
> packages and delivers it. For each playback session the player makes a fresh key, the
> **session key**, and signs with it: [settlement](settlement.md) acknowledgements, `WDA1`, of
> every object it fetched, each named by plain SHA-256 under settlement §14, one chain per
> origin; a presentation claim, `WPC1` of [ad-presentation](ad-presentation.md), for each ad the
> stream marks; and a **session record**, `WAS1`, which binds them: the channel, the owner's
> collector, the spans of time it presented and whether each was in view and audible, its
> start time and stalls (recomputed from its [delivery-telemetry](delivery-telemetry.md) log),
> its sink energy per role with each figure's acquisition class, how the session ended, and the
> digests of every record it names. The player sends the records to the owner's **collector**,
> which verifies them, stores them, and states a **channel report** per period: time presented,
> in view and audible; ads presented and the quartile each reached; start time and stalls once
> enough sessions hold them; acknowledged bytes per origin and object class; and joules per role
> and acquisition class, never summed across classes. A party holding the stored records
> recomputes the report byte for byte. No new transport, and no price, tariff or amount. The
> reference implementation is the `stream_audit` module of `wai-rs` (feature `stream_audit`);
> the collector is `wai/wai-audit`, whose `web/` directory holds the player integration; the
> corpus is [`live-conformance/audit/`](../live-conformance/audit/), and its second
> implementation is `live-conformance/stream_audit_verify.py`.
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
>
> Texts this revision relies on: the WAI texts it cites; RFC 9110 §8.4 (content codings) and
> §14 (range requests); RFC 9530 §2 and §3 (`Content-Digest`, `Repr-Digest`); RFC 6454 §6.2
> (the ASCII serialization of an origin); RFC 8032; FIPS 180-4; the WHATWG Fetch Standard's
> CORS-safelisted response-header names and `Access-Control-Expose-Headers`; and
> `draft-pantos-hls-rfc8216bis-22` §4.4.5.1 (`EXT-X-DATERANGE`) as
> [live-ads](live-ads.md) reads it, with its `EXTINF`, `EXT-X-PROGRAM-DATE-TIME` and
> `EXT-X-BYTERANGE` tags.
## 1. Scope
In scope: the channel and the session key (§3); what a player acknowledges and how (§4); the
session record (§5); the ads it presented (§6); its telemetry (§7); its energy and how the
session ended (§8); the bundle a player sends (§9); verification (§10); the channel report and
its recomputation (§11); and what a collector does (§12).
Out of scope: a transport; how a player reaches the collector, beyond §12; the service that
delivers the stream, which this extension asks for nothing; the authenticity of what was
delivered, since nothing the delivering service signs is read here (§15); the markers of a DASH
`EventStream` and of an in-band event, which this revision does not read; and any price, tariff,
amount or invoice: a report states what was presented and delivered, never what it is worth.
Relationship to the core spec: `wai.delivery.audit` is a **record** ([SPEC](../SPEC.md) §5): a
signed record a WAI tool emits and verifies. It names no payload format and is not
dispatchable.
## 2. Terms
- **Owner**: the party a stream's content belongs to, which runs the collector. Its **key** is
the collector's Ed25519 key (RFC 8032).
- **Channel**: a stream the owner names, live or on demand, under one name (§3.1).
- **Player**: the software that fetches and presents the stream and signs this extension's
records, with an integration of §13 or one of its own.
- **Session**: one playback of one channel by one player, from the player's first request to
its end (§8.2).
- **Object**: one HTTP response body the player fetched for the presentation, as settlement §14
defines it: a playlist, a manifest, an initialization segment, a media segment, a timed-text
segment.
- **Origin**: the origin (RFC 6454) of the URL an object came from, after redirects.
- **Time**: microseconds since 1970-01-01T00:00:00Z, as settlement §2's. A span from `start`
(included) to `end` (excluded).
- *u64*, *u32*, *u16*, *u8*: little-endian unsigned integers. A **digest** is SHA-256 (FIPS
180-4), 32 bytes. **Zero** is 32 zero bytes. Ed25519 is verified strictly
([SPEC](../SPEC.md) §7 *Signatures*): a key that fails step 1 or 2 is `bad_key`, a signature
that fails a later step `bad_signature`.
## 3. The channel and the session key
### 3.1 The channel
A channel's **name** is 1 to 255 bytes of printable ASCII (0x21 to 0x7E), which the owner
chooses and configures its players with. Its **id** is SHA-256 over `wai:audit-channel` (ASCII),
0x01 and the name. A name is the owner's label, never a URL: a URL can carry a token, and a
channel outlives the URLs its stream is served from.
### 3.2 The session key and the session
A player **MUST** make a fresh Ed25519 key for each session, from its platform's random
generator, and **MUST NOT** use it for another session or derive it from an identifier of the
viewer, the device or an account: ad-presentation §3's rule, for a channel in place of a room.
The **session** is the first 16 bytes of SHA-256 over `wai:ad-presentation-session` (ASCII),
0x01, the channel id and the session key: ad-presentation §3's derivation with the channel id in
the room id's place, so that the claims of §6 name it exactly as a room's claims name theirs.
## 4. Acknowledgements
### 4.1 What a player acknowledges
A player acknowledges every object of the presentation it receives, under settlement §14: each
named by the SHA-256 of its bytes as received, from the upstream named by its origin. It does not
acknowledge a key or a licence response: their bytes are secrets, and their digest would let a
party holding a candidate key confirm it.
Each acknowledgement (settlement §3) has `mode` 1, `role` 2 (a sink), `upstream` the origin's
upstream (settlement §14.2), `downstream` the session key, which signs it, and zero `grant` and
`proof` in every entry. Its entries are by object class (settlement §3.3):
- class 1 (`live`): the initialization and media segments of a live presentation (a playlist
with no `EXT-X-ENDLIST`);
- class 2 (`fetch`): those of an on-demand presentation, a retrieval no grant was presented for;
- class 4 (`ancillary`): every other object: playlists, manifests (each refresh is an object),
timed text.
Every item is a whole object: `start` 0 and `end` its length. A byte-range segment is an object
of its own (settlement §14.1).
### 4.2 Windows and chains
A player cuts its session into **windows** of *W* microseconds from the session's `start`: the
*k*-th from `start` + *kW* to `start` + (*k* + 1)*W*, the last ending at the session's `end`.
*W* is the player's choice, 10 s by default and at least 1 s. For each origin and each window in
which it received an object from that origin, it signs one acknowledgement of the objects
received in that window, in the order received; an object's time is when its body was complete.
An origin's acknowledgements form one chain (settlement §3.7), each naming in `prev` the digest
of the one before it, the first naming zero.
A window's item list names an object once. An object the window already lists for that origin,
received again with the same bytes (a retried request, a playlist refreshed unchanged) is a
**repeat**: the session record counts repeats, their objects and bytes (§5.1), and lists none.
### 4.3 What an acknowledgement states
That the holder of the session key received those bytes from that origin in that window. Not
that the bytes are what the owner published, nor that they were presented: presentation is the
session record's spans (§5.2) and the claims (§6).
## 5. The session record
### 5.1 Layout
When a session ends, the player signs one record:
| field | size | meaning |
|---|---|---|
| `magic` | 4 | `WAS1` (ASCII) |
| `version` | 1 | 1 in this revision |
| `channel` | 32 | the channel id (§3.1) |
| `owner` | 32 | the owner's key: the collector the session reports to |
| `player` | 32 | the session key, which signs the record |
| `session` | 16 | the session (§3.2) |
| `presentation` | 1 | 1 live, 2 on demand |
| `start`, `end` | 8 each | u64: the session's span |
| `ended` | 1 | how the session ended (§8.2) |
| `origins` | 1 + … | a u8 count *n*, at least 1, then each origin: a u16 length, 1 to 2048, and its ASCII serialization (RFC 6454 §6.2) |
| `acks` | 2 + 32 × *n* | a u16 count, at least 1, then the digests of the session's acknowledgements |
| `claims` | 2 + 32 × *n* | a u16 count, then the digests of its presentation claims (§6) |
| `spans` | 4 + 11 × *n* | a u32 count, then each span (§5.2) |
| `startup_ms` | 4 | u32: the start time, delivery-telemetry's `vst_ms` (§7.1); 4 294 967 295 when nothing was presented |
| `stalls` | 4 | u32: the stalls begun after the first presentation |
| `stall_ms` | 8 | u64: their total duration |
| `repeat_objects` | 4 | u32: the repeats (§4.2) |
| `repeat_bytes` | 8 | u64: their bytes |
| `log` | 32 | the digest of the session's observation log (§7.1) |
| `notes` | 32 | the digest of its notes document (§7.3), or zero for none |
| `energy` | 1 + … | a u8 count, then each energy entry (§5.3) |
| `attachments` | 2 + 32 × *n* | a u16 count, then the digests of the records it attaches (§8.3) |
| `sig` | 64 | §5.4 |
Each list of digests is in strictly ascending order, and so are the origins, bytewise.
### 5.2 Spans
A span is a stretch of the session's time during which the player presented the stream at normal
speed; 11 bytes:
| field | size | meaning |
|---|---|---|
| `start_ms` | 4 | u32: where it starts, in milliseconds after the session's `start` |
| `length_ms` | 4 | u32: its length, at least 1 |
| `visible_bp` | 2 | u16: the least fraction of the player's pixels in view it observed over the span, in basis points (0 to 10 000) |
| `flags` | 1 | bit 0 **audible**: audible throughout; bit 1 **ad**: within a marked ad (§6) |
A player observes at least every 200 ms and begins a new span whenever playback stops or
resumes, its quantised visible fraction or audible state changes, or a marked ad begins or ends,
by ad-presentation §10's rules: a fraction of 0 while the page or application is hidden, out of
focus or locked; audible only when unmuted at a volume above 0. The spans are in order and do not
overlap.
### 5.3 Energy entries
An entry states the energy of one **role** of the session:
| field | size | meaning |
|---|---|---|
| `role` | 1 | 1 **origin**, 2 **delivery** (the network between origin and player), 3 **sink decode**, 4 **display** |
| `joules` | 8 | u64: microjoules over the session; 0 is the unmetered marker |
| `class` | 1 + … | its acquisition class: energy-measurement §2.7's tag byte, then that tag's evidence |
A role with a figure states it with its class; a role the player knows took part and did not
measure states the unmetered marker (tag 0 and 0 J, live-metering §2); a role it knows nothing
of has no entry (§8.1).
### 5.4 Digest and signature
The **digest** of a record is SHA-256 over its bytes before `sig`. Its **signature** is Ed25519
by `player` over the 18 bytes `wai:audit-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: fewer than 4 bytes, `truncated`; a magic other than `WAS1`,
`malformed`; no fifth byte, `truncated`; a version other than 1, `unknown_version`; the bytes end
before a field the layout requires, `truncated`; a class whose tag 5 reference is longer than 255
bytes, `malformed`; bytes after `sig`, `malformed`. It then refuses as `audit_session_invalid` a
record that breaks any of these:
- `presentation` is 1 or 2, `start` is below `end`, and `ended` is 1 to 5;
- there is at least one origin, each 1 to 2048 bytes of printable ASCII beginning `http://` or
`https://`, with no `/`, `?`, `#` or `@` after those, in strictly ascending order;
- there is at least one acknowledgement; `acks`, `claims` and `attachments` are each in strictly
ascending order;
- each span's `length_ms` is at least 1, its `visible_bp` at most 10 000 and its `flags` at most
3; each starts at or after the end of the one before; the last ends at or before
⌊(`end` − `start`) / 1000⌋;
- `startup_ms` is 4 294 967 295 exactly when there is no span; `stall_ms` is 0 when `stalls` is;
`repeat_bytes` is 0 when `repeat_objects` is, and at least it otherwise;
- `log` is not zero;
- the energy entries are in strictly ascending order of `role`, each 1 to 4;
and as `energy_invalid` one whose energy entry breaks live-metering §3.6's rules for a figure
and its class (the tag is 0 exactly when `joules` is; no tag 1; the hardware classes' evidence
and resolution floor). Then the signature (§5.4): `bad_key`, `bad_signature`.
## 6. Ads presented
### 6.1 Marked ads and their markers
An ad is **marked** when the media playlist the player presents carries an `EXT-X-DATERANGE`
tag (`draft-pantos-hls-rfc8216bis-22` §4.4.5.1) over it with a `SCTE35-OUT` or `SCTE35-CMD`
attribute, or with a `CLASS` the player is configured to treat as an ad. Its **marker** is the
tag's line as the playlist carries it, from `#` to the last byte before the line terminator, and
its **marker digest** is SHA-256 over `wai:audit-marker` (ASCII), 0x01 and the marker. The marked
span runs from the tag's `START-DATE` for its `DURATION` (or `PLANNED-DURATION`), placed in the
playlist by its `EXT-X-PROGRAM-DATE-TIME` tags.
### 6.2 The creative
The ad's **creative** (ad-presentation §4) is the media segments of the presented playlist
whose span meets the marked span, in order, each by the SHA-256 of its bytes as received
(settlement §14.1) and its `EXTINF` duration in milliseconds, rounded half up and at least 1. Its
media time starts at the first such segment's start. A creative's objects are among the
objects the session acknowledged.
### 6.3 The claim
For each marked ad it presented, the player signs a claim of ad-presentation §5, with `room` the
channel id, `cue` the marker digest, `session` the session, `slot` 0, `creative_hash` the
creative digest, `player` the session key, and spans over the creative's media time by
ad-presentation §10's rules. A player that plays into the ad from before it, with no pause or
seek between its last observation before the ad and its first within it, starts the ad's first
span at media time 0; one that plays out of it the same way ends its last span where it left, at
most the creative's duration: playback was continuous across the boundary, which fell between two
observations. A claim states what ad-presentation §13 says one states, and here no
decider's receipt stands beside it: its quartile and viewability are computed (§10.2 step 9) and
reported, not compared with anyone's count.
This revision claims every marked ad with `WPC1` under the channel binding above. A record of
its own for ads another party stitches into a stream, `wai.video.stitched_presentation`, names an
ad by its marker in a manifest whose SHA-256 it states; where a deployment holds that record, a
stitched ad is claimed with it, the bundle then carries the bytes of each manifest its claims
name, and `WPC1` stays for a marked ad that is not stitched. This revision does not read it.
## 7. Telemetry
### 7.1 The observation log
The player keeps a delivery-telemetry log of the session (delivery-telemetry §2 and its §7.1),
whose `sid` is the session as 32 lowercase hexadecimal digits: fresh per session and derived
from no identifier, as delivery-telemetry §8 requires. Its `object` members report `url` as
`null`, since a third party's URL can carry a token in its path, and `content_hash` as the
object's SHA-256. The **observation log** is live-metering §6.1's logs document holding exactly
this one log, as UTF-8 bytes. The record's `startup_ms`, `stalls` and `stall_ms` are the log's
`vst_ms`, `stalls` and `stall_ms` (delivery-telemetry §7).
### 7.2 CMCD
The log's Event-mode reports, CMCD version 2 (CTA-5004-B) as delivery-telemetry §3 maps them,
go to the owner's collector as `application/cmcd` batches (live-metering §6.3) while the session
runs. Request-mode reports **MAY** ride on the player's requests to an origin where the owner
configures it; they are off by default, since a query added to a third party's URL can break the
signature that URL carries. A batch is advisory, as every observation is (delivery-telemetry
§1); what the report reads is the log the record binds.
### 7.3 CMSD and the notes document
Where a response exposes `CMSD-Static` or `CMSD-Dynamic` (CTA-5006) to the player (in a page,
only when the origin lists them in `Access-Control-Expose-Headers`; they are not CORS-safelisted
names), the player records them. The **notes document** is a JSON array, as UTF-8 bytes, of one
object per response that exposed either, in the order received, with exactly the members
`content_hash` (the object's SHA-256, 64 lowercase hexadecimal digits), `cmsd_static` and
`cmsd_dynamic` (each the field's value as received, a string, or `null`). The values are the
origin's statements, recorded, not parsed or checked. The record names the document by its
digest, zero when no response exposed either field.
## 8. Energy, the end of a session and attachments
### 8.1 Energy
The entries of §5.3 follow one rule: a figure with its class, the unmetered marker where a role
took part unmeasured, no entry where the player knows nothing. A page has no meter: it states
roles 3 and 4 as the unmetered marker and has no entry for roles 1 and 2. A native host or a
sink with an energy profile states its figure with the class and evidence its counter or model
gives (energy-measurement §2.7). Nothing sums figures of two classes: the report totals each
role and class apart (§11.2), as live-metering §4.5 totals a session's hops.
### 8.2 How the session ended
| `ended` | name | when |
|---|---|---|
| 1 | `ended` | the presentation reached its end |
| 2 | `user` | the viewer stopped it, or left the page or application |
| 3 | `hidden` | the player ended the video because nobody could see it for its grace period |
| 4 | `idle` | the player ended it after its idle bound passed with no interaction |
| 5 | `error` | playback failed |
### 8.3 Attachments
A player may attach other signed records of the session (a termination record of
`wai.live.termination`, a sink energy receipt of `wai.sink.energy`, each where the player keeps
one), each named in `attachments` by the SHA-256 over all its bytes. This revision binds an
attachment to the session and does not read it; a collector stores it with the session.
## 9. The bundle
A player sends a session to the collector as one **bundle**: a JSON object (RFC 8259) with
exactly these members:
| member | value |
|---|---|
| `v` | 1 |
| `session` | the record (§5), in lowercase hexadecimal |
| `acks` | an array, one per digest of `acks`: `{ "record": hex, "items": [hex, …] }`, the acknowledgement and each of its entries' item lists in entry order |
| `claims` | an array, one per digest of `claims`: `{ "claim": hex, "marker": string, "creative": [{ "content_hash": hex, "duration_ms": integer }, …] }` |
| `log` | the observation log (§7.1), a string whose UTF-8 bytes are the document |
| `notes` | the notes document (§7.3) as such a string, or `null` |
| `attachments` | an array: `{ "name": string, "record": hex }`, the record's registered name and its bytes |
A bundle that is not such an object, holds a member not listed or lacks one, a value of another
type, hexadecimal that is not lowercase or not whole bytes, a creative object whose `duration_ms`
is not 1 to 4 294 967 295 or whose `content_hash` is not 32 bytes, or a creative with no object,
is refused `audit_bundle_invalid`.
## 10. Verification
### 10.1 Inputs
A verifier is given a bundle, the owner's key, and the names of the channels the owner serves.
### 10.2 Steps
It runs these steps and stops at the first refusal, which it reports with the reason and where:
1. **The bundle** (§9), at `bundle`: `audit_bundle_invalid`.
2. **The record** (§5.5), at `session`: `truncated`, `malformed`, `unknown_version`,
`audit_session_invalid`, `energy_invalid`, `bad_key`, `bad_signature`.
3. **Its binding**, at `session`: `owner` is the owner's key, `audit_wrong_owner`; `channel` is
the id of a channel the owner serves, `audit_unknown_channel`; `session` is the session of
`channel` and `player` (§3.2), `audit_session_unbound`.
4. **Each acknowledgement**, in the record's order, at `ack` and its index:
1. one of the bundle's acknowledgements has the digest (settlement §8.2: SHA-256 over all but
the last 64 bytes): otherwise `record_missing`;
2. it reads and verifies (settlement §3.6): `truncated`, `malformed`, `unknown_version`,
`ack_invalid`, `self_acknowledged`, `bad_key`, `bad_signature`;
3. its `mode` is 1 and its `downstream` is `player`: otherwise `audit_ack_unbound`;
4. its `upstream` is the upstream of one of the record's origins (settlement §14.2):
otherwise `audit_origin_unknown`;
5. its window is within the session, the session's `start` ≤ its `start` and its `end` ≤ the
session's `end`: otherwise `ack_outside_period`;
6. its `role` is 2, every entry's `grant` and `proof` are zero, and every entry's class is 4 or
the class of the record's `presentation` (§4.1): otherwise `audit_ack_invalid`;
7. each entry, at `entry` and its index: its item list is given (`record_missing`), reads
(`items_invalid`) and recomputes (`items_mismatch`) as settlement §3.6 says, and every
item's `start` is 0 (`audit_ack_invalid`); an item list beyond its entries is
`audit_bundle_invalid`.
A bundle acknowledgement whose digest the record does not name, or that repeats one, is
`audit_bundle_invalid`, at `ack` and its index in the bundle.
5. **The chains**, at `ack` and an index: each origin's acknowledgements ordered by (`start`,
index), as settlement §8.2 step 4 orders a chain: the first with a `prev` other than zero,
`ack_chain_broken`; a later one whose window overlaps the one before it, `ack_overlap`, or
whose `prev` is not that one's digest, `ack_chain_broken`. Of every such break, the one at the
acknowledgement first in the record's order is reported.
6. **The log**, at `log`: the SHA-256 of the bundle's `log` bytes is `log`, otherwise
`audit_document_mismatch`; it reads as a logs document of exactly one log (live-metering
§6.1), otherwise `qoe_invalid`; and its `sid` is the session's and its `vst_ms`, `stalls` and
`stall_ms` are the record's `startup_ms` (`null` for 4 294 967 295), `stalls` and `stall_ms`,
otherwise `audit_qoe_mismatch`.
7. **The notes**, at `notes`: with `notes` zero, the bundle's `notes` is `null`; otherwise its
SHA-256 is `notes`: otherwise `audit_document_mismatch`. A notes document that does not read
as §7.3 says, or that names a `content_hash` no item of the session's acknowledgements has,
is `audit_notes_invalid`.
8. **The attachments**, at `attachment` and its index: each digest the record names is an
attachment's (`record_missing`), and each attachment's digest is one the record names
(`audit_document_mismatch`).
9. **Each claim**, in the record's order, at `claim` and its index: a bundle claim with the digest
(SHA-256 over all but its last 64 bytes) is given, otherwise `record_missing`, and a bundle
claim whose digest the record does not name is `audit_bundle_invalid`. Then each claim gets a
verdict, **presented** or the reason of the first of these it fails:
1. it reads (ad-presentation §5.4): `claim_malformed`;
2. its signature verifies under its `player` (ad-presentation §5.3): `bad_key`,
`bad_signature`;
3. its `room` is the channel id, its `session` the session and its `player` the session key:
`audit_claim_unbound`;
4. its `cue` is the digest of the marker beside it (§6.1): `audit_marker_mismatch`;
5. no claim before it that passed steps 1 to 4 has its `cue` and `slot`: `claim_duplicate`;
6. its `creative_hash` is the digest of the creative beside it, every span ends within the
creative's duration, and its `objects` are exactly the content hashes of the creative's
objects its spans touch, in order (ad-presentation §9.2 step 6): `claim_asset_mismatch`;
7. every one of its `objects` is the content hash of an item of one of the session's
acknowledgements: `audit_ad_not_acknowledged`.
A claim's verdict does not refuse the session. A presented claim's facts are the quartile it
reaches (ad-presentation §6.3, with the creative's duration), and its run and viewable time
under the video rule (ad-presentation §6.2, §6.1 `viewability` 1), and whether that run is at
least 2 000 ms.
### 10.3 Outcome
- **refuse**: the reason and where.
- **accept**: the record's digest; the channel; `presentation`, `start`, `end` and `ended`; the
time presented (the sum of the spans' `length_ms`), of it in view (spans whose `visible_bp` is at
least 5 000), audible, and within ads; for each origin and object class, the acknowledged
objects and bytes, and the repeats; the start time, stalls and stall time; each energy entry
with its role, class label and figure, or the unmetered marker; each claim's verdict and, for a
presented claim, its facts; the notes' count of responses; and the attachments' count.
## 11. The channel report
### 11.1 Its sessions
A report covers one channel of one owner over a **period** from `from` to `to`. Its sessions are
the accepted bundles of that channel and owner whose record's `start` lies in the period. Two
records with one `session` are refused `audit_session_duplicate`, at `report`: a session reports
once.
### 11.2 The document
The report is a JSON object with exactly these members:
| member | value |
|---|---|
| `v` | 1 |
| `channel`, `owner` | the channel id and the owner's key, hexadecimal |
| `from`, `to` | the period |
| `sessions` | the records' digests, hexadecimal, in ascending order |
| `presented` | `{ "presented_ms", "viewable_ms", "audible_ms", "ad_ms" }`: §10.3's sums over the sessions |
| `ended` | `{ "ended", "user", "hidden", "idle", "error" }`: the sessions by `ended` |
| `ads` | `{ "claims", "presented", "quartiles", "viewable", "refused" }`: the claims, those presented, an array of five counts (the presented claims reaching quartiles 1 to 5), those whose run under the video rule is at least 2 000 ms, and an object of each refusing verdict's count by its code |
| `rebuffering` | `{ "sessions", "suppressed", "presented", "startup_ms", "stalls", "stall_ms" }`: the sessions; `true` when they are fewer than 50, and then every other member `null` (delivery-telemetry §8); otherwise `false`, the sessions with a span, the sum of their `startup_ms`, and the sums of `stalls` and `stall_ms` over all |
| `delivery` | an array of `{ "origin", "class", "objects", "bytes" }`, one per origin and object class with an acknowledged object (class by settlement §3.3's name), in ascending order of origin then class |
| `repeats` | `{ "objects", "bytes" }`: the sums of the records' repeats |
| `energy` | an array of `{ "role", "class", "figures", "joules_micro" }`, one per role and acquisition class among the sessions' metered entries (role `origin`, `delivery`, `sink_decode` or `display`; class live-metering §4.5's label), in ascending order of role number then tag |
| `unmetered` | an object with a member per role name: the sessions whose entry for the role is the unmetered marker |
| `absent` | an object with a member per role name: the sessions with no entry for the role |
Joules are summed within one role and one class and never across: a role's figures of two classes
are two members of `energy`, and a session whose entry is the unmetered marker counts in
`unmetered`, never as zero joules.
### 11.3 Its bytes and signature
The report's **bytes** are its JSON with every object's members in bytewise order of their
names, no white space, integers in decimal, and strings with no escape but `\"` and `\\`: the
form RFC 8785 gives such a document. The collector signs them: Ed25519 by the owner's key over
the 17 bytes `wai:audit-report` (ASCII) and 0x01, followed by the report's bytes. A served report
is `{"report": <the report>, "sig": <hex>}`.
### 11.4 Recomputation
A party given the bundles, the channel's name, the owner's key and the served report recomputes
the report over the report's own period, in this order: each bundle verifies as §10 verifies it,
otherwise its refusal, at `bundle`, its index and its place; a served report that is not an object
of `report` and `sig`, or whose `from`, `to` or `sessions` do not read, is
`audit_report_mismatch`; two of the sessions it reads with one `session` are
`audit_session_duplicate`; a digest the report names that no given bundle has is `record_missing`,
at `report`, `session` and its index; a report whose bytes are not the recomputed report's bytes
is `audit_report_mismatch`; and one whose signature does not verify under the owner's key is
`bad_signature`, each at `report` where no other place is named.
## 12. The collector
A collector:
- **MUST** verify a bundle (§10) before it stores it, and refuses with its reason one that does
not verify;
- stores an accepted bundle under its record's digest, and accepts the same bundle again with no
change; a record naming the `session` of a stored record that differs from it is refused
`audit_session_duplicate`;
- serves a channel's report for a period (§11), signed with its key, and its key;
- **MAY** accept Event-mode CMCD batches (§7.2), which it stores apart and never reads into a
report;
- names nothing a player has not sent: no cookie, no account and no identifier of a viewer.
The reference collector, `wai-audit serve`, keeps one directory per channel, a file per session,
and answers on loopback; its README states its routes and limits.
## 13. The player integration
The reference integration is `wai/wai-audit/web/audit.js`, a module with no dependency, and its
adapters: one that wraps the page's `fetch` and `XMLHttpRequest` for any player that fetches over
them, and one for a player whose loader interface a page can wrap. The module hashes each object
with WebCrypto's SHA-256, writes and signs the acknowledgements, the claims and the record with
WebCrypto's Ed25519, computes jwp-receipts §2.2's tree root with a BLAKE3 of its own for inputs of
one chunk, observes the media element every 200 ms as ad-presentation §10 says, writes the
observation log and its Event-mode CMCD, and sends the bundle when the session ends. Its tests
drive it with the corpus's inputs and require the corpus's records, byte for byte.
## 14. Reasons
The codes this extension adds, each with where it is raised:
| reason | raised at |
|---|---|
| `audit_bundle_invalid` | §9, §10.2 steps 1, 4 and 9 |
| `audit_session_invalid` | §5.5, §10.2 step 2 |
| `audit_wrong_owner` | §10.2 step 3 |
| `audit_unknown_channel` | §10.2 step 3 |
| `audit_session_unbound` | §10.2 step 3 |
| `audit_ack_unbound` | §10.2 step 4 |
| `audit_origin_unknown` | §10.2 step 4 |
| `audit_ack_invalid` | §10.2 step 4 |
| `audit_document_mismatch` | §10.2 steps 6 to 8 |
| `audit_qoe_mismatch` | §10.2 step 6 |
| `audit_notes_invalid` | §10.2 step 7 |
| `audit_claim_unbound` | §10.2 step 9 |
| `audit_marker_mismatch` | §10.2 step 9 |
| `audit_ad_not_acknowledged` | §10.2 step 9 |
| `audit_session_duplicate` | §11.1, §12 |
| `audit_report_mismatch` | §11.4 |
A verifier of this extension also raises `truncated`, `malformed` and `unknown_version` for a
record that does not read; `bad_key` and `bad_signature`; `record_missing` (live-metering §9);
`energy_invalid` and `qoe_invalid` (live-metering §9); settlement §11's `ack_invalid`,
`self_acknowledged`, `items_invalid`, `items_mismatch`, `ack_outside_period`, `ack_overlap` and
`ack_chain_broken`; and ad-presentation §11's `claim_malformed`, `claim_duplicate` and
`claim_asset_mismatch`, as claim verdicts.
## 15. Conformance
[`live-conformance/audit/`](../live-conformance/audit/) holds the corpus, written from fixed
seeds by `wai_stream_audit_vectors gen` as `vectors.json`. It holds the owner's key, the channel
names, the objects' bytes, and:
- **sessions**, each a bundle with the outcome §10.3 gives it. **Accepted**: a live session from
two origins with a marked ad presented in view throughout and one presented half out of view,
repeats, CMSD notes and a sink-decode figure of the `Estimator` class beside the unmetered
display; an on-demand session ended `hidden` with an `OnChipCounter` figure and an attachment;
a session that presented nothing and ended `error`; and a session whose claims get every claim
verdict of §14 and of ad-presentation's that §10.2 step 9 names. **Refused**: each reason of §14
that a session raises, and each code it borrows, each vector a control of one defect.
- **reports**, each a channel, a period, the sessions it reads and the outcome §11.4 gives it:
a report over the accepted sessions, whose rebuffering is suppressed; a report over a channel
of 52 sessions, whose rebuffering is stated; and refused, a report whose bytes do not
recompute, one whose signature fails, one naming a session not given, and one over two records
of one session.
A conforming verifier gives every session and every report the outcome, the reason, the place and
every figure the corpus states. `wai_stream_audit_vectors verify` and the test
`tests/stream_audit.rs` check that the file regenerates byte for byte, that `wai-rs` gives every
vector its outcome, that every reason of §14 and every borrowed code refuses a vector, and that §14
lists exactly the reference's codes. `live-conformance/stream_audit_verify.py` is the second
implementation, written from this text: it shares no code with `wai-rs`; it reads acknowledgements
and item lists with the settlement corpus's second implementation (`settlement_verify.py`), claims
with the ad-presentation corpus's (`ad_presentation_verify.py`), and logs and energy classes with
the live-metering corpus's (`live_metering_verify.py`), verifies every signature with WAI's strict
rule (`claim-conformance/_ed25519.py`), recomputes every report itself, gives every vector the same
outcome, requires every mutated copy of the corpus to fail, and requires the corpus to fail a copy
of itself with each of its checks dropped.
## 16. Security and privacy considerations
- **What the evidence shows, and to whom.** The relying party is the owner. A session's records
show what the holder of a fresh key received, from which origins, what it presented and how
that went; they show nothing of the delivering service's own logs, and they need nothing from
it. A report's figures are recomputable from the stored records, so the owner can show a party
it deals with (an ad buyer, a rights holder) a figure that party recomputes.
- **What the evidence does not show.** That the bytes were what the owner published: nothing the
delivering service signs is read, so a stream altered on the way is acknowledged as received.
An owner that also signs what it publishes (jwp-receipts) can compare its own content hashes with
the acknowledged ones. That a person watched (ad-presentation §13). That the player is honest: a
modified player signs what it is told to, and a party can make keys and sign sessions it never
played. The records make each session attributable to one key and each figure recomputable; they
do not make fabrication impossible, and an owner weighs sessions from keys it cannot place as
ad-presentation §13 weighs claims without attestation.
- **Collusion between the session key's holder and an origin** can produce acknowledgements of
bytes that never moved, as settlement §13 says of any downstream party; repeats and the log's
timing make a replayed session's figures stand out, and nothing here detects one.
- **What a session reveals.** To the collector: the session's objects by digest, their origins,
its timing, what it presented and its energy entries. The session key is fresh and the session
derived from it (§3.2), so sessions do not link to each other or to a viewer, device or account;
`sid` is the session (§7.1); the log reports no URL; the record names the channel by the owner's
label, never by a URL. A digest of a public segment names a position in the stream, which is
what an acknowledgement is for; a key response is never acknowledged (§4.1). The collector sends
records to no one; a report states sums over sessions and suppresses rebuffering below 50
sessions (delivery-telemetry §8). This extension sets no gate of its own: sending the records is
part of playing the owner's stream, as acknowledging is part of a delivery (settlement §13).
Where a regime asks the operator to state the lawful basis on which it processes these records,
the operator states it as a `lawful-basis` declaration (jwp-receipts §2.4.6), which this
extension does not restate or check.
- **Replay.** A record names its owner and channel, and its session derives from its key: a record
sent to another owner's collector is `audit_wrong_owner`, and a record for one session counts
once in a report (§11.1).
- **Size.** A collector bounds a bundle's bytes and a client's rate; the reference's limits are in
its README.