Skip to main content

WAI Extension: Key Release

Mirrored from the canonical text at commit 260cbd09 ().

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, and, where the content has one, its release time, before which no key of the list is released whatever the entitlement says. 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, the content’s release time, 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/, 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: 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

3. Encryption schemes and key deliveries

3.1 Schemes

The scheme is what was applied to the media’s bytes:

codelabeldefined bywhat it encryptskey octets
1cencISO/IEC 23001-7AES-CTR, full samples and video subsamples; under HLS, SAMPLE-AES-CTR of fMP4 segments16
2cbc1ISO/IEC 23001-7AES-CBC, full samples and video subsamples16
3censISO/IEC 23001-7AES-CTR, a pattern of video subsamples16
4cbcsISO/IEC 23001-7AES-CBC, a pattern of video subsamples with a constant IV; under HLS, SAMPLE-AES of fMP4 segments16
5AES-128draft-pantos-hls-rfc8216bis-22 §4.4.4.4whole segments, AES-128-CBC with PKCS7 padding16
6SAMPLE-AESdraft-pantos-hls-rfc8216bis-22 §4.4.4.4samples of segments other than fMP4 (MPEG-2 transport streams, packed audio)16
7AES-256-GCMdraft-pantos-hls-rfc8216bis-22 §4.4.4.4whole segments, AES-256-GCM32

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

codekindhow the client gets the keysystem ID
1hls-key-urithe 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). Clearnone
2clear-keythe JSON Web Key Set of the org.w3.clearkey key system (EME §9.1.4). Clearnone
3drma licence of the content protection system the system ID names, which carries the key encrypted for the client’s decryption module. Wrapped16 bytes: a system ID of the DASH-IF registry, not all zero

3.3 Which deliveries carry which schemes

deliveryschemes
hls-key-uricenc, cbcs, AES-128, SAMPLE-AES, AES-256-GCM: the methods HLS defines with an identity key file
clear-keycenc, cbc1, cens, cbcs: ISO/IEC 23001-7, which is what EME decrypts
drmall 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:

fieldbytes
magicWKL1
versionu8, 1 or 2
content_idstring, not empty
required_entitlementstring, not empty: the entitlement class the content demands (§7.2 check 6)
cpixu8 0, or u8 1 and 32 bytes: the SHA-256 of the CPIX document the keys came in, its exact bytes
issuedu64: when the list was signed, in seconds since 1970-01-01T00:00:00Z
not_beforeversion 2 only: u64, the content’s release time, in seconds since 1970-01-01T00:00:00Z: no key of the list is released by a decision made before it (§7.2 check 5)
key countu32, from 1 to 4096
each keykid (16), scheme (u8, §3.1), key_commit (32), then its period (§4.2)
signer_pubkey32: the key holder’s Ed25519 key
signer_idstring
sig64

A list of version 1 has no not_before, and its content no release time. A key holder writes version 2 exactly for a list that states a release time, so a list that states none keeps version 1 and its bytes, and every list of version 1 reads as it did. The release time is the key holder’s, signed with the keys: a content’s release, before which nobody receives its keys whatever their entitlement. It is not read from the CPIX document (§4.4).

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:

