open-standards/wai/wai-rs/tests/key_release.rs
Transaction Science eec5d8dfa0 wai: key release, the proxy form of wai.video.keys (extensions/key-release.md)
The scheme (cenc, cbc1, cens, cbcs; the HLS methods AES-128, SAMPLE-AES,
AES-256-GCM) and the key delivery (HLS key URI, Clear Key, a content protection
system by its system ID) of each released key; the commitment list WKL1 derived
from a CPIX document, so the proxy copies each key_commit and holds no key; the
entitlement WKE1 bound to a device key and the device's proof over each request;
the release decision as a pure function of signed inputs (freshness, replay, key
periods, offline licences and renewals); the release ticket WKT1 for a clear
delivery, so the proxy never relays a key; verification of a proxy's log.

The key_release module of wai-rs (feature key_release), its unit tests, and the
corpus live-conformance/keys/, generated from fixed seeds by
wai_key_release_vectors and checked by tests/key_release.rs.

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

83 lines
4.3 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");
let mut decisions: Vec<(String, String)> = rows("### 7.2 The checks, in order").iter().map(|r| (r[1].clone(), r[2].clone())).collect();
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");
}