All checks were successful
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 27s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 27s
wai-app and Create / wai-app · does this change touch it (push) Successful in 30s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 48s
wai-ledger service / does this change touch it (push) Successful in 25s
wai-quantum for JavaScript / does this change touch it (push) Successful in 26s
wai-audit collector and player integration / does this change touch it (push) Successful in 45s
wai-key-proxy / does this change touch it (push) Successful in 44s
wai-quantum studio and desktop / does this change touch it (push) Successful in 53s
WAI operation catalogue / does this change touch it (push) Successful in 1m2s
wai-fleet service / does this change touch it (push) Successful in 1m1s
WAI Studio / does this change touch it (push) Successful in 35s
wai-sink-android native sink library / does this change touch it (push) Successful in 48s
wai-sink-swift native sink package / does this change touch it (push) Successful in 36s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 2m0s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 14s
wai-quantum energy-landscape jobs / does this change touch it (push) Successful in 14s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 35s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 4m15s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 50s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 6m10s
wai-ledger service / service · linux (push) Successful in 2m42s
wai-ledger service / service · macos (push) Successful in 1m11s
wai-key-proxy / proxy · linux (push) Successful in 1m3s
wai-quantum for JavaScript / javascript · linux (push) Successful in 4m0s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 7m36s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m14s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 2m6s
wai-audit collector and player integration / collector · linux (push) Successful in 5m18s
wai-fleet service / service · linux (push) Successful in 2m24s
wai-quantum studio and desktop / desktop app · windows (push) Successful in 10m8s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 2m3s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 6m45s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 8m6s
wai-quantum studio and desktop / studio · browser (push) Successful in 7m35s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 5m4s
wai-fleet service / service · macos (push) Successful in 45s
wai-quantum energy-landscape jobs / the same bits · linux-arm64 (push) Successful in 12m29s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 16m4s
wai-quantum energy-landscape jobs / reader, kernels, answers and the stated result · linux, release (push) Successful in 9m18s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 10m18s
wai-quantum energy-landscape jobs / native against WebAssembly, over many seeds (push) Successful in 9m50s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 15m22s
WAI Studio / Studio in WebKit · linux (push) Successful in 13m34s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 7m17s
wai-quantum energy-landscape jobs / the module under wasm32-wasip2 (wasmtime) (push) Successful in 14m15s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 10m40s
wai-quantum energy-landscape jobs / the same bits · macos (push) Successful in 7m5s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 15m44s
ad-presentation §14 and stitched-presentation §12: a verifier applies six invalid-traffic rules to the impressions it states, each a comparison of signed bytes or integers with no score or model: one impression presented twice, a span past its creative or a media time in two claims, a claim with no attestation where the terms require one, more sessions under one device key than the policy's cap, an address in a pinned list of ranges, and a user agent holding a token of a pinned list of declared crawlers. New records: the traffic policy (WTP1), the session origin an edge signs (WSO1), the device binding a device key and the session key sign (WDB1), and the two lists pinned by digest with their source (WAL1, WUL1). Each impression is valid, invalid with every rule and the records it rests on, or undecided where a turned-on rule had no record; invalid impressions that counted are excluded and counted, undecided ones withheld, and the valid spend is what remains (by currency for a stream). No claim's verdict or record changes. live_ads::traffic and stitched_presentation::traffic read a finished verification; live_ads::verify and stitched_presentation::verify are unchanged. Corpus live-conformance/ad-traffic/ (wai_ad_traffic_vectors), tests/ad_traffic.rs, and the second implementation ad_traffic_verify.py; a verifier with any one rule off is caught by the vectors in both. Decision D5 records what is and is not a rule here. Co-Authored-By: Claude <[email protected]>
819 lines
53 KiB
Markdown
819 lines
53 KiB
Markdown
# WAI Extension: Stitched Presentation
|
||
|
||
> Status: Draft. Registers the record `wai.video.stitched_presentation`: a player's signed
|
||
> claim, `WSC1`, that it presented one ad another party wrote into the stream, either stitched
|
||
> into the segments of an HLS or DASH presentation or served as an HLS interstitial, beside the
|
||
> **pod-input record**, `WSI1`, that the stitcher or a proxy in front of the ad server signs for
|
||
> each ad decision. A claim names its ad by the **marker** the stream carries for it (an
|
||
> `EXT-X-DATERANGE` tag, an asset of an interstitial's asset list, or an `Event` of a DASH
|
||
> `EventStream`) and the ad identity that marker carries, in the manifest whose SHA-256 it
|
||
> states; it carries ad-presentation's spans of visible and audible play, its viewability rules
|
||
> and quartile, and its device attestation; and, for an ad shown in a composited frame (a
|
||
> squeeze or a double box of [live-compose](live-compose.md) §5.9), the share of the picture the
|
||
> ad's region holds. The pod-input record states the digest of the ad server's response, the
|
||
> ads it chose with their markers, creatives and prices, the avail, the frame and the
|
||
> measurement terms. A verifier recomputes, from the claims that verify, each ad's
|
||
> presentations, the quartiles they reached and the spend that counts; the choice stays the ad
|
||
> server's. No new transport. The reference implementation is the `stitched_presentation`
|
||
> module of `wai-rs` (feature `stitched_presentation`); the corpus is
|
||
> [`live-conformance/stitched/`](../live-conformance/stitched/), and its second implementation
|
||
> is `live-conformance/stitched_presentation_verify.py`.
|
||
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
|
||
>
|
||
> Texts read for this revision, each at its source:
|
||
>
|
||
> - *Video Ad Serving Template (VAST)* 4.3, December 2022: §3.1 to §3.4.4, §3.4.8, §3.6 to
|
||
> §3.7.1 and §3.8 to §3.8.1, beside the sections [ad-presentation](ad-presentation.md) read.
|
||
> - *Server-Side Ad Insertion and OTT Guidance*, August 2021, §1.2, §2 and §3.
|
||
> - For HLS and DASH, the texts [live-ads](live-ads.md) read, at the revisions it names:
|
||
> `draft-pantos-hls-rfc8216bis-22` §4.2 (attribute lists), §4.4.5.1 (`EXT-X-DATERANGE`) and
|
||
> Appendix D (interstitials and the asset list); ISO/IEC 23009-1:2026's MPD schema (its
|
||
> `EventStream` and `Event`). Nothing here depends on a part of them live-ads did not read.
|
||
> - RFC 8259 (JSON) and XML 1.0, for the subset of §3.5.
|
||
> - For the consent signal a record of version 2 carries (§4.7), the texts
|
||
> [live-ads](live-ads.md) read for its §8.6, at the revisions it names.
|
||
>
|
||
> A section number of one of these texts is that revision's.
|
||
>
|
||
> A pod-input record of version 2 also carries the consent signal its decision was made under,
|
||
> a Global Privacy Platform string with the sections that apply and a child-directed flag, bound
|
||
> into what the signer signs (§4.7); a verifier states it beside the record's verdict.
|
||
|
||
## 1. Scope
|
||
|
||
In scope: the marker of an ad in a stream, its body and digest, and how each is read from a
|
||
playlist, an asset list or an MPD (§3); the pod-input record, its rules and signature, and a
|
||
form-1 `wai.video.ssai` receipt as its response (§4); the stitched claim, its session, its
|
||
rules, its signature and its attestation challenge (§5); composited frames (§6); verification:
|
||
each record's and each claim's verdict, each ad's presentations and quartiles, and the spend
|
||
that counts, by currency (§7); and the reference producer, the tool and the service (§8).
|
||
The consent signal a record of version 2 carries, and what a player and a signer do with it, are
|
||
in scope too (§4.7), and so are the invalid-traffic rules a verifier applies to what §7 states
|
||
(§12).
|
||
|
||
Out of scope: a transport; how a player's session key reaches the signer (§4.6 says what the
|
||
signer does with it, not how it arrives); the ad decision itself, which stays the ad server's;
|
||
how a stitcher conditions a creative into segments; the contents of a winning bid's document
|
||
and of a VMAP document, which a record binds by digest and this revision does not read; the
|
||
SCTE-35 messages a stream may carry; a billing period, an invoice or a payment, which an
|
||
operator's system makes from what §7 states; and the detection of invalid traffic by any means
|
||
but the rules of §12, which stays statistical and the measurer's (ad-presentation §13).
|
||
|
||
Relationship to the core spec: `wai.video.stitched_presentation` 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.
|
||
|
||
Relationship to [ad-presentation](ad-presentation.md): that extension's claim names an
|
||
impression of a break-form receipt of a room; it puts the stitched form of `wai.video.ssai`
|
||
out of its scope, since such receipts name no session of a room. This extension brings stitched
|
||
and interstitial ads into scope with a claim of its own. It takes from ad-presentation, by
|
||
reference: the creative digest (§4 there); the span and its rules (§5.2, §5.4 rules 4 to 6);
|
||
the viewability rules, the qualifying run and the quartile (§6); the token, its challenge and
|
||
its issuer's verifier (§7); and the presentation fields of a policy (§8), here carried in the
|
||
record. It changes nothing in ad-presentation.
|
||
|
||
Relationship to [live-ads](live-ads.md) and `wai.video.ssai`: a room's breaks keep live-ads'
|
||
policy and break-form receipts. A stitcher that runs `wai.video.ssai`'s own deterministic
|
||
decision seals a form-1 receipt as before; a record whose response is that receipt (§4.5) lets
|
||
a verifier set the receipt's stated quartiles and billable spend beside the ones the players'
|
||
claims give. Every receipt of either form keeps its bytes.
|
||
|
||
Relationship to [live-compose](live-compose.md): a record's frame (§4.1) is live-compose
|
||
§5.9's ad frame, and the ad's region and its share are that section's. A record of a session can
|
||
show its ad in a room's ad frame on the session's own ad track, which its signer binds to the
|
||
room's composition with live-compose §5.10's ad-track binding (§4.6, §6).
|
||
|
||
## 2. Terms
|
||
|
||
- **Stream**: an HLS presentation or a DASH presentation, live or on demand.
|
||
- **Stitcher**: the server that writes ads into a stream: into its segments (server-side ad
|
||
insertion), or as interstitials a player fetches (server-guided ad insertion).
|
||
- **Ad server**: the party whose response decides which ads fill a break.
|
||
- **Response**: the ad server's answer, as the signer received it: a VAST document, a VMAP
|
||
document, or a form-1 `wai.video.ssai` receipt (§4.5).
|
||
- **Signer**: the key that signs a pod-input record: the stitcher's, or that of a proxy that
|
||
forwards the stitcher's ad requests to the ad server and their responses back.
|
||
- **Break**, **avail**: a span of the stream that ads fill, and its duration.
|
||
- **Manifest**: a document a player reads a marker from: a Media Playlist, an asset list or an
|
||
MPD (§3).
|
||
- **Marker**: the fields that place one ad in a stream (§3).
|
||
- **Ad identity**: the string a marker carries for its ad (§3), 1 to 255 bytes of UTF-8.
|
||
- **Player**, **session key**, **creative**, **span**, **geometry**, **token**, **issuer**,
|
||
**basis point**: as in ad-presentation §2.
|
||
- **Record**: a pod-input record (§4). **Claim**: a stitched claim (§5).
|
||
- **Presented**: the verdict of a claim that passes every step of §7.3.
|
||
|
||
Integers are little-endian unless this text says otherwise. A digest is SHA-256 (FIPS 180-4),
|
||
32 bytes. Ed25519 is RFC 8032's, verified strictly ([SPEC](../SPEC.md) §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 *short* field is a u8 length and that many bytes; a *long* field is a u16
|
||
length and that many bytes. A *text* field is a short field of 1 to 255 bytes of UTF-8.
|
||
|
||
## 3. Markers
|
||
|
||
### 3.1 The marker, its body and its digest
|
||
|
||
A marker is one of three kinds, by its **carriage**:
|
||
|
||
| carriage | where the stream carries it | its fields |
|
||
|---|---|---|
|
||
| 1 | an `EXT-X-DATERANGE` tag of a Media Playlist (§3.2) | the tag's `ID` and `START-DATE` |
|
||
| 2 | an asset of an HLS interstitial's asset list (§3.3) | the interstitial date range's `ID` and `START-DATE`, and the asset's index in its asset list |
|
||
| 3 | an `Event` of a DASH `EventStream` (§3.4) | the `id` of its `Period`, the `schemeIdUri` and `value` of its `EventStream`, and its own `id` |
|
||
|
||
A marker's **body** is its fields in that order, each a long field of its bytes as the
|
||
manifest writes them (an attribute value with its quotes removed; an XML attribute value with
|
||
its references replaced, §3.5), with an absent `Period` `id` or `EventStream` `value` empty;
|
||
and, for carriage 2, the asset's index as a u16 after the two long fields. A body of a carriage
|
||
**reads** when it is exactly those fields, with no byte after them.
|
||
|
||
The **marker digest** is SHA-256 over `wai:stitched-marker` (ASCII) and 0x01, the carriage (one
|
||
byte) and the body.
|
||
|
||
An HLS date range's `ID` names it within its playlist, and its `START-DATE` is fixed for the
|
||
`ID` (`draft-pantos-hls-rfc8216bis-22` §4.4.5.1); together they place an ad on the stream's
|
||
timeline, and stay the same across the reloads of a live playlist. A DASH event is named by its
|
||
scheme, its value and its `id` within its Period.
|
||
|
||
### 3.2 Carriage 1: a date range
|
||
|
||
A Media Playlist is read as its lines: its bytes are UTF-8, split at each line feed with a
|
||
carriage return before it dropped, and its first line is `#EXTM3U`. Each line that begins
|
||
`#EXT-X-DATERANGE:` is a **date range tag**, and the rest of the line its **attribute list**:
|
||
`NAME=VALUE` pairs separated by single commas, each name of `A`–`Z`, `0`–`9` and `-` and given
|
||
once; a value is a quoted string, `"` then bytes other than `"`, CR and LF then `"`, or else one
|
||
or more bytes other than `"`, `,` and white space. A tag whose list does not read so, or whose
|
||
`ID` or `START-DATE` is not a quoted string, names no ad. (These are §4.2's rules for an
|
||
attribute list, as live-ads reads them.)
|
||
|
||
An ad of carriage 1 is a date range tag that carries the attribute the record's `id_attr` names
|
||
(§4.1). Its marker is the tag's `ID` and `START-DATE`, and its ad identity is that attribute's
|
||
value: a quoted string's contents, or the value as written. A stitcher writes the ad identity
|
||
in an attribute of the date range of each ad it stitches; the attribute's name is the
|
||
stitcher's (a client attribute, `X-` and a name, by §4.4.5.1), and a record names it.
|
||
|
||
### 3.3 Carriage 2: an interstitial's asset
|
||
|
||
An **interstitial date range** is a date range tag (§3.2) whose `CLASS` is the quoted string
|
||
`com.apple.hls.interstitial` (Appendix D.2, as live-ads §5.1 writes it). <!-- allow-name: the interstitial date-range class, a protocol identifier -->
|
||
Its asset list is the JSON object (RFC 8259) Appendix D.2 defines: its `ASSETS` member is an
|
||
array of objects, each with a string member `URI` (live-ads §7.2). An ad of carriage 2 is the
|
||
asset at index *k* of the asset list a player was served for an interstitial date range. Its
|
||
marker is the date range's `ID` and `START-DATE` and *k*, and its ad identity is the asset's
|
||
`URI`.
|
||
|
||
The asset list is the manifest a claim of carriage 2 names (§5.1), since each viewer may be
|
||
served another one under the same date range; the playlist that carries the date range is held
|
||
beside it (§7.3 step 6).
|
||
|
||
### 3.4 Carriage 3: an event
|
||
|
||
An MPD is read as XML by the subset of §3.5. Its root element is `MPD`; each child `Period` of
|
||
the root, each child `EventStream` of a Period that has a `schemeIdUri`, and each child `Event`
|
||
of such an EventStream that has an `id` is considered. An ad of carriage 3 is such an Event
|
||
that carries the attribute the record's `id_attr` names. Its marker is its Period's `id`, its
|
||
EventStream's `schemeIdUri` and `value`, and its own `id`; its ad identity is the value of that
|
||
attribute.
|
||
|
||
### 3.5 The XML subset
|
||
|
||
An XML document (an MPD of §3.4, a VAST document of §7.2) is read over this subset of XML 1.0,
|
||
and one outside it does not read:
|
||
|
||
- its bytes are UTF-8 with no byte order mark;
|
||
- before and after its one root element there are only white space, comments and processing
|
||
instructions (an XML declaration among them); a document type declaration, or any other
|
||
markup that begins `<!` other than a comment or a CDATA section, is outside the subset;
|
||
- an element is a start tag, its content and its end tag with the same name, or an empty-element
|
||
tag; a tag's attributes each follow white space and are a name, `=` and a value in double or
|
||
single quotes holding no `<`, each name once;
|
||
- content is character data, CDATA sections, comments, processing instructions and elements,
|
||
nested at most 64 deep, the root counted as 1;
|
||
- in character data and attribute values, the references `<`, `>`, `&`, `"`,
|
||
`'`, `&#`*n*`;` (decimal) and `&#x`*h*`;` (hexadecimal) are replaced by the character
|
||
they name; any other `&` is outside the subset. An attribute value is otherwise taken as
|
||
written: no white space in it is normalised.
|
||
|
||
Elements and attributes are named by their **local name**: the part after the last `:`. An
|
||
element's **text** is its character data and CDATA children, joined in order, with white space
|
||
(space, tab, CR, LF) removed from both ends.
|
||
|
||
## 4. The pod-input record
|
||
|
||
### 4.1 Layout
|
||
|
||
| size | field | meaning |
|
||
|---|---|---|
|
||
| 4 | `magic` | `WSI1` (ASCII) |
|
||
| 1 | `version` | 1; or 2 for a record that carries the consent signal (§4.7) |
|
||
| 1 | `carriage` | how the stream marks its ads, 1 to 3 (§3.1) |
|
||
| 16 | `session` | the session the decision was made for (§5.2); 16 zero bytes for a **shared** break, one every viewer of the stream is shown (§4.6) |
|
||
| short | `avail_id` | the break, as the signer names it: a text field |
|
||
| 4 | `avail_ms` | u32: the avail the ad request asked to fill, in milliseconds, at least 1 |
|
||
| 1 | `response_format` | 1 a VAST document; 2 a VMAP document; 3 a form-1 `wai.video.ssai` receipt (§4.5) |
|
||
| 32 | `response` | formats 1 and 2: SHA-256 of the response's bytes as the signer received them; format 3: the receipt's identity (its receipt hash) |
|
||
| 1 | `id_kind` | what an ad's `serving_id` is: 1 the `AdServingId` of its `InLine` (VAST 4.3 §3.4.3); 2 the `id` of its `Ad` (VAST 4.3 §3.3.2); 3 a form-1 impression's `ad_id` |
|
||
| short | `id_attr` | the attribute that carries the ad identity: for carriage 1 an HLS attribute name of `A`–`Z`, `0`–`9` and `-`; for carriage 3 an XML name (§4.4); for carriage 2 no bytes |
|
||
| 3 | `currency` | the prices' currency: an ISO 4217 alphabetic code, three of `A`–`Z` |
|
||
| 1 | `exponent` | its minor unit, as ISO 4217 lists it: 0 to 4 |
|
||
| 13 | `frame` | the frame the break is shown in (§4.3) |
|
||
| 4 + long | measurement terms | `presentation`, `viewability`, `token_type` (u16) and `issuer` (long), as ad-presentation §8 defines them |
|
||
| 1 | `n` | the ads' count, 1 to 16 |
|
||
| … | `ads` | `n` ads, §4.2 |
|
||
| long + 2 + 2 × `k` | consent signal | version 2 only: `gpp`, `k`, `gpp_sid` and `child_directed` (§4.7) |
|
||
| 32 | `signer` | the signer's Ed25519 key |
|
||
| 64 | `sig` | §4.4 |
|
||
|
||
### 4.2 An ad
|
||
|
||
| size | field | meaning |
|
||
|---|---|---|
|
||
| 1 | `slot` | its place in the pod: 0, 1, 2, … in order |
|
||
| 32 | `marker` | its marker digest (§3.1) |
|
||
| short | `ad_id` | the ad identity its marker carries (§3): a text field |
|
||
| short | `serving_id` | its identity in the response, as `id_kind` names it: a text field |
|
||
| short | `creative_id` | its creative's universal identifier, a `UniversalAdId`'s text (VAST 4.3 §3.7.1), or no bytes |
|
||
| short | `registry` | that identifier's `idRegistry`; no bytes exactly when `creative_id` has none |
|
||
| 32 | `creative_hash` | the creative digest (ad-presentation §4) of the media objects the stream serves for it |
|
||
| 4 | `duration_ms` | u32: the sum of those objects' durations, at least 1 |
|
||
| 8 | `price_micros` | u64: the price of one presented impression, in millionths of a minor unit of `currency`, at most 10^12 |
|
||
| 1 | `price_source` | 1 the response's `Pricing` (VAST 4.3 §3.4.8) of model CPM, checked (§7.2); 2 a winning bid's price; 3 a price the signer states |
|
||
| 32 | `evidence` | for `price_source` 2, SHA-256 of the winning bid's document, which this revision binds and does not read; otherwise 32 zero bytes |
|
||
|
||
A price is per presented impression, so that the spend of §7.4 is a count of presentations
|
||
times a price. A CPM price *P* in a currency of exponent *e* is *P* × 10^(*e* + 3) millionths of
|
||
a minor unit per impression: *P* × 10^*e* minor units a thousand impressions, times 10^6 over
|
||
1000. CPM 25.00 in a currency of two decimals is 2 500 000.
|
||
|
||
### 4.3 The frame
|
||
|
||
| size | field | meaning |
|
||
|---|---|---|
|
||
| 1 | `kind` | 0 the full picture; 3 a squeeze; 4 a double box (live-compose §5.9) |
|
||
| 2 | `width` | u16: the picture's width in pixels |
|
||
| 2 | `height` | u16: its height |
|
||
| 1 | `corner` | live-compose §5.9's `corner`: a squeeze's program window's corner, 1 to 4; a double box's program side, 1 left, 2 right |
|
||
| 2 | `window_w` | u16: a squeeze's program window's width; otherwise 0 |
|
||
| 2 | `window_h` | u16: its height; otherwise 0 |
|
||
| 2 | `gap` | u16: a double box's gap; otherwise 0 |
|
||
| 1 | `aspect` | a double box's tile aspect, live-compose §3.1's `aspect`, 0 to 3; otherwise 0 |
|
||
|
||
A frame of kind 0 has every other field 0. A squeeze or a double box has a `width` and a
|
||
`height` live-compose §3.4 admits (each even, 16 to 1920, at most 2 073 600 pixels), and
|
||
fields live-compose §3.3 admits for its layout: a squeeze's `window_w` from 1 to `width` − 1 and
|
||
`window_h` from 1 to `height` − 1, with `gap` and `aspect` 0; a double box's `corner` 1 or 2,
|
||
`window_w` and `window_h` 0, and `gap` + 2 at most `width`.
|
||
|
||
### 4.4 Digest, signature and rules
|
||
|
||
The **digest** of a record is SHA-256 over its bytes from `magic` through `signer`. The
|
||
**signature** is Ed25519 by `signer` over `wai:stitched-pod-input` (ASCII) and 0x01, followed by
|
||
the bytes the digest covers.
|
||
|
||
A reader reads exactly the bytes it is given as one record, in the order of §4.1, and refuses
|
||
it `pod_input_malformed` when the bytes end before a field the layout requires or go on after
|
||
`sig`, or when:
|
||
|
||
1. its magic is not `WSI1` or its version not 1 or 2, or its `carriage` is not 1, 2 or 3;
|
||
2. `avail_id` is not a text field, or `avail_ms` is 0;
|
||
3. `response_format` is not 1, 2 or 3; or `id_kind` is not 1 or 2 with format 1 or 2, or not 3
|
||
with format 3;
|
||
4. `id_attr` is, for carriage 1, not 1 to 255 bytes of `A`–`Z`, `0`–`9` and `-`; for carriage
|
||
3, not an XML name of 1 to 255 bytes (an ASCII letter or `_`, then ASCII letters, digits,
|
||
`_`, `-` and `.`); for carriage 2, not empty;
|
||
5. `currency` is not three of `A`–`Z`, or `exponent` is above 4;
|
||
6. the frame breaks §4.3;
|
||
7. the measurement terms break ad-presentation §8, or do not set its bit 0, **claim**: an ad
|
||
of a record counts only through a claim;
|
||
8. `n` is 0 or above 16; an ad's `slot` is not its index; `ad_id` or `serving_id` is not a text
|
||
field; `creative_id` and `registry` are not both empty, or both text fields; `duration_ms`
|
||
is 0; `price_micros` is above 10^12; `price_source` is not 1, 2 or 3, is 1 with a response
|
||
format other than 1, is 2 with format 3, or is other than 3 with format 3; `evidence` is not
|
||
zero exactly when `price_source` is not 2;
|
||
9. two ads have one `marker`;
|
||
10. of version 2, its consent signal breaks a rule of live-ads §8.6, or, in a shared record
|
||
(§4.6), carries a GPP string.
|
||
|
||
### 4.5 A form-1 receipt as the response
|
||
|
||
A stitcher that decides by `wai.video.ssai`'s own deterministic decision (SPEC §5) seals a
|
||
form-1 receipt, whose signed bytes are unchanged here. A record of format 3 names it by its
|
||
identity, and a verifier that holds it checks it (§7.2). Its **JSON carriage**, for a verifier
|
||
to be given it, is one JSON object with exactly these members, a byte field lowercase
|
||
hexadecimal of its exact length:
|
||
|
||
| member | value |
|
||
|---|---|
|
||
| `form` | 1 |
|
||
| `avail_id` | a string |
|
||
| `manifest_hash` | 32 bytes: the attested manifest's hash |
|
||
| `pod_hash` | 32 bytes |
|
||
| `impressions` | an array, each `{"slot", "ad_id", "creative_hash", "duration_ms", "price_micros", "quartiles_reached"}`, as live-ads §8.2 writes a form-2 impression |
|
||
| `merkle_root` | 32 bytes |
|
||
| `budget_spent_micros`, `work_decide_ops`, `joules_micro` | integers |
|
||
| `energy_provenance` | its class's label, or `null` unmetered; and the members a class with evidence carries, as live-ads §8.2 has them. The legacy label `HwShunt` does not read in this carriage |
|
||
| `grant` | `{"grant_hash", "capability", "joule_ceiling_micro", "funds_ceiling"}`, `funds_ceiling` `null` for none |
|
||
| `parent_receipt_hash` | 32 bytes, or `null` |
|
||
| `signer_pubkey` | 32 bytes |
|
||
| `signer_id` | a string |
|
||
| `sig` | 64 bytes |
|
||
|
||
Its **signing payload** is the concatenation of `wai:video-ssai-receipt` (ASCII) and 0x01; the
|
||
length (u32) and bytes of `avail_id`; `manifest_hash`; `pod_hash`; `merkle_root`;
|
||
`budget_spent_micros`, `work_decide_ops` and `joules_micro` (u64 each); the class in its signed
|
||
encoding (energy-measurement §2.7), the tag byte 0 for none; the grant reference as live-ads
|
||
§8.4 writes it; 1 and `parent_receipt_hash`, or 0; the impressions' count (u64) and each one's
|
||
leaf (live-ads §8.3); and `signer_pubkey`, then the length (u32) and bytes of `signer_id`. Its
|
||
integers are big-endian. `sig` is Ed25519 by `signer_pubkey` over the payload, and the
|
||
receipt's identity is BLAKE3 over `wai:video-ssai-receipt-id` (ASCII) and 0x01, the payload
|
||
and `sig`. A receipt **verifies** when its signature verifies; its `merkle_root` is the root of
|
||
its impressions (live-ads §8.3); its `budget_spent_micros` is their billable spend and, when
|
||
`funds_ceiling` is not `null`, at most it; `work_decide_ops` is at least the impressions'
|
||
count; its slots are 0, 1, 2, … in order with each `quartiles_reached` at most 5; and its
|
||
figure and class cohere as energy-measurement §2 has them.
|
||
|
||
### 4.6 The session and the shared break
|
||
|
||
A record of a session is a decision made for one playback session: a personalised stitched
|
||
playlist, or the asset list one viewer was served. Its `session` is the session the player's
|
||
session key names (§5.2). The signer **MUST** take it from the player's session key, which the
|
||
player gives with its request for the session's playlist or asset list, by a means this revision
|
||
does not fix (for one, a parameter of that request, which a stitcher forwards in the ad request
|
||
it makes for the session). A signer **MUST NOT** derive a session from anything else, and a
|
||
claim counts toward a record of a session only when its key names that session (§7.3).
|
||
|
||
A **shared** record is a decision every viewer of the stream is shown: a break of a linear
|
||
channel, or a composited break whose ad region every viewer sees showing the same ad. Each
|
||
viewer's player that presents it signs a claim under its own session key, and each claim of a
|
||
distinct session that is presented is one presentation (§7.4). A shared break is where a party
|
||
that makes session keys of its own can sign claims for sessions nobody watched; a record that
|
||
buys such breaks **SHOULD** set ad-presentation §8's **attest** bit, so that each presentation
|
||
costs that party a vouched device (ad-presentation §13).
|
||
|
||
A composited break whose ad region shows each session its own ad, on the session's ad track
|
||
(live-compose §5.10), is decided session by session: each session's ads are a record of that
|
||
session, as a personalised playlist's are, and never a shared record. The record's frame is the
|
||
room's ad frame, and its signer binds each ad to the composition with an ad-track binding
|
||
(live-compose §5.10.2), which a verifier refuses when the record is shared, is another
|
||
session's, or states a frame other than the descriptor's (live-compose §5.10.3). The program
|
||
around the ad stays every viewer's.
|
||
|
||
### 4.7 The consent signal
|
||
|
||
A record of version 2 carries, after its ads and before `signer`, the consent signal its
|
||
decision was made under: live-ads §8.6's, in this record's layout.
|
||
|
||
| size | field | meaning |
|
||
|---|---|---|
|
||
| long | `gpp` | the GPP string the player gave, byte for byte; no bytes when it gave none |
|
||
| 1 | `k` | the count of `gpp_sid`, 0 to 16 |
|
||
| 2 × `k` | `gpp_sid` | u16 each: the string's sections the player gave as applying, in the order given |
|
||
| 1 | `child_directed` | 1 when the request is directed to children, 0 otherwise |
|
||
|
||
The signal holds live-ads §8.6's rules (§4.4 rule 10), and the parties do with it what that
|
||
section says, the signer in the decider's place:
|
||
|
||
- **The player** gives the string, its sections and a child-directed request with its request
|
||
for the session's playlist or asset list, as it gives its session key (§4.6), by a means this
|
||
revision does not fix, and **MUST NOT** write, alter or complete a GPP string.
|
||
- **The signer** states in the record what it was given, byte for byte, **MUST** pass it
|
||
unchanged in every ad request it makes for the decision (an ad request URL's `gpp` and
|
||
`gpp_sid` parameters, as the GPP string's text names them, or an OpenRTB 2.6 request's
|
||
`Regs.gpp`, `Regs.gpp_sid` and `Regs.coppa`), and **MUST NOT** state a string, a section or a
|
||
flag it was not given, or clear a flag it was given.
|
||
- **A shared record** (§4.6) is a decision every viewer is shown, and carries no viewer's
|
||
string: its `gpp` and `gpp_sid` are empty, and its `child_directed` is 1 when the stream is
|
||
directed to children. A signer **MUST NOT** apply one viewer's signal to a break every viewer
|
||
sees.
|
||
- **What no party infers** is live-ads §8.6's: an empty string is no string given, a flag of 0
|
||
is no mark given, and neither names the viewer. A string that is not empty can still link
|
||
every record and receipt that carries it (§11).
|
||
|
||
The signal is in the bytes the digest and the signature cover (§4.4): a record whose signal
|
||
changed after signing fails its signature.
|
||
|
||
## 5. The stitched claim
|
||
|
||
### 5.1 Layout
|
||
|
||
| size | field | meaning |
|
||
|---|---|---|
|
||
| 4 | `magic` | `WSC1` (ASCII) |
|
||
| 1 | `version` | 1 in this revision |
|
||
| 1 | `carriage` | the marker's carriage (§3.1) |
|
||
| 32 | `manifest` | SHA-256 of the manifest the player read the marker from: the Media Playlist for carriage 1, the asset list for carriage 2, the MPD for carriage 3 |
|
||
| 16 | `session` | the session its key names (§5.2) |
|
||
| 32 | `creative_hash` | the creative presented: its creative digest (ad-presentation §4) |
|
||
| 32 | `player` | the session key, which signs the claim |
|
||
| 1 | `geometry` | ad-presentation §5.1's: 1 the ad's own pixels (in a frame, its region, §6), 2 a full-screen television output, 3 the player's area |
|
||
| 2 | `share_bp` | u16: the share of the picture the ad's region holds, in basis points (§6) |
|
||
| 4 | `frames` | u32: as ad-presentation §5.1 |
|
||
| long | `marker` | the marker's body (§3.1) |
|
||
| short | `ad_id` | the ad identity the marker carries: a text field |
|
||
| 2 | `n` | u16: the presented objects' count |
|
||
| 32 × `n` | `objects` | the content hashes of the creative's objects the spans touch, in order |
|
||
| 2 | `m` | u16: the spans' count |
|
||
| 11 × `m` | `spans` | ad-presentation §5.2's spans, over the ad's media time |
|
||
| long | `attestation` | a token (ad-presentation §7.1), or no bytes |
|
||
| 64 | `sig` | §5.3 |
|
||
|
||
### 5.2 The session key and the session
|
||
|
||
A player **MUST** make a fresh Ed25519 key for each playback session of a stream, 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). The **session** a key
|
||
names is the first 16 bytes of SHA-256 over `wai:stitched-presentation-session` (ASCII) and
|
||
0x01, and the key (32 bytes). It is a function of a key made for one session alone, so it links
|
||
nothing across sessions.
|
||
|
||
### 5.3 Digest and signature
|
||
|
||
The **digest** of a claim is SHA-256 over its bytes from `magic` through `attestation`. The
|
||
**signature** is Ed25519 by `player` over `wai:stitched-presentation-claim` (ASCII) and 0x01,
|
||
followed by the bytes the digest covers.
|
||
|
||
### 5.4 Reading and rules
|
||
|
||
A reader reads exactly the bytes it is given as one claim, in the order of §5.1, and refuses it
|
||
`stitched_claim_malformed` when the bytes end before a field the layout requires or go on after
|
||
`sig`, or when:
|
||
|
||
1. its magic is not `WSC1` or its version not 1;
|
||
2. `carriage` is not 1, 2 or 3, or `marker` is not a body of that carriage that reads (§3.1);
|
||
3. `geometry` is not 1, 2 or 3, or `share_bp` is not 1 to 10 000;
|
||
4. `ad_id` is not a text field;
|
||
5. its objects and spans break ad-presentation §5.4 rules 4 to 6.
|
||
|
||
### 5.5 The attestation challenge
|
||
|
||
A token a claim carries answers ad-presentation §7.2's TokenChallenge with the record's
|
||
`token_type` and `issuer` and this **redemption context**: SHA-256 over
|
||
`wai:stitched-presentation-attest` (ASCII) and 0x01, `carriage` (one byte), the marker digest,
|
||
`session`, `creative_hash`, `player`, and the length (one byte) and bytes of `ad_id`, the
|
||
claim's own. It is a function of the ad's place in the stream and of the session key, each
|
||
known before the ad plays, so a token answers one presentation of one session key; lifted onto
|
||
another claim, its `challenge_digest` is not that claim's. The token is verified as
|
||
ad-presentation §7.3 verifies one.
|
||
|
||
## 6. Composited frames
|
||
|
||
A break may show the ad and the program in one picture: a **squeeze**, the program shrunk into
|
||
a window in one corner with the ad in the L around it; or a **double box**, the program and the
|
||
ad in two boxes side by side. A record states such a frame (§4.3), and the ad's **region** is
|
||
then live-compose §5.9's ad region of a canvas of the frame's `width` and `height` under its
|
||
layout, `corner`, program window (`window_w` × `window_h` as `pip_width` × `pip_height`),
|
||
`gap` and `aspect`: an integer function of the signed fields, so a player and a verifier compute
|
||
the same rectangles. A frame of kind 0 is the full picture, and the region is the whole of it.
|
||
|
||
- The **share** of the picture the region holds is live-compose §5.9's, in basis points; 10 000
|
||
for the full picture. A claim states it as `share_bp`, and a claim whose `share_bp` is not its
|
||
record's frame's share is refused (§7.3 step 9).
|
||
- A player that measures the ad's own pixels (geometry 1) **MUST** measure the fraction in
|
||
view of the ad's **region**, not of the whole picture: the share of the region's area, as the
|
||
picture is displayed, that lies within the visible part of its player (ad-presentation §10).
|
||
One that cannot place the region states geometry 3, its own area, as ad-presentation §10
|
||
allows.
|
||
- The viewability rules of ad-presentation §6.1 then apply to the region's pixels: half of the
|
||
ad's region in view for the video rule, all of it for the cross-media rule. No text read for
|
||
this revision defines a viewability rule of its own for a composited ad; a buyer that weighs a
|
||
composited presentation by how much of the picture the ad held reads `share_bp` among the
|
||
claim's facts.
|
||
- **One presentation is one claim per signing player.** When the room's ad fills the ad
|
||
region, every viewer of a composited break sees the same picture, and each player that
|
||
presents it signs its own claim; a record of the break is shared (§4.6), and each presented
|
||
claim of a distinct session is one presentation. When each session is shown its own ad on its
|
||
ad track (live-compose §5.10), the program is every viewer's and the ad region the session's:
|
||
the break has a record of each session, whose frame is the composition's ad frame, and the
|
||
session's claim of its ad is that record's one presentation (§7.4). Either way, a count of
|
||
devices that requested the stream is not a count of presentations.
|
||
|
||
## 7. Verification
|
||
|
||
### 7.1 Inputs
|
||
|
||
A verifier is given:
|
||
|
||
- **records**: pod-input records, a list of byte strings, in an order;
|
||
- **claims**: claims, a list of byte strings, in an order, at most 2^20 of them at once, so that
|
||
no spend of §7.4 passes 2^64 − 1 (a price is at most 10^12, and a claim is one presentation
|
||
of one ad at most);
|
||
- **manifests**: the manifests the players read (playlists, asset lists and MPDs), by their
|
||
bytes;
|
||
- **responses**: VAST and VMAP documents, by their bytes; and form-1 receipts in their JSON
|
||
carriage (§4.5);
|
||
- **creatives**: the creatives it holds (ad-presentation §4), each its objects;
|
||
- **issuers**: the attestation issuers it trusts (ad-presentation §7.3);
|
||
- **signers**: the keys whose records it accepts: the stitchers and proxies the buyer and the
|
||
seller agreed on.
|
||
|
||
### 7.2 Each record
|
||
|
||
The records are judged first, in order. Each gets one verdict, **accepted** or the reason of
|
||
the first step it fails:
|
||
|
||
1. It reads (§4.4): `pod_input_malformed`.
|
||
2. Its signature verifies under `signer` (§4.4): `bad_key`, `bad_signature`.
|
||
3. `signer` is among the signers: `pod_signer_unknown`.
|
||
4. No record before it that was accepted has its `session` and an ad of a `marker` it has:
|
||
`pod_duplicate`. A decision is recorded once.
|
||
5. **The response.** When the verifier holds it, the record agrees with it, as below:
|
||
otherwise `pod_response_mismatch`.
|
||
- Format 1, the response whose SHA-256 is `response`: it reads as XML (§3.5) with a root of
|
||
local name `VAST`; and for each ad, exactly one `Ad` child of the root has the ad's
|
||
`serving_id` as its identity (for `id_kind` 1, the text of the first `AdServingId` child
|
||
of its first `InLine` child; for 2, its `id` attribute); that `Ad` has an `InLine` child;
|
||
when `creative_id` is not empty, a `UniversalAdId` child of a `Creative` child of a
|
||
`Creatives` child of that `InLine` has the text `creative_id` and the `idRegistry`
|
||
`registry`; and when `price_source` is 1, that `InLine`'s first `Pricing` child has
|
||
`model` `CPM` and `currency` the record's `currency`, and its text is a decimal number
|
||
(digits, then optionally `.` and digits) *P* with *P* × 10^(`exponent` + 3) an integer of
|
||
at most 2^64 − 1 equal to `price_micros`.
|
||
- Format 3, the receipt whose identity is `response`, among those that read (§4.5): it
|
||
verifies (§4.5), and its impressions are the record's ads slot for slot: each one's
|
||
`slot` the ad's, its `ad_id` the ad's `serving_id`, and its `creative_hash`,
|
||
`duration_ms` and `price_micros` the ad's.
|
||
- Format 2 is bound by its digest and not read.
|
||
|
||
A record whose response the verifier does not hold is accepted with its response unchecked,
|
||
and the verification states so (§7.5).
|
||
|
||
A record that is not accepted names no ad: a claim of one of its ads is judged as though it
|
||
were not there.
|
||
|
||
### 7.3 Each claim
|
||
|
||
Then the claims, in order. Each gets one verdict, **presented** or the reason of the first
|
||
step it fails:
|
||
|
||
1. It reads (§5.4): `stitched_claim_malformed`.
|
||
2. Its signature verifies under `player` (§5.3): `bad_key`, `bad_signature`.
|
||
3. Its `player` names its `session` (§5.2): `claim_key_unbound`.
|
||
4. **Its record.** Its record is the first accepted record of its `carriage` that has an ad
|
||
whose `marker` is the claim's marker digest and whose `ad_id` is the claim's, and whose
|
||
`session` is the claim's or which is shared. When an accepted record has such an ad but none
|
||
is the claim's this way: `claim_session_unbound`. When none has such an ad:
|
||
`claim_no_decision`. From here on the claim names that record and that ad.
|
||
5. **Its manifest.** The verifier holds a manifest whose SHA-256 is `manifest`:
|
||
`claim_manifest_unknown`.
|
||
6. **Its marker.** The marker, with the claim's ad identity, is read from that manifest by §3
|
||
under the record's `id_attr`: for carriage 1, the manifest is a Media Playlist with a date
|
||
range tag of the marker's `ID` and `START-DATE` whose `id_attr` attribute's value is the
|
||
ad identity; for carriage 2, it is an asset list whose asset at the marker's index has the
|
||
ad identity as its `URI`, and a manifest the verifier holds is a Media Playlist with an
|
||
interstitial date range of the marker's `ID` and `START-DATE`; for carriage 3, it is an MPD
|
||
with an Event of the marker's fields whose `id_attr` attribute's value is the ad identity:
|
||
`claim_marker_absent`.
|
||
7. **One claim a session.** No claim before it passed steps 1 to 6 for the same record, ad and
|
||
session: `claim_duplicate`. The first that does is that session's **own claim** of the ad,
|
||
whatever its later verdict; steps 8 to 11 are for own claims alone.
|
||
8. **The asset.** Its `creative_hash` is the ad's; the verifier holds a creative of that
|
||
creative digest whose durations sum to the ad's `duration_ms`; every span ends at most at
|
||
that `duration_ms`; and `objects` is exactly the content hashes of the creative's objects
|
||
the spans touch, in order (ad-presentation §9.2 step 6): `claim_asset_mismatch`.
|
||
9. **The frame.** Its `share_bp` is the share of its record's frame (§6):
|
||
`claim_frame_mismatch`.
|
||
10. **Viewability.** It is viewable under the record's rule (ad-presentation §6.2, with
|
||
`geometry` and the **audible** bit as there): `claim_not_viewable`.
|
||
11. **Attestation**, when the record sets **attest**: its token is not empty
|
||
(`attestation_absent`); it reads as a token of the record's `token_type`, its
|
||
`challenge_digest` is SHA-256 of the TokenChallenge of §5.5, and its issuer's verifier
|
||
accepts it under the record's `issuer` (`attestation_invalid`).
|
||
|
||
### 7.4 Presentations, quartiles and the spend that counts
|
||
|
||
For each ad of each accepted record:
|
||
|
||
- its **presentations** are its own claims (one per session, step 7) that are presented. A
|
||
record of a session has at most one; a shared record has one for each session whose claim is
|
||
presented;
|
||
- the **quartile** a presentation reaches is ad-presentation §6.3's, over the claim's spans
|
||
with *D* the ad's `duration_ms`: 1 started, then a quarter, a half, three quarters and all of
|
||
it, at VAST 4.3 §3.14.1's marks. The ad's **quartiles** are five counts: its presentations
|
||
whose quartile is at least 1, 2, 3, 4 and 5;
|
||
- its **verdict** is presented when it has a presentation; otherwise `claim_absent` when it has
|
||
no own claim, or the verdict of its first own claim;
|
||
- its **presented spend** is its `price_micros` times its presentations.
|
||
|
||
A record's presented spend is the sum over its ads. The **spend** of a verification is, for
|
||
each currency, the sum of the presented spend of the accepted records of that currency. Spends
|
||
of different currencies are never summed.
|
||
|
||
A quartile is recomputed from what a player signed, not compared with a beacon count: an ad
|
||
whose segments were fetched ahead of its break (a prefetch) and never presented has no claim,
|
||
so no presentation and no spend, whatever a request-based count says. For a record of format 3
|
||
whose receipt was checked, the verification states the receipt's `quartiles_reached` for each
|
||
ad and its `budget_spent_micros` beside the presented ones, so a buyer and a seller see both
|
||
figures from the same signed bytes.
|
||
|
||
### 7.5 What a verification states
|
||
|
||
- For each record, in order: its digest when it reads, and its verdict; and for an accepted
|
||
record, whether its response was checked, each ad's slot, own claims (their indices among the
|
||
claims given), presentations, verdict, quartiles, presented spend and, for a checked format-3
|
||
response, its stated quartile; the record's presented spend, and its stated spend for a
|
||
checked format-3 response; and its frame's share (§6) and ad region, the rectangles of
|
||
live-compose §5.9, none for the full picture; and, for a record of version 2, its consent
|
||
signal (§4.7), as live-ads §8.6 writes it.
|
||
- For each claim, in order: its digest when it reads; its verdict; from step 4 on, the record
|
||
and the ad's slot it names; and for an own claim, from step 8 on, the quartile it reaches,
|
||
its run and its viewable time (ad-presentation §6.2).
|
||
- The spend, by currency.
|
||
|
||
A verification refuses nothing as a whole: each record and each claim gets its own verdict.
|
||
|
||
## 8. The producer, the tool and the service
|
||
|
||
**The reference producer** is `web/ads/stitched.js` of the browser client `joule-moq-web`,
|
||
beside ad-presentation's `web/ads/presentation.js`, whose span recorder it uses. From the
|
||
manifest a player fetched, it reads the markers and ad identities of §3 (`hlsAds`,
|
||
`interstitialAds`, `mpdAds`); it measures the fraction in view of an ad's region in a frame
|
||
(`regionVisibleBp`); and it writes and signs the claim with the platform's Ed25519. It needs no
|
||
player of a particular kind: a player integration hands it the manifest bytes, the ad's media
|
||
time and the observations ad-presentation §10 lists. Its tests require the corpus's claims from
|
||
the corpus's manifests, byte for byte.
|
||
|
||
**The tool**, `wai_stitched_presentation` (feature `stitched_presentation`): `verify` reads a
|
||
bundle (the inputs of §7.1 as JSON, in the corpus README's form) and prints what §7.5 states;
|
||
`sign-pod` writes and signs a record from a draft with the signer's key; `vectors` writes and
|
||
checks the corpus. **The service**, `wai_stitched_presentation serve`, answers `POST /v1/verify`
|
||
with the same statement for a bundle and `GET /v1/health`, over HTTP/1.1 on a loopback address
|
||
for a reverse proxy to publish; it reads one request a connection, bounds the head, the body
|
||
and the time it waits, and holds no state between requests.
|
||
|
||
## 9. Reasons
|
||
|
||
The codes this extension adds, each with where it is raised:
|
||
|
||
| reason | raised at |
|
||
|---|---|
|
||
| `pod_input_malformed` | §4.4, §7.2 step 1 |
|
||
| `pod_signer_unknown` | §7.2 step 3 |
|
||
| `pod_duplicate` | §7.2 step 4 |
|
||
| `pod_response_mismatch` | §7.2 step 5 |
|
||
| `stitched_claim_malformed` | §5.4, §7.3 step 1 |
|
||
| `claim_session_unbound` | §7.3 step 4 |
|
||
| `claim_manifest_unknown` | §7.3 step 5 |
|
||
| `claim_marker_absent` | §7.3 step 6 |
|
||
| `claim_frame_mismatch` | §7.3 step 9 |
|
||
|
||
A verification also gives live-events' `bad_key` and `bad_signature` (§7.2 step 2, §7.3
|
||
step 2), and ad-presentation's `claim_key_unbound` (§7.3 step 3), `claim_no_decision` (step 4),
|
||
`claim_duplicate` (step 7), `claim_asset_mismatch` (step 8), `claim_not_viewable` (step 10),
|
||
`attestation_absent` and `attestation_invalid` (step 11), and `claim_absent` (§7.4), each at the
|
||
step named. None of them refuses a verification: each is one record's, one claim's or one ad's.
|
||
|
||
## 10. Conformance
|
||
|
||
[`live-conformance/stitched/`](../live-conformance/stitched/) holds the corpus, written from
|
||
fixed seeds by `wai_stitched_presentation vectors gen`: `vectors.json`, which holds the
|
||
creatives, the attestation issuer (RFC 9578 Appendix A.2's first test key) and the signers the
|
||
verifier holds, the players' session keys, and each vector's records, claims and facts; and
|
||
each vector's documents as files of their own. Every document is a fixture written for its
|
||
case. The vectors:
|
||
|
||
- **hls_stitched**: two viewers' personalised playlists with ads stitched in, each ad under a
|
||
date range that carries its ad identity; a record per viewer priced from its VAST response;
|
||
viewer 2 left before its second ad, whose segments had been fetched ahead of the break, so
|
||
that ad has no presentation; a control of each check of a record (an unknown signer, a price
|
||
the response does not give, a decision recorded twice, terms that count without a claim, a
|
||
flipped signature) and of a claim (a duplicate, a key that does not name the session, a
|
||
session the record was not decided for, a marker no record names, a manifest not held, a
|
||
manifest without the marker, a truncated claim, a flipped signature);
|
||
- **hls_interstitial**: one shared playlist with an interstitial date range and an asset list
|
||
for each viewer; records under the cross-media rule with an audible run and attestation, one
|
||
ad priced by a winning bid whose document is bound by digest; attested presentations, a claim
|
||
with no token and one carrying another presentation's token;
|
||
- **squeezeback**: a squeeze on a DASH channel marked by an event; one shared record; three
|
||
presentations, among them one that stopped at the midpoint, and claims whose share is not the
|
||
frame's, whose region was too little in view, given twice, and of another object;
|
||
- **double_box**: a double box on an HLS channel, its record in another currency at a price the
|
||
signer states, its VMAP response bound and not read; a presentation measured over the ad's box
|
||
and one over the player's area;
|
||
- **form1_prefetch**: a stitcher's own decision sealed in a form-1 receipt whose beacons counted
|
||
the second ad as started when its first segment was fetched ahead of the break; the viewer
|
||
left after the first ad; and a record naming the same receipt at another price;
|
||
- **consent**: two viewers' personalised playlists, each with one stitched ad, and a record of
|
||
version 2 per viewer: one carrying the GPP string its player gave with its applicable
|
||
section, one a child-directed request with no string, each presented; a record whose GPP
|
||
string was changed after signing (`bad_signature`), whose ad's claim then names no decision;
|
||
a shared record carrying a viewer's string, and a record stating a section with no string
|
||
(each `pod_input_malformed`).
|
||
|
||
A conforming verifier gives every record and claim its verdict and facts, every ad its
|
||
presentations, quartiles and spend, and every vector its spend by currency, as the corpus
|
||
states. `wai_stitched_presentation vectors verify` and the test `tests/stitched_presentation.rs`
|
||
check that every file regenerates byte for byte, that `wai-rs` gives every vector its facts,
|
||
and that §9 lists exactly the codes the reference adds; the generator checks that the corpus
|
||
gives every reason of §9 and every other code of a claim or a record but `bad_key`.
|
||
`live-conformance/stitched_presentation_verify.py` is the second implementation, written from
|
||
this text: it shares no code with `wai-rs`; it verifies Ed25519 with WAI's strict rule
|
||
(`claim-conformance/_ed25519.py`), BLAKE3 with `claim-conformance/_blake3.py`, a form-1
|
||
receipt's leaves and Merkle root as the live-ads corpus's second implementation computes them,
|
||
the spans, the run and the quartile and RSASSA-PSS with the ad-presentation corpus's second
|
||
implementation, and an ad frame with the live-compose corpus's; it reads playlists, asset lists,
|
||
MPDs and VAST documents itself; and it mutates copies of the corpus and requires every copy to
|
||
fail. The reference producer's tests (§8) require the corpus's claims from its manifests.
|
||
|
||
## 11. Security and privacy considerations
|
||
|
||
- **What a claim proves.** As ad-presentation §13 states for its claim: that the holder of the
|
||
session key stated these facts of this ad, at this place of a manifest it holds the bytes of.
|
||
Without attestation it removes the third party that strips, redirects or forges beacons, and
|
||
the count of segment requests as a count of impressions; it does not remove a dishonest player
|
||
or a party that makes session keys of its own, which a shared record (§4.6) is open to.
|
||
- **What a record proves.** That its signer stated these inputs of one decision: the response's
|
||
digest, the ads, their creatives and prices. A record of format 1 checked against the
|
||
response shows the ads and the CPM prices are the response's; one of a winning bid binds the
|
||
bid's document without reading it; one of format 3 checked against its receipt shows the
|
||
stitcher's decision and its stated quartiles. The signer is the party a buyer trusts for the
|
||
decision, and §7.1's signers are who those parties are.
|
||
- **The manifest.** A claim names the manifest by its digest, and a verifier that holds the
|
||
bytes checks that the stream carried the marker with that ad identity. A personalised playlist
|
||
is a viewer's; its bytes reach whoever verifies the claim, and a collector **SHOULD** keep
|
||
them no longer than the reconciliation they serve.
|
||
- **Linking.** A session key is made for one playback session, and the session derived from it
|
||
alone, so claims link nothing across sessions. A record of a session carries that session; a
|
||
shared record carries none. A claim carries no identifier of the viewer, the device or an
|
||
account.
|
||
- **Withholding.** A party between the player and the verifier can withhold a claim; the ad
|
||
then has no presentation, which is the seller's lost spend, never the buyer's overpayment.
|
||
- **A composited frame.** The region is fixed by the record's frame; a player that states
|
||
another share is refused, and one that measures the whole picture where it should measure the
|
||
region overstates its fraction in view, which attestation does not bound: a buyer that needs
|
||
the measurement itself bounded needs the player's integrity, as ad-presentation §13 says of
|
||
every claim. For a session's own ad on its ad track, the binding holds the record's frame to
|
||
the room's descriptor (live-compose §5.10.3), so the region the claim's share is checked
|
||
against is the one the session's sink draws.
|
||
- **The consent signal.** A record of version 2 binds the signal into what the signer signs, so
|
||
one whose string, sections or flag changed after signing is refused at its signature, and its
|
||
ads count nothing. A verified record shows what the signer stated it was given and passed on
|
||
(live-ads §8.6), not that the viewer gave it or that the ad server honoured it. A shared
|
||
record carries no viewer's string, so a break every viewer sees links to no viewer's signal.
|
||
A record of one session whose `gpp` is not empty can be linked through it, by whoever holds
|
||
them, to every other record and every live-ads receipt that carries the same string, in any
|
||
session or room, as live-ads §12 says of a receipt: the string is made for the viewer, not for
|
||
one session, and is in the bytes the signature covers. What **Linking** above says of claims
|
||
does not extend to such a record.
|
||
|
||
## 12. Invalid traffic rules
|
||
|
||
### 12.1 The rules and their impressions
|
||
|
||
A verifier applies [ad-presentation](ad-presentation.md) §14's rules to what §7 states: the
|
||
same six rules, the same two marks, the same records (a traffic policy, session origins,
|
||
device bindings and the pinned lists, its §14.2 to §14.5), decided the same way from signed
|
||
bytes and integers, with no score and no model. This section says only what differs for a
|
||
stream. The rules change nothing §7 stated: every record and claim keeps its verdict, every ad
|
||
its presentations and its presented spend.
|
||
|
||
An **impression** here is one session's own claim of one ad of one accepted record (§7.3 step
|
||
7). Its **claims** are that own claim and every claim after it that §7.3 refused
|
||
`claim_duplicate` for the same record, ad and session; its session key is its own claim's
|
||
`player`, its `duration_ms` and `price_micros` the ad's, and it **counted** when its own claim
|
||
is presented. A record of a session has at most one impression an ad; a shared record (§4.6)
|
||
has one for each session whose claim passed step 7.
|
||
|
||
The rules read as ad-presentation §14.6 has them, with these terms:
|
||
|
||
- `ivt_unattested` reads the record's measurement terms: the record sets **attest** (§4.1)
|
||
and the own claim's `attestation` has no bytes;
|
||
- a device key's sessions are counted over the session keys of every impression of the
|
||
verification, across its records. A shared break is where one party can sign claims under
|
||
many session keys (§4.6); under **cap** each of those sessions needs an accepted binding,
|
||
and one device key binds no more of them than the cap.
|
||
|
||
### 12.2 The traffic policy and the origin
|
||
|
||
A traffic policy for a stream is of form 2 (ad-presentation §14.2), its `room` 32 zero bytes.
|
||
It is **bound** when its `authority` is one of the verifier's signers (§7.1): the parties
|
||
trusted for the decisions are the parties trusted to pin the lists and name the edges.
|
||
|
||
A session origin's edge is the party that terminated the player's own connection: for the
|
||
session's playlist or asset list, or, where the stitcher fetches on the player's behalf, for
|
||
the request the player made to the stitcher. An ad request a stitcher makes to an ad server
|
||
comes from the stitcher's address, which is never a session's origin. The addendum
|
||
ad-presentation read notes of server-side insertion that tracking "coming from one IP address"
|
||
is open to being filtered (§4.2.2 there); an origin signed at the player's own connection is
|
||
what keeps the address rule from reading the stitcher's.
|
||
|
||
### 12.3 What a verification states
|
||
|
||
As ad-presentation §14.7, with each impression named by its record, its ad's slot and its own
|
||
claim's index, and every sum of prices stated **by currency** (§7.4): for each currency of an
|
||
accepted record, the presented spend, the excluded spend, the withheld spend and the valid
|
||
spend, and for each rule and mark the excluded or withheld sum; and each record's valid
|
||
spend. Sums of different currencies are never added. A policy that is not bound, or whose
|
||
list is not held or does not read, refuses the traffic verification with ad-presentation
|
||
§14.8's reason, and §7's statement stands.
|
||
|
||
### 12.4 Conformance
|
||
|
||
`stitched.json` of [`live-conformance/ad-traffic/`](../live-conformance/ad-traffic/) holds one
|
||
stream, a linear channel's shared break that nine sessions claimed and one session's
|
||
personalised playlist in another currency, with everything §7.5 states of it, and vectors
|
||
over it: a policy with every rule on (three sessions of one device key in the shared break
|
||
against a cap of two, a session at the first address of a listed range, one in a listed IPv6
|
||
range, a declared crawler, a session that signed two claims of one ad, a claim given twice
|
||
and a session with no binding), the same stream with no policy, and a policy signed by a key
|
||
that is not a signer and one of the room form, each refused. Its tools, its test and its
|
||
second implementation are ad-presentation §14.9's.
|