open-standards/wai/wai-key-proxy/README.md
Transaction Science 8428bd120d wai-key-proxy: the signing proxy and key server of extensions/key-release.md
A proxy in front of any licence server: it decides each licence request from signed
inputs, appends its signed receipt to a durable single-writer log before it
answers, refuses without forwarding, forwards a granted content protection
system's request and returns the licence unchanged, and answers a clear key with a
307 and a release ticket instead of relaying it. A replay sent while the first
request is still being forwarded waits and is refused after it, so the log stays in
decision order. The key server, run by the key holder, redeems each ticket once.

CLI: serve, keyserver, keygen, pubkey, list (a commitment list from a CPIX
document), entitle, sign-request, verify. tests/end_to_end.rs runs everything on
loopback against a fixture licence server; smoke.sh runs the release binary.

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

122 lines
5.1 KiB
Markdown

# 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`](../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
```sh
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`:
```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`:
```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
```sh
$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.