open-standards/wai/wai-key-proxy/smoke.sh
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

115 lines
6.7 KiB
Shell
Executable file

#!/usr/bin/env bash
# The proxy's binary end to end, as an operator runs it: keys, a commitment list from a CPIX
# document, an entitlement, the proxy in front of a licence server and the key server, then
# a refused request, a granted one, an HLS key file by ticket, and the whole log verified.
# The licence server is a fixture written here that answers every request with fixed bytes.
#
# bash smoke.sh [<release-binary>]
#
# Exit 0 only if every check holds. Needs curl and python3.
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
bin="${1:-${CARGO_TARGET_DIR:-$here/target}/release/wai-key-proxy}"
corpus="$here/../live-conformance/keys"
t="$(mktemp -d)"
pids=()
cleanup() {
for p in ${pids[@]+"${pids[@]}"}; do kill "$p" 2>/dev/null || true; wait "$p" 2>/dev/null || true; done
rm -rf "$t"
}
trap cleanup EXIT
fail() { echo "FAIL $*"; exit 1; }
pass() { echo "PASS $*"; }
port() { python3 -I -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1])'; }
wait_up() { for _ in $(seq 1 100); do curl -s -o /dev/null "$1" && return 0; sleep 0.1; done; fail "$1 did not come up"; }
holder=$("$bin" keygen "$t/holder.key")
authority=$("$bin" keygen "$t/authority.key")
proxy=$("$bin" keygen "$t/proxy.key")
device=$("$bin" keygen "$t/device.key")
[ "$(stat -c %a "$t/proxy.key" 2>/dev/null || stat -f %Lp "$t/proxy.key")" = 600 ] || fail "a key file is not mode 0600"
pass "keys written, mode 0600"
cp "$corpus/film.cpix.xml" "$corpus/episode.cpix.xml" "$t/"
"$bin" list "$t/film.cpix.xml" svod:premium "$t/holder.key" https://packager.example "$t/film.wkl1" >/dev/null
"$bin" list "$t/episode.cpix.xml" svod:basic "$t/holder.key" https://packager.example "$t/episode.wkl1" >/dev/null
pass "commitment lists derived from the CPIX documents"
now=$(date +%s)
premium=$("$bin" entitle "$t/authority.key" https://entitlements.example "$device" svod:premium $((now - 60)) $((now + 3600)))
basic=$("$bin" entitle "$t/authority.key" https://entitlements.example "$device" svod:basic $((now - 60)) $((now + 3600)))
lp=$(port); pp=$(port); ap=$(port); kp=$(port)
python3 -I -c '
import http.server, sys
class H(http.server.BaseHTTPRequestHandler):
def do_POST(self):
body = self.rfile.read(int(self.headers.get("content-length", 0)))
self.send_response(200); self.send_header("content-type", "application/octet-stream"); self.end_headers()
self.wfile.write(b"LICENCE:" + body)
def log_message(self, *a): pass
http.server.HTTPServer(("127.0.0.1", int(sys.argv[1])), H).serve_forever()
' "$lp" & pids+=($!)
cat > "$t/keys.json" <<EOF
{"listen": "127.0.0.1:$kp", "cpix": ["$t/episode.cpix.xml"], "proxies": ["$proxy"], "redeemed": "$t/redeemed.txt"}
EOF
cat > "$t/proxy.json" <<EOF
{
"listen": "127.0.0.1:$pp",
"admin_listen": "127.0.0.1:$ap",
"signer_key": "$t/proxy.key",
"signer_id": "https://keys.proxy.example",
"authorities": ["$authority"],
"lists": ["$t/film.wkl1", "$t/episode.wkl1"],
"routes": [
{"name": "film.a", "content_id": "film-0001", "delivery": "drm", "system_id": "edef8ba9-79d6-4ace-a3c8-27dcd51d21ed",
"upstream": "http://127.0.0.1:$lp/licence"},
{"name": "episode.hls", "content_id": "episode-0002", "delivery": "hls-key-uri", "key_url": "http://127.0.0.1:$kp/v1/key"}
],
"log": "$t/receipts.jsonl"
}
EOF
"$bin" keyserver "$t/keys.json" 2>"$t/keyserver.log" & pids+=($!)
"$bin" serve "$t/proxy.json" 2>"$t/proxy.log" & pids+=($!)
wait_up "http://127.0.0.1:$pp/v1/health"
wait_up "http://127.0.0.1:$kp/v1/key"
pass "the proxy, the key server and the licence server are up"
kids=$(python3 -I -c 'import json,sys; d=json.load(open(sys.argv[1])); print(",".join(k["kid"] for v in d["vectors"] if v["name"]=="release_drm" for k in v["log"][0]["keys"]))' "$corpus/vectors.json")
printf 'licence challenge' > "$t/challenge"
# The signed header fields of a request, as curl arguments in the array h.
sign() {
h=()
while IFS= read -r line; do h+=(-H "$line"); done < <("$bin" sign-request "$t/device.key" "$1" "$2" "$3" "$4" streaming "$5" "$6")
[ "${#h[@]}" -gt 0 ] || fail "sign-request wrote nothing"
}
code=$(curl -s -o "$t/refused.json" -w '%{http_code}' -X POST --data-binary @"$t/challenge" \
-H "WAI-Key-Ids: $kids" "http://127.0.0.1:$pp/v1/licence/film.a")
[ "$code" = 403 ] || fail "a request with no entitlement answered $code"
grep -q '"decision":"DeniedEntitlementInvalid"' "$t/refused.json" || fail "the refusal's receipt"
pass "a request with no entitlement is refused with its receipt"
sign "$basic" film-0001 drm edef8ba9-79d6-4ace-a3c8-27dcd51d21ed "$kids" "$t/challenge"
code=$(curl -s -o "$t/wrong.json" -w '%{http_code}' -X POST --data-binary @"$t/challenge" "${h[@]}" "http://127.0.0.1:$pp/v1/licence/film.a")
[ "$code" = 403 ] && grep -q '"decision":"DeniedNoEntitlement"' "$t/wrong.json" || fail "the wrong class answered $code"
pass "the wrong entitlement class is refused"
sign "$premium" film-0001 drm edef8ba9-79d6-4ace-a3c8-27dcd51d21ed "$kids" "$t/challenge"
code=$(curl -s -D "$t/granted.h" -o "$t/licence" -w '%{http_code}' -X POST --data-binary @"$t/challenge" "${h[@]}" "http://127.0.0.1:$pp/v1/licence/film.a")
[ "$code" = 200 ] && [ "$(cat "$t/licence")" = "LICENCE:licence challenge" ] || fail "a granted request answered $code"
id=$(tr -d '\r' < "$t/granted.h" | awk -F': ' 'tolower($1)=="wai-key-receipt"{print $2}')
curl -sf "http://127.0.0.1:$pp/v1/receipts/$id" | grep -q '"released":true' || fail "the release's receipt"
pass "a granted request is forwarded and its licence returned, with its receipt"
hkid=$(python3 -I -c 'import json,sys; d=json.load(open(sys.argv[1])); print([v for v in d["vectors"] if v["name"]=="release_hls_key_uri"][0]["log"][0]["keys"][0]["kid"])' "$corpus/vectors.json")
sign "$basic" episode-0002 hls-key-uri - "$hkid" -
loc=$(curl -s -o /dev/null -w '%{redirect_url}' "${h[@]}" "http://127.0.0.1:$pp/v1/licence/episode.hls")
[ -n "$loc" ] || fail "no ticket"
curl -sf -o "$t/key.bin" "$loc" || fail "the key server did not redeem the ticket"
[ "$(wc -c < "$t/key.bin" | tr -d ' ')" = 16 ] || fail "the key file is not 16 octets"
code=$(curl -s -o /dev/null -w '%{http_code}' "$loc")
[ "$code" = 409 ] || fail "a ticket redeemed twice answered $code"
keyhex=$(od -An -tx1 "$t/key.bin" | tr -d ' \n')
! grep -q "$keyhex" "$t/receipts.jsonl" || fail "the key is in the proxy's log"
pass "an HLS key file is released by a ticket the key server redeems once; the key is not in the proxy's log"
curl -sf -o "$t/log.jsonl" "http://127.0.0.1:$ap/v1/log" || fail "the log listener"
out=$("$bin" verify "$t/log.jsonl" "$authority" "$proxy" "$t/film.wkl1" "$t/episode.wkl1") || fail "the log does not verify: $out"
echo "$out" | grep -q '^accept: 4 receipts, 2 released' || fail "the log states: $out"
pass "the whole log verifies: $out"