//! The proxy's and the key server's configuration, as JSON. use std::collections::{BTreeMap, HashMap}; use std::net::SocketAddr; use std::path::PathBuf; use serde::Deserialize; use wai::key_release::{chain_of, cpix, unhex, unhex_vec, CommitmentList, Delivery, DeliveryKind, Revocation}; /// The proxy's configuration file. #[derive(Deserialize)] #[serde(deny_unknown_fields)] pub struct ProxyFile { /// Where the proxy listens for licence requests. pub listen: SocketAddr, /// Where it serves its whole log; not public (§9.6). None for no such listener. #[serde(default)] pub admin_listen: Option, /// The file holding the proxy's Ed25519 seed (64 hexadecimal digits, mode 0600). pub signer_key: PathBuf, pub signer_id: String, /// The entitlement authority keys it trusts, hexadecimal. pub authorities: Vec, /// The keys it trusts to sign output statements of class 2 (§14.2), hexadecimal. #[serde(default)] pub attesters: Vec, /// The commitment lists (`WKL1` files) of the content it serves: for each content one list, /// or one chain (§4.6), in any order. pub lists: Vec, /// Where lists given by `POST /v1/lists` are kept, one file a list, read again at the start /// with `lists`. None: the proxy takes no list while it runs. #[serde(default)] pub list_dir: Option, pub routes: Vec, /// The log: one receipt a line (§9.5). pub log: PathBuf, /// The revocations it holds (`WKR1`, §16), one a line in hexadecimal: read at the start and /// added to by `POST /v1/revocations` on the listener that is not public. None: the proxy /// holds none and takes none. #[serde(default)] pub revocations: Option, /// Sign a checkpoint of the log (§17) after this many receipts since the last; 0 for none /// but those `POST /v1/checkpoint` asks for. #[serde(default)] pub checkpoint_every: u64, #[serde(default = "ticket_ttl")] pub ticket_ttl_s: u64, #[serde(default = "upstream_timeout")] pub upstream_timeout_ms: u64, #[serde(default = "max_body")] pub max_body: usize, } fn ticket_ttl() -> u64 { 60 } fn upstream_timeout() -> u64 { 10_000 } fn max_body() -> usize { 1 << 16 } /// One route (§9.1). #[derive(Deserialize)] #[serde(deny_unknown_fields)] pub struct RouteFile { pub name: String, pub content_id: String, /// `drm`, `hls-key-uri` or `clear-key`. pub delivery: String, /// For `drm`: the system ID, a UUID. #[serde(default)] pub system_id: Option, /// For `drm`: the licence server, `http://host:port/path`. #[serde(default)] pub upstream: Option, /// For `drm`: header fields sent to the licence server with each forwarded request. #[serde(default)] pub upstream_headers: BTreeMap, /// For a clear delivery: the key server's URI the ticket is appended to. #[serde(default)] pub key_url: Option, } /// A route, checked. #[derive(Clone, Debug)] pub struct Route { pub name: String, pub content_id: String, pub delivery: Delivery, pub upstream: Option, pub upstream_headers: Vec<(String, String)>, pub key_url: Option, } /// The proxy's configuration, checked. pub struct Proxy { pub listen: SocketAddr, pub admin_listen: Option, pub signer: ed25519_dalek::SigningKey, pub signer_id: String, pub authorities: Vec<[u8; 32]>, pub attesters: Vec<[u8; 32]>, /// Each content's chain of lists (§4.6), its first list first. pub lists: HashMap>, pub list_dir: Option, pub routes: HashMap, pub log: PathBuf, /// Where its revocations are kept, and those the file held at the start (§16.4). pub revocations: Option<(PathBuf, Vec)>, pub checkpoint_every: u64, pub ticket_ttl_s: u64, pub upstream_timeout_ms: u64, pub max_body: usize, } fn route_name_ok(n: &str) -> bool { !n.is_empty() && n.len() <= 64 && n.bytes().all(|c| c.is_ascii_alphanumeric() || matches!(c, b'.' | b'-' | b'_')) } impl Proxy { pub fn load(path: &std::path::Path) -> Result { let text = std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?; let f: ProxyFile = serde_json::from_str(&text).map_err(|e| format!("{}: {e}", path.display()))?; Proxy::from_file(f) } pub fn from_file(f: ProxyFile) -> Result { let signer = crate::read_key(&f.signer_key)?; let authorities = f .authorities .iter() .map(|a| unhex::<32>(a).ok_or_else(|| format!("authority {a}: not 64 lowercase hexadecimal digits"))) .collect::, _>>()?; let mut paths = f.lists.clone(); if let Some(dir) = &f.list_dir { match std::fs::read_dir(dir) { Ok(entries) => { let mut kept: Vec = entries.flatten().map(|e| e.path()).filter(|p| p.extension().is_some_and(|x| x == "wkl1")).collect(); kept.sort(); paths.extend(kept); } Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} Err(e) => return Err(format!("{}: {e}", dir.display())), } } let mut by_content: HashMap> = HashMap::new(); for p in &paths { let b = std::fs::read(p).map_err(|e| format!("{}: {e}", p.display()))?; let l = CommitmentList::parse(&b).map_err(|e| format!("{}: the list is refused ({e:?})", p.display()))?; by_content.entry(l.content_id.clone()).or_default().push(l); } let mut lists = HashMap::new(); for (content, ls) in by_content { let chain = chain_of(ls).ok_or_else(|| format!("content {content}: its lists are not one list or one chain (key-release §4.6)"))?; lists.insert(content, chain); } if !(1..=wai::key_release::MAX_TICKET_S).contains(&f.ticket_ttl_s) { return Err("ticket_ttl_s must be from 1 to 300".into()); } let mut routes = HashMap::new(); for r in f.routes { if !route_name_ok(&r.name) { return Err(format!("route {:?}: a name is 1 to 64 of A-Z a-z 0-9 . - _", r.name)); } let kind = DeliveryKind::from_label(&r.delivery).ok_or_else(|| format!("route {}: delivery {:?}", r.name, r.delivery))?; let system_id = match (kind, &r.system_id) { (DeliveryKind::Drm, Some(s)) => Some(cpix::uuid(s).filter(|u| *u != [0; 16]).ok_or_else(|| format!("route {}: system_id {s}", r.name))?), (DeliveryKind::Drm, None) => return Err(format!("route {}: a drm route names its system_id", r.name)), (_, Some(_)) => return Err(format!("route {}: only a drm route has a system_id", r.name)), (_, None) => None, }; let upstream = match (kind, &r.upstream) { (DeliveryKind::Drm, Some(u)) => Some(crate::http::Target::parse(u).ok_or_else(|| format!("route {}: upstream {u} is not http://host:port/path", r.name))?), (DeliveryKind::Drm, None) => return Err(format!("route {}: a drm route names its licence server", r.name)), (_, Some(_)) => return Err(format!("route {}: a clear delivery is never forwarded (§9.3)", r.name)), (_, None) => None, }; if kind.is_clear() != r.key_url.is_some() { return Err(format!("route {}: a clear delivery, and only one, names its key server", r.name)); } if let Some(chain) = lists.get(&r.content_id) { if let Some(k) = chain.iter().flat_map(|l| &l.keys).find(|k| !kind.carries(k.scheme)) { return Err(format!("route {}: its delivery does not carry the {} key {} (§3.3)", r.name, k.scheme.label(), wai::key_release::hex(&k.kid))); } } else { eprintln!("wai-key-proxy: route {} names content {} with no list: every request to it is refused", r.name, r.content_id); } let route = Route { name: r.name.clone(), content_id: r.content_id, delivery: Delivery { kind, system_id }, upstream, upstream_headers: r.upstream_headers.into_iter().collect(), key_url: r.key_url, }; if routes.insert(r.name.clone(), route).is_some() { return Err(format!("route {}: named twice", r.name)); } } let revocations = match f.revocations { None => None, Some(p) => { let held = match std::fs::read_to_string(&p) { Ok(text) => text .lines() .enumerate() .filter(|(_, l)| !l.trim().is_empty()) .map(|(n, l)| { unhex_vec(l.trim()) .and_then(|b| Revocation::parse(&b)) .filter(|v| authorities.contains(&v.authority_pubkey)) .ok_or_else(|| format!("{} line {}: not a revocation that holds, by a trusted authority (§16.1)", p.display(), n + 1)) }) .collect::, _>>()?, Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(), Err(e) => return Err(format!("{}: {e}", p.display())), }; Some((p, held)) } }; Ok(Proxy { listen: f.listen, admin_listen: f.admin_listen, signer, signer_id: f.signer_id, authorities, attesters: f .attesters .iter() .map(|a| unhex::<32>(a).ok_or_else(|| format!("attester {a}: not 64 lowercase hexadecimal digits"))) .collect::, _>>()?, lists, list_dir: f.list_dir, routes, log: f.log, revocations, checkpoint_every: f.checkpoint_every, ticket_ttl_s: f.ticket_ttl_s, upstream_timeout_ms: f.upstream_timeout_ms, max_body: f.max_body, }) } } /// The key server's configuration file. #[derive(Deserialize)] #[serde(deny_unknown_fields)] pub struct KeyServerFile { pub listen: SocketAddr, /// The CPIX documents holding the keys it serves, in the clear. pub cpix: Vec, /// The proxy keys whose tickets it redeems, hexadecimal. pub proxies: Vec, /// The identities of the tickets redeemed, one a line. pub redeemed: PathBuf, } /// The key provider's configuration file (key-release §15.1). #[derive(Deserialize)] #[serde(deny_unknown_fields)] pub struct ProviderFile { pub listen: SocketAddr, /// The file holding the key holder's Ed25519 seed: it signs the lists and the receipts. pub signer_key: PathBuf, pub signer_id: String, /// The packagers' keys it serves, hexadecimal. pub requesters: Vec, /// The contents it serves. pub contents: Vec, /// The file holding the seed its test keys are derived from (64 hexadecimal digits): the /// test derivation (§15.3). Exactly one of `seed` and `secret`. #[serde(default)] pub seed: Option, /// The file holding the secret of the keyed derivation (§15.3), 64 hexadecimal digits, mode /// 0600: a provider whose keys nobody makes without that file. #[serde(default)] pub secret: Option, /// The log: one receipt a line, with the list it signed. pub log: PathBuf, } /// A content a provider serves: what its lists state (§4.1). #[derive(Deserialize)] #[serde(deny_unknown_fields)] pub struct ProviderContent { pub content_id: String, pub required_entitlement: String, /// The content's release time, in seconds since 1970. #[serde(default)] pub not_before: Option, /// Its key periods as a schedule (§15.1), when they are one. #[serde(default)] pub schedule: Option, } /// A content's schedule (§15.1): `count` periods of `length` seconds from `origin`. #[derive(Deserialize, Clone, Copy)] #[serde(deny_unknown_fields)] pub struct ScheduleFile { pub origin: u64, pub length: u64, pub count: u32, }