aacs: KeyProvider abstraction + PK walker + external-UK key source

Two coherent additions to the AACS resolver:

KeyProvider abstraction (provider.rs) — key material comes from pluggable
backends; KeyDb implements it (device/processing keys, host certs,
disc-by-hash / disc-by-vid lookup) plus orphan-DK parsing. ResolveContext
takes a provider array. Adds the SD-tree PK walker
(derive_media_key_from_pk_walked) and a `probe` module (km_verifies MK
oracle, mkb_* record parsers) used for offline key verification. Cvalue
record selection prefers 0x05, falls back to 0x07.

External-UK key source — the second, mutually-exclusive key source for the
keyserver path. ScanOptions/InputOptions gain `unit_key`; when set,
resolve_encryption_static skips keydb entirely and uses the caller-supplied
Unit Key directly (KeySource::ExternalUk). Disc::read_aacs_inputs exposes a
disc's Unit_Key_RO.inf + MKB so a caller can fetch the UK out-of-band; the
library makes no network call itself.

CHANGELOG: redact test-disc title in historical notes.
This commit is contained in:
MattJackson
2026-06-01 20:47:14 -07:00
parent c0478e1273
commit 8d54a3c64e
10 changed files with 785 additions and 90 deletions
+41 -3
View File
@@ -912,6 +912,9 @@ pub enum KeySource {
/// Pre-decrypted unit keys taken directly from KEYDB by disc hash.
/// No VUK present in the entry — `AacsState::vuk` is `None`.
KeyDbUnitKeys,
/// Unit key supplied directly by the caller (the keyserver path).
/// No keydb, no derivation — `AacsState::vuk` is `None`.
ExternalUk,
}
impl KeySource {
@@ -922,6 +925,7 @@ impl KeySource {
KeySource::KeyDbDerived => "KEYDB (derived)",
KeySource::KeyDb => "KEYDB",
KeySource::KeyDbUnitKeys => "KEYDB (unit keys)",
KeySource::ExternalUk => "external UK",
}
}
}
@@ -941,6 +945,13 @@ pub struct ScanOptions {
/// Path to KEYDB.cfg for AACS key lookup.
/// If None, searches standard locations ($HOME/.config/aacs/ and /etc/aacs/).
pub keydb_path: Option<std::path::PathBuf>,
/// Caller-supplied Unit Key — the second, mutually-exclusive key source
/// (the online-keyserver path). When set, libfreemkv skips keydb lookup
/// and all derivation and uses this key directly to decrypt. Takes
/// precedence over `keydb_path` if both are set. The caller obtains it
/// however it likes (e.g. POSTing the disc's `Unit_Key_RO.inf` + MKB to a
/// keyserver); libfreemkv stays free of any network dependency.
pub unit_key: Option<[u8; 16]>,
}
impl ScanOptions {
@@ -1140,6 +1151,26 @@ impl Disc {
Self::scan_with(reader, capacity, None, None, opts, udf_fs)
}
/// Read a disc's AACS key-input files from an ISO image: returns
/// `(Unit_Key_RO.inf, MKB)` raw bytes. For callers that resolve keys
/// out-of-band (the keyserver path) — POST these to the keyserver, get the
/// Unit Key, then scan with `ScanOptions { unit_key: Some(uk), .. }`.
/// libfreemkv itself never makes the network call.
pub fn read_aacs_inputs(iso_path: &std::path::Path) -> Result<(Vec<u8>, Vec<u8>)> {
let mut reader = crate::io::file_sector_source::FileSectorSource::open(iso_path)
.map_err(|_| Error::AacsNoKeys)?;
let udf_fs = udf::read_filesystem(&mut reader)?;
let inf = udf_fs
.read_file(&mut reader, "/AACS/Unit_Key_RO.inf")
.or_else(|_| udf_fs.read_file(&mut reader, "/AACS/DUPLICATE/Unit_Key_RO.inf"))
.map_err(|_| Error::AacsNoKeys)?;
let mkb = udf_fs
.read_file(&mut reader, "/AACS/MKB_RW.inf")
.or_else(|_| udf_fs.read_file(&mut reader, "/AACS/MKB_RO.inf"))
.map_err(|_| Error::AacsNoKeys)?;
Ok((inf, mkb))
}
/// Core scan pipeline — works with any SectorSource.
///
/// `handshake_error` is plumbed from `do_handshake` so failures
@@ -1159,7 +1190,16 @@ impl Disc {
let encrypted =
udf_fs.find_dir("/AACS").is_some() || udf_fs.find_dir("/BDMV/AACS").is_some();
let (aacs, aacs_error) = if encrypted {
let (aacs, aacs_error) = if !encrypted {
(None, None)
} else if let Some(unit_key) = opts.unit_key {
// Second key source: caller supplied the Unit Key directly
// (keyserver path). Skip keydb entirely.
match Self::resolve_encryption_static(&udf_fs, reader, unit_key, handshake.as_ref()) {
Ok(state) => (Some(state), None),
Err(e) => (None, Some(e)),
}
} else {
match opts.resolve_keydb() {
Some(keydb_path) => {
match Self::resolve_encryption(&udf_fs, reader, &keydb_path, handshake.as_ref())
@@ -1207,8 +1247,6 @@ impl Disc {
(None, Some(final_err))
}
}
} else {
(None, None)
};
// 3. Titles — BD (MPLS playlists) or DVD (IFO title sets)