open-standards/wai/wai-rs/tests/key_release.rs
Transaction Science c13dcac6ad
Some checks failed
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 15s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 26s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 27s
wai-fleet service / does this change touch it (push) Successful in 14s
wai-quantum studio and desktop / does this change touch it (push) Successful in 17s
WAI Studio / does this change touch it (push) Successful in 21s
wai-sink-swift native sink package / does this change touch it (push) Successful in 26s
wai-sink-android native sink library / does this change touch it (push) Successful in 4m46s
wai-app and Create / wai-app · does this change touch it (push) Successful in 5m23s
wai-ledger service / does this change touch it (push) Successful in 5m7s
joule-moq-http live HTTP egress / joule-moq-http · does this change touch it (push) Successful in 5m43s
wai-audit collector and player integration / does this change touch it (push) Successful in 5m24s
wai-key-proxy / does this change touch it (push) Successful in 5m9s
WAI operation catalogue / does this change touch it (push) Successful in 5m15s
wai-quantum for JavaScript / does this change touch it (push) Successful in 5m2s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 3m5s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 8s
joule-moq-http live HTTP egress / joule-moq-http, every target (push) Successful in 2m48s
wai-fleet service / service · linux (push) Successful in 2m11s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 49s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 34s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 6m46s
wai-key-proxy / proxy · linux (push) Failing after 44s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 8m58s
wai-ledger service / service · linux (push) Successful in 2m38s
wai-audit collector and player integration / collector · linux (push) Successful in 3m53s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 9m34s
wai-quantum for JavaScript / javascript · linux (push) Successful in 5m6s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 1m54s
wai-quantum studio and desktop / studio · browser (push) Successful in 10m13s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 10m8s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 1m12s
WAI Studio / Studio in WebKit · linux (push) Successful in 11m42s
wai-fleet service / service · macos (push) Successful in 1m10s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 13m30s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 4m21s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 10m6s
wai-ledger service / service · macos (push) Successful in 2m17s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 2m41s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 13m40s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 7m33s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 10m41s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 12m55s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 15m38s
wai-quantum studio and desktop / desktop app · windows (push) Successful in 14m14s
Key release: a key provider a packager asks for content keys, CPIX over HTTP
key-release §15 (new). The key provider is the key holder's service a packager asks, in
the workflow the CPIX guidelines call the encryptor consumer. Its routes are neutral: they
carry CPIX documents and signed records and name no content protection system.

- A request is a CPIX document that names a content's keys (KID, scheme, key period) and
  carries none, signed by the requester over its bytes and its clock.
- The provider decides it from signed inputs (a requester it serves, the proof, freshness,
  replay, a document that reads, a content it serves), answers a provision with the same
  keys' document and a refusal with nothing, signs the commitment list §4.4 derives from the
  answer's exact bytes, and signs a receipt naming request, answer and list by digest.
- A verifier holding the three checks each against the others (§15.5).

Reference: key_release::provider (wai-rs), key_release::cpix::{request, write}, and
wai-key-release provider (wai-key-proxy/src/provider.rs), whose keys are test keys derived
from a seed and are stated as such. tests/provider.rs asks it for a rotating channel's
keys, loads the list in a signing proxy and the answer in a key server, and has a device's
Clear Key request released by a ticket whose key is the provider's.

Planted faults: an answer whose key is not the one the list commits to, under a receipt
signed for it, is refused cpix_mismatch (n_response_not_the_lists, and over HTTP in
tests/provider.rs); a key of a period after the entitlement ends is refused
DeniedPeriodOutside; and a negative control for every other reason of §15.5.
live-conformance/keys/provider.json holds 26 exchanges; key_release_verify.py verifies each
from the text, derives every key again from the seed, and requires twelve mutated copies
to fail. The 51 logs of vectors.json keep their bytes (its principals list gains two).

The WAI-Provider-* fields and the receipt's kind are registered. Decision D17. Not done and
not claimed: CPIX's encrypted delivery, and any content protection system's own key or
licence exchange.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-11 08:08:07 -05:00

106 lines
5.6 KiB
Rust

