aacs: libredrive raw-read VID path + revert v0.25.9 built-ins + walker fix follow-through

Three coherent threads landing for v0.25.11:

1. Libredrive raw-read VID path. When Mt1959::do_unlock sees both the
   MMkv active-mode marker at [12..16] and the LbDr mode-ID marker at
   [16..20], Drive::is_libredrive_active() returns true and
   do_handshake skips the AACS cert dance — VID is retrieved via
   READ_DISC_STRUCTURE format 0x80 with AGID=0 and bus encryption is
   already off. This unblocks UHD ripping on drives whose leaked host
   cert is on the AACS HRL.

   - platform/mt1959/mod.rs: detection + active flag + 4 unit tests.
   - platform/mod.rs: PlatformDriver::is_libredrive_active trait method.
   - drive/mod.rs: Drive::is_libredrive_active accessor.
   - disc/encrypt.rs: do_handshake branches on the flag; new
     read_volume_id_libredrive helper. Return type widened to
     (Option<HandshakeResult>, Option<Error>) so callers see which
     specific failure happened.
   - disc/mod.rs: scan_with plumbs the new tuple through and preserves
     handshake errors as disc.aacs_error.

2. Revert v0.25.9 built-in AACS keys + plugin slot. Single source of
   AACS truth: keydb.cfg. The compiled-in DKs/PKs were a slim
   convenience that didn't move the hard problem (no v77+ DKs) and
   added a maintenance surface. Plugin slot was overlapping
   functionality with the main keydb.

   - Deleted src/aacs/builtin_keys.rs (4 DKs + 3 PKs).
   - Removed KeyDb::with_builtins, load_or_builtins, merge_from,
     merge_local_plugin, local_plugin_path, internal dedup helpers.
     KeyDb::empty kept for unit-test use.
   - KeyDb::load reverts to pre-0.25.9 form: read file or return I/O
     error; no fallback.
   - disc::encrypt::resolve_encryption keydb_path back to required
     (&Path), not Option<&Path>.
   - disc::scan_with surfaces KeydbLoad { path: "<no keydb in search
     paths>" } sentinel when encrypted + no keydb — same sentinel
     autorip's message switch already handles.
   - CSS player keys in src/css/auth.rs stay compiled in; they're
     1999-era public inputs separate from AACS and pre-date the 0.25.9
     additions.

3. Walker fix follow-through (libaacs-parity validate_processing_key,
   cvalues 0x07-then-0x05 preference, path-2/3/4 short-circuit on
   zero VID) + NIST AES-CMAC KAT + VID MAC round-trip / mutation /
   zero-rejection tests.

5 new Error variants for finer-grained AACS failure reporting:
AacsHostCertRejected (E7015), AacsLibredriveUnsupported (E7016),
AacsVidUnavailable (E7017), AacsMkUnavailable (E7018),
AacsVukNotInKeydb (E7019). Lets CLIs/UIs render which piece of the
AACS chain failed instead of always saying "no keys."
This commit is contained in:
MattJackson
2026-05-21 11:10:35 -07:00
parent 7dbbfc6726
commit 4d83b69c20
13 changed files with 818 additions and 490 deletions
+14
View File
@@ -18,4 +18,18 @@ pub(crate) trait PlatformDriver: Send {
/// True after successful init().
fn is_ready(&self) -> bool;
/// True if the drive is currently in libredrive raw-read mode (the
/// per-drive runtime firmware has been uploaded AND the drive
/// confirms active mode via the `MMkv` / `LbDr` markers in the
/// unlock response). When true the host can read sectors without
/// AACS bus encryption and retrieve VID without cert-based mutual
/// auth — the cert/HRL gate on the drive's standard AACS path is
/// effectively bypassed by the alternate data path.
///
/// Default `false` — platforms that don't implement this mode are
/// always reported as inactive.
fn is_libredrive_active(&self) -> bool {
false
}
}
+153
View File
@@ -27,6 +27,10 @@ const UNLOCK_RESPONSE_SIZE: u8 = 64;
const VALIDATE_RESPONSE_SIZE: u8 = 4;
const FIRMWARE_ACTIVE_OFFSET: usize = 12;
const FIRMWARE_ACTIVE_SIG: [u8; 4] = [0x4D, 0x4D, 0x6B, 0x76];
/// Mode-identifier marker repeated through bytes 16..64 of the unlock
/// response on a drive whose runtime firmware is uploaded and active.
const FIRMWARE_MODE_OFFSET: usize = 16;
const FIRMWARE_MODE_SIG: [u8; 4] = [0x4C, 0x62, 0x44, 0x72];
// ── Init address (per disc type) ──────────────────────────────────────
const INIT_ADDR_BD: u16 = 0x0100;
@@ -47,6 +51,11 @@ pub struct Mt1959 {
pub(crate) mode: u8,
pub(crate) buffer_id: u8,
pub(crate) unlocked: bool,
/// True when the unlock response carried both the per-drive
/// signature AND the active-mode markers (`MMkv` at [12..16],
/// `LbDr` at [16..20]). When true the drive will accept raw-read
/// SCSI traffic without AACS bus encryption / cert auth.
libredrive_active: bool,
probed: bool,
}
@@ -62,6 +71,7 @@ impl Mt1959 {
mode,
buffer_id,
unlocked: false,
libredrive_active: false,
probed: false,
}
}
@@ -141,6 +151,18 @@ impl Mt1959 {
return Err(Error::UnlockFailed);
}
// Raw-read mode is active when BOTH the per-drive signature
// matched AND the response carries the secondary `LbDr` marker
// repeated through bytes 16..64. The active-mode signature at
// [12..16] checked above is the primary gate; the [16..20]
// marker is the redundant confirmation Mt1959 firmware writes
// through the rest of the response. Requiring both before we
// tell the AACS layer "skip the cert dance" keeps any partial
// / corrupted response from steering us into the bypass.
self.libredrive_active = response.len() >= FIRMWARE_MODE_OFFSET + 4
&& response[FIRMWARE_ACTIVE_OFFSET..FIRMWARE_ACTIVE_OFFSET + 4] == FIRMWARE_ACTIVE_SIG
&& response[FIRMWARE_MODE_OFFSET..FIRMWARE_MODE_OFFSET + 4] == FIRMWARE_MODE_SIG;
self.unlocked = true;
Ok(response)
}
@@ -334,4 +356,135 @@ impl PlatformDriver for Mt1959 {
fn is_ready(&self) -> bool {
self.unlocked
}
fn is_libredrive_active(&self) -> bool {
self.libredrive_active
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::profile::{DriveProfile, Identity};
use crate::scsi::{DataDirection, ScsiResult, ScsiTransport};
/// Minimal mock transport that returns a scripted response to the
/// next `execute()` call. Only used for verifying that `do_unlock`
/// classifies the response correctly — no general SCSI coverage.
struct ScriptedTransport {
response: Vec<u8>,
}
impl ScsiTransport for ScriptedTransport {
fn execute(
&mut self,
_cdb: &[u8],
_dir: DataDirection,
data: &mut [u8],
_timeout_ms: u32,
) -> Result<ScsiResult> {
let n = self.response.len().min(data.len());
data[..n].copy_from_slice(&self.response[..n]);
Ok(ScsiResult {
status: 0,
bytes_transferred: n,
sense: [0u8; 32],
})
}
}
fn fixture_profile(signature: [u8; 4]) -> DriveProfile {
DriveProfile {
identity: Identity {
vendor_id: "TEST".into(),
product_revision: String::new(),
vendor_specific: String::new(),
firmware_date: String::new(),
},
signature,
firmware: Vec::new(),
}
}
/// Build a synthetic 64-byte unlock response.
///
/// `mode_marker`: bytes [12..16]. Pass `FIRMWARE_ACTIVE_SIG` for the
/// active-mode primary marker.
/// `id_marker`: bytes [16..20] (and repeated through [20..64] in
/// real responses; only [16..20] is checked).
fn build_response(signature: [u8; 4], mode_marker: [u8; 4], id_marker: [u8; 4]) -> Vec<u8> {
let mut r = vec![0u8; 64];
r[0..4].copy_from_slice(&signature);
// bytes [4..12] left as zeros (version + reserved per format)
r[12..16].copy_from_slice(&mode_marker);
// Real firmware repeats LbDr through [16..64]; the parser only
// checks [16..20], so we just write the marker once.
r[16..20].copy_from_slice(&id_marker);
r
}
#[test]
fn do_unlock_sets_libredrive_active_when_both_markers_present() {
let sig = [0x99, 0x9E, 0xC3, 0x75];
let response = build_response(sig, FIRMWARE_ACTIVE_SIG, FIRMWARE_MODE_SIG);
let mut transport = ScriptedTransport { response };
let mut mt = Mt1959::new(fixture_profile(sig), false);
let raw = mt.do_unlock(&mut transport).expect("unlock should succeed");
assert_eq!(raw.len(), 64);
assert!(mt.unlocked, "unlocked flag set after success");
assert!(
mt.is_libredrive_active(),
"both MMkv and LbDr present -> libredrive_active"
);
}
#[test]
fn do_unlock_unlocked_but_not_libredrive_when_id_marker_missing() {
// Active-mode primary marker present (so unlock passes) but the
// secondary LbDr marker is replaced with zeros — drive isn't
// serving raw-read traffic on this path.
let sig = [0x99, 0x9E, 0xC3, 0x75];
let response = build_response(sig, FIRMWARE_ACTIVE_SIG, [0u8; 4]);
let mut transport = ScriptedTransport { response };
let mut mt = Mt1959::new(fixture_profile(sig), false);
mt.do_unlock(&mut transport).expect("unlock should succeed");
assert!(mt.unlocked);
assert!(
!mt.is_libredrive_active(),
"missing LbDr marker -> raw-read not active"
);
}
#[test]
fn do_unlock_rejects_signature_mismatch() {
let response = build_response(
[0xAA, 0xBB, 0xCC, 0xDD],
FIRMWARE_ACTIVE_SIG,
FIRMWARE_MODE_SIG,
);
let mut transport = ScriptedTransport { response };
let mut mt = Mt1959::new(fixture_profile([0x99, 0x9E, 0xC3, 0x75]), false);
let err = mt.do_unlock(&mut transport).unwrap_err();
assert!(matches!(err, Error::SignatureMismatch { .. }));
assert!(!mt.unlocked);
assert!(!mt.is_libredrive_active());
}
#[test]
fn do_unlock_rejects_inactive_mode_marker() {
// Signature matches but [12..16] is NOT MMkv -> drive is not in
// active mode; both unlock and libredrive flag must stay false.
let sig = [0x99, 0x9E, 0xC3, 0x75];
let response = build_response(sig, [0u8; 4], FIRMWARE_MODE_SIG);
let mut transport = ScriptedTransport { response };
let mut mt = Mt1959::new(fixture_profile(sig), false);
let err = mt.do_unlock(&mut transport).unwrap_err();
assert!(matches!(err, Error::UnlockFailed));
assert!(!mt.unlocked);
assert!(!mt.is_libredrive_active());
}
}