567 lines
34 KiB
Markdown
567 lines
34 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`.
|
||
> 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.
|