From 48570ac0653d6310d6fcf35ae6f43aea48772457 Mon Sep 17 00:00:00 2001 From: MattJackson <1085847+MattJackson@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:14:09 -0700 Subject: [PATCH] Local resolve: MK-pool brute via km_verifies (path 2.5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit keydb stores Media Keys per-disc, but an MK is MKB-scoped (shared across a pressing/MKB-family). A disc whose own hash/VID isn't keyed can still resolve if any stored MK verifies against its MKB. New path 2.5 (between PK and the VID lookup) collects the distinct MK pool from the providers, km_verifies each against the disc MKB, and on a UNIQUE pass derives VUK (with the disc VID) then the UK — matching the online resolver's behavior so local keydb mode resolves the same discs (e.g. an MK present in keydb under a sibling pressing). km_verifies is one AES-D + magic check per candidate (cheap). Adds KeyProvider::media_keys() + a path-2.5 unit test. --- src/aacs/keydb.rs | 7 ++++ src/aacs/keys.rs | 86 ++++++++++++++++++++++++++++++++++++++++++++ src/aacs/provider.rs | 17 +++++++++ 3 files changed, 110 insertions(+) diff --git a/src/aacs/keydb.rs b/src/aacs/keydb.rs index f3a8c34..a62e4a5 100644 --- a/src/aacs/keydb.rs +++ b/src/aacs/keydb.rs @@ -223,6 +223,13 @@ impl super::provider::KeyProvider for KeyDb { fn processing_keys(&self) -> Vec<[u8; 16]> { self.processing_keys.clone() } + fn media_keys(&self) -> Vec<[u8; 16]> { + // Every per-disc Media Key in the db. The resolver dedups; MKs are + // MKB-scoped so the same value recurs across a pressing's discs. + self.iter_disc_entries() + .filter_map(|e| e.media_key) + .collect() + } fn host_certs(&self) -> Vec { self.host_certs.clone() } diff --git a/src/aacs/keys.rs b/src/aacs/keys.rs index 85e74da..bb9eb72 100644 --- a/src/aacs/keys.rs +++ b/src/aacs/keys.rs @@ -1103,6 +1103,30 @@ fn resolve_keys_classical(ctx: &ResolveContext<'_>, version: AacsVersion) -> Opt return Some(build(Some(vuk), derive_uks(&vuk), 2)); } tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_path2_miss", pk_count = all_pks.len(), "PK derivation failed"); + + // Path 2.5: MK-pool brute. keydb stores Media Keys per-disc, but an + // MK is MKB-scoped (shared across a pressing/MKB-family). A disc + // whose own hash/VID isn't keyed can still resolve if ANY stored MK + // verifies against its MKB. Try every distinct MK via km_verifies; + // a UNIQUE pass is this disc's Km → derive VUK (needs VID) → UK. + // km_verifies is one AES-D + magic check per candidate (cheap). + let mks = providers.media_keys(); + let mut mk_hits: Vec<[u8; 16]> = Vec::new(); + for mk in &mks { + if probe::km_verifies(mkb, mk) && !mk_hits.contains(mk) { + mk_hits.push(*mk); + if mk_hits.len() > 1 { + break; // ambiguous — bail to avoid a wrong key + } + } + } + if mk_hits.len() == 1 { + let vuk = derive_vuk(&mk_hits[0], ctx.volume_id); + tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_path2_5_hit", mk_pool = mks.len(), "media key from keydb MK-pool brute (km_verifies)"); + // Same class as path 3 (KEYDB MK → derived VUK). + return Some(build(Some(vuk), derive_uks(&vuk), 3)); + } + tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_path2_5_miss", mk_pool = mks.len(), mk_hits = mk_hits.len(), "MK-pool brute: no unique verifying MK"); } else { tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_no_mkb", "no MKB; paths 1/2 skipped"); } @@ -1954,6 +1978,68 @@ mod tests { ); } + #[test] + fn resolve_keys_path2_5_mk_pool_brute_resolves_unkeyed_disc() { + // The Dunkirk case: this disc's own hash/VID are NOT in keydb, but its + // Media Key IS — filed under a sibling disc that shares its MKB. Path + // 2.5 must km_verifies that MK against the MKB and resolve. + use super::super::decrypt::aes_ecb_encrypt as enc; + + let km = [0x11u8; 16]; + let vid = [0x22u8; 16]; + + // MKB: 0x10 type/version + 0x86 verify record whose mk_dv decrypts under + // km to the AACS verify magic, so km_verifies(mkb, km) == true. + let mut vd = [0u8; 16]; + vd[..8].copy_from_slice(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); + let mk_dv = enc(&km, &vd); + let mut mkb = mkb_record(0x10, &[0, 0, 0, 0x20, 0, 0, 0, 0x4D]); + mkb.extend_from_slice(&mkb_record(0x86, &mk_dv)); + assert!( + probe::km_verifies(&mkb, &km), + "fixture: km must verify the MKB" + ); + + // This disc's inf (its hash will NOT be in keydb). + let uk_ro = minimal_unit_key_ro(); + + // keydb: a SIBLING disc carries our km, keyed by the sibling's own + // hash + VID (neither matches THIS disc) — so only the MK-pool brute + // (km_verifies) can find it. + let mut keydb = KeyDb::empty(); + keydb.disc_entries.insert( + "0xsibling".to_string(), + DiscEntry { + disc_hash: "0xsibling".to_string(), + title: "sibling".to_string(), + media_key: Some(km), + disc_id: Some([0x99u8; 16]), + vuk: None, + unit_keys: Vec::new(), + }, + ); + + let providers: &[&dyn super::super::KeyProvider] = &[&keydb]; + let ctx = ResolveContext { + unit_key_ro: &uk_ro, + content_cert: None, + volume_id: &vid, + providers, + mkb: Some(&mkb), + }; + let resolved = resolve_keys_v1(&ctx) + .expect("MK-pool brute (path 2.5) must resolve a disc whose MK is in keydb"); + assert_eq!( + resolved.key_source, 3, + "MK-pool brute is the KEYDB-derived class" + ); + assert_eq!( + resolved.vuk, + Some(derive_vuk(&km, &vid)), + "VUK must derive from the verified Km + this disc's VID" + ); + } + #[test] fn test_content_cert_parse() { // AACS 1.0 cert diff --git a/src/aacs/provider.rs b/src/aacs/provider.rs index 876399f..531cd38 100644 --- a/src/aacs/provider.rs +++ b/src/aacs/provider.rs @@ -48,6 +48,14 @@ pub trait KeyProvider: Send + Sync { Vec::new() } + /// Every Media Key this provider holds, regardless of which disc it was + /// filed under. An MK is MKB-scoped (shared across a pressing/MKB-family), + /// so the resolver can verify each against the disc's MKB (`km_verifies`) + /// and resolve a disc whose own hash/VID isn't directly keyed. + fn media_keys(&self) -> Vec<[u8; 16]> { + Vec::new() + } + /// AACS host certificates (with their private keys) for drive /// authentication. Multiple in case some are revoked. fn host_certs(&self) -> Vec { @@ -85,6 +93,15 @@ impl Providers<'_> { self.0.iter().flat_map(|p| p.processing_keys()).collect() } + /// Union of distinct Media Keys across every provider, for the MK-pool + /// brute (`km_verifies` against the disc's MKB). + pub fn media_keys(&self) -> Vec<[u8; 16]> { + let mut v: Vec<[u8; 16]> = self.0.iter().flat_map(|p| p.media_keys()).collect(); + v.sort_unstable(); + v.dedup(); + v + } + /// Union — gather host certs from every provider. Not yet wired into /// the SCSI handshake (which still reads `KeyDb.host_certs` directly); /// kept here so a provider-aware handshake refactor is a drop-in.