open-standards/wai/wai-key-proxy
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Transaction Science 3b3cedfc21 wai-quantum 0.7.0, wai-quantum-cli 0.2.0, wai-quantum-qpu 0.3.0 and the job service 0.2.0: the release's versions, its changelog heading, and every lock
The engine, the command line, the provider crate and the job service move to
their next 0.y, which is a major bump before 1.0 (VERSIONING.md); the Python
package stays 0.1.0 and the editor extension keeps its version. Every
dependent's requirement names the new version, and the sixteen lockfiles that
resolve any of them move with them, each read with --locked.

cargo-semver-checks keeps its baselines at the versions last published (0.6.16,
0.1.4 and 0.2.0); wai-quantum-qpu joins the check. Against them it lists the
breaking changes and passes, as each major is bumped.

The changelog's release heading summarises the twelve entries below it and
lists what a caller may have to change. DEPRECATIONS.md stays empty.

The engine's version is among the sealed bytes of a stamped result, so the
page's pinned digests move: the compile's 0a165415... to ac948133..., the
estimate's 0cd25cf6... to 8715e76b... The job pin strips the engine and
schema and does not move. The notebooks' stored outputs are re-stored.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 23:44:28 -04:00
..
src wai-key-proxy: the signing proxy and key server of extensions/key-release.md 2026-10-25 21:41:51 -04:00
tests wai-key-proxy: the signing proxy and key server of extensions/key-release.md 2026-10-25 21:41:51 -04:00
Cargo.lock wai-quantum 0.7.0, wai-quantum-cli 0.2.0, wai-quantum-qpu 0.3.0 and the job service 0.2.0: the release's versions, its changelog heading, and every lock 2026-10-27 23:44:28 -04:00
Cargo.toml wai-key-proxy: the signing proxy and key server of extensions/key-release.md 2026-10-25 21:41:51 -04:00
README.md wai-key-proxy: the signing proxy and key server of extensions/key-release.md 2026-10-25 21:41:51 -04:00
smoke.sh wai-key-proxy: the signing proxy and key server of extensions/key-release.md 2026-10-25 21:41:51 -04:00

wai-key-proxy

A signing proxy in front of any licence server, and the key server it hands clear keys to. It is the service of extensions/key-release.md.

For every licence request it answers, the proxy signs a receipt: a release or a refusal. The decision is computed from signed inputs, so anyone holding the receipts recomputes it: the viewer's entitlement (signed by the operator's entitlement authority and bound to a key the viewer's device holds), the device's signature over the request, the content's commitment list (signed by whoever holds the content keys, and derived from the CPIX document the keys came in), the proxy's clock and the receipts before it. A licensor checks that keys went only to entitled, unexpired sessions from the log, not from a statement.

The proxy holds no content key:

  • each key_commit it signs is copied from the key holder's commitment list;
  • a content protection system's licence carries the key encrypted for the client's decryption module, and the proxy forwards it unchanged;
  • a key the client gets in the clear (an HLS key file, a Clear Key licence) is never relayed: the proxy answers 307 with a release ticket, and the key server, run by the key holder, redeems the ticket once.

A refused request never reaches the licence server.

Run it

cargo build --release
B=target/release/wai-key-proxy

$B keygen holder.key          # the key holder's signing key; prints its public key
$B keygen authority.key       # the entitlement authority's
$B keygen proxy.key           # the proxy's

# A commitment list from the CPIX document the packager and the key server exchanged:
$B list title.cpix.xml svod:premium holder.key https://packager.example title.wkl1

$B serve proxy.json           # the proxy
$B keyserver keys.json        # the key server, for HLS key files and Clear Key

proxy.json:

{
  "listen": "127.0.0.1:9710",
  "admin_listen": "127.0.0.1:9711",
  "signer_key": "/var/lib/wai-key-proxy/proxy.key",
  "signer_id": "https://keys.operator.example",
  "authorities": ["<entitlement authority public key, hex>"],
  "lists": ["/var/lib/wai-key-proxy/lists/title.wkl1"],
  "routes": [
    {"name": "title.a", "content_id": "title-0001", "delivery": "drm",
     "system_id": "<the content protection system's DASH-IF system ID>",
     "upstream": "http://127.0.0.1:9720/licence",
     "upstream_headers": {"<a header the licence server expects>": "<its value>"}},
    {"name": "title.hls", "content_id": "title-0001", "delivery": "hls-key-uri",
     "key_url": "https://keys.operator.example/v1/key"}
  ],
  "log": "/var/lib/wai-key-proxy/receipts.jsonl",
  "ticket_ttl_s": 60,
  "upstream_timeout_ms": 10000,
  "max_body": 65536
}

keys.json:

{
  "listen": "127.0.0.1:9712",
  "cpix": ["/var/lib/wai-key-server/title.cpix.xml"],
  "proxies": ["<proxy public key, hex>"],
  "redeemed": "/var/lib/wai-key-server/redeemed.txt"
}

The proxy refuses to start on a route whose delivery cannot carry a listed key's scheme (§3.3), on a list whose signature does not verify, on a log written by another proxy key, and on a log another process holds.

A licence server reached over TLS is reached through a local forwarder that originates TLS (upstream is http://): a reverse proxy listening on 127.0.0.1 whose upstream is the licence server's https:// address. The proxy carries no TLS stack of its own; the reverse proxy in front of it terminates the client's TLS.

Requests

A client sends the licence challenge (or nothing, for an HLS key file) to /v1/licence/<route> with the header fields of §6.2: WAI-Key-Ids, WAI-Key-Kind, WAI-Key-Time, WAI-Key-Entitlement, WAI-Key-Proof and, for a renewal, WAI-Key-Renews. wai-key-proxy sign-request writes them for a test or a tool; a player computes the proof with the Web Cryptography API (Ed25519 and SHA-256).

answer when
200 (or the licence server's status), the licence a content protection system's licence was granted and forwarded
307 to the key server with wai-ticket a clear key was granted
403 and the receipt, application/json refused; nothing was forwarded
400, 404 the request names no key or no route; it has no receipt

Every decided request names its receipt in WAI-Key-Receipt, readable at /v1/receipts/<identity>. /v1/lists and /v1/health describe the proxy; the whole log is on admin_listen at /v1/log, which is not public.

Check a log

$B verify receipts.jsonl <authorities,hex> <proxies,hex> title.wkl1 ...

runs the reference verifier of §10 over the log. The independent verifier of the conformance corpus is live-conformance/key_release_verify.py.

Tests

cargo test runs the proxy, the key server and a fixture licence server on loopback: releases, refusals that never reach the licence server, a replay sent while the first request is still being forwarded, offline licences and renewals, a licence server's refusal, tickets redeemed once, no key byte in the log, a restart that keeps the replay and renewal history, and the whole log under the reference verifier. bash smoke.sh runs the release binary as an operator would.