//! joule-moq's reading of rating claims against WAI's age-assurance corpus //! (`wai/live-conformance/age/vectors.json`, age-assurance §12), as a third reader beside //! `wai-rs` and the corpus's Python verifier. //! //! - every claim of a `ratings`, `serving` or `predicate` case reads here exactly where the //! corpus has it read: a case refused at `["claim", i]` with `record_malformed`, `bad_key` //! or `bad_signature` has its claim `i` refused with that code here, and every other claim //! of the corpus reads, with the age and the signer the corpus states; //! - every request of an accepted `serving` case gets the age the corpus states, from the //! claims the relay holds when it serves it, through [`Claim::covers`], and the track's //! digest is the corpus's; a request whose session presented a proof of a room key //! (age-assurance §5.5) is checked as the relay checks it ([`grant_proof::check_proof`] with //! [`grant_proof::NO_GRANT`]), with the room's authority and clock as the relay's room keys, //! and gets the outcome the corpus states: served as the room's own service, served by its //! threshold, or refused with the proof's reason. //! //! The corpus lives beside this crate in the monorepo. If it cannot be read the test fails; //! it never passes on nothing. use joule_moq::grant_proof; use joule_moq::rating::{read, track_digest, Claim}; use joule_moq::{TrackName, TrackNamespace}; use serde_json::Value; const CORPUS: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../../../wai/live-conformance/age/vectors.json"); /// The reading refusals, the codes this crate reports (age-assurance §11). const READING: [&str; 3] = ["record_malformed", "bad_key", "bad_signature"]; fn corpus() -> Value { serde_json::from_str(&std::fs::read_to_string(CORPUS).expect("the age-assurance corpus")).expect("JSON") } fn unhex(s: &str) -> Vec { assert!(s.len().is_multiple_of(2), "hexadecimal of whole bytes"); (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("hexadecimal")).collect() } fn key32(s: &str) -> [u8; 32] { unhex(s).try_into().expect("32 bytes") } fn texts(c: &Value) -> Vec { c["claims"].as_array().map(|a| a.iter().map(|t| t.as_str().expect("a claim's text").to_string()).collect()).unwrap_or_default() } #[test] fn every_claim_reads_as_the_corpus_has_it() { let doc = corpus(); let (mut read_ok, mut refused) = (0, 0); for c in doc["cases"].as_array().expect("cases") { let kind = c["kind"].as_str().expect("a kind"); if !["ratings", "serving", "predicate"].contains(&kind) { continue; } let name = c["name"].as_str().expect("a name"); let e = &c["expect"]; // The claim at which a reading refusal stops the case, and its code. let stop = (e["outcome"] == "refuse" && e["at"][0] == "claim" && READING.contains(&e["reason"].as_str().unwrap_or(""))).then(|| (e["at"][1].as_u64().expect("an index") as usize, e["reason"].as_str().expect("a code"))); for (i, t) in texts(c).iter().enumerate() { let got = read(t.as_bytes()); if let Some((at, code)) = stop && at == i { assert_eq!(got.map(|_| ()).map_err(|r| r.code()), Err(code), "{name}: claim {i}"); refused += 1; break; } // A sink passes over a claim that does not read or verify (§6.2): the corpus // states no place for it. if kind == "predicate" { read_ok += usize::from(got.is_ok()); continue; } let cl = got.unwrap_or_else(|r| panic!("{name}: claim {i} refused {} here, read by the corpus", r.code())); read_ok += 1; if kind == "ratings" && e["outcome"] == "accept" { let f = &e["claims"][i]; assert_eq!(cl.age(), f["age"].as_u64().expect("an age"), "{name}: claim {i}'s age"); assert_eq!(joule_moq::rating::to_hex(&cl.signer), f["signer"].as_str().expect("a signer"), "{name}: claim {i}'s signer"); } } } assert!(read_ok > 40 && refused > 10, "{read_ok} claims read, {refused} refused: the corpus was reached"); } #[test] fn every_request_of_a_serving_case_gets_its_age() { let doc = corpus(); let mut requests = 0; let mut services = 0; // The relay's room keys: the room's authority and its clock. let room_keys = [key32(doc["principals"]["authority"].as_str().expect("the authority")), key32(doc["principals"]["clock"].as_str().expect("the clock"))]; for c in doc["cases"].as_array().expect("cases").iter().filter(|c| c["kind"] == "serving" && c["expect"]["outcome"] == "accept") { let name = c["name"].as_str().expect("a name"); let fields: Vec = c["track"]["namespace"].as_array().expect("a namespace").iter().map(|f| f.as_str().expect("a field").to_string()).collect(); let ns = TrackNamespace::from_strs(fields.iter().map(String::as_str)); let track_name = TrackName::from_str(c["track"]["name"].as_str().expect("a name")); let digest = track_digest(&ns, &track_name); assert_eq!(joule_moq::rating::to_hex(&digest), c["expect"]["track"].as_str().expect("a digest"), "{name}: the track's digest"); let room = joule_moq::rating::room_of(&ns); let claims: Vec = texts(c).iter().map(|t| read(t.as_bytes()).expect("an accepted case's claim reads")).collect(); for (q, want) in c["requests"].as_array().expect("requests").iter().zip(c["expect"]["requests"].as_array().expect("outcomes")) { let group = q["group"].as_u64().expect("a group"); let held = q["held"].as_array().expect("held").iter().map(|i| &claims[i.as_u64().expect("an index") as usize]); let age = held.filter(|cl| cl.covers(&digest, room.as_ref(), group)).map(Claim::age).max().unwrap_or(0); assert_eq!(age, want["age"].as_u64().expect("an age"), "{name}: a request at group {group}"); // A proof of a room key, checked as the relay checks it in SETUP (age-assurance §5.5). let service = match q.get("service") { None => Ok(false), Some(s) => { services += 1; let proof = unhex(s["proof"].as_str().expect("a proof")); let challenge = key32(s["challenge"].as_str().expect("a challenge")); grant_proof::GrantProof::read(&proof) .and_then(|p| grant_proof::check_proof(&grant_proof::NO_GRANT, &p.key, Some(&proof), &challenge, |_| false, s["context"].as_str().expect("a context"))) .map(|p| room_keys.contains(&p.key)) } }; match service { Err(r) => assert_eq!((want["outcome"].as_str(), want["reason"].as_str()), (Some("refuse"), Some(r.code())), "{name}: a request at group {group}"), Ok(true) => assert_eq!((want["outcome"].as_str(), want["service"].as_bool()), (Some("serve"), Some(true)), "{name}: a request at group {group}"), Ok(false) => { let served = q["threshold"].as_u64().expect("a threshold") >= age; assert_eq!(served, want["outcome"] == "serve", "{name}: a request at group {group}"); assert!(want.get("service").is_none(), "{name}: a request at group {group} is no room service"); } } requests += 1; } } assert!(requests > 10 && services >= 10, "{requests} requests, {services} with a proof of a room key: the corpus was reached"); }