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:
Matthew Jackson
2026-06-22 10:50:47 -07:00
parent 6dc62bcd84
commit 159e967760
3 changed files with 190 additions and 22 deletions
+125 -5
View File
@@ -32,6 +32,24 @@ pub trait Unlocker: Send + Sync {
/// Unlock the drive. The unlocker issues its own CDBs through `scsi`.
/// Returns `Ok(())` once the drive is prepared for reads.
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.
@@ -75,6 +93,35 @@ pub(crate) fn route_unlock(scsi: &mut dyn ScsiTransport, id: &DriveId) -> Result
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.
#[doc(hidden)]
pub fn registered_count() -> usize {
@@ -122,10 +169,31 @@ mod tests {
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 {
want_vendor: String,
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 {
fn name(&self) -> &str {
@@ -138,6 +206,14 @@ mod tests {
self.ran.store(true, Ordering::SeqCst);
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
@@ -149,10 +225,7 @@ mod tests {
#[test]
fn registry_routes_match_else_oem() {
let ran = Arc::new(AtomicBool::new(false));
register_unlocker(Box::new(FakeUnlocker {
want_vendor: "MATCHVND".into(),
ran: ran.clone(),
}));
register_unlocker(Box::new(FakeUnlocker::new("MATCHVND", ran.clone())));
// Matching identity → unlocker runs, returns its name.
let mut scsi = NoopTransport;
@@ -169,4 +242,51 @@ mod tests {
"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");
}
}