From a8be43ea693eee186f3bafa2dc1a34a1752a68c8 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Tue, 23 Jun 2026 01:53:36 -0700 Subject: [PATCH] keydb: require content samples so per-disc UK entries are ciphertext-validated A keydb can hand out a per-disc terminal Key::Unit (a UK entry keyed on disc_hash). Unlike a derived key (Device/Processing/Media/Volume), a terminal UK is applied as-is by Disc::decrypt_with: it is NOT re-derived through the MKB-verified AACS resolver, so a UK entry whose hash matches the disc but whose key bytes are wrong is only disproved by descrambling real ciphertext. KeydbSource inherited the default needs_samples() == false, so on the autorip auto-resume / mux-worker path (which samples units only when some source reports needs_samples()) a keydb-only resolve ran with empty samples and committed a wrong UK as success, muxing undecryptable video while reporting done. The CLI was unaffected because it always samples. Override needs_samples() to true on KeydbSource so every consumer samples encrypted units before resolving and a wrong keydb UK is rejected on all paths. Regression test asserts the override. --- src/keydb.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/keydb.rs b/src/keydb.rs index 03ae6f7..f706827 100644 --- a/src/keydb.rs +++ b/src/keydb.rs @@ -106,6 +106,21 @@ impl KeySource for KeydbSource { KeydbSource::host_certs(self) } + /// The keydb can hand out a per-disc **terminal** `Key::Unit` (a UK entry + /// keyed on `disc_hash` alone — see `candidates_from`). Unlike a derived key + /// (Device/Processing/Media/Volume), a terminal UK is applied as-is by + /// `Disc::decrypt_with`: it is NOT re-derived through the MKB-verified AACS + /// resolver, so a UK entry whose hash matches the disc but whose key bytes + /// are wrong would commit and mux undecryptable video as "success". The only + /// thing that disproves a wrong UK is descrambling real ciphertext, so this + /// source requires content samples — without them `decrypt_with` skips + /// validation and the wrong UK is taken. Returning `true` makes every + /// consumer (autorip resume/mux-worker AND the CLI) sample units before + /// resolving, so a keydb UK is ciphertext-validated on every path. + fn needs_samples(&self) -> bool { + true + } + fn next_key(&mut self, inputs: &DiscInputs) -> Option { // On the first ask, parse the keydb once and build the ordered candidate // list; later asks just advance the cursor. A missing/unreadable keydb @@ -232,6 +247,22 @@ mod tests { assert!(KeydbSource::candidates_from(&db, &inputs("0xaabb")).is_empty()); } + /// Regression: a keydb can hand out a per-disc terminal `Key::Unit` that + /// `Disc::decrypt_with` applies WITHOUT re-deriving through the MKB-verified + /// AACS resolver. The only thing that disproves a wrong UK is descrambling + /// ciphertext, so the source MUST request content samples — otherwise the + /// autorip resume/mux-worker path (which only samples when some source + /// reports `needs_samples()`) resolves with empty samples and commits a + /// wrong UK as success. Was `false` (inherited default); must be `true`. + #[test] + fn keydb_source_needs_samples() { + let src = KeydbSource::new("/nonexistent/path/keydb.cfg"); + assert!( + src.needs_samples(), + "keydb emits terminal Key::Unit entries that need ciphertext validation" + ); + } + /// No keydb (or a LibreDrive deployment) → no host credentials, not an /// error. (The positive parse is NOT tested here — it would require host /// key material, which must never appear in code.)