open-standards/wai/extensions/ad-presentation.md
2026-10-21 12:28:47 -04:00

567 lines
34 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# WAI Extension: 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`.
> 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.
>
> 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); and what a player
measures and how (§10).
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, which stays statistical and the measurer's
(§13); 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.
## 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.** The detection of general and sophisticated invalid traffic stays
statistical and the measurer's. 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.