//! Key release (extensions/key-release.md): the committed corpus `live-conformance/keys/`
//! regenerates byte for byte from its fixed seeds, holds no other file, every case in it gets
//! the outcome it states, and the extension's §11 lists exactly the reasons the reference
//! reports. The generator itself asserts that the corpus gives every one of them.
#![cfg(feature = "key_release")]
use std::collections::BTreeMap;
use std::path::PathBuf;
use wai::key_release::{corpus, Decision, DeliveryKind, Reason, Scheme};
fn wai_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..")
}
fn committed() -> BTreeMap<String, String> {
let dir = wai_dir().join("live-conformance/keys");
std::fs::read_dir(&dir)
.expect("the committed corpus")
.flatten()
.map(|e| (e.file_name().to_string_lossy().into_owned(), e.path()))
.filter(|(n, _)| n != "README.md")
.map(|(n, p)| (n, std::fs::read_to_string(p).expect("a text file")))
.collect()
}
#[test]
fn the_committed_corpus_regenerates_from_its_seeds() {
let files = committed();
let generated: BTreeMap<String, String> = corpus::generate().into_iter().collect();
let extra: Vec<&String> = files.keys().filter(|k| !generated.contains_key(*k)).collect();
let missing: Vec<&String> = generated.keys().filter(|k| !files.contains_key(*k)).collect();
let differ: Vec<&String> = generated.iter().filter(|(k, v)| files.get(*k).is_some_and(|f| f != *v)).map(|(k, _)| k).collect();
assert!(extra.is_empty() && missing.is_empty() && differ.is_empty(), "extra {extra:?}, missing {missing:?}, differ {differ:?}: run `wai_key_release_vectors gen`");
}
#[test]
fn every_case_gets_its_stated_outcome() {
let fails = corpus::verify(&committed());
assert!(fails.is_empty(), "{}", fails.join("\n"));
}
/// §11 of the extension lists exactly the codes `Reason::ALL` has, each once, in order.
#[test]
fn the_extension_lists_the_reasons() {
let text = std::fs::read_to_string(wai_dir().join("extensions/key-release.md")).expect("the extension");
let at = text.find("\n## 11. Reasons").expect("a §11");
let end = text[at + 1..].find("\n## ").map_or(text.len(), |e| at + 1 + e);
let listed: Vec<String> =
text[at..end].lines().filter(|l| l.starts_with("| `")).filter_map(|l| l.split('`').nth(1).map(String::from)).collect();
let want: Vec<String> = Reason::ALL.iter().map(|r| r.code().to_string()).collect();
assert_eq!(listed, want, "§11's table and Reason::ALL differ");
}
/// The rows of the table under heading `head`, up to the next heading, split on `|`.
fn rows(head: &str) -> Vec<Vec<String>> {
let text = std::fs::read_to_string(wai_dir().join("extensions/key-release.md")).expect("the extension");
let at = text.find(&format!("\n{head}\n")).unwrap_or_else(|| panic!("a heading {head}"));
let body = &text[at + head.len() + 2..];
let end = body.find("\n#").unwrap_or(body.len());
body[..end]
.lines()
.filter(|l| l.starts_with("| ") && !l.starts_with("| ---") && !l.starts_with("|---"))
.skip(1)
.map(|l| l.trim_matches('|').split('|').map(|c| c.trim().trim_matches('`').to_string()).collect())
.collect()
}
/// §3.1, §3.2 and §7.2 give each scheme, delivery and decision the code and label the
/// reference writes, and no other.
#[test]
fn the_extension_tables_are_the_reference_codes() {
let schemes: Vec<(String, String)> = rows("### 3.1 Schemes").iter().map(|r| (r[0].clone(), r[1].clone())).collect();
let want: Vec<(String, String)> = Scheme::ALL.iter().map(|s| (s.code().to_string(), s.label().to_string())).collect();
assert_eq!(schemes, want, "§3.1");
let deliveries: Vec<(String, String)> = rows("### 3.2 Key deliveries").iter().map(|r| (r[0].clone(), r[1].clone())).collect();
let want: Vec<(String, String)> = DeliveryKind::ALL.iter().map(|d| (d.code().to_string(), d.label().to_string())).collect();
assert_eq!(deliveries, want, "§3.2");
// §7.2's fourteen, and the four of §14.4 that only an entitlement with rules reaches.
let mut decisions: Vec<(String, String)> = rows("### 7.2 The checks, in order").iter().chain(rows("### 14.4 The checks").iter()).map(|r| (r[1].clone(), r[2].clone())).collect();
assert_eq!(rows("### 14.4 The checks").iter().map(|r| r[1].clone()).collect::<Vec<_>>(), Decision::ALL.iter().filter(|d| d.of_rules()).map(|d| d.label().to_string()).collect::<Vec<_>>(), "§14.4");
decisions.sort();
let mut want: Vec<(String, String)> = Decision::ALL.iter().map(|d| (d.label().to_string(), d.code().to_string())).collect();
want.sort();
assert_eq!(decisions, want, "§7.2 and §14.4");
}
/// §15.3 and §15.5 give each decision of a key provider its code and label, and each step of
/// an exchange's verification its reasons, as the reference has them.
#[test]
fn the_key_providers_tables_are_the_reference_codes() {
use wai::key_release::provider::{Fault, Provision};
let mut decisions: Vec<(String, String)> = rows("### 15.3 The decision and the answer").iter().map(|r| (r[1].clone(), r[2].clone())).collect();
decisions.sort();
let mut want: Vec<(String, String)> = Provision::ALL.iter().map(|d| (d.label().to_string(), d.code().to_string())).collect();
want.sort();
assert_eq!(decisions, want, "§15.3");
let mut reasons: Vec<String> = Vec::new();
for r in rows("### 15.5 Verification") {
for code in r[1].split(',').map(|c| c.trim().trim_matches('`').to_string()) {
if !reasons.contains(&code) {
reasons.push(code);
}
}
}
assert_eq!(reasons, Fault::ALL.iter().map(|f| f.code().to_string()).collect::<Vec<_>>(), "§15.5");
}