open-standards/wai/extensions/key-release.md
Transaction Science 45d564ace8 wai: register key release, its CI steps, SPEC §5 and the changelog
The wire identifiers of extensions/key-release.md (WKL1, WKE1, WKT1 and the WAI-Key-*
header fields) in the registry table, and the export regenerated; the wai.video.keys
row of SPEC §5 names its proxy form, and a §5 paragraph describes it; §6.2 states
how a request missing a field is read. CI: the key-release step in the independent
verifiers job (the module's tests, form 1's, tests/key_release.rs and
key_release_verify.py), the extension under the workflow's paths, and the
wai-key-proxy workflow (tests, clippy, the release binary end to end).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 21:45:41 -04:00

654 lines
38 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: Key Release
> Status: Draft. Adds a second form to the record `wai.video.keys` (SPEC §5): the
> **proxy form**, signed by a **signing proxy** that sits in front of a licence server and
> signs one receipt for every licence request it answers, a release or a refusal. The receipt
> names the encryption **scheme** of each key (ISO/IEC 23001-7's `cenc`, `cbc1`, `cens` and
> `cbcs`; the HLS methods `AES-128`, `SAMPLE-AES` and `AES-256-GCM`), the **key delivery**
> (an HLS key URI, W3C Clear Key, or a content protection system by its system ID) and,
> optionally, the SHA-256 of the **CPIX document** (DASH-IF CPIX 2.3, ETSI TS 103 799) the
> keys came in. The key holder signs a **commitment list**, `WKL1`, derived from the CPIX key
> material: each key's identifier, scheme, key period and commitment. The proxy copies the
> commitments from that list, so it never holds a key. The viewer's entitlement is a record
> its **entitlement authority** signs, `WKE1`, bound to a key the viewer's device holds, and
> every request carries the device's signature over what it asks for. The decision is a pure
> function of signed inputs that a verifier recomputes: entitlement, freshness, replay, key
> period (key rotation), offline licences and their renewals. A key the client receives in
> the clear (an HLS key URI, Clear Key) is never relayed by the proxy: it signs a **release
> ticket**, `WKT1`, which a key server redeems once. The form-1 receipt is unchanged. No new
> transport, and WAI still speaks no licence protocol: a licence travels as its system
> defines it. The reference implementation is the `key_release` module of `wai-rs` (feature
> `key_release`) and the service `wai/wai-key-proxy`; the corpus is
> [`live-conformance/keys/`](../live-conformance/keys/), and its second implementation is
> `live-conformance/key_release_verify.py`.
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
>
> Texts read for this revision: the WAI texts this one cites; the DASH-IF CPIX
> Implementation Guidelines in the commit snapshot of 30 September 2024, which states its
> changes against version 2.3.1, for its §6.4.4, §7.4.2, §7.4.7, §7.4.10, §7.4.13 to
> §7.4.17, §8.1 and §8.2 (every element and attribute §4.4 reads is in version 2.3, which ETSI
> publishes as TS 103 799; the per-key `@contentId` that snapshot adds is not read);
> `draft-pantos-hls-rfc8216bis-22` §4.4.4.4 (`EXT-X-KEY`) and §5 (key files); W3C Encrypted
> Media Extensions, Working Draft of 13 October 2026, §9.1 (Clear Key) and its note on
> `encryptionScheme`; the DASH-IF registry of content protection system identifiers; RFC
> 8032; RFC 4648 §5; RFC 9110 §15. The text of ISO/IEC 23001-7 was not available to this
> revision: its scheme types are named here as CPIX §7.4.7, the EME note and the HLS draft
> name them. A section number of one of these texts is that revision's.
## 1. Scope
In scope: the encryption schemes and key deliveries a receipt names (§3); the commitment list
and its derivation from a CPIX document (§4); the entitlement (§5); the licence request and
the device's proof over it (§6); the release decision, its offline licences, renewals and key
periods (§7); the proxy form of the `wai.video.keys` receipt (§8); the signing proxy, the
release ticket and the key server (§9); verification of a proxy's log (§10).
Out of scope: a licence protocol, and the licence a content protection system issues, which
the proxy forwards and never reads (§9.2); how an entitlement authority decides whom to
entitle (its subscription or purchase records); a CPIX document whose content keys are
encrypted (CPIX §8.1), which this revision does not read (§4.4); and a device limit, which is
a separate rule.
Relationship to the core spec: `wai.video.keys` stays a **record** (SPEC §5). Form 1, the
receipt SPEC §5 registers, is unchanged: its signed bytes, its identity and every receipt of
it stay as they were. This extension adds form 2 (§8). Both are records of Lane A (SPEC §6.5):
they cover the release decision, never a frame the key opens.
Relationship to [energy-measurement](energy-measurement.md): a form-2 receipt carries the
decision's energy as the OTT receipts do, a figure and its acquisition class, and its identity
is under the identity domain energy-measurement §6.1 names for `wai.video.keys`.
## 2. Terms
- **Content key**: a key that encrypts media. Its **KID** is its 16-byte key identifier, in a
CPIX document the UUID of `ContentKey@kid` (CPIX §7.4.7), its 16 bytes in the UUID's order.
- **Key holder**: the party that holds the content keys: the packager or encryptor, or the key
server it exchanges CPIX documents with. It signs the commitment list (§4).
- **Entitlement authority**: the operator's system that decides who may watch what. It signs
entitlements (§5).
- **Holder key**: an Ed25519 key a viewer's device holds. An entitlement names it, and the
device signs each request with it (§6).
- **Licence server**: any server that issues licences for a content protection system. It
is not changed by this extension.
- **Signing proxy**, or **proxy**: the service that decides each licence request, signs its
receipt, and forwards a granted request to the licence server or issues a release ticket
(§9). It holds no content key.
- **Key server**: the service that redeems a release ticket for a key delivered in the clear
(§9.4). It is run by the key holder.
- **Log**: every receipt one proxy signed, in the order it signed them (§9.5).
- **Wrapped delivery**: a delivery whose licence carries the key encrypted for the client's
decryption module (`drm`). **Clear delivery**: a delivery whose response is the key itself
(`hls-key-uri`, `clear-key`).
- Byte fields are big-endian where they are integers; a **string** in a binary layout is its
length as a u32 and its UTF-8 bytes; **base64url** is RFC 4648 §5 without padding.
## 3. Encryption schemes and key deliveries
### 3.1 Schemes
The scheme is what was applied to the media's bytes:
| code | label | defined by | what it encrypts | key octets |
|---|---|---|---|---|
| 1 | `cenc` | ISO/IEC 23001-7 | AES-CTR, full samples and video subsamples; under HLS, `SAMPLE-AES-CTR` of fMP4 segments | 16 |
| 2 | `cbc1` | ISO/IEC 23001-7 | AES-CBC, full samples and video subsamples | 16 |
| 3 | `cens` | ISO/IEC 23001-7 | AES-CTR, a pattern of video subsamples | 16 |
| 4 | `cbcs` | ISO/IEC 23001-7 | AES-CBC, a pattern of video subsamples with a constant IV; under HLS, `SAMPLE-AES` of fMP4 segments | 16 |
| 5 | `AES-128` | `draft-pantos-hls-rfc8216bis-22` §4.4.4.4 | whole segments, AES-128-CBC with PKCS7 padding | 16 |
| 6 | `SAMPLE-AES` | `draft-pantos-hls-rfc8216bis-22` §4.4.4.4 | samples of segments other than fMP4 (MPEG-2 transport streams, packed audio) | 16 |
| 7 | `AES-256-GCM` | `draft-pantos-hls-rfc8216bis-22` §4.4.4.4 | whole segments, AES-256-GCM | 32 |
The label is the value CPIX's `ContentKey@commonEncryptionScheme` takes for that scheme (CPIX
§7.4.7: a four-character Common Encryption scheme, or an HLS method). An fMP4 segment that HLS
encrypts with `SAMPLE-AES` is `cbcs`, and with `SAMPLE-AES-CTR` is `cenc`; `SAMPLE-AES` here
names only the other segment formats, so one scheme never has two labels.
### 3.2 Key deliveries
| code | kind | how the client gets the key | system ID |
|---|---|---|---|
| 1 | `hls-key-uri` | the key file of an `EXT-X-KEY` URI with `KEYFORMAT` `"identity"` (or none): the key's octets, packed (`draft-pantos-hls-rfc8216bis-22` §5.1). Clear | none |
| 2 | `clear-key` | the JSON Web Key Set of the `org.w3.clearkey` key system (EME §9.1.4). Clear | none |
| 3 | `drm` | a licence of the content protection system the system ID names, which carries the key encrypted for the client's decryption module. Wrapped | 16 bytes: a system ID of the DASH-IF registry, not all zero |
### 3.3 Which deliveries carry which schemes
| delivery | schemes |
|---|---|
| `hls-key-uri` | `cenc`, `cbcs`, `AES-128`, `SAMPLE-AES`, `AES-256-GCM`: the methods HLS defines with an identity key file |
| `clear-key` | `cenc`, `cbc1`, `cens`, `cbcs`: ISO/IEC 23001-7, which is what EME decrypts |
| `drm` | all seven |
A receipt that names a key under a delivery this table does not give its scheme is refused
`scheme_delivery_mismatch` (§8.4). A proxy MUST NOT serve such a key on such a route (§9.1).
## 4. The commitment list `WKL1`
### 4.1 Layout
A commitment list is one content's keys, as the key holder states them. It is these bytes in
this order:
| field | bytes |
|---|---|
| magic | `WKL1` |
| version | u8, 1 |
| `content_id` | string, not empty |
| `required_entitlement` | string, not empty: the entitlement class the content demands (§7.2 check 5) |
| `cpix` | u8 0, or u8 1 and 32 bytes: the SHA-256 of the CPIX document the keys came in, its exact bytes |
| `issued` | u64: when the list was signed, in seconds since 1970-01-01T00:00:00Z |
| key count | u32, from 1 to 4096 |
| each key | `kid` (16), `scheme` (u8, §3.1), `key_commit` (32), then its period (§4.2) |
| `signer_pubkey` | 32: the key holder's Ed25519 key |
| `signer_id` | string |
| `sig` | 64 |
`key_commit` is the commitment of form 1, unchanged: BLAKE3 over `wai:video-keys-commit` 0x01,
the key's length (u32) and its octets. A key's period is one byte, then:
| tag | then | means |
|---|---|---|
| 0 | nothing | the key has no key period |
| 1 | `index` (u32) | a period known by its index only (CPIX §7.4.14) |
| 2 | `start` (u64), `end` (u64) | a period of wall-clock seconds, from `start` up to but excluding `end` |
| 3 | `index` (u32), `start` (u64), `end` (u64) | both |
### 4.2 Signature and identity
`sig` is Ed25519 by `signer_pubkey`, verified as SPEC §7 *Signatures* has it, over
`wai:video-keys-list` 0x01 and every byte of the list before `sig`. The list's **identity** is
BLAKE3 over `wai:video-keys-list-id` 0x01 and every byte of the list, `sig` included.
### 4.3 Rules
A list holds: its magic and version; each string UTF-8, `content_id` and
`required_entitlement` not empty; the KIDs in strictly ascending byte order; every scheme a
code of §3.1; no `key_commit` all zero; every period tag of §4.1, and `start` below `end`; no
byte after `sig`; and its signature verifies. A list that fails any of these is refused
`list_malformed`, or `list_bad_signature` when only its signature fails.
### 4.4 A list derived from a CPIX document
A list whose `cpix` is present states that it is derived from that CPIX document. A verifier
given the document derives a list from it by these steps, refusing the document at the first
that fails, and then refuses the list `cpix_mismatch` when its `content_id`, its `cpix`, or
its keys (each `kid`, `scheme`, `key_commit` and period) differ from the derived ones. Element
names are in the namespace `urn:dashif:org:cpix`, except `Secret`, `PlainValue` and
`EncryptedValue`, which are in `urn:ietf:params:xml:ns:keyprov:pskc`; attributes have no
namespace.
1. The document is well-formed XML 1.0 in UTF-8, has no document type declaration, and its
root is `CPIX`; otherwise `cpix_malformed`. `cpix` is the SHA-256 of its bytes, as
exchanged.
2. The root has `@contentId`, which is `content_id`; otherwise `cpix_unsupported`.
3. The keys are the `ContentKey` children of the root's `ContentKeyList` child, in document
order; none at all is `cpix_malformed`. For each, in turn:
1. its `@kid` is a UUID in its canonical text form (8-4-4-4-12 hexadecimal digits, either
case), read as 16 bytes in that order, and no earlier key has it; otherwise
`cpix_malformed`;
2. it has neither `@contentId` nor `@dependsOnKey`, and its `@commonEncryptionScheme` is a
label of §3.1, which is its `scheme`; otherwise `cpix_unsupported`;
3. its `Data` child holds a `Secret` child holding a `PlainValue` child; otherwise (an
`EncryptedValue` among them, CPIX §8.1's encrypted key, which this revision does not
read) `cpix_unsupported`;
4. that `PlainValue`'s text, without its ASCII whitespace, is base64 (RFC 4648 §4) in its
canonical form, and decodes to the scheme's key octets, which are the key; otherwise
`cpix_malformed`.
4. The periods: each `ContentKeyPeriod` child of the root's `ContentKeyPeriodList` child is a
period, known by its `@id`. Each `KeyPeriodFilter` child of each `ContentKeyUsageRule` child
of the root's `ContentKeyUsageRuleList` child, in document order, names a period by
`@periodId` for the key the rule's `@kid` names. A rule whose `@kid` names no key, or a
filter whose `@periodId` names no period, is `cpix_malformed`; a key named for two different
periods is `cpix_unsupported`.
5. Each key named for a period, in ascending KID order, has that period. A period with
`@startOffset`, `@endOffset` or `@duration`, with `@start` and no `@end` or the reverse, with
neither `@index` nor `@start`, with an `@index` that is not a decimal integer from 0 to
2^32 − 1, or with a time not of the form `YYYY-MM-DDThh:mm:ssZ`, is `cpix_unsupported`. A
period whose `@start` is not below its `@end` is `cpix_malformed`. Otherwise `index` is its
`@index` and `start` and `end` its `@start` and `@end` in seconds since
1970-01-01T00:00:00Z, each where present. A key named for no period has none.
The list's keys are the document's, in ascending KID order, each with the `key_commit` of its
key. `required_entitlement`, `issued` and the signer are the key holder's and are not in the
document. A `DRMSystem` element (CPIX §7.4.10) is not read: the delivery is the route's
(§9.1).
## 5. The entitlement `WKE1`
### 5.1 Layout
| field | bytes |
|---|---|
| magic | `WKE1` |
| version | u8, 1 |
| `session` | 16: the playback session or purchase it entitles |
| `holder` | 32: the holder key, an Ed25519 key the device holds |
| `entitlement` | string, not empty: the class it grants, compared byte for byte with a list's `required_entitlement` |
| `not_before` | u64, seconds |
| `not_after` | u64, seconds: the entitlement holds from `not_before` to `not_after`, both included |
| `offline_s` | u64: the longest offline licence it permits, in seconds; 0 for none |
| `renewals` | u32: how many times an offline licence may be renewed; 0 when `offline_s` is 0 |
| `authority_pubkey` | 32: the entitlement authority's Ed25519 key |
| `authority_id` | string |
| `sig` | 64 |
### 5.2 Signature, digest and rules
`sig` is Ed25519 by `authority_pubkey` over `wai:video-keys-entitlement` 0x01 and every byte
before `sig`. An entitlement's **digest** is the SHA-256 of all its bytes. An entitlement
holds when its magic and version are these, its strings are UTF-8 and `entitlement` is not
empty, `not_before` is at most `not_after`, `renewals` is 0 when `offline_s` is 0, no byte
follows `sig`, and its signature verifies. One that does not hold, or whose authority the
proxy does not trust, entitles nothing (§7.2 check 1). It is not a refusal of the receipt
that carries it: the receipt is a signed refusal of the request.
## 6. The licence request
### 6.1 What a request asks for, and its proof
A request names: the content (by the proxy's route, §9.1, which gives its `content_id` and its
delivery); its KIDs, from 1 to 64, distinct; its **kind**, `streaming` (1), `offline` (2) or
`renewal` (3); for a renewal, `renews`, the identity of the receipt that released the licence
it renews; the SHA-256 of the request body as the proxy received it (`request_sha256`; of no
bytes for a request without a body); and `request_t`, the device's clock, in seconds. With it
come the entitlement and the **proof**: Ed25519 by the entitlement's `holder` over
1. `wai:video-keys-request` (ASCII) and 0x01;
2. the entitlement's digest (32);
3. `content_id` (string);
4. the delivery's code (u8), then 1 and its system ID (16), or 0 for none;
5. the KIDs' count (u32) and each KID, in ascending order;
6. the kind's code (u8), then 1 and `renews` (32), or 0 for none;
7. `request_sha256` (32) and `request_t` (u64).
The proof makes a copied entitlement useless without the device's key, and binds the request
body, the KIDs and the kind to it. Every hash here is SHA-256 so that a browser computes the
proof with the Web Cryptography API.
### 6.2 HTTP carriage
A request is an HTTP request to the route's URI (§9.6), with these header fields:
| field | value |
|---|---|
| `WAI-Key-Ids` | the KIDs, each 32 lowercase hexadecimal digits, separated by commas |
| `WAI-Key-Kind` | `streaming`, `offline` or `renewal` |
| `WAI-Key-Renews` | for a renewal, `renews` in 64 lowercase hexadecimal digits |
| `WAI-Key-Time` | `request_t`, a decimal integer |
| `WAI-Key-Entitlement` | the entitlement's bytes, base64url |
| `WAI-Key-Proof` | the proof's 64 bytes, base64url |
The body is what the client's decryption module or player sends: a licence challenge, an EME
Clear Key licence request (EME §9.1.3), or nothing for an HLS key file. The proxy answers with
the receipt's identity in `WAI-Key-Receipt`, 64 lowercase hexadecimal digits, and makes the
receipt itself readable at its identity (§9.6). A request with no `WAI-Key-Ids`, or one that
names no KID, more than 64, a KID twice or a KID that is not 32 lowercase hexadecimal digits,
a `WAI-Key-Kind` that is none of the three, or a route the proxy does not have, names nothing to
decide: it is answered 400 or 404 and has no receipt. Every other request has one, whatever
else it lacks, read so:
- no `WAI-Key-Kind` is `streaming`;
- no `WAI-Key-Time`, or one that is not a decimal integer below 2^64, is `request_t` 0;
- a renewal with no `WAI-Key-Renews` that reads renews 32 zero bytes;
- an entitlement or a proof that does not read as base64url (a proof of other than 64 bytes
included), or one without the other, is recorded as neither, so the request is refused
`DeniedEntitlementInvalid` (§7.2 check 1).
## 7. The release decision
### 7.1 Inputs
The decision of a request is a function of: the entitlement as presented, or none; the
**authorities**, the entitlement authority keys the proxy trusts; the proof; the request's
fields (§6.1); the content's commitment list, or none; `issued`, the proxy's clock when it
decided, in seconds; and the receipts earlier in the proxy's log. Every input is in the
receipt (§8.2) or is a signed record a verifier is given (§10.1), so a verifier recomputes the
decision.
### 7.2 The checks, in order
The decision is the first of these that applies:
| check | decision | code | when |
|---|---|---|---|
| 1 | `DeniedEntitlementInvalid` | 4 | no entitlement was presented; or it does not hold (§5.2); or its `authority_pubkey` is not one of the authorities; or the proof does not verify under its `holder` over the request (§6.1) |
| 2 | `DeniedStale` | 5 | `issued` and `request_t` differ by more than 300 |
| 3 | `DeniedReplayed` | 6 | a receipt earlier in the log carries the same proof |
| 4 | `DeniedUnknownKey` | 7 | the proxy has no commitment list for the content, or a requested KID is not in it |
| 5 | `DeniedNoEntitlement` | 2 | the entitlement's `entitlement` is not the list's `required_entitlement` |
| 6 | `DeniedNotYetValid` | 8 | `issued` is below `not_before` |
| 7 | `DeniedExpired` | 3 | `issued` is above `not_after` |
| 8 | `DeniedPeriodOutside` | 9 | a requested key's period has `start` and `end`, and the period does not meet the entitlement: `start` above `not_after`, or `end` at most `not_before` |
| 9 | `DeniedOfflineNotEntitled` | 10 | the kind is `offline` or `renewal`, and `offline_s` is 0 |
| 10 | `DeniedRenewalUnknown` | 11 | the kind is `renewal`, and no receipt earlier in the log has the identity `renews`, is released, is of kind `offline` or `renewal`, names the same `content_id` and the same KIDs, and carries an entitlement with the same `session` and `holder` |
| 11 | `DeniedRenewalsExhausted` | 12 | the kind is `renewal`, and that receipt's `renewal` plus 1 is above `renewals` |
| 12 | `DeniedRenewalLapsed` | 13 | the kind is `renewal`, and `issued` is above that receipt's `licence_expiry` |
| — | `Granted` | 1 | none of these applies |
Codes 1 to 3 and their labels are form 1's (SPEC §5): `Granted`, `DeniedNoEntitlement` and
`DeniedExpired` mean in form 2 what they mean there. Check 2 bounds how long a captured request
is good for; check 3 makes it good once. A client that retries a request signs it again. A
refused request is refused whole: a request for several keys, one of which fails a check, is
refused for all of them.
### 7.3 The licence a release permits
A `Granted` request of kind `streaming` permits a licence with no stated expiry: its duration
is the licence server's. A `Granted` request of kind `offline` or `renewal` permits an
**offline licence** that expires at `licence_expiry`, the lesser of `issued` + `offline_s` and
`not_after`, and is renewal number `renewal`: 0 for kind `offline`, and the renewed receipt's
`renewal` plus 1 for kind `renewal`. So a licence never outlives the entitlement, a renewal
must come before the licence it renews lapses, and an entitlement bounds how many renewals
there are. A licence that lapsed is not renewed; the device asks for a new offline licence,
which the entitlement must still permit.
### 7.4 Key rotation
A key whose list entry has a period is one key of a rotation: the CPIX document holds one key
per period (CPIX §8.2). Check 8 releases the key of a period only to an entitlement that holds
during some of it, so a viewer whose entitlement ends gets no key of a later period, and one
whose entitlement begins gets none of an earlier one. A period known by its index alone has no
time, and check 8 does not apply to it.
## 8. The proxy form of the `wai.video.keys` receipt
### 8.1 Forms
A `wai.video.keys` receipt has two forms: **form 1**, SPEC §5's, unchanged; and **form 2**, the
proxy form, defined here. They sign under different domains (§8.3), so no signature of one is
a signature of the other; they share the key commitment (§4.1) and the identity domain, so a
figure profile of energy-measurement §6.1, `wai:video-keys-receipt-id`, names either.
### 8.2 Fields and their JSON carriage
A form-2 receipt is one JSON object (RFC 8259) with exactly these members. A byte field is
lowercase hexadecimal of its exact length, an integer a JSON integer from 0 to 2^64 − 1, and
an absent value `null`:
| member | value |
|---|---|
| `form` | 2 |
| `content_id` | a string: the route's content |
| `list` | 32 bytes: the identity of the content's commitment list (§4.2), or `null` when the proxy has none |
| `cpix_sha256` | 32 bytes: the list's `cpix`, or `null` |
| `required_entitlement` | a string: the list's, or `null` |
| `delivery` | `{"kind", "system_id"}`: the kind's label (§3.2), and its system ID, 16 bytes, or `null` |
| `keys` | an array, in ascending KID order: each requested key, `{"kid", "scheme", "period", "key_commit"}`, `kid` 16 bytes; `scheme` its label (§3.1), or `null` for a KID not in the list; `period` `null` or `{"index", "start", "end"}`, each `null` where the period has none; `key_commit` 32 bytes or `null` |
| `kind` | `streaming`, `offline` or `renewal` |
| `renews` | 32 bytes, or `null` |
| `entitlement` | the entitlement's bytes, or `null` when none was presented |
| `request_sha256` | 32 bytes |
| `request_t` | an integer |
| `proof` | 64 bytes, or `null` when no entitlement was presented |
| `issued` | an integer |
| `decision` | its label (§7.2) |
| `licence_expiry` | an integer, or `null` |
| `renewal` | an integer from 0 to 2^32 − 1, or `null` |
| `upstream_status` | the licence server's HTTP status, from 100 to 599, or `null` when the request was not forwarded |
| `response_sha256` | 32 bytes: the SHA-256 of the response body the licence server returned and the proxy forwarded, or `null` |
| `released` | `true` when the key was released (§9.2, §9.3), `false` otherwise |
| `joules_micro` | the decision's energy, or 0 unmetered |
| `energy_provenance` | its class's label (energy-measurement §2.7: `ModelBased`, `Estimator`, `OnChipCounter`, `CalibratedInstrument`), or `null` unmetered |
| `energy_uncertainty`, `energy_counter_filtering`, `energy_calibration_ref` | only for a class that carries them, as energy-measurement §2.7 names and writes them |
| `signer_pubkey` | 32 bytes: the proxy's Ed25519 key |
| `signer_id` | a string |
| `sig` | 64 bytes |
A receipt that does not read so, whose `entitlement` is not hexadecimal, or that carries the
legacy class label `HwShunt` (energy-measurement §2.6), is refused `receipt_malformed`.
### 8.3 Signing payload and identity
The **signing payload** is the concatenation of:
1. `wai:video-keys-proxy-receipt` (ASCII) and 0x01;
2. `content_id` (string);
3. `list`, `cpix_sha256`: each 0 for `null`, or 1 and its 32 bytes;
4. `required_entitlement`: 0, or 1 and the string;
5. the delivery's code (u8), then `system_id`: 0, or 1 and its 16 bytes;
6. the keys' count (u32), then each key: `kid`; its scheme's code (u8), 0 for `null`; its
period, as §4.1 writes one (tag 0 for `null`; tag 1, 2 or 3 as `index`, `start` and `end`
are present); and `key_commit`: 0, or 1 and its 32 bytes;
7. the kind's code (u8); `renews`: 0, or 1 and its 32 bytes;
8. `entitlement`: 0, or 1, its length (u32) and its bytes;
9. `request_sha256`; `request_t` (u64); `proof`: 0, or 1 and its 64 bytes;
10. `issued` (u64); the decision's code (u8); `licence_expiry`: 0, or 1 and u64; `renewal`: 0,
or 1 and u32;
11. `upstream_status`: 0, or 1 and u16; `response_sha256`: 0, or 1 and its 32 bytes;
`released`: u8, 1 or 0;
12. `joules_micro` (u64), then the class in its signed encoding (energy-measurement §2.7), the
tag byte 0 for none;
13. `signer_pubkey`, then `signer_id` (string).
`sig` is Ed25519 by `signer_pubkey` over the signing payload. The receipt's **identity** is
BLAKE3 over `wai:video-keys-receipt-id` 0x01, the signing payload and `sig`: form 1's identity
domain, over a payload whose first bytes no form-1 payload has.
### 8.4 Its own rules
A form-2 receipt holds, on its own:
1. its signature verifies (`bad_key`, `bad_signature`);
2. `keys` has from 1 to 64 entries, their KIDs strictly ascending;
3. `system_id` is present when, and only when, the delivery is `drm`, and is not all zero; an
`hls-key-uri` receipt has one key;
4. every key whose `scheme` is present has a scheme its delivery carries (§3.3), or the
receipt is refused `scheme_delivery_mismatch`;
5. when `list` is `null`, so are `cpix_sha256`, `required_entitlement` and every key's
`scheme`, `period` and `key_commit`; when it is present, `required_entitlement` is too;
6. `entitlement` and `proof` are both present or both `null`, and when both are `null` the
decision is `DeniedEntitlementInvalid`; `renews` is present when, and only when, the kind is
`renewal`;
7. `released` only with the decision `Granted`, and every key's `key_commit` present when,
and only when, `released`;
8. for a `drm` delivery, `upstream_status` and `response_sha256` are present when, and only
when, the decision is `Granted`, and `released` is `true` when, and only when,
`upstream_status` is from 200 to 299; for a clear delivery both are `null`, and `released`
is `true` when the decision is `Granted`;
9. `licence_expiry` and `renewal` are present when, and only when, `released` and the kind is
not `streaming`; then `licence_expiry` is at least `issued`, and `renewal` is 0 when, and
only when, the kind is `offline`;
10. the figure and its class cohere as energy-measurement §2 has them: `joules_micro` 0 with
no class, or above 0 with a class, a hardware class's declared uncertainty with
`relative_ppm` and `window_us` above 0 and at the resolution floor (`joules_micro` ×
`relative_ppm` at least 288 676), and a calibrated instrument's reference holding a
character that is not Unicode White_Space.
A rule of 2, 3 or 5 to 10 that fails is `receipt_invalid`. Rule 8 means a refused request
was never forwarded, and a released key was released by the licence server's answer or by a
ticket, never by the proxy's word alone. These rules are what a licensor holding one receipt
checks alone; §10 adds what the lists, the authorities and the log let a verifier check.
## 9. The signing proxy
### 9.1 What it holds, and what it never holds
A proxy holds: its Ed25519 signing key; the authorities (§7.1); the commitment lists of the
content it serves; its **routes**, each a name, a `content_id`, a delivery (§3.2) and, for a
`drm` delivery, the licence server's URI, for a clear delivery the key server's; and its log.
It holds no content key and needs none: every `key_commit` it signs is copied from a list the
key holder signed. A proxy MUST refuse to load a route whose list holds a key whose scheme the
route's delivery does not carry (§3.3).
For each request with a receipt (§6.2) the proxy computes the decision (§7.2) with its clock as
`issued`, and then:
- a refusal is answered 403 with the receipt as its body (`application/json`), and the request
goes no further;
- a `Granted` request of a `drm` delivery is forwarded (§9.2);
- a `Granted` request of a clear delivery is answered with a release ticket (§9.3).
The proxy appends the receipt to its log before it answers (§9.5).
### 9.2 Wrapped delivery: forwarding
The proxy sends the request body, unchanged, to the route's licence server, with the
request's `Content-Type`, the field `WAI-Key-Ids` as received, and, for an offline licence,
`WAI-Key-Licence-Expiry`, `licence_expiry` as a decimal integer, so that a licence server
configured to read it issues no longer licence. It does not send the entitlement or the proof.
It answers the client with the licence server's status and body, unchanged, and
`WAI-Key-Receipt`. A licence server that gives no HTTP response is recorded as status 502
with the SHA-256 of no bytes, and the client is answered 502. The receipt binds the status
and the SHA-256 of that body: `released` is `true` when the status is from 200 to 299 (§8.4
rule 8). The licence carries the key encrypted for the client's decryption module, so the
proxy relays bytes it cannot open.
### 9.3 Clear delivery: the release ticket `WKT1`
The response to an `hls-key-uri` or `clear-key` request is the key itself, so the proxy does
not relay it. It answers 307 (RFC 9110 §15.4.8) with `WAI-Key-Receipt` and `Location` set to
the route's key server URI with the query parameter `wai-ticket`, the base64url of this
ticket:
| field | bytes |
|---|---|
| magic | `WKT1` |
| version | u8, 1 |
| `receipt` | 32: the identity of the receipt that released it |
| `delivery` | u8: 1 or 2 (§3.2) |
| `session_type` | u8: 1, `temporary`, for kind `streaming`; 2, `persistent-license`, otherwise (EME §9.1.1) |
| KID count | u32, then each KID (16), in ascending order |
| `expires` | u64: `issued` plus from 1 to 300 seconds |
| `proxy_pubkey` | 32: the receipt's `signer_pubkey` |
| `sig` | 64: Ed25519 by `proxy_pubkey` over `wai:video-keys-ticket` 0x01 and every byte before `sig` |
A 307 keeps the method and the body, so a Clear Key licence request reaches the key server as
it was sent, and an HLS key file request is a GET.
### 9.4 The key server
A key server holds the content keys of its content (it is the key holder's), the proxy keys it
trusts, and the identities of the tickets it has redeemed. It answers a request carrying
`wai-ticket` with the keys the ticket names only when: the ticket reads as §9.3 has it and its
signature verifies; `proxy_pubkey` is one it trusts; its clock is at most `expires`; and no
ticket with the same `receipt` was redeemed before. It answers an `hls-key-uri` ticket with the
key's octets (`application/octet-stream`, `draft-pantos-hls-rfc8216bis-22` §5.1), and a
`clear-key` ticket with a JSON Web Key Set (EME §9.1.4): `{"keys": [{"kty": "oct", "k", "kid"},
...], "type"}`, `k` and `kid` base64url and `type` the ticket's session type. Otherwise it
answers 403, or 409 for a ticket already redeemed, and releases nothing. A ticket is a
single-use bearer of one release for at most 300 seconds, carried over TLS.
### 9.5 The log
The proxy appends each receipt, as the JSON of §8.2 on one line, to its log, and makes the
append durable before it answers the request. The log is the input to checks 3 and 10 to 12
(§7.2): a proxy that restarts reads its log back before it answers again, so a replay or a
renewal is decided against every receipt it ever signed. A proxy MUST hold one log, written by
one process at a time.
### 9.6 HTTP interface
| method and path | answer |
|---|---|
| `POST /v1/licence/{route}`, `GET /v1/licence/{route}` | a licence request (§6.2) |
| `GET /v1/receipts/{identity}` | the receipt with that identity, `application/json`, or 404 |
| `GET /v1/lists` | the identity, `content_id` and key count of each list the proxy holds |
| `GET /v1/health` | the proxy's key, its route count and its log's length |
The whole log is an operator's record of sessions: a proxy serves it, as JSON lines, only on a
listener that is not public.
## 10. Verification
### 10.1 Inputs
A key-release verifier is given: the **authorities** and the **proxies**, the Ed25519 keys
trusted to sign entitlements and receipts; the **lists**; for some lists, the **CPIX
document** each is derived from; the **log**, a proxy's receipts in the order it signed them,
from its first; and **tickets**, each with the receipt it names.
### 10.2 Steps
1. Each list: §4.3 (`list_malformed`, `list_bad_signature`); two lists for one `content_id`
are refused `list_malformed`.
2. Each CPIX document with its list: §4.4 (`cpix_malformed`, `cpix_unsupported`,
`cpix_mismatch`).
3. Each receipt, in the log's order:
1. it reads as §8.2 (`receipt_malformed`);
2. its own rules (§8.4: `bad_key`, `bad_signature`, `receipt_invalid`,
`scheme_delivery_mismatch`);
3. its `signer_pubkey` is one of the proxies (`signer_unknown`);
4. its identity is not one an earlier receipt has (`receipt_duplicate`);
5. its `list`, when present, is the identity of a list given (`list_unknown`) whose
`content_id` is the receipt's; when `null`, no list given has the receipt's
`content_id`; `cpix_sha256` and `required_entitlement` are the list's; each key in the
list has the list's scheme and period, each key not in it has `null` ones, and each
present `key_commit` is the list's (`list_mismatch`);
6. the decision is the one §7.2 computes from the receipt's fields, the authorities, the
list and the receipts before it, and when `released` and the kind is not `streaming`,
`licence_expiry` and `renewal` are the ones §7.3 computes (`decision_mismatch`).
4. Each ticket: it reads as §9.3 and its signature verifies; its `receipt` is the identity of
a receipt of the log that is released, of a clear delivery, whose `delivery`, kind and
KIDs the ticket's `delivery`, `session_type` and KIDs follow, whose `signer_pubkey` is
`proxy_pubkey`, and whose `issued` is from 1 to 300 seconds below `expires`
(`ticket_invalid`).
The first step that fails refuses the log, with its reason and its place: `["list", i]`,
`["cpix", i]`, `["receipt", i]` or `["ticket", i]`.
### 10.3 What a verification states
An accepted log states, for each receipt: its identity, decision and `released`; for a
released one, each key's KID and `key_commit`, and `licence_expiry` and `renewal` when present.
A licensor holding a content key confirms that a release named it by computing the key's
commitment (§4.1), as form 1 lets it.
## 11. Reasons
The codes this extension adds, each with where it is raised:
| reason | raised at |
|---|---|
| `receipt_malformed` | §8.2, §10.2 step 3.1 |
| `bad_key` | §8.4 rule 1 |
| `bad_signature` | §8.4 rule 1 |
| `receipt_invalid` | §8.4 |
| `scheme_delivery_mismatch` | §3.3, §8.4 rule 4 |
| `signer_unknown` | §10.2 step 3.3 |
| `receipt_duplicate` | §10.2 step 3.4 |
| `list_malformed` | §4.3, §10.2 step 1 |
| `list_bad_signature` | §4.3 |
| `list_unknown` | §10.2 step 3.5 |
| `list_mismatch` | §10.2 step 3.5 |
| `cpix_malformed` | §4.4 |
| `cpix_unsupported` | §4.4 |
| `cpix_mismatch` | §4.4 |
| `decision_mismatch` | §10.2 step 3.6 |
| `ticket_invalid` | §10.2 step 4 |
A refused entitlement, a stale or replayed request and the other outcomes of §7.2 are not
reasons: each is a decision, signed in a valid receipt.
## 12. Conformance
The corpus is [`live-conformance/keys/`](../live-conformance/keys/): logs of a proxy over
CPIX-derived lists, each with the outcome its verification reaches (§10). It gives releases
over each delivery; refusals of each decision of §7.2; an expired entitlement; key rotation
over three periods; offline licences, their renewals and their lapse; a licence server that
refused a granted request; and negative controls that each verification refuses with a reason
of §11. Its second implementation, `live-conformance/key_release_verify.py`, is written from
this text and shares no code with `wai-rs`.
A conforming proxy computes every decision §7.2 computes, signs every receipt §8 describes and
none it does not, forwards no refused request, relays no clear key (§9.3), and holds its log as
§9.5 has it. A conforming verifier reaches the outcome, reason and place the corpus states for
every log.
## 13. What a receipt does not prove
- **A licence's contents.** For a `drm` delivery the receipt binds the SHA-256 of the licence
the licence server returned, which the proxy cannot open. A licence server that puts in a
licence a key the request did not name, or a longer duration than `licence_expiry`, is not
caught by the receipt: the proxy forwards `WAI-Key-Ids` and `WAI-Key-Licence-Expiry` so that
a licence server can be configured to follow them.
- **The kind of licence.** The kind is the device's signed statement (§6.1). A licence
challenge asking for a persistent licence under a `streaming` request is the device's
misstatement, and an operator that issues persistent licences only through routes whose
licence server issues nothing else closes it.
- **The clock.** `issued` is the proxy's clock and `request_t` the device's. Check 2 bounds
their difference; neither is a trusted time.
- **The redemption.** A released clear-delivery receipt records that a ticket was issued. The
key server's record of what it redeemed is its own.
- **That the list is right.** The proxy copies its commitments from the key holder's list, and
a verifier with the CPIX document checks that list (§4.4). Without the document, the list is
the key holder's signed statement.