# 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.