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.
This commit is contained in:
Matthew Jackson
2026-06-23 01:53:36 -07:00
parent 6f164ca355
commit a8be43ea69
+31
View File
@@ -106,6 +106,21 @@ impl KeySource for KeydbSource {
KeydbSource::host_certs(self) 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<Key> { fn next_key(&mut self, inputs: &DiscInputs) -> Option<Key> {
// On the first ask, parse the keydb once and build the ordered candidate // On the first ask, parse the keydb once and build the ordered candidate
// list; later asks just advance the cursor. A missing/unreadable keydb // 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()); 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 /// No keydb (or a LibreDrive deployment) → no host credentials, not an
/// error. (The positive parse is NOT tested here — it would require host /// error. (The positive parse is NOT tested here — it would require host
/// key material, which must never appear in code.) /// key material, which must never appear in code.)