unlock: add OEM read_vid capability to Unlocker seam
An Unlocker unlocks drive functionality, not just the disc: unlock() is one capability, OEM VID retrieval is another. Widen the Unlocker trait with a default-no-op read_vid(), add an unlocker_read_vid registry helper that mirrors route_unlock resolution, and consult it in do_handshake_cert before the cert-based VID read. A matching unlocker that serves a VID via its OEM path short-circuits the cert handshake — VID is obtained without the host certificate + HRL (restoring the pre-refactor decoupled OEM VID path, now living inside the unlocker). Non-matching drives, and unlockers without an OEM VID path, fall through to cert auth unchanged. is_unlocked() now reports the honest signal (a registered unlocker matched this drive) instead of const false.
This commit is contained in:
+56
-9
@@ -17,11 +17,12 @@ impl Disc {
|
|||||||
/// SCSI handshake — drives the VID-acquisition flow and returns
|
/// SCSI handshake — drives the VID-acquisition flow and returns
|
||||||
/// a structured `HandshakeResult` for downstream key resolution.
|
/// a structured `HandshakeResult` for downstream key resolution.
|
||||||
///
|
///
|
||||||
/// Drive unlock now lives behind the pluggable
|
/// VID acquisition runs through [`Self::do_handshake_cert`], which first
|
||||||
/// [`crate::unlock::Unlocker`] seam, which reports no extended-access
|
/// asks the pluggable [`crate::unlock::Unlocker`] seam for the OEM VID
|
||||||
/// marker back to libfreemkv. VID is therefore always acquired via the
|
/// (a drive-functionality capability decoupled from the host cert + HRL)
|
||||||
/// cert-based mutual-auth handshake (the OEM route); the cert path also
|
/// and falls back to the cert-based mutual-auth handshake when no
|
||||||
/// yields `read_data_key`, required for AACS 2.0 bus decryption.
|
/// unlocker serves one. The cert path also yields `read_data_key`,
|
||||||
|
/// required for AACS 2.0 bus decryption.
|
||||||
///
|
///
|
||||||
/// Returns `(handshake, error)`:
|
/// Returns `(handshake, error)`:
|
||||||
/// * `(Some(_), None)` — VID acquired
|
/// * `(Some(_), None)` — VID acquired
|
||||||
@@ -36,9 +37,9 @@ impl Disc {
|
|||||||
) -> (Option<HandshakeResult>, Option<Error>) {
|
) -> (Option<HandshakeResult>, Option<Error>) {
|
||||||
let t0 = std::time::Instant::now();
|
let t0 = std::time::Instant::now();
|
||||||
tracing::info!(target: "freemkv::scan", phase = "do_handshake", "begin");
|
tracing::info!(target: "freemkv::scan", phase = "do_handshake", "begin");
|
||||||
// Drive unlock moved behind the pluggable `Unlocker` seam, which
|
// VID comes from the unlocker's OEM path when available (decoupled
|
||||||
// reports no extended-access marker — so VID always comes via the
|
// from the host cert + HRL), else the cert-based handshake — both
|
||||||
// cert-based handshake (the OEM route).
|
// resolved inside `do_handshake_cert`.
|
||||||
let (result, err) = Self::do_handshake_cert(session, opts);
|
let (result, err) = Self::do_handshake_cert(session, opts);
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
target: "freemkv::scan",
|
target: "freemkv::scan",
|
||||||
@@ -51,13 +52,59 @@ impl Disc {
|
|||||||
(result, err)
|
(result, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cert-based AACS handshake — the OEM route for VID acquisition.
|
/// Cert-based AACS handshake — the cert route for VID acquisition.
|
||||||
|
///
|
||||||
|
/// Before running the cert mutual-auth, this asks the pluggable
|
||||||
|
/// [`crate::unlock::Unlocker`] seam for the OEM Volume ID. An unlocker
|
||||||
|
/// unlocks *drive functionality*, not just the disc: VID retrieval via
|
||||||
|
/// the drive's OEM CDB is a capability separate from `unlock`. When the
|
||||||
|
/// matching unlocker serves a VID, we use it and SKIP the cert handshake
|
||||||
|
/// entirely — the OEM path gets the VID *without* the host certificate +
|
||||||
|
/// HRL, decoupling VID from the cert chain. The OEM path yields no
|
||||||
|
/// `read_data_key` (no bus-key is derived); AACS 2.0 content needing
|
||||||
|
/// read_data_key for bus decryption must still use the cert path, so an
|
||||||
|
/// unlocker with no OEM VID capability returns `None` and we fall through
|
||||||
|
/// to cert auth unchanged.
|
||||||
fn do_handshake_cert(
|
fn do_handshake_cert(
|
||||||
session: &mut crate::drive::Drive,
|
session: &mut crate::drive::Drive,
|
||||||
opts: &ScanOptions,
|
opts: &ScanOptions,
|
||||||
) -> (Option<HandshakeResult>, Option<Error>) {
|
) -> (Option<HandshakeResult>, Option<Error>) {
|
||||||
use crate::aacs;
|
use crate::aacs;
|
||||||
|
|
||||||
|
// OEM VID shortcut. Resolve the SAME unlocker that would unlock this
|
||||||
|
// drive and ask it for the VID via its OEM mechanism. Cloning the
|
||||||
|
// DriveId first releases the immutable borrow before we hand the
|
||||||
|
// mutable transport to the registry.
|
||||||
|
let drive_id = session.drive_id.clone();
|
||||||
|
match crate::unlock::unlocker_read_vid(session.scsi_mut(), &drive_id) {
|
||||||
|
Ok(Some(volume_id)) => {
|
||||||
|
tracing::debug!(
|
||||||
|
target: "freemkv::disc",
|
||||||
|
phase = "oem_vid_ok",
|
||||||
|
"VID acquired via unlocker OEM path; skipping cert handshake"
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
Some(HandshakeResult {
|
||||||
|
volume_id,
|
||||||
|
read_data_key: None,
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
// No unlocker matched, or the matching unlocker has no OEM
|
||||||
|
// VID path — fall through to the cert handshake unchanged.
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(
|
||||||
|
target: "freemkv::disc",
|
||||||
|
phase = "oem_vid_failed",
|
||||||
|
error_code = e.code(),
|
||||||
|
"unlocker OEM VID retrieval failed; falling back to cert handshake"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Host certs come from the caller's DriveCredentials (e.g. the keydb's
|
// Host certs come from the caller's DriveCredentials (e.g. the keydb's
|
||||||
// host_certs(), sourced app-side) — the library does not load a keydb.
|
// host_certs(), sourced app-side) — the library does not load a keydb.
|
||||||
// Absent ⇒ no cert auth: resolution proceeds with VID=zero and relies
|
// Absent ⇒ no cert auth: resolution proceeds with VID=zero and relies
|
||||||
|
|||||||
+9
-8
@@ -573,15 +573,16 @@ impl Drive {
|
|||||||
|
|
||||||
/// Whether libfreemkv should take the OEM extended-access read path.
|
/// Whether libfreemkv should take the OEM extended-access read path.
|
||||||
///
|
///
|
||||||
/// The pluggable [`crate::unlock::Unlocker`] seam reports only
|
/// Whether a registered [`crate::unlock::Unlocker`] matches this drive.
|
||||||
/// success/failure from `unlock()` — it carries no extended-access
|
///
|
||||||
/// marker back into libfreemkv. With no marker channel, libfreemkv
|
/// An unlocker unlocks *drive functionality* — firmware unlock, OEM VID
|
||||||
/// always uses the standard host-certificate AACS handshake to acquire
|
/// retrieval, and other vendor capabilities. When one matches, libfreemkv
|
||||||
/// the Volume ID (the OEM route), so this is always `false`. A firmware
|
/// routes both `unlock` and OEM VID through it (VID via the OEM path is
|
||||||
/// unlocker still removes riplock / enables BD-UHD reads at `init()`;
|
/// decoupled from the host cert + HRL). This mirrors [`Self::has_profile`]
|
||||||
/// VID acquisition just stays on the cert path.
|
/// — the honest signal is "a registered unlocker claims this drive" —
|
||||||
|
/// rather than the old const `false`.
|
||||||
pub fn is_unlocked(&self) -> bool {
|
pub fn is_unlocked(&self) -> bool {
|
||||||
false
|
crate::unlock::matching_name(&self.drive_id).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read sectors from the disc. Single-shot — no inline retries, no
|
/// Read sectors from the disc. Single-shot — no inline retries, no
|
||||||
|
|||||||
+125
-5
@@ -32,6 +32,24 @@ pub trait Unlocker: Send + Sync {
|
|||||||
/// Unlock the drive. The unlocker issues its own CDBs through `scsi`.
|
/// Unlock the drive. The unlocker issues its own CDBs through `scsi`.
|
||||||
/// Returns `Ok(())` once the drive is prepared for reads.
|
/// Returns `Ok(())` once the drive is prepared for reads.
|
||||||
fn unlock(&self, scsi: &mut dyn ScsiTransport, id: &DriveId) -> Result<()>;
|
fn unlock(&self, scsi: &mut dyn ScsiTransport, id: &DriveId) -> Result<()>;
|
||||||
|
|
||||||
|
/// Read the AACS Volume ID via this unlocker's OEM mechanism, if it
|
||||||
|
/// has one.
|
||||||
|
///
|
||||||
|
/// An [`Unlocker`] unlocks *drive functionality*, not just the disc:
|
||||||
|
/// `unlock` is one capability, OEM VID retrieval is another. Once the
|
||||||
|
/// matching unlocker is identified for a drive, libfreemkv uses it for
|
||||||
|
/// BOTH unlock and VID. The OEM path returns the VID *without* the host
|
||||||
|
/// certificate + HRL, decoupling VID from the cert handshake.
|
||||||
|
///
|
||||||
|
/// Default is a no-op: an unlocker that provides no OEM VID path (or
|
||||||
|
/// any unlocker that doesn't override this) returns `Ok(None)`, and
|
||||||
|
/// libfreemkv falls back to the cert-based VID read. Implementors that
|
||||||
|
/// can serve the VID directly (e.g. a per-drive OEM CDB) return
|
||||||
|
/// `Ok(Some(vid))`.
|
||||||
|
fn read_vid(&self, _scsi: &mut dyn ScsiTransport, _id: &DriveId) -> Result<Option<[u8; 16]>> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process-wide ordered registry of unlockers.
|
/// Process-wide ordered registry of unlockers.
|
||||||
@@ -75,6 +93,35 @@ pub(crate) fn route_unlock(scsi: &mut dyn ScsiTransport, id: &DriveId) -> Result
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Walk the registry in order and ask the first matching unlocker for the
|
||||||
|
/// OEM Volume ID.
|
||||||
|
///
|
||||||
|
/// Mirrors [`route_unlock`]'s resolution so the SAME identified unlocker
|
||||||
|
/// that unlocks the drive is the one consulted for VID. Returns:
|
||||||
|
/// * `Ok(Some(vid))` — a matching unlocker served the VID via its OEM
|
||||||
|
/// path (no cert handshake needed; VID is decoupled from the HRL).
|
||||||
|
/// * `Ok(None)` — no unlocker matched, or the matching unlocker has no
|
||||||
|
/// OEM VID path; the caller falls through to the cert-based VID read.
|
||||||
|
/// * `Err(_)` — the matching unlocker's `read_vid` failed (e.g. the OEM
|
||||||
|
/// CDB returned a malformed response).
|
||||||
|
pub(crate) fn unlocker_read_vid(
|
||||||
|
scsi: &mut dyn ScsiTransport,
|
||||||
|
id: &DriveId,
|
||||||
|
) -> Result<Option<[u8; 16]>> {
|
||||||
|
let reg = match REGISTRY.read() {
|
||||||
|
Ok(r) => r,
|
||||||
|
// Poisoned lock ⇒ treat as "no unlocker available" so the cert
|
||||||
|
// fallback still runs.
|
||||||
|
Err(_) => return Ok(None),
|
||||||
|
};
|
||||||
|
for u in reg.iter() {
|
||||||
|
if u.matches(id) {
|
||||||
|
return u.read_vid(scsi, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
/// Number of registered unlockers — test/introspection helper.
|
/// Number of registered unlockers — test/introspection helper.
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub fn registered_count() -> usize {
|
pub fn registered_count() -> usize {
|
||||||
@@ -122,10 +169,31 @@ mod tests {
|
|||||||
DriveId::from_inquiry(&inquiry, "")
|
DriveId::from_inquiry(&inquiry, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fake unlocker that records whether it ran, and matches on vendor id.
|
/// Fake unlocker that records whether it ran, matches on vendor id, and
|
||||||
|
/// optionally serves an OEM VID (mirroring the read_vid capability).
|
||||||
struct FakeUnlocker {
|
struct FakeUnlocker {
|
||||||
want_vendor: String,
|
want_vendor: String,
|
||||||
ran: Arc<AtomicBool>,
|
ran: Arc<AtomicBool>,
|
||||||
|
/// VID this unlocker's OEM path returns: `Some(vid)` (capability
|
||||||
|
/// present), `None` (no OEM path → cert fallback). `vid_ran` records
|
||||||
|
/// whether read_vid was consulted.
|
||||||
|
vid: Option<[u8; 16]>,
|
||||||
|
vid_ran: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
impl FakeUnlocker {
|
||||||
|
fn new(vendor: &str, ran: Arc<AtomicBool>) -> Self {
|
||||||
|
Self {
|
||||||
|
want_vendor: vendor.into(),
|
||||||
|
ran,
|
||||||
|
vid: None,
|
||||||
|
vid_ran: Arc::new(AtomicBool::new(false)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn with_vid(mut self, vid: Option<[u8; 16]>, vid_ran: Arc<AtomicBool>) -> Self {
|
||||||
|
self.vid = vid;
|
||||||
|
self.vid_ran = vid_ran;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
impl Unlocker for FakeUnlocker {
|
impl Unlocker for FakeUnlocker {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
@@ -138,6 +206,14 @@ mod tests {
|
|||||||
self.ran.store(true, Ordering::SeqCst);
|
self.ran.store(true, Ordering::SeqCst);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
fn read_vid(
|
||||||
|
&self,
|
||||||
|
_scsi: &mut dyn ScsiTransport,
|
||||||
|
_id: &DriveId,
|
||||||
|
) -> Result<Option<[u8; 16]>> {
|
||||||
|
self.vid_ran.store(true, Ordering::SeqCst);
|
||||||
|
Ok(self.vid)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A registered, matching unlocker runs; a non-matching identity leaves
|
/// A registered, matching unlocker runs; a non-matching identity leaves
|
||||||
@@ -149,10 +225,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn registry_routes_match_else_oem() {
|
fn registry_routes_match_else_oem() {
|
||||||
let ran = Arc::new(AtomicBool::new(false));
|
let ran = Arc::new(AtomicBool::new(false));
|
||||||
register_unlocker(Box::new(FakeUnlocker {
|
register_unlocker(Box::new(FakeUnlocker::new("MATCHVND", ran.clone())));
|
||||||
want_vendor: "MATCHVND".into(),
|
|
||||||
ran: ran.clone(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Matching identity → unlocker runs, returns its name.
|
// Matching identity → unlocker runs, returns its name.
|
||||||
let mut scsi = NoopTransport;
|
let mut scsi = NoopTransport;
|
||||||
@@ -169,4 +242,51 @@ mod tests {
|
|||||||
"unlock() not invoked on no-match"
|
"unlock() not invoked on no-match"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `unlocker_read_vid` consults the FIRST matching unlocker's `read_vid`.
|
||||||
|
/// A matching unlocker that returns `Some(vid)` yields that VID (the OEM
|
||||||
|
/// path — cert handshake skipped). A matching unlocker that returns
|
||||||
|
/// `None`, or no match at all, yields `Ok(None)` (cert fallback).
|
||||||
|
///
|
||||||
|
/// Distinct vendor ids keep this independent of the other registry test
|
||||||
|
/// despite the process-wide shared registry.
|
||||||
|
#[test]
|
||||||
|
fn unlocker_read_vid_routes_match_else_cert() {
|
||||||
|
let mut scsi = NoopTransport;
|
||||||
|
|
||||||
|
// Unlocker WITH an OEM VID capability. Vendor ids are exactly 8
|
||||||
|
// chars: INQUIRY field [8..16] has no null padding to trim, so the
|
||||||
|
// trimmed compare in `matches` is exact.
|
||||||
|
let vid = [0x5Au8; 16];
|
||||||
|
let vid_ran = Arc::new(AtomicBool::new(false));
|
||||||
|
register_unlocker(Box::new(
|
||||||
|
FakeUnlocker::new("VIDVNDOR", Arc::new(AtomicBool::new(false)))
|
||||||
|
.with_vid(Some(vid), vid_ran.clone()),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Matching identity → read_vid consulted, its VID used.
|
||||||
|
let got = unlocker_read_vid(&mut scsi, &fake_id("VIDVNDOR")).unwrap();
|
||||||
|
assert_eq!(got, Some(vid), "matching unlocker's OEM VID is used");
|
||||||
|
assert!(vid_ran.load(Ordering::SeqCst), "read_vid() was consulted");
|
||||||
|
|
||||||
|
// Unlocker that MATCHES but has NO OEM VID path (read_vid → None).
|
||||||
|
let none_ran = Arc::new(AtomicBool::new(false));
|
||||||
|
register_unlocker(Box::new(
|
||||||
|
FakeUnlocker::new("NOVIDVND", Arc::new(AtomicBool::new(false)))
|
||||||
|
.with_vid(None, none_ran.clone()),
|
||||||
|
));
|
||||||
|
let got = unlocker_read_vid(&mut scsi, &fake_id("NOVIDVND")).unwrap();
|
||||||
|
assert!(
|
||||||
|
got.is_none(),
|
||||||
|
"unlocker without OEM VID falls through to cert"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
none_ran.load(Ordering::SeqCst),
|
||||||
|
"read_vid() consulted even when it returns None"
|
||||||
|
);
|
||||||
|
|
||||||
|
// No matching unlocker → Ok(None), nothing consulted.
|
||||||
|
let got = unlocker_read_vid(&mut scsi, &fake_id("UNKNWNVD")).unwrap();
|
||||||
|
assert!(got.is_none(), "no match → cert fallback");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user