//! What a package's verifier must refuse, each case a package altered and, where the //! alteration would otherwise be caught by the receipt or `LISTING` alone, re-sealed with //! the package key (what a dishonest packager holding the key could publish): the //! package bound to its policy, malformed packages refused and never panicked on, the //! floor's key frames and members, hidden files, the receipt's canonical bytes, and a //! seeded run of random alterations that must never panic. use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use wai::stage_index::StageIndex; use wai_pack::golden; type Files = BTreeMap>; fn corpus() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("../pack-conformance") } fn golden_files() -> Files { wai::pin::read_dir_set(&corpus().join("packs/video-base-01/package")).unwrap() } fn key() -> ed25519_dalek::SigningKey { let t = std::fs::read_to_string(corpus().join("packs/video-base-01/key.hex")).unwrap(); let t = t.trim(); let mut seed = [0u8; 32]; for (i, s) in seed.iter_mut().enumerate() { *s = u8::from_str_radix(&t[2 * i..2 * i + 2], 16).unwrap(); } ed25519_dalek::SigningKey::from_bytes(&seed) } fn codes(f: &Files) -> Vec { match wai_pack::verify::verify_files(f) { Ok(_) => vec!["ok".into()], Err(v) => v.into_iter().map(|r| r.code).collect(), } } fn first(f: &Files) -> String { codes(f).into_iter().next().unwrap() } fn index(f: &Files) -> StageIndex { StageIndex::parse(&f["index-v.wsix"]).unwrap() } fn set_index(f: &mut Files, ix: &StageIndex) { f.insert("index-v.wsix".into(), ix.to_bytes()); } fn resealed(mut f: Files) -> Files { golden::reseal(&mut f, &key()).unwrap(); f } /// The package mutations of the corpus (golden.rs), by name. fn mutations() -> BTreeMap<&'static str, golden::PackageMutation> { golden::package_mutations(&golden_files(), &key()).into_iter().map(|m| (m.name, m)).collect() } /// Each named case, written out as a directory and read by the verifier's own walk, is /// refused with the reason the corpus gives it, first. fn holds(cases: &[(&str, &str)]) { let m = mutations(); for (name, reason) in cases { let c = m.get(name).unwrap_or_else(|| panic!("no case {name}")); assert_eq!(c.reason, *reason, "{name}: the corpus's reason"); assert_eq!(golden::package_case_verdict(&c.files, &c.empty_dirs, &c.unreadable_dirs).unwrap(), *reason, "{name}"); } } #[test] fn the_golden_verifies_and_reseals_to_itself() { let g = golden_files(); assert_eq!(first(&g), "ok"); assert_eq!(resealed(g.clone()), g, "the re-sealer reproduces the golden byte for byte"); } /// Item 1 of the review: a package is bound to the policy its receipt signs. #[test] fn a_package_is_bound_to_its_policy() { holds(&[ // A version-2 receipt over no component at all, signed with the package key. ("no-component", "policy-component-mismatch"), ("no-component-live", "policy-component-mismatch"), // The index's class records (with its class plan, and a rate alone), an entry's // capability, the string table's order and a class plan, each not the policy's. ("index-class-records", "index-policy-mismatch"), ("index-class-rate", "index-policy-mismatch"), ("index-capability", "index-policy-mismatch"), ("index-string-order", "index-policy-mismatch"), ("index-class-plan", "index-policy-mismatch"), // A class named classic, in any case; under royalty_free_only (the policy's, and // the packer's override as policy.json states it), a patent-pool floor, and a // policy.json that turns it off; the policy's component of another medium. ("class-named-classic", "pack-id"), ("class-named-classic-in-capitals", "pack-id"), ("policy-json-relaxes-royalty-free", "policy-json-mismatch"), ("patent-pool-floor", "license-refused:wai.video.avc:patent-pool"), ("patent-pool-floor-by-override", "license-refused:wai.video.avc:patent-pool"), ("component-medium", "policy-component-mismatch"), ]); } /// Item 2: a malformed package is refused, never panicked on. #[test] fn malformed_packages_are_refused() { holds(&[("floor-object-missing", "object-missing"), ("live-window-past-2-53", "catalog-mismatch")]); // A corrupted floor object is refused for its hash, and for nothing it would have led // to. let m = mutations(); let c = codes(&m["floor-object-corrupt"].files); assert!(c.iter().all(|c| c == "object-hash"), "{c:?}"); // An on-demand index whose first piece does not start at 0: a packager cannot seal it. let g = golden_files(); let mut f = g.clone(); let mut ix = index(&f); for p in ix.pieces.iter_mut() { p.start += 10; } set_index(&mut f, &ix); golden::relist(&mut f); assert!(golden::reseal(&mut f, &key()).is_err(), "a packager cannot seal it either"); // A fragment's decode time past 2^64 − 1. let (_, b) = golden::floor_entry(&g, 0); let (_, _, units) = golden::floor_parts(&b); let tus: Vec = units.iter().map(|u| wai_pack::av1::temporal_units(u).unwrap().remove(0)).collect(); let seq = wai_pack::av1::SequenceHeader::parse(tus[0].sequence_header().unwrap().payload).unwrap(); let fl = wai_pack::floor::Floor { seq, seq_obu: tus[0].sequence_header().unwrap().bytes.to_vec(), pieces: vec![wai_pack::floor::FloorPiece { units: units.clone(), key: vec![true; units.len()] }] }; assert!(wai_pack::pack::cmaf_files("v", &fl, &[u64::MAX / 2], 3, 25).is_err()); } /// Item 3: a floor fragment opens on a shown key frame, and holds no other unless the /// policy allows; every sequence header is the first's. #[test] fn a_floor_piece_opens_on_a_key_frame() { holds(&[ ("floor-opens-on-inter", "floor-keyframe-mismatch"), ("floor-extra-key-frame", "floor-keyframe-mismatch"), ("floor-extra-key-frame-allowed", "ok"), ("floor-sequence-header", "floor-av1-malformed"), ]); } /// Item 6: a supplied floor's members are §7.2's; it presents its piece's base, and its /// CMAF files are the ones its units give. #[test] fn a_floor_object_is_bound_to_its_profile() { holds(&[ ("floor-framing-header", "floor-object-mismatch"), ("floor-renderer-settings", "floor-object-mismatch"), ("floor-target", "floor-object-mismatch"), ("floor-motif", "floor-motif-mismatch"), ("floor-cmaf", "floor-cmaf-mismatch"), ]); } /// Item 7: a directory read strictly, dot-names and `*.sha256` files included; and a /// directory that holds nothing or cannot be read. #[test] fn hidden_files_are_refused() { holds(&[ ("dot-file", "unexpected-file"), ("dot-directory", "unexpected-file"), ("dot-object", "unexpected-file"), ("sha256-file", "unexpected-file"), ("empty-directory", "unexpected-file"), ("empty-dot-directory", "unexpected-file"), ("unreadable-directory", "unexpected-file"), ]); // The strict walk reads hidden files rather than skipping them, and refuses a // directory that holds nothing. let dir = std::env::temp_dir().join(format!("wai-pack-hidden-{}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); let m = mutations(); for (name, extra) in [("dot-file", ".hidden"), ("dot-directory", ".cache/blob"), ("dot-object", "objects/.x.wai"), ("sha256-file", "payload.sha256")] { let d = dir.join(name); for (p, b) in &m[name].files { let path = d.join(p); std::fs::create_dir_all(path.parent().unwrap()).unwrap(); std::fs::write(path, b).unwrap(); } let files = wai_pack::verify::read_strict(&d).unwrap(); assert!(files.contains_key(extra), "{extra} is read"); } std::fs::create_dir_all(dir.join("dot-file/hls/empty")).unwrap(); assert!(wai_pack::verify::read_strict(&dir.join("dot-file")).is_err(), "a directory that holds nothing"); let _ = std::fs::remove_dir_all(&dir); } /// Item 4: the receipt file is its canonical form; a member named twice is refused. #[test] fn the_receipt_file_is_canonical() { holds(&[("receipt-pretty", "receipt-json"), ("receipt-newline", "receipt-json"), ("receipt-duplicate-member", "receipt-json")]); } /// Item 2: random alterations of the golden, some re-sealed, never make the verifier /// panic. Seeded, so a failure reproduces. #[test] fn random_alterations_never_panic() { let g = golden_files(); let paths: Vec = g.keys().cloned().collect(); let mut s = 0x9e37_79b9_7f4a_7c15u64; let mut next = || { s ^= s << 13; s ^= s >> 7; s ^= s << 17; s }; let mut refused = 0; for round in 0..240 { let mut f = g.clone(); for _ in 0..1 + next() % 3 { let p = &paths[(next() % paths.len() as u64) as usize]; let Some(b) = f.get_mut(p) else { continue }; match next() % 6 { 0 if !b.is_empty() => { let i = (next() % b.len() as u64) as usize; b[i] ^= 1 << (next() % 8); } 1 => { let n = (next() % (b.len() as u64 + 1)) as usize; b.truncate(n); } 2 => { b.extend_from_slice(&[0u8; 9]); } 3 if p.ends_with(".wsix") && b.len() > 200 => { let i = 160 + (next() % (b.len() as u64 - 160)) as usize; b[i] = (next() & 0xff) as u8; } 4 => { f.remove(p); } _ => { let i = (next() % b.len().max(1) as u64) as usize; if i < b.len() { b[i] = b'"'; } } } } if round % 2 == 0 { golden::relist(&mut f); } if round % 3 == 0 { let _ = golden::reseal(&mut f, &key()); } let r = std::panic::catch_unwind(|| wai_pack::verify::verify_files(&f)); assert!(r.is_ok(), "round {round} panicked"); if r.unwrap().is_err() { refused += 1; } } // A re-sealed alteration of a derived file can be the golden again; most are refused. assert!(refused > 120, "{refused} of 240 refused"); } /// Round 3, item F: alterations of the floor, a base and the index that keep the package /// consistent (the index following the new bytes through `golden::replace_object`, the /// package re-sealed with its key) reach the verifier's floor, prefix and catalog checks, /// and never make it panic. Seeded and bounded, so a failure reproduces. /// /// What a verifier without the source accepts, by design; an altered package it accepts /// must be in one of these classes (or be the golden again): /// - the AV1 frame data a floor's units carry: the verifier checks each unit's framing, /// the sequence headers and where the key frames are, not the pictures (it decodes no /// AV1), so other bytes inside a unit, and inter units moved or repeated within a /// piece, are accepted; /// - producer-statement fields only the source decides: a piece's kind and flags (§4.3, /// §4.6) and an entry's `sse` (§6.6), where the rules, moments and class plan they lead /// to are unchanged; /// - equivalent WIV1 encodings: a base payload whose bytes differ but which decodes to /// the same frames, so its prefix digest holds (§6.5). /// /// A floor object's framing header, a unit cut short and a unit with a second temporal /// unit appended are never accepted. #[test] fn deep_alterations_never_panic() { #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Alteration { UnitBytes, UnitOrder, Framing, UnitCut, UnitExtended, BasePayload, IndexField, } use Alteration::*; let g = golden_files(); let key = key(); let mut s = 0x2545_f491_4f6c_dd1du64; let mut next = move || { s ^= s << 13; s ^= s >> 7; s ^= s << 17; s }; let (mut accepted, mut unsealed) = (0u32, 0u32); let mut refused: BTreeMap = BTreeMap::new(); for round in 0..300 { let mut f = g.clone(); let ix = index(&f); let p = (next() % ix.pieces.len() as u64) as usize; let alteration = match next() % 5 { kind @ 0..=2 => { let (k, b) = golden::floor_entry(&f, p); let (man, mut hdr, mut units) = golden::floor_parts(&b); let u = (next() % units.len() as u64) as usize; let a = match (kind, next() % 6) { (0, _) => { let n = units[u].len(); let i = (next() % n as u64) as usize; units[u][i] ^= 1 << (next() % 8); UnitBytes } (1, 0) => { let n = units[u].len() as u64; units[u].truncate((next() % (n + 1)) as usize); UnitCut } (1, 1) => { let x = units.remove(u); let j = (next() % (units.len() as u64 + 1)) as usize; units.insert(j, x); UnitOrder } (1, 2) => { let x = units[u].clone(); units.insert(u, x); units.pop(); UnitOrder } (1, 3) => { // A temporal delimiter OBU: a second temporal unit in one unit. units[u].extend_from_slice(&[0x12, 0x00]); UnitExtended } (1, 4) => { if units[u].len() > 2 { units[u][1] = (next() & 0xff) as u8; } UnitBytes } (1, _) => { units.swap(0, u); UnitOrder } (_, w) => { match w % 4 { 0 => hdr.0 = (next() % 70_000) as u32, 1 => hdr.1 = 0, 2 => hdr.2 = u32::MAX, _ => hdr.3 = 0, } Framing } }; golden::replace_object(&mut f, k, golden::floor_envelope(&man, hdr, &units)); a } 3 => { let k = ix.pieces[p].first_entry as usize; let mut b = f[&wai_pack::objects::object_path(&ix.entries[k].content_hash)].clone(); let ml = u32::from_le_bytes(b[4..8].try_into().unwrap()) as usize; let at = 12 + ml + (next() % (b.len() - 12 - ml) as u64) as usize; b[at] = (next() & 0xff) as u8; golden::replace_object(&mut f, k, b); BasePayload } _ => { let mut ix = ix.clone(); let k = (next() % ix.entries.len() as u64) as usize; match next() % 5 { 0 => ix.pieces[p].kind = (next() % 4) as u8, 1 => ix.pieces[p].flags = (next() % 16) as u8, 2 => ix.entries[k].sse = next(), 3 => ix.pieces[p].moments = (next() % 4) as u32, _ => ix.pieces[p].rules = (next() % 4) as u32, } set_index(&mut f, &ix); IndexField } }; // What a packager cannot seal (an index its own rules refuse) is not a package. if golden::reseal(&mut f, &key).is_err() { unsealed += 1; continue; } let verdict = std::panic::catch_unwind(|| wai_pack::verify::verify_files(&f)).unwrap_or_else(|_| panic!("round {round} ({alteration:?}): the verifier panicked")); if let Err(v) = verdict { *refused.entry(v[0].code.clone()).or_default() += 1; } else { accepted += 1; assert!( f == g || matches!(alteration, UnitBytes | UnitOrder | BasePayload | IndexField), "round {round}: a {alteration:?} alteration accepted, outside the classes a verifier without the source accepts" ); } } // The alterations reach the floor and prefix checks, past the receipt and the index. for reached in ["floor-object-mismatch", "floor-keyframe-mismatch", "prefix-digest", "index-policy-mismatch"] { assert!(refused.contains_key(reached), "no round reached {reached}: {refused:?}"); } assert!(accepted > 0 && unsealed < 100, "{refused:?} refused, {accepted} accepted, {unsealed} not sealable"); }