tagthenmeans
0nothingthe key has no key period
1index (u32)a period known by its index only (CPIX §7.4.14)
2start (u64), end (u64)a period of wall-clock seconds, from start up to but excluding end
3index (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 a version of 1 or 2; 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, not_before 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

fieldbytes
magicWKE1
versionu8, 1
session16: the playback session or purchase it entitles
holder32: the holder key, an Ed25519 key the device holds
entitlementstring, not empty: the class it grants, compared byte for byte with a list’s required_entitlement
not_beforeu64, seconds
not_afteru64, seconds: the entitlement holds from not_before to not_after, both included
offline_su64: the longest offline licence it permits, in seconds; 0 for none
renewalsu32: how many times an offline licence may be renewed; 0 when offline_s is 0
authority_pubkey32: the entitlement authority’s Ed25519 key
authority_idstring
sig64

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:

fieldvalue
WAI-Key-Idsthe KIDs, each 32 lowercase hexadecimal digits, separated by commas
WAI-Key-Kindstreaming, offline or renewal
WAI-Key-Renewsfor a renewal, renews in 64 lowercase hexadecimal digits
WAI-Key-Timerequest_t, a decimal integer
WAI-Key-Entitlementthe entitlement’s bytes, base64url
WAI-Key-Proofthe 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:

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:

checkdecisioncodewhen
1DeniedEntitlementInvalid4no 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)
2DeniedStale5issued and request_t differ by more than 300
3DeniedReplayed6a receipt earlier in the log carries the same proof
4DeniedUnknownKey7the proxy has no commitment list for the content, or a requested KID is not in it
5DeniedNotReleased14the list has a not_before, and issued is below it
6DeniedNoEntitlement2the entitlement’s entitlement is not the list’s required_entitlement
7DeniedNotYetValid8issued is below the entitlement’s not_before
8DeniedExpired3issued is above the entitlement’s not_after
9DeniedPeriodOutside9a requested key’s period has start and end, and the period does not meet the entitlement: start above the entitlement’s not_after, or end at most the entitlement’s not_before
10DeniedOfflineNotEntitled10the kind is offline or renewal, and offline_s is 0
11DeniedRenewalUnknown11the 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
12DeniedRenewalsExhausted12the kind is renewal, and that receipt’s renewal plus 1 is above renewals
13DeniedRenewalLapsed13the kind is renewal, and issued is above that receipt’s licence_expiry
—Granted1none 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.

Check 5 is the content’s release time, the key holder’s, and comes before every check that reads the entitlement’s class, window or allowance, so a request decided before it is refused DeniedNotReleased whatever its entitlement grants; an entitlement that does not hold, or no entitlement, is still refused by check 1 first, and a stale or replayed request by checks 2 and 3. The release time is read from the list the receipt names by its identity, so a verifier holding the list recomputes it (§10.2 step 3.6). From the release time on, issued is not below it and check 5 refuses nothing. Code 14 follows form 2’s other codes, so no earlier receipt’s code changes.

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 9 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 9 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:

membervalue
form2
content_ida string: the route’s content
list32 bytes: the identity of the content’s commitment list (§4.2), or null when the proxy has none
cpix_sha25632 bytes: the list’s cpix, or null
required_entitlementa string: the list’s, or null
delivery{"kind", "system_id"}: the kind’s label (§3.2), and its system ID, 16 bytes, or null
keysan 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
kindstreaming, offline or renewal
renews32 bytes, or null
entitlementthe entitlement’s bytes, or null when none was presented
request_sha25632 bytes
request_tan integer
proof64 bytes, or null when no entitlement was presented
issuedan integer
decisionits label (§7.2)
licence_expiryan integer, or null
renewalan integer from 0 to 2^32 − 1, or null
upstream_statusthe licence server’s HTTP status, from 100 to 599, or null when the request was not forwarded
response_sha25632 bytes: the SHA-256 of the response body the licence server returned and the proxy forwarded, or null
releasedtrue when the key was released (§9.2, §9.3), false otherwise
joules_microthe decision’s energy, or 0 unmetered
energy_provenanceits class’s label (energy-measurement §2.7: ModelBased, Estimator, OnChipCounter, CalibratedInstrument), or null unmetered
energy_uncertainty, energy_counter_filtering, energy_calibration_refonly for a class that carries them, as energy-measurement §2.7 names and writes them
signer_pubkey32 bytes: the proxy’s Ed25519 key
signer_ida string
sig64 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:

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:

fieldbytes
magicWKT1
versionu8, 1
receipt32: the identity of the receipt that released it
deliveryu8: 1 or 2 (§3.2)
session_typeu8: 1, temporary, for kind streaming; 2, persistent-license, otherwise (EME §9.1.1)
KID countu32, then each KID (16), in ascending order
expiresu64: issued plus from 1 to 300 seconds
proxy_pubkey32: the receipt’s signer_pubkey
sig64: 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 11 to 13 (§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 pathanswer
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/liststhe identity, content_id and key count of each list the proxy holds
GET /v1/healththe 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:

reasonraised 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/: 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; a list with a release time, refusing before it under every entitlement and releasing from it; 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