test: drive the AACS 2.1 variant chain to a Media Key, and pin AES-G3
163 of 322 surviving mutants across src/aacs and src/css. No production line changed — every function read correct; the finding was always an absent test. Two structural holes, both verified against HEAD before landing. variant.rs had no test that ever produced a Media Key. Every terminal assertion in the module was an Err classification — NotVariantMkb, SoftCorrectionRequired, OnlineChallengeRequired. So the entire 2.1 success path (VARIANTS lookup, VKD selection, Kpnew, the final unwrap, the verify gate) was pinned by nothing, and that path produces the Media Key that becomes the VUK that decrypts every byte of a 2.1 disc. Built the first complete planted variant MKB: the VARIANTS entry is chosen as Kvn ^ 1 so the real VKD sits behind a decoy at table index 1, making the lookup load-bearing rather than incidentally correct. That one fixture kills 23 operator mutants across three functions. aesg3 — the subset-difference tree node function — was in the survivor list as replaceable by [0; 16], meaning every device key in the crate would derive the same Processing Key. It is caught today only as a side effect of a negative test added after the mutation run; nothing asserted the relation itself. Pinned now via the spec relation ([C] 3.2.2) using the FORWARD primitive, with s0 transcribed independently rather than read back from AESG3_SEED, so the test cannot agree with a mutated constant. Same shape in derive.rs: plant_mkb was one slot with zero descent, so slot indexing was the identity permutation and the ancestor-descent branch never ran — which is why 39 of recover_dk_position's mutants survived. Added a 3-slot fixture keyed at index 2 and a four-level descent fixture whose expected Processing Key is written out as an explicit aesg3 chain rather than computed by calc_pk_from_dk; a fixture built by the function under test moves with its own mutations. Two latent panics on untrusted input now have tests: a 0x05 cvalue table shorter than the 0x04 slot index, and a drive declaring more payload than the 32772-byte response buffer holds. 23 equivalents claimed with reasoning, and confirmed empirically where possible — all eight css/lfsr mutants were run and exactly the seven disjoint-bit-lane ones survived. Explicitly NOT claimed equivalent: derive.rs 146:32 and 154:30 are reachable, but only on the non-convergent bounded-exit path where the function's sole contract is termination. A test there would pin defined-but-meaningless output. Noted for the next pass: the pre-existing walk_mkb_be24_high_byte_is_honored used total length 0x0110, whose high byte is zero — it exercised the middle byte only, which is why << 16 -> >> 16 survived it. Left in place; a real one was added at 0x01_0004.
This commit is contained in:
@@ -1906,6 +1906,77 @@ mod tests {
|
||||
assert_eq!(r.vuk, Some(derive_vuk(&mk, &vid)));
|
||||
}
|
||||
|
||||
/// `resolve_keys_v21` gates paths 1 and 3 on `has_vid`, and an all-zero
|
||||
/// Volume ID is the crate's "the VID was never read" sentinel — the SCSI
|
||||
/// handshake leaves the buffer zeroed when it does not run or fails.
|
||||
///
|
||||
/// Both directions matter and both fail silently:
|
||||
/// - treating the zero sentinel as a real VID runs path 3 and derives
|
||||
/// `Kvu = AES-G(Km, 0…0)`, a perfectly well-formed but WRONG VUK. It
|
||||
/// unwraps the title keys to garbage, and nothing downstream errors —
|
||||
/// the rip just decodes to noise.
|
||||
/// - treating a real VID as absent skips paths 1 and 3 entirely, so a
|
||||
/// disc that could have been resolved from its Media Key reports no key.
|
||||
///
|
||||
/// Asserted through the final VUK, not through the flag.
|
||||
#[test]
|
||||
fn resolve_keys_v21_treats_the_all_zero_volume_id_as_no_vid() {
|
||||
let uk_ro = minimal_unit_key_ro();
|
||||
let vid = [0x42u8; 16];
|
||||
let mk = [0x24u8; 16];
|
||||
// A VID-keyed entry carrying an MK and nothing else: no VUK and no unit
|
||||
// keys, so paths 4 and 5 cannot fire and ONLY the VID-gated path 3 can
|
||||
// produce a result.
|
||||
let entry = DiscEntry {
|
||||
disc_hash: "not-this-disc".to_string(),
|
||||
title: "sibling".to_string(),
|
||||
media_key: Some(mk),
|
||||
disc_id: Some(vid),
|
||||
vuk: None,
|
||||
unit_keys: Vec::new(),
|
||||
};
|
||||
let keydb = SuppliedKey {
|
||||
device_keys: Vec::new(),
|
||||
processing_keys: Vec::new(),
|
||||
media_keys: Vec::new(),
|
||||
disc_entry: Some(entry),
|
||||
};
|
||||
let providers: &[&dyn super::super::provider::KeyProvider] = &[&keydb];
|
||||
|
||||
// A real VID → path 3 fires and the VUK derives from Km + THIS VID.
|
||||
let with_vid = ResolveContext {
|
||||
unit_key_ro: &uk_ro,
|
||||
content_cert: None,
|
||||
volume_id: &vid,
|
||||
providers,
|
||||
mkb: None,
|
||||
};
|
||||
let r = resolve_keys_v21(&with_vid).expect("a real VID must reach path 3");
|
||||
assert_eq!(r.key_source, 3);
|
||||
assert_eq!(
|
||||
r.vuk,
|
||||
Some(derive_vuk(&mk, &vid)),
|
||||
"VUK must derive from the Media Key and the disc's own VID"
|
||||
);
|
||||
|
||||
// The all-zero sentinel → paths 1 and 3 are skipped entirely; with no
|
||||
// VUK and no unit keys on the entry, nothing resolves.
|
||||
let no_vid = ResolveContext {
|
||||
unit_key_ro: &uk_ro,
|
||||
content_cert: None,
|
||||
volume_id: &[0u8; 16],
|
||||
providers,
|
||||
mkb: None,
|
||||
};
|
||||
let got = resolve_keys_v21(&no_vid);
|
||||
assert!(
|
||||
got.is_none(),
|
||||
"a zero VID must not be used to derive a VUK; got key_source {:?} vuk {:?}",
|
||||
got.as_ref().map(|r| r.key_source),
|
||||
got.as_ref().map(|r| r.vuk.is_some())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_keys_returns_none_when_no_provider_has_anything() {
|
||||
// Empty provider array + VID present + no MKB → all paths miss → None.
|
||||
|
||||
Reference in New Issue
Block a user