All checks were successful
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 12s
joule-moq-http live HTTP egress / joule-moq-http · does this change touch it (push) Successful in 22s
wai-app and Create / wai-app · does this change touch it (push) Successful in 16s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 17s
wai-sink-swift native sink package / does this change touch it (push) Successful in 17s
WAI Studio / does this change touch it (push) Successful in 15s
wai-sink-android native sink library / does this change touch it (push) Successful in 20s
wai-ledger service / does this change touch it (push) Successful in 1m2s
wai-quantum for JavaScript / does this change touch it (push) Successful in 1m6s
wai-fleet service / does this change touch it (push) Successful in 1m8s
WAI operation catalogue / does this change touch it (push) Successful in 1m8s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 1m13s
wai-quantum studio and desktop / does this change touch it (push) Successful in 1m8s
wai-key-proxy / does this change touch it (push) Successful in 1m8s
wai-audit collector and player integration / does this change touch it (push) Successful in 1m9s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 9s
wai-quantum energy-landscape jobs / does this change touch it (push) Successful in 8s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 39s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m8s
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Successful in 2m38s
wai-ledger service / service · linux (push) Successful in 2m13s
joule-code conformance gate / joule-moq-relay, every target (push) Successful in 6m28s
wai-quantum for JavaScript / javascript · linux (push) Successful in 3m47s
wai-key-proxy / proxy · linux (push) Successful in 1m22s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 3m10s
wai-audit collector and player integration / collector · linux (push) Successful in 2m18s
joule-moq-http live HTTP egress / joule-moq-http, every target (push) Successful in 4m54s
wai-fleet service / service · linux (push) Successful in 3m50s
joule-code conformance gate / joule-moq-ingest, every target (push) Successful in 7m51s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 5m52s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 9m49s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 11m34s
wai-quantum studio and desktop / studio · browser (push) Successful in 8m7s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 11m25s
WAI Studio / Studio in WebKit · linux (push) Successful in 13m57s
wai-quantum studio and desktop / desktop app · windows (push) Successful in 11m7s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 28m12s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 1m46s
wai-quantum energy-landscape jobs / the same bits · linux-arm64 (push) Successful in 13m21s
wai-quantum energy-landscape jobs / native against WebAssembly, over many seeds (push) Successful in 9m10s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 10m1s
wai-quantum energy-landscape jobs / reader, kernels, answers and the stated result · linux, release (push) Successful in 15m25s
wai-quantum energy-landscape jobs / the module under wasm32-wasip2 (wasmtime) (push) Successful in 18m35s
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Successful in 49s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 7m51s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 15m54s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 10m24s
wai-ledger service / service · macos (push) Successful in 1m7s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 53s
wai-fleet service / service · macos (push) Successful in 45s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 15m53s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 6m58s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 2m10s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 4m59s
wai-quantum energy-landscape jobs / the same bits · macos (push) Successful in 7m31s
Three mappings, each with a reference module, a corpus and a second implementation written from its text. None changes a byte of a decision, a cue, a policy, a receipt or a claim, and nothing is wired into a relay, a room service or a player. - SCTE 224 import (audience-decision §10, new; audience_decision::scte224). A Media document whose policies are applied by time, over audiences stated in region codes and with the blackout action, imports to the blackouts in force over each span of its programme; spans are written back as a document that imports to them; a decision or a sink predicate is held to its document, so a verifier recomputes the decision. What does not carry exactly refuses the document with its own reason: an audience of postal codes or one that excludes, another action, a signal-matched media point, an eligibility window, a priority, an open-ended policy, an unresolved reference. Read: the schema of ANSI/SCTE 224 2021 (scte_224_20211011.xsd) and a public encoding practice (BP-EPG-BLK v1.0 draft); the standard's own text was refused to an unattended fetch and was not read, and §10.1 lists what that leaves unbound (decision D10). - Ad creative signalling (draft-riedl-moq-ad-creative-signaling-01; live-ads §13, new; live_ads::creative). A session's fill of a break as the draft's record on an MSF event timeline, signed by the decider as a detached JWS over the record's RFC 8785 form; records read as untrusted input and held to the room's schedule and receipts. The draft's own vectors are in the corpus and its signed record verifies with its test key. The catalog member wai_ads (moq-streaming-format §4.7) is registered and held by catalog_check.py --strict, with ten planted faults. - Content stitching (draft-englishm-moq-content-stitching-00; live-ads §14, new; live_ads::stitch). The composed track is a function of the programme's listing, the schedule and the receipts, changing ingredient at Group boundaries alone, and a verifier recomputes it; a break is stitched at a break candidate of a package's marks where marks are given. The stitcher is the edge of ad-presentation §14.3 and writes the session origin (WSO1); the player writes the device binding (WDB1); the invalid-traffic rules decide from those records in the corpus (decision D11). Planted faults, each refused with its own reason by the reference and by the second implementations: a policy with an unmappable audience (scte224_audience_unmappable); a creative signal for a break that does not exist (creative_break_unknown); 31 more documents, 15 more room cases, 13 stitch cases and 23 record cases. Corpora: wai/live-conformance/audience-scte224/ (audience_scte224_verify.py) and wai/live-conformance/ad-signalling/ (ad_signalling_verify.py); wai conform --independent runs both. Each second implementation prints the checks of its own that the corpus holds no case for (39 and 14). Co-Authored-By: Claude <[email protected]>
880 lines
54 KiB
Markdown
880 lines
54 KiB
Markdown
# WAI Extension: Ad Presentation
|
||
|
||
> Status: Draft. Registers the record `wai.live.ad_presentation`: a player's signed claim that
|
||
> it presented one impression of a [live-ads](live-ads.md) break, with what it measured while
|
||
> it did. The claim, `WPC1`, names the impression by the break, the session and the slot of a
|
||
> `wai.video.ssai` receipt of the break form; it lists the creative's media objects the player
|
||
> presented, by content hash, and spans of the creative's media time with the fraction of
|
||
> pixels in view and whether the ad was audible; and it may carry a device attestation, a
|
||
> Privacy Pass token (RFC 9577) whose challenge binds it to this one impression. It is signed
|
||
> by the playback session's own key, from which the session the receipt names is derived. An
|
||
> ad policy of version 2 can require a claim for each impression, a viewability rule, an
|
||
> audible run and an attestation; then a receipt's spend counts only the impressions whose
|
||
> claim verifies, and the room verifier holds that spend to the decider grant's ceiling. No
|
||
> new transport. The reference implementation is the `presentation` module of `wai-rs`'s
|
||
> `live_ads` (feature `live_ads`); the corpus is
|
||
> [`live-conformance/presentation/`](../live-conformance/presentation/), and its second
|
||
> implementation is `live-conformance/ad_presentation_verify.py`.
|
||
> A verifier also applies a set of **invalid-traffic rules** (§14) to the impressions it states:
|
||
> six rules, each decided from signed records by comparing bytes and integers, each refusal
|
||
> coded, counted and stated with the records it rests on, and a **valid spend** that leaves out
|
||
> what they refuse. Their module is `traffic` of the same `live_ads`, their corpus
|
||
> [`live-conformance/ad-traffic/`](../live-conformance/ad-traffic/) and its second
|
||
> implementation `live-conformance/ad_traffic_verify.py`.
|
||
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
|
||
>
|
||
> Texts read for this revision, each at its source:
|
||
>
|
||
> - *Viewable Ad Impression Measurement Guidelines (Desktop)*, Version 2.0, 18 August 2015, in
|
||
> full. Its video requirements are word for word those of its Version 1.0, 30 June 2014. It
|
||
> numbers no section, so it is cited by heading and printed page.
|
||
> - *Mobile Viewable Ad Impression Measurement Guidelines*, 28 June 2016: its video section
|
||
> only (pp. 10–11).
|
||
> - *Cross-Media Audience Measurement Standards (Phase I Video)*, September 2019: its executive
|
||
> summary and §2.1.1, §2.1.5, §2.2.1, §2.3.1 and §4.1.1, which hold all of its viewability,
|
||
> duration and audio text. The rest of it was not read.
|
||
> - *Server-Side Ad Insertion and OTT Guidance*, August 2021, in full.
|
||
> - *Digital Video Impression Measurement Guidelines*, Version 1.1, June 2018: §3.0, §4.1, §4.4
|
||
> and §4.5.
|
||
> - *Video Ad Serving Template (VAST)* 4.3, December 2022: §1.1 to §1.2.1, §2.3.4, §2.4 to
|
||
> §2.4.3, §3.4.4, §3.5 to §3.5.3, §3.14 to §3.14.2, §3.16 to §3.17.5 and §6. Its other
|
||
> sections were not read.
|
||
> - *Secure Interactive Media Interface Definition (SIMID)* 1.2: §1 to §3.5.1, §4.2.10,
|
||
> §4.2.11, §4.4.5, §4.4.7, §8.1 and §8.4.
|
||
> - *Open Measurement SDK – OMID API*, Version 1.5, June 2024: its sections on the impression,
|
||
> the media events, `volumeChange`, `playerStateChange`, `geometryChange`, the ad view and its
|
||
> reasons.
|
||
> - *Open Measurement Device Attestation Implementation Guidance*, Version 1.0, November 2025,
|
||
> in full. Not read: the native SDKs that implement it, and any text of the exchange between
|
||
> a player and a verifier's URL; no public text of that exchange was found.
|
||
> - RFC 9576, RFC 9577 and RFC 9578 (June 2024), in full, and RFC 9474 §5. RFC 8017's
|
||
> RSASSA-PSS is used as RFC 9578 §6.4 cites it, and this revision's implementation of it is
|
||
> checked against the token of RFC 9578 Appendix A.2's first test vector.
|
||
> - For §14: *Invalid Traffic Detection and Filtration Standards Addendum*, June 2020 update
|
||
> (final): §1 to §2.4.1 and §4.2 to §4.2.2. Its other sections were not read. RFC 4291
|
||
> §2.5.5.2 for the IPv4-mapped form of an address, and RFC 9110 §10.1.5 for the `User-Agent`
|
||
> field.
|
||
>
|
||
> A section number of one of these texts is that revision's.
|
||
|
||
## 1. Scope
|
||
|
||
In scope: the session key and the session it names (§3); the creative digest (§4); the
|
||
presentation claim, its spans, its signature and its rules (§5); the viewability rules, the
|
||
qualifying run and the quartile a claim reaches (§6); the device attestation a claim carries
|
||
and how it is bound and verified (§7); the presentation fields of an ad policy (§8); each
|
||
claim's verdict, each impression's count and the spend that counts (§9); what a player
|
||
measures and how (§10); and the invalid-traffic rules, their records and the valid spend
|
||
(§14).
|
||
|
||
Out of scope: a transport, and how a claim reaches the decider or a buyer; how a player
|
||
obtains a token from an attester and its issuer (the issuance protocols of RFC 9578, run by a
|
||
platform); the detection of invalid traffic by any means but the rules of §14, which stays
|
||
statistical and the measurer's (§13, §14.1); attention, and anything a sensor of the viewer would measure; and the stitched form of
|
||
`wai.video.ssai`, whose receipts name no session of a room.
|
||
|
||
Relationship to the core spec: `wai.live.ad_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 [live-ads](live-ads.md): this extension adds the presentation fields of an ad
|
||
policy of version 2 (its §3.1, here §8), the session a session key names (its §2, here §3),
|
||
and the spend its §9.3 step 12 counts under a policy that requires claims (here §9.3). Every
|
||
other rule of live-ads stands, and a policy of version 1 is verified as before.
|
||
|
||
Relationship to [jwp-receipts](jwp-receipts.md): a `reconstruct-session` claim (its §2.4.12)
|
||
is a sink's record of a title it reconstructed, signed by its decoder host's key, and "not
|
||
evidence that a person watched". A presentation claim is a player's record of one ad
|
||
impression, made for the billing rule of §9; it states what the player measured and is no more
|
||
evidence that a person watched (§13).
|
||
|
||
## 2. Terms
|
||
|
||
- **Room**, **break**, **decider**, **pod**, **fill**, **asset list**, **session**, **policy**:
|
||
as in live-ads §2.
|
||
- **Receipt**: a `wai.video.ssai` receipt of the break form (live-ads §8).
|
||
- **Impression**: one filled slot of a receipt: its `slot`, `ad_id`, `creative_hash`,
|
||
`duration_ms`, `price_micros` and `quartiles_reached` (live-ads §8.2).
|
||
- **Player**: the software that presents the ad to a viewer and makes the claim (§10).
|
||
- **Session key**: an Ed25519 key a player makes for one playback session of one room (§3).
|
||
- **Creative**: an ad's media, as an ordered list of media objects (§4).
|
||
- **Span**: a stretch of the creative's media time the player presented at normal speed, over
|
||
which its observations held (§5.2).
|
||
- **Claim**: a presentation claim (§5).
|
||
- **Token**, **TokenChallenge**, **issuer**, **attester**: as in RFC 9576 §2 and RFC 9577 §2.
|
||
- **Basis point**: one ten-thousandth; a fraction of pixels is written in basis points, 0 to
|
||
10 000.
|
||
|
||
Integers of the records here are little-endian unless this text says big-endian, as live-ads
|
||
§2 has them; 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 *long* field is a u16 length and that
|
||
many bytes.
|
||
|
||
## 3. The session key
|
||
|
||
Under a policy that requires claims (§8), a player **MUST** make a fresh Ed25519 key for each
|
||
playback session of a room, from its platform's random generator, and **MUST NOT** use it for
|
||
another session or another room, or derive it from an identifier of the viewer, the device or
|
||
an account. It presents the key's public part with each asset-list request of the session
|
||
(live-ads §7), by a means this revision does not fix.
|
||
|
||
The **session** a session key names in a room is the first 16 bytes of SHA-256 over
|
||
`wai:ad-presentation-session` (ASCII) and 0x01, the room id (32 bytes) and the key (32 bytes).
|
||
Under such a policy the decider **MUST** give each receipt of the session that session
|
||
(live-ads §8.2 `session`), and a claim is counted only when its key names its session (§9.2
|
||
step 4). A player computes the session itself; the decider never chooses it.
|
||
|
||
The session is a function of a key made for this room alone, so it links nothing across
|
||
rooms; live-ads §12's rule that a session not be derived from a viewer's key is met, since the
|
||
session key is not one.
|
||
|
||
## 4. The creative digest
|
||
|
||
A creative is its media objects in presentation order: each object's **content hash**, SHA-256
|
||
of the object's bytes as served, and its presentation duration in milliseconds (a u32, at
|
||
least 1). The object at index *j* holds the creative's media time from the sum of the
|
||
durations before it, *s*, to *s* + its duration.
|
||
|
||
The **creative digest** is SHA-256 over `wai:ad-creative` (ASCII) and 0x01, the objects' count
|
||
(u32), and for each object in order its content hash and its duration (u32).
|
||
|
||
Under a policy that requires claims, an ad's `creative_hash` in the pod, in the receipt and in
|
||
the asset list's URIs (live-ads §7.2, §7.3) is its creative digest, and its `duration_ms` the
|
||
sum of its objects' durations. A creative's URI then names the very bytes a player presents,
|
||
object by object.
|
||
|
||
## 5. The presentation claim
|
||
|
||
### 5.1 Layout
|
||
|
||
| offset | size | field | meaning |
|
||
|---|---|---|---|
|
||
| 0 | 4 | `magic` | `WPC1` (ASCII) |
|
||
| 4 | 1 | `version` | 1 in this revision |
|
||
| 5 | 32 | `room` | the room id |
|
||
| 37 | 32 | `cue` | the break's digest (live-ads §4.4) |
|
||
| 69 | 16 | `session` | the session (§3) |
|
||
| 85 | 1 | `slot` | the impression's slot, 0 to 15 |
|
||
| 86 | 32 | `creative_hash` | the creative presented: its creative digest (§4) |
|
||
| 118 | 32 | `player` | the session key (§3), which signs the claim |
|
||
| 150 | 1 | `geometry` | what the visible fractions are measured over: 1 **ad**, the ad's own pixels, measured; 2 **screen**, a full-screen output to a television, whose pixels are assumed in view; 3 **player**, the player's area, measured, where the ad's own pixels are not known |
|
||
| 151 | 4 | `frames` | u32: the frames rendered over the spans, where the platform exposes a count; 4 294 967 295 where it does not |
|
||
| 155 | 2 | `n` | u16: the presented objects' count |
|
||
| 157 | 32 × `n` | `objects` | the content hashes (§4) of the creative's objects the spans touch, in order (§9.2 step 6) |
|
||
| … | 2 | `m` | u16: the spans' count |
|
||
| … | 11 × `m` | `spans` | §5.2 |
|
||
| … | long | `attestation` | a token (§7.1), or no bytes |
|
||
| … | 64 | `sig` | §5.3 |
|
||
|
||
### 5.2 Spans
|
||
|
||
A span is 11 bytes:
|
||
|
||
| offset | size | field | meaning |
|
||
|---|---|---|---|
|
||
| 0 | 4 | `start_ms` | u32: where it starts, in the creative's media time |
|
||
| 4 | 4 | `length_ms` | u32: its length, at least 1 |
|
||
| 8 | 2 | `visible_bp` | u16: the least fraction of the pixels in view the player observed over it, in basis points (§10) |
|
||
| 10 | 1 | `flags` | bit 0 **audible**: the ad was audible throughout it; bit 1 **resumed**: playback stopped between the span before it and this one |
|
||
|
||
A span's **end** is `start_ms` + `length_ms`, computed without overflow. The spans are in
|
||
presentation order, and no media time is in two of them: a player that presents a stretch
|
||
again states it once. A span whose start is after the end of the span before it follows a
|
||
skip, and is a resumption.
|
||
|
||
### 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:ad-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
|
||
`claim_malformed` when:
|
||
|
||
1. its magic is not `WPC1` or its version not 1;
|
||
2. the bytes end before a field the layout requires, or there are bytes after `sig`;
|
||
3. `slot` is above 15, or `geometry` is not 1, 2 or 3;
|
||
4. `n` is 0 and `m` is not, or `m` is 0 and `n` is not;
|
||
5. a span's `length_ms` is 0, its `visible_bp` above 10 000 or its `flags` above 3;
|
||
6. the first span has **resumed** set; or a later span starts before the end of the span
|
||
before it, or after that end without **resumed** set.
|
||
|
||
## 6. Viewability and the quartile
|
||
|
||
### 6.1 The rules
|
||
|
||
A policy names one of two viewability rules (§8). Both require two continuous seconds; they
|
||
differ in the pixels.
|
||
|
||
| `viewability` | rule | a span qualifies when | admits `geometry` | text |
|
||
|---|---|---|---|---|
|
||
| 1 | **video** | `visible_bp` is at least 5 000 | 1, 3 | *Viewable Ad Impression Measurement Guidelines (Desktop)* 2.0, "Requirements for Viewable Video Advertising Impressions" (p. 8); the 2016 mobile guidelines' video section states the same time |
|
||
| 2 | **cross-media** | `visible_bp` is 10 000 | 1, 2, 3 | *Cross-Media Audience Measurement Standards (Phase I Video)* §2.2.1 and §4.1.1 |
|
||
|
||
and, under a policy whose `presentation` sets **audible**, the span is audible too. A span of
|
||
a `geometry` the rule does not admit never qualifies.
|
||
|
||
- The video rule is the guidelines' "50% of the ad's pixels" and "2 continuous seconds of the
|
||
video advertisement is played", where "This required time is not necessarily the first 2
|
||
seconds of the video ad; any unduplicated content of the ad comprising 2 continuous seconds
|
||
qualifies". The same guidelines (footnote 2 of p. 8) ask that a measurement over the
|
||
player's pixels rather than the ad's be disclosed: geometry 3 is that disclosure, and the
|
||
verifier states it (§9.4).
|
||
- The cross-media rule is "a viewability qualification threshold of 100% of pixels on screen
|
||
for at least two continuous seconds". Of television it says: "Where ads and content are
|
||
displayed to a TV (linear or OTT), it may be assumed that the 100% pixel criteria for
|
||
viewability is met without direct measurement, although time threshold requirements still
|
||
apply (2 continuous seconds for video)". Geometry 2 is that assumption, and the video rule
|
||
does not admit it.
|
||
- No text read for this revision defines a viewability rule for connected television of its
|
||
own. The *Server-Side Ad Insertion and OTT Guidance* (2021) §2 holds such impressions "To
|
||
the extent it can be measured" to the rules of online digital video; the cross-media rule
|
||
above is the one that speaks of a television's pixels. A room's authority picks the rule its
|
||
policy names.
|
||
- Audio: the video rule does not require it ("detection of audio is not currently a
|
||
requirement for a viewable video ad impression", p. 8); the cross-media standard requires
|
||
"the presence of audio (non-zero volume or non-mute conditions) to be measured" where
|
||
feasible (§2.1.5). A policy that wants an audible run says so (§8).
|
||
|
||
### 6.2 The qualifying run
|
||
|
||
Over a claim's spans in order, a span **continues** the one before it when it starts at that
|
||
span's end and does not have **resumed** set. A **run** is a sequence of qualifying spans each
|
||
of which, after the first, continues the one before it; its length is the sum of their
|
||
`length_ms`. A claim's **run** is the length of its longest run, and its **viewable time** the
|
||
sum of `length_ms` over its qualifying spans. A claim is **viewable** when its run is at least
|
||
2 000 ms.
|
||
|
||
A pause, a stall or a skip between two spans ends a run, so two stretches of 1.5 s in view are
|
||
not two continuous seconds. That spans state no media time twice is how a run holds only
|
||
"unduplicated content".
|
||
|
||
### 6.3 The quartile
|
||
|
||
The **coverage** of a claim's spans is 0 when the first does not start at 0; otherwise it is
|
||
the end of the longest prefix of the spans in which each starts at the end of the one before
|
||
(a pause does not end it, a skip does). With *D* the impression's `duration_ms` and *C* the
|
||
coverage, the **quartile** a claim reaches is:
|
||
|
||
- 0 when it has no span;
|
||
- otherwise 5 when *C* ≥ *D*; 4 when 4*C* ≥ 3*D*; 3 when 4*C* ≥ 2*D*; 2 when 4*C* ≥ *D*; and
|
||
1 otherwise.
|
||
|
||
These are `quartiles_reached`'s values (live-ads §8.2): 1 started, then a quarter, a half,
|
||
three quarters and all of it, at the marks VAST 4.3 §3.14.1 names for `firstQuartile`,
|
||
`midpoint`, `thirdQuartile` and `complete` ("The creative played continuously for at least 25%
|
||
of the total duration at normal speed", and so on). A claim with a span has begun to play,
|
||
which is when an impression is counted (*Digital Video Impression Measurement Guidelines* 1.1
|
||
§4.1: "begins to play").
|
||
|
||
## 7. Device attestation
|
||
|
||
### 7.1 The token
|
||
|
||
A claim's `attestation` is a token of RFC 9577 §2.2.1:
|
||
|
||
```
|
||
struct {
|
||
uint16_t token_type;
|
||
uint8_t nonce[32];
|
||
uint8_t challenge_digest[32];
|
||
uint8_t token_key_id[Nid];
|
||
uint8_t authenticator[Nk];
|
||
} Token;
|
||
```
|
||
|
||
in its TLS presentation, big-endian. A policy names one of RFC 9578's two token types, each of
|
||
`Nid` 32: 0x0001, VOPRF(P-384, SHA-384), privately verifiable, `Nk` 48 (§5, §8.2.1 there);
|
||
and 0x0002, Blind RSA (2048-bit), publicly verifiable, `Nk` 256 (§6, §8.2.2 there). A token
|
||
**reads** when it is exactly 98 + `Nk` bytes for the type its first two bytes name, and that
|
||
type is one of the two.
|
||
|
||
A token attests what its issuer's attester checked of the device that asked for it (RFC 9576
|
||
§3): that a device of a kind the attester vouches for asked. It attests nothing of the
|
||
rendering or of what was in view.
|
||
|
||
### 7.2 The challenge
|
||
|
||
The TokenChallenge a claim's token answers is RFC 9577 §2.1.1's:
|
||
|
||
```
|
||
struct {
|
||
uint16_t token_type;
|
||
opaque issuer_name<1..2^16-1>;
|
||
opaque redemption_context<0..32>;
|
||
opaque origin_info<0..2^16-1>;
|
||
} TokenChallenge;
|
||
```
|
||
|
||
with `token_type` and `issuer_name` the policy's `token_type` and `issuer` (§8),
|
||
`redemption_context` the 32-byte **redemption context** below, and `origin_info` empty: in its
|
||
TLS presentation, the type as a big-endian u16, the issuer's length as a big-endian u16 and its
|
||
bytes, the byte 32 and the context, and two zero bytes. The **redemption context** is SHA-256
|
||
over `wai:ad-presentation-attest` (ASCII) and 0x01, `room`, `cue`, `session`, `slot` (one
|
||
byte), `creative_hash` and `player`, the claim's own.
|
||
|
||
The challenge is a function of the impression and the session key alone, each known before the
|
||
ad plays, so a party that issues it as a Privacy Pass origin (RFC 9577 §2.1) issues these
|
||
bytes, and every verifier recomputes them. A token answers one impression of one session key
|
||
and no other: lifted onto another claim, its `challenge_digest` is not that claim's. This is
|
||
the *Open Measurement Device Attestation Implementation Guidance*'s "Against one impression ID,
|
||
only one attestation request may be counted" (§5.1), made a property of the token. A
|
||
TokenChallenge with no `origin_info` scopes the token to no origin (RFC 9577 §2.1.1.3), which
|
||
the redemption context makes unnecessary.
|
||
|
||
### 7.3 The issuer's verifier
|
||
|
||
A token is verified by its issuer's verifier. A verifier holds, for each issuer it trusts, the
|
||
issuer's name, its token type and its keys, from its issuer directory (RFC 9578 §4); a key's
|
||
`token_key_id` is SHA-256 of its encoding (for type 0x0002, the DER SubjectPublicKeyInfo of
|
||
§6.5 there; for 0x0001, `SerializeElement(pkI)` of §5.5).
|
||
|
||
- **Type 0x0002** is publicly verifiable. The token verifies when the verifier holds a key of
|
||
the issuer whose `token_key_id` is the token's, the key is a SubjectPublicKeyInfo of the
|
||
object identifier id-RSASSA-PSS (1.2.840.113549.1.1.10) with a 2048-bit modulus, and
|
||
RSASSA-PSS-VERIFY (RFC 8017 §8.1.2), with SHA-384, MGF1 with SHA-384 and a 48-byte salt
|
||
(RFC 9578 §6.4), accepts the token's `authenticator` as a signature over its first 98 bytes
|
||
(`token_type`, `nonce`, `challenge_digest`, `token_key_id`). RFC 9578 §8.2.2 also lists a
|
||
variant of zero-length salt; §6.4 states 48 bytes, and this revision verifies with 48, so a
|
||
token of the other variant does not verify here.
|
||
- **Type 0x0001** is privately verifiable: only its issuer, holding the private key, computes
|
||
the authenticator (RFC 9578 §5.4). A verifier of a policy of this type asks its issuer, by a
|
||
means this revision does not fix, and a verifier that cannot does not count the token. The
|
||
reference implementation verifies type 0x0002 only.
|
||
|
||
The verifier requires, too, that the token's `challenge_digest` is SHA-256 of the
|
||
TokenChallenge of §7.2: what WAI checks itself. Double spending (RFC 9577 §2.2.2) needs no
|
||
nonce ledger here: a token answers one impression, and an impression counts once (§9.2 step 5).
|
||
|
||
## 8. The policy's presentation fields
|
||
|
||
An ad policy of version 2 (live-ads §3.1) carries, after `creative_base` and before `sig`:
|
||
|
||
| size | field | meaning |
|
||
|---|---|---|
|
||
| 1 | `presentation` | bit 0 **claim**: an impression counts only when its claim verifies; bit 1 **attest**: the claim carries a token that verifies (§7); bit 2 **audible**: a qualifying span is audible too (§6.1) |
|
||
| 1 | `viewability` | 0 none; 1 the video rule; 2 the cross-media rule (§6.1) |
|
||
| 2 | `token_type` | u16: the token type of §7.1 when **attest** is set, otherwise 0 |
|
||
| long | `issuer` | the issuer's name, the TokenChallenge's `issuer_name` (§7.2), when **attest** is set; otherwise no bytes |
|
||
|
||
A policy of version 2 is refused `ad_policy_malformed` (live-ads §3.3) when `presentation` sets
|
||
a bit above bit 2; sets **attest** or **audible** without **claim**; has `viewability` above 2,
|
||
or 0 with **claim** set, or not 0 with **claim** clear; or, with **attest** set, has a
|
||
`token_type` other than 1 or 2 or an empty `issuer`, or with it clear, a `token_type` other
|
||
than 0 or an `issuer` that is not empty. A policy of version 2 with `presentation` 0 is verified
|
||
as a policy of version 1.
|
||
|
||
A policy of version 3 (live-ads §3.1) carries the same four fields, under the same rules, after
|
||
`creative_base` and before its `audience`; with `presentation` 0 it requires no claim.
|
||
|
||
## 9. Verification
|
||
|
||
### 9.1 Inputs
|
||
|
||
Under a policy that requires claims, an ads verifier (live-ads §9.1) is given, too:
|
||
|
||
- **claims**: the claims, a list of byte strings, in an order;
|
||
- **creatives**: the creatives it holds (§4), each its objects;
|
||
- **issuers**: the issuers it trusts (§7.3), each a name, a token type and its keys.
|
||
|
||
Under a policy that does not, it reads none of them.
|
||
|
||
### 9.2 Each claim
|
||
|
||
The claims are read once, before the receipts (steps 1 and 2), and the rest of each claim's
|
||
steps run when the verifier reaches the receipt whose impression it names, after that
|
||
receipt's live-ads §9.3 steps 1 to 11. Each claim gets one verdict, **presented** or the
|
||
reason of the first step it fails:
|
||
|
||
1. It reads (§5.4): `claim_malformed`.
|
||
2. Its signature verifies under `player` (§5.3): `bad_key`, `bad_signature`.
|
||
3. Its `room`, `cue` and `session` are a receipt's, among the receipts that passed steps 1 to
|
||
11, and that receipt has an impression of its `slot`: `claim_no_decision`. A claim that no
|
||
such receipt names when the verifier has read every receipt gets this verdict then.
|
||
4. Its `player` names its `session` (§3): `claim_key_unbound`.
|
||
5. No claim before it in the list passed steps 1 to 4 for the same impression:
|
||
`claim_duplicate`. The first such claim is the impression's **own claim**, whatever its
|
||
verdict; steps 6 to 9 are for it alone.
|
||
6. **The asset.** Its `creative_hash` is the impression's; the verifier holds a creative whose
|
||
creative digest that is (§4), and the sum of that creative's durations is the impression's
|
||
`duration_ms`; every span's end is at most that `duration_ms`; and `objects` is exactly the
|
||
content hashes of the creative's objects that the spans touch, in the creative's order,
|
||
where a span touches the object of media time *s* to *e* when it starts before *e* and ends
|
||
after *s*: `claim_asset_mismatch`.
|
||
7. **The quartile.** The quartile it reaches (§6.3), with *D* the impression's `duration_ms`,
|
||
is the impression's `quartiles_reached`: `claim_quartile_mismatch`.
|
||
8. **Viewability.** It is viewable (§6.2) under the policy's rule: `claim_not_viewable`.
|
||
9. **Attestation**, when the policy sets **attest**: `attestation` is not empty
|
||
(`attestation_absent`); it reads as a token (§7.1) of the policy's `token_type`, its
|
||
`challenge_digest` is SHA-256 of the TokenChallenge of §7.2, and its issuer's verifier
|
||
accepts it under the policy's `issuer` (§7.3): `attestation_invalid`.
|
||
|
||
An impression **counts** when its own claim is presented. An impression with no own claim is
|
||
stated `claim_absent`; one whose own claim was refused is stated with that claim's reason.
|
||
|
||
### 9.3 The spend that counts
|
||
|
||
Under a policy that requires claims, a receipt's **presented spend** is the sum of
|
||
`price_micros` over its impressions that count, and live-ads §9.3 step 12 holds the presented
|
||
spend of this receipt and every receipt before it, together, to the decider grant's ceiling,
|
||
in place of their billable spend. A receipt's own rules (live-ads §8.5) are unchanged: its
|
||
`budget_spent_micros` is still the decider's billable spend, signed, and the verifier states
|
||
both.
|
||
|
||
So a stripped, redirected or spoofed beacon counts nothing: an impression counts when a claim
|
||
the session key signed is presented, and no other party holds that key. The decider's
|
||
`quartiles_reached` must be the quartile the player's own claim reaches (step 7), so buyer and
|
||
seller reconcile one figure from the same signed bytes.
|
||
|
||
### 9.4 What a verification states
|
||
|
||
With accept, beside live-ads §9.4, the verifier states:
|
||
|
||
- for each receipt: its presented spend, and each impression's slot, the index of its own
|
||
claim among the claims given or none, and whether it counts or the reason it does not;
|
||
- for each claim, in the order given: its digest when it reads; its verdict; from step 3 on,
|
||
the receipt and the slot it names; and for an impression's own claim, the quartile it
|
||
reaches, its run and its viewable time;
|
||
- the spend of every receipt together, which under such a policy is their presented spend.
|
||
|
||
The claims' facts state each claim's `geometry` through its bytes; a buyer that weighs a
|
||
measurement over the player's area (geometry 3) or a television assumption (geometry 2) reads
|
||
it there.
|
||
|
||
## 10. The player
|
||
|
||
A player that makes claims **MUST**:
|
||
|
||
- observe the ad at least every 200 ms while it plays, the minimum polling of the video
|
||
guidelines ("for viewable video impression processes, 200 milliseconds", p. 9), and begin a
|
||
new span whenever its quantised visible fraction, its audible state or the continuity of
|
||
playback changes;
|
||
- state as a span's `visible_bp` the least fraction it observed over the span, of the ad's
|
||
rendered picture (geometry 1), of its own area (geometry 3), or 10 000 on a full-screen
|
||
television output (geometry 2), less any part it knows its own overlays cover;
|
||
- state a fraction of 0 while the page or application is hidden or in the background, while
|
||
its window does not have focus, or while the display is locked: the guidelines' "in-focus
|
||
browser tab";
|
||
- set **audible** only for a span throughout which the ad's audio played at a volume above
|
||
zero, unmuted, at the player and, where the platform exposes it, at the device;
|
||
- set **resumed** on a span that follows a pause, a stall, a seek or a stretch hidden from
|
||
view; and state no media time twice;
|
||
- list as `objects` the content hashes of the objects it decoded for the spans, which it
|
||
computes over each object's bytes as it received them;
|
||
- state `frames` as the frames presented and not dropped over the spans, where the platform
|
||
exposes such a count.
|
||
|
||
A player that measures through an Open Measurement integration reads the same facts from it:
|
||
the ad view's `percentageInView` (0 to 100, so `visible_bp` is 100 times it) and its
|
||
`reasons`, the `mediaPlayerVolume` and `deviceVolume` of `volumeChange`, and the media events
|
||
(OMID API 1.5). An interactive layer of SIMID 1.2 does not measure: "SIMID should not be set up
|
||
to measure viewability" (§3.5).
|
||
|
||
**The reference producer** is `web/ads/presentation.js` of the browser client `joule-moq-web`:
|
||
a recorder that turns observations (a visible fraction from an `IntersectionObserver` over the
|
||
video element, the page's visibility and focus, the element's volume and muted state, its
|
||
current time, and its playback-quality frame counts) into spans by the rules above, and writes
|
||
and signs the claim with the platform's Ed25519. Its tests drive it with observation sequences
|
||
and require the claims of the corpus, byte for byte. An `IntersectionObserver` measures the
|
||
element's box against the viewport, not what other elements obstruct; a page that can obstruct
|
||
its player states geometry 3 or measures obstruction itself.
|
||
|
||
## 11. Reasons
|
||
|
||
The codes this extension adds, each with where it is raised:
|
||
|
||
| reason | raised at |
|
||
|---|---|
|
||
| `claim_malformed` | §5.4, §9.2 step 1 |
|
||
| `claim_no_decision` | §9.2 step 3 |
|
||
| `claim_key_unbound` | §9.2 step 4 |
|
||
| `claim_duplicate` | §9.2 step 5 |
|
||
| `claim_asset_mismatch` | §9.2 step 6 |
|
||
| `claim_quartile_mismatch` | §9.2 step 7 |
|
||
| `claim_not_viewable` | §9.2 step 8 |
|
||
| `attestation_absent` | §9.2 step 9 |
|
||
| `attestation_invalid` | §9.2 step 9 |
|
||
| `claim_absent` | §9.2, an impression with no own claim |
|
||
|
||
A claim also gets live-events' `bad_key` and `bad_signature` (§9.2 step 2), and a policy
|
||
live-ads' `ad_policy_malformed` (§8). These verdicts never refuse a room: a room whose
|
||
receipts verify is accepted, and its claims' verdicts decide what counts. A room is refused
|
||
only as live-ads §9 refuses it, `funds_exceeded` among them when the presented spend passes
|
||
the ceiling.
|
||
|
||
## 12. Conformance
|
||
|
||
[`live-conformance/presentation/`](../live-conformance/presentation/) holds the corpus, written
|
||
from fixed seeds by `wai_ad_presentation_vectors gen`: `vectors.json`, and each accepted room's
|
||
HLS tags as a file of its own, as live-ads §11 writes them. `vectors.json` holds the creatives
|
||
and the issuer the verifier holds; the public keys of the session keys; the first test vector of
|
||
RFC 9578 Appendix A.2 (its `pkI`, TokenChallenge and token), whose issuer key signs the corpus's
|
||
tokens; and:
|
||
|
||
- **Rooms**, each a room, its policy of version 2, its receipts with their pods and its claims,
|
||
and the outcome with everything §9.4 and live-ads §9.4 state:
|
||
- a mid-roll under the video rule: impressions in view throughout, one partly out of view
|
||
with three continuous seconds at more than half its pixels, and one measured over the
|
||
player's area; and a claim for each control: too few pixels, two runs broken by a pause,
|
||
a duplicate, a key that does not name the session, a presented object that is not the
|
||
creative's, a quartile the receipt misstates, a flipped signature (whose impression is
|
||
then `claim_absent`), a signed claim whose spans skip with no resumption stated, a session
|
||
with no receipt, a truncated claim, and a television assumption the video rule does not
|
||
admit;
|
||
- a live break under the cross-media rule with an audible run and attestation: attested
|
||
impressions on television outputs and screens, and a claim with no token, one carrying
|
||
another impression's token, one whose authenticator is altered, one muted and one with 90%
|
||
of its pixels in view;
|
||
- two receipts that together pass the decider grant's ceiling, one with a claim and one
|
||
without: accepted, the presented spend within it; and the same with both claims, refused
|
||
`funds_exceeded`;
|
||
- a policy requiring an attestation without a claim, refused `ad_policy_malformed`.
|
||
|
||
A conforming verifier verifies RFC 9578's own token under its key, and gives every room the
|
||
outcome, the reason, the place and everything else the corpus states, each claim its verdict
|
||
and facts, and every tag file byte for byte. `wai_ad_presentation_vectors verify` and the test
|
||
`tests/ad_presentation.rs` check that every file regenerates byte for byte, that `wai-rs` gives
|
||
every case its outcome, that every reason of §11 appears, and that §11 lists exactly the
|
||
reference's codes. `live-conformance/ad_presentation_verify.py` is the second implementation,
|
||
written from this text: it shares no code with `wai-rs`; it verifies each room, policy and
|
||
receipt with the live-ads corpus's second implementation (`live_ads_verify.py`), each Ed25519
|
||
signature with WAI's strict rule (`claim-conformance/_ed25519.py`), and RSASSA-PSS with an
|
||
implementation of its own over the standard library; and it mutates copies of the corpus and
|
||
requires every copy to fail. The reference producer's tests (§10) require the corpus's claims
|
||
from observation sequences.
|
||
|
||
## 13. Security and privacy considerations
|
||
|
||
- **What a claim proves without attestation.** A claim proves that the holder of the session
|
||
key the receipt's session is derived from stated these facts about this impression. It does
|
||
not prove that they are true. A modified player can state anything; so can a decider, or
|
||
anyone, that makes a session key of its own, asks for asset lists under it and signs claims
|
||
for the sessions it names. Without attestation the billing rule removes the third party who
|
||
strips, redirects or forges beacons on the way, since such a party holds no session key; it
|
||
does not remove a dishonest player or decider.
|
||
- **What attestation bounds.** A token is issued only to a device its attester vouched for,
|
||
answering a challenge of this impression and this session key (§7.2). A party that makes
|
||
claims of its own then needs a vouched device to ask for each token, impression by
|
||
impression; one token cannot serve two claims. The bound is the attester's: a genuine device
|
||
running modified or malicious software, or a farm of genuine devices, attests truthfully and
|
||
still signs what its software tells it to. Attestation makes such fraud cost a device per
|
||
token and makes every claim attributable to a session key; it does not make it impossible.
|
||
- **Invalid traffic.** §14's rules refuse what signed records decide, and nothing else. The
|
||
detection of every other kind of general and sophisticated invalid traffic stays statistical
|
||
and the measurer's (§14.1). Claims give it signed, non-repudiable inputs: per-impression
|
||
facts bound to a session key, and, with attestation, to a vouched device.
|
||
- **What no claim states.** That a person watched; that a television was on (the 2021 guidance
|
||
does not require "TV Off" detection, §2); that nothing outside the player covered it, beyond
|
||
what its geometry discloses (§10).
|
||
- **Privacy.** The session key is made for one session of one room (§3), and the session is
|
||
derived from it and the room, so claims link nothing across rooms or to a viewer, device or
|
||
account. A token is unlinkable to its issuance (RFC 9576 §3.3), and its redemption context is
|
||
a digest of the impression. A claim carries no identifier of the device; the *Device
|
||
Attestation Implementation Guidance* likewise states that "Device-level information such as
|
||
Device ID will not be available" (§4.1.3).
|
||
- **Withholding.** A party between the player and the decider can withhold a claim, and the
|
||
impression then does not count: the failure is the seller's lost spend, never the buyer's
|
||
overpayment.
|
||
- **Keys.** A session key is held by the player for one session and then discarded; a claim
|
||
whose key does not derive its session is never counted (§9.2 step 4), whoever signed it.
|
||
|
||
## 14. Invalid traffic rules
|
||
|
||
### 14.1 What a rule is
|
||
|
||
A **rule** of this section refuses an impression as invalid traffic. Every rule is decided from
|
||
signed records alone, by comparing bytes and integers: the claims a session key signed, a
|
||
**session origin** an edge signed (§14.3), a **device binding** a device key and the session
|
||
key signed (§14.4), and the **traffic policy** the room's authority signed (§14.2), which pins
|
||
two lists by digest (§14.5). No rule computes a score, a rate, a model's output or a
|
||
threshold other than the policy's own session cap, so the same records give the same verdict
|
||
in every implementation.
|
||
|
||
The rules judge the impressions §9 states and change nothing §9 stated: every claim keeps
|
||
its verdict, every impression its count, every receipt its presented spend, and no record's
|
||
bytes change. An impression a rule refuses is **excluded** and counted as excluded; it is
|
||
never dropped from a statement (§14.7).
|
||
|
||
What signed records do not decide is not a rule here. The addendum's general invalid traffic
|
||
also covers traffic that does not come from a known browser type, data-center traffic
|
||
"determined to be a consistent source of invalid traffic", and activity-based removal by
|
||
speed, interval, repetition and outliers (§1.1.2, §4.2 there); and all of its sophisticated
|
||
invalid traffic is found by "advanced analytics, multi-point corroboration/coordination,
|
||
significant human intervention". None of these is a comparison of signed bytes, and this
|
||
section approximates none of them:
|
||
|
||
- The address rule (§14.6) decides that a signed address lies in a range of a pinned list.
|
||
Whether traffic from that range is invalid is the judgment of whoever published the list
|
||
and of the authority that pinned it; the addendum itself requires that "proxy and data
|
||
center traffic must be known to be invalid in order to be filtered" (§4.2.2).
|
||
- The crawler rule decides that a signed user agent holds a token of a pinned list of
|
||
crawlers that declare themselves. A crawler that sends another user agent is not refused
|
||
by it. The addendum requires exception strings beside robotic indicators (§4.2); a list
|
||
of this revision has none, so its tokens are chosen to be unambiguous by themselves.
|
||
- The session cap decides how many sessions of this verification one device key bound. It
|
||
does not decide that two device keys are one device, or that one is a person.
|
||
- A pre-fetch needs no rule: an ad fetched and never presented has no claim, so it never
|
||
counts (§9.2).
|
||
|
||
An impression a rule could not be decided for is stated **undecided** and its spend
|
||
**withheld**, apart from both the valid and the excluded (§14.6); the addendum likewise has
|
||
impressions "without sufficient information to make an IVT decision" reported as such and
|
||
kept out of the invalid counts (§2.4.1).
|
||
|
||
This section sets no rule for how an operator identifies a person, and names no person: a
|
||
device key is a key, and an address is what an edge saw.
|
||
|
||
### 14.2 The traffic policy
|
||
|
||
| size | field | meaning |
|
||
|---|---|---|
|
||
| 4 | `magic` | `WTP1` (ASCII) |
|
||
| 1 | `version` | 1 in this revision |
|
||
| 1 | `form` | 1 a room's breaks (this extension); 2 stitched and interstitial ads ([stitched-presentation](stitched-presentation.md) §12) |
|
||
| 32 | `room` | form 1: the room id; form 2: 32 zero bytes |
|
||
| 1 | `rules` | bit 0 **cap**: the session cap; bit 1 **ranges**: the address ranges; bit 2 **crawlers**: the declared crawlers (§14.6) |
|
||
| 2 | `session_cap` | u16: the most sessions one device key binds, at least 1, when **cap** is set; otherwise 0 |
|
||
| 1 + 32 × *a* | `attesters` | a count *a*, 0 to 16, and that many Ed25519 keys, each above the one before it as bytes: the keys that vouch for device keys (§14.4). None when any device key is taken, or when **cap** is clear |
|
||
| 1 + 32 × *e* | `edges` | a count *e*, 0 to 16, and that many keys in the same order: the keys whose session origins are taken (§14.3). At least one when **ranges** or **crawlers** is set; otherwise none |
|
||
| 32 | `ranges` | SHA-256 of the address range list (§14.5) when **ranges** is set; otherwise 32 zero bytes |
|
||
| long | `ranges_source` | where and when that list was published, as UTF-8 text of at least one byte, when **ranges** is set; otherwise no bytes |
|
||
| 32 | `crawlers` | SHA-256 of the crawler token list when **crawlers** is set; otherwise 32 zero bytes |
|
||
| long | `crawlers_source` | its source, likewise |
|
||
| 32 | `authority` | the key that signs the policy |
|
||
| 64 | `sig` | Ed25519 by `authority` over `wai:ad-traffic-policy` (ASCII) and 0x01, followed by the bytes from `magic` through `authority` |
|
||
|
||
The policy's **digest** is SHA-256 over its bytes from `magic` through `authority`. A reader
|
||
reads exactly the bytes it is given as one policy and refuses it `traffic_policy_malformed`
|
||
when the bytes end early or go on after `sig`, or a field breaks the table: a magic, version
|
||
or form it does not define, a `rules` above 7, a cap that is not at least 1 exactly when
|
||
**cap** is set, keys out of order or more than 16, a list's digest or source present without
|
||
its bit, or its source absent or not UTF-8 with it.
|
||
|
||
A policy of form 1 is **bound** when its `room` is the room's id and its `authority` the
|
||
room descriptor's authority. A source is text for a reader: it names the publication so that
|
||
another party can fetch the same list and check its digest. No verifier fetches it.
|
||
|
||
A verifier given no traffic policy applies the first three rules of §14.6, which read claims
|
||
alone.
|
||
|
||
### 14.3 The session origin
|
||
|
||
A session origin is what an **edge** states of the connection over which a session key was
|
||
presented (§3): the party that terminated the player's connection for the session's asset-list
|
||
request, or for its subscription to a composed track a stitcher publishes for it
|
||
([live-ads](live-ads.md) §14.5), under a key the policy names.
|
||
|
||
| size | field | meaning |
|
||
|---|---|---|
|
||
| 4 | `magic` | `WSO1` (ASCII) |
|
||
| 1 | `version` | 1 |
|
||
| 32 | `policy` | the traffic policy's digest |
|
||
| 32 | `player` | the session key |
|
||
| 1 | `family` | 4 or 6 |
|
||
| 4 or 16 | `address` | the connection's remote address as the edge saw it, in network byte order: 4 bytes for family 4, 16 for family 6 |
|
||
| long | `user_agent` | the `User-Agent` field value the request carried (RFC 9110 §10.1.5), byte for byte; no bytes when it carried none |
|
||
| 32 | `edge` | the edge's key |
|
||
| 64 | `sig` | Ed25519 by `edge` over `wai:ad-session-origin` (ASCII) and 0x01, followed by the bytes from `magic` through `edge` |
|
||
|
||
Its **digest** is SHA-256 over its bytes from `magic` through `edge`. It is refused
|
||
`origin_malformed` when it does not read so, its family is neither, or its family is 6 and
|
||
its address is an IPv4-mapped one (`::ffff:0:0/96`, RFC 4291 §2.5.5.2): an edge states an
|
||
IPv4 peer with family 4, so that one address has one form. An edge states the address of its
|
||
own transport peer and never one a request header claims.
|
||
|
||
Each origin gets one verdict, **accepted** or the first of: `origin_malformed`; `bad_key` or
|
||
`bad_signature` for its signature; `origin_unbound` when its `policy` is not the policy's
|
||
digest or its `edge` not one of the policy's `edges`. A session may have several accepted
|
||
origins (a player that reconnects), and each is read.
|
||
|
||
### 14.4 The device binding
|
||
|
||
A device binding states that a session is one device's. It is signed twice, by the **device
|
||
key**, an Ed25519 key the device holds, and by the session key, so that a device cannot claim
|
||
a session whose key did not agree, nor a session a device.
|
||
|
||
| size | field | meaning |
|
||
|---|---|---|
|
||
| 4 | `magic` | `WDB1` (ASCII) |
|
||
| 1 | `version` | 1 |
|
||
| 32 | `policy` | the traffic policy's digest |
|
||
| 32 | `player` | the session key |
|
||
| 32 | `device` | the device key |
|
||
| 1 | `vouched` | 0, or 1 when a voucher follows |
|
||
| 32 + 64 | voucher | when `vouched` is 1: an attester's key, and its Ed25519 signature over `wai:ad-device-voucher` (ASCII) and 0x01, followed by `device` |
|
||
| 64 | `device_sig` | Ed25519 by `device` over `wai:ad-device-binding` (ASCII) and 0x01, followed by the bytes from `magic` through the voucher |
|
||
| 64 | `player_sig` | Ed25519 by `player` over `wai:ad-device-binding-session` (ASCII) and 0x01, followed by the same bytes |
|
||
|
||
Its **digest** is SHA-256 over its bytes from `magic` through the voucher. Each binding gets
|
||
one verdict, **accepted** or the first of: `binding_malformed` when it does not read so;
|
||
`bad_key` or `bad_signature` for `device_sig`, then for `player_sig`; `binding_unbound` when
|
||
its `policy` is not the policy's digest; under a policy that names attesters,
|
||
`binding_unvouched` when it has no voucher or its voucher's key is not one of them, then
|
||
`bad_key` or `bad_signature` for the voucher; `binding_duplicate` when a binding before it in
|
||
the list was accepted for the same `player`. A session is bound once, to one device key.
|
||
|
||
What an attester checks before it vouches for a device key is the attester's, as what a token's
|
||
attester checks is (§7.1). Under a policy with no attester any device key is taken, and the cap
|
||
then bounds only a party that does not make a device key for each session.
|
||
|
||
### 14.5 The pinned lists
|
||
|
||
An **address range list** is `WAL1` (ASCII), a u32 count, and that many ranges, each a
|
||
`family` (4 or 6, one byte), a `prefix` length in bits (one byte, at most 32 or 128) and an
|
||
address of 4 or 16 bytes whose bits after the prefix are zero. The ranges are in ascending
|
||
order of family, then address as bytes, then prefix, none given twice. An address **lies in**
|
||
a range of its own family when its first `prefix` bits are the range's. Families are never
|
||
compared with each other.
|
||
|
||
A **crawler token list** is `WUL1` (ASCII), a u32 count, and that many tokens, each a u8
|
||
length of at least 1 and that many bytes, in ascending order as bytes, none given twice. A
|
||
user agent **holds** a token when the token's bytes occur in it, together and in order,
|
||
compared byte for byte: no case is folded and nothing is normalised.
|
||
|
||
A list is found among those the verifier holds by its SHA-256, and one that does not read so is
|
||
refused. Converting a publication (a provider's published ranges, a registry of crawlers)
|
||
into these bytes is done once by whoever pins it; two parties that convert the same
|
||
publication the same way hold the same digest, and one that holds other bytes is told so
|
||
(`traffic_list_unknown`) before any rule runs.
|
||
|
||
### 14.6 The rules
|
||
|
||
An impression's **claims** are its own claim (§9.2 step 5) and every claim after it that §9.2
|
||
refused `claim_duplicate` for the same impression; its **session key** is its own claim's
|
||
`player`. Two of its claims with one digest are one claim: the same bytes given again are a
|
||
repeat of one statement, not a second presentation. An impression with no own claim has no
|
||
session key for a rule to read; no rule refuses it, and it did not count.
|
||
|
||
A device key's **sessions** are the distinct session keys, among the session keys of the
|
||
impressions of this verification, that an accepted binding binds to it.
|
||
|
||
| code | refuses an impression when | rests on |
|
||
|---|---|---|
|
||
| `ivt_presented_twice` | it has two claims of different digests | those claims |
|
||
| `ivt_span_excess` | a span of one of its claims ends after its `duration_ms`, or a span of one of its claims and a span of another share a millisecond of media time | each such claim |
|
||
| `ivt_unattested` | its policy sets **attest** (§8) and its own claim's `attestation` has no bytes | its own claim |
|
||
| `ivt_session_cap` | under **cap**, its session key is bound to a device key whose sessions number more than `session_cap` | the binding, the device key and its count of sessions |
|
||
| `ivt_cloud_address` | under **ranges**, the address of an accepted origin of its session key lies in a range of the list | the first such origin, and the first range of the list that holds its address |
|
||
| `ivt_declared_crawler` | under **crawlers**, the user agent of an accepted origin of its session key holds a token of the list | the first such origin, and the first token of the list it holds |
|
||
|
||
Every rule that refuses an impression is stated, in this order. Within one claim no media
|
||
time is stated twice (§5.4), so the second rule's shared time is always across two claims.
|
||
The cap refuses every impression of every session of a device key past the cap, since no
|
||
record says which of its sessions were the first.
|
||
|
||
A rule the policy turns on that has no record to decide from leaves the impression
|
||
**undecided**:
|
||
|
||
| mark | an impression has it when |
|
||
|---|---|
|
||
| `device_unbound` | under **cap**, no accepted binding names its session key |
|
||
| `origin_absent` | under **ranges** or **crawlers**, no accepted origin names its session key |
|
||
|
||
An impression is **invalid** when a rule refuses it; otherwise **undecided** when it has a
|
||
mark; otherwise **valid**.
|
||
|
||
### 14.7 Verification and what it states
|
||
|
||
Beside §9.1's inputs a verifier is given a traffic policy or none, session origins and device
|
||
bindings, each a list of byte strings in an order, and the lists it holds. It runs §9 first,
|
||
and applies the rules only to a room §9 accepted under a policy that requires claims. Then:
|
||
|
||
1. With a traffic policy: it reads (`traffic_policy_malformed`); its signature verifies
|
||
(`bad_key`, `bad_signature`); it is bound (§14.2: `traffic_policy_unbound`); each list its
|
||
`rules` turn on is held (`traffic_list_unknown`) and reads (`traffic_list_malformed`). A
|
||
failure here refuses the traffic verification, and no valid spend is stated; §9's
|
||
statement stands.
|
||
2. Under **ranges** or **crawlers**, each origin gets its verdict (§14.3); under **cap**, each
|
||
binding (§14.4). Otherwise they are not read.
|
||
3. Each impression gets its rules and marks (§14.6).
|
||
|
||
It states: the policy's digest; each origin's and each binding's digest, verdict and session
|
||
key; each device key with its sessions and the bindings that give them; for each impression
|
||
its receipt and slot, its price, whether §9 counted it, its verdict of §14.6, each rule that
|
||
refuses it with what it rests on (claims, origins and bindings by their index among those
|
||
given, a list entry by its index in its list) and each mark; and these counts, in which every
|
||
impression appears exactly once among valid, invalid and undecided:
|
||
|
||
- for each rule, the impressions it refuses, and of those the ones §9 counted, which are
|
||
**excluded**, with the sum of their prices;
|
||
- for each mark, the impressions that have it, and of those the ones §9 counted that no rule
|
||
refuses, which are **withheld**, with the sum of their prices;
|
||
- the impressions, those §9 counted, the valid, the invalid, the undecided, the excluded and
|
||
the withheld;
|
||
- the presented spend (§9.3), the excluded spend, the withheld spend and the **valid spend**,
|
||
the sum of `price_micros` over the impressions that §9 counted and that are valid; and each
|
||
receipt's valid spend.
|
||
|
||
The presented spend is the excluded, the withheld and the valid spend together, to the
|
||
micro-unit. An impression two rules refuse is in each rule's count and once in the excluded.
|
||
|
||
Under a traffic policy the spend a buyer owes is the valid spend. It is never more than the
|
||
presented spend, which live-ads §9.3 step 12 already held to the decider grant's ceiling. A
|
||
withheld impression is neither billed nor called invalid: a session whose origin or binding
|
||
arrives later is decided by a later verification over the same records and the new one.
|
||
|
||
### 14.8 Reasons
|
||
|
||
| reason | raised at |
|
||
|---|---|
|
||
| `traffic_policy_malformed` | §14.2, §14.7 step 1 |
|
||
| `traffic_policy_unbound` | §14.7 step 1 |
|
||
| `traffic_list_unknown` | §14.7 step 1 |
|
||
| `traffic_list_malformed` | §14.5, §14.7 step 1 |
|
||
| `origin_malformed` | §14.3 |
|
||
| `origin_unbound` | §14.3 |
|
||
| `binding_malformed` | §14.4 |
|
||
| `binding_unbound` | §14.4 |
|
||
| `binding_unvouched` | §14.4 |
|
||
| `binding_duplicate` | §14.4 |
|
||
|
||
A policy, an origin and a binding also get live-events' `bad_key` and `bad_signature`. The
|
||
first four refuse a traffic verification; the others are one record's verdict. None of them,
|
||
and no rule of §14.6, refuses a room or changes a verdict of §9.
|
||
|
||
### 14.9 Conformance and considerations
|
||
|
||
[`live-conformance/ad-traffic/`](../live-conformance/ad-traffic/) holds the corpus, written
|
||
from fixed seeds by `wai_ad_traffic_vectors gen`. `rooms.json` holds two rooms §9 accepts, with
|
||
everything §9.4 states of them, and vectors over them: a room of twelve sessions under a
|
||
policy with every rule on (two claims of one impression with and without shared media time,
|
||
the same claim given twice, a span past its creative, three sessions of one device key
|
||
against a cap of two, sessions from listed IPv4 and IPv6 addresses, an address one past a
|
||
range, a declared crawler, a user agent that differs from a token only in case, a session
|
||
with no accepted binding and one with no accepted origin, and a control of every check of an
|
||
origin and a binding); the same room with no policy, with the cap alone and any device key,
|
||
and with the lists alone; an attested room one of whose claims carries no token; and a policy
|
||
with a flipped signature, signed by a key that is not the authority's, of another room, of
|
||
the other form, that breaks its own rules, whose list is not held, and whose list is out of
|
||
order. `stitched.json` holds the stitched vectors (stitched-presentation §12). The lists hold
|
||
documentation address ranges and tokens of crawlers that do not exist.
|
||
|
||
A conforming verifier gives every vector the statement the corpus states. `wai_ad_traffic_vectors
|
||
verify` and the test `tests/ad_traffic.rs` check that both files regenerate byte for byte, that
|
||
`wai-rs` gives every vector its statement, that a verifier with any one rule turned off gives
|
||
some vector another statement, and that §14.6 and §14.8 list exactly the reference's codes.
|
||
`live-conformance/ad_traffic_verify.py` is the second implementation, written from this text: it
|
||
shares no code with `wai-rs`; it verifies each room with `ad_presentation_verify.py` and each
|
||
stream with `stitched_presentation_verify.py`, reads every policy, origin, binding and list
|
||
itself, mutates copies of the corpus and requires every copy to fail, and drops each rule and
|
||
each check in turn and requires the corpus to catch it.
|
||
|
||
- **What a refusal proves.** That the signed records state it: two claims, a span, an absent
|
||
token, a count of sessions, an address in a range, a token in a user agent. Each is
|
||
recomputed by anyone who holds the records, and names them.
|
||
- **What the rules do not bound.** A party that makes a device key for each session under a
|
||
policy with no attester; an edge that states an address it did not see, which is why a
|
||
policy names its edges; a crawler that does not declare itself; traffic through an address
|
||
no list holds. §13's limits of a claim and of attestation stand.
|
||
- **False refusals.** A viewer behind an address in a pinned range is refused with the rest of
|
||
it. The authority that pins a list answers for it; the statement names the origin and the
|
||
range, so a seller can contest one refusal with the records in hand.
|
||
- **What the records reveal.** A session origin holds the network address and the user agent of
|
||
a session, and a binding links the sessions one device key bound under one policy. A
|
||
statement names records by index and a device by its key; whoever is given the records
|
||
themselves is given those addresses, as a fleet bundle's holder is ([fleet](fleet.md) §12).
|
||
A session key still links nothing across rooms (§3); a device key used under two policies
|
||
links their bindings to whoever holds both.
|