aacs: relocate the AACS cert handshake into freemkv-unlock
Stage 2: the AACS host-certificate handshake (the AKE + bus-key derivation +
P-160/P-256 EC crypto, ~2050 lines) moves out of libfreemkv into the
self-contained src/aacs module, with its own error type (the Aacs* failure
points + structured ScsiError) and an aes_ecb_decrypt helper. AacsCert impls
crate::Unlocker — matches DiscKind::Aacs, runs run_cert_handshake against the
host certs the consumer passes via UnlockCtx, and returns Unlocked { vid,
bus_key }. collect_host_certs stays in libfreemkv (it reads keysources). The
SCSI contract gains ScsiSense + the AACS/REPORT-KEY opcodes. libfreemkv is
untouched (still green); it rewires onto this in stage 4.
72 tests pass (the handshake brought its full EC-crypto test suite).
This commit is contained in:
@@ -12,3 +12,9 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
num-bigint = "0.4"
|
||||||
|
num-traits = "0.2"
|
||||||
|
sha1 = "0.10"
|
||||||
|
sha2 = "0.10"
|
||||||
|
aes = "0.8"
|
||||||
|
rand = "0.8"
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
//! aacs's internal error for the cert-handshake SCSI/crypto code. Mirrors the
|
||||||
|
//! handshake's original libfreemkv error surface (the specific Aacs* failure
|
||||||
|
//! points + a structured SCSI error), so the moved handshake body is unchanged.
|
||||||
|
|
||||||
|
use crate::scsi::{SCSI_STATUS_TRANSPORT_FAILURE, ScsiSense};
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
// A few variants are matched (defensive arms in the handshake) but never
|
||||||
|
// constructed in the wired path — kept for completeness.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Error {
|
||||||
|
AacsAgidAlloc,
|
||||||
|
AacsCertRead,
|
||||||
|
AacsCertRejected,
|
||||||
|
AacsCertShort,
|
||||||
|
AacsCertVerify,
|
||||||
|
AacsDataKey,
|
||||||
|
AacsKeyRead,
|
||||||
|
AacsKeyRejected,
|
||||||
|
AacsKeyVerify,
|
||||||
|
AacsNoKeys,
|
||||||
|
AacsVidMac,
|
||||||
|
AacsVidRead,
|
||||||
|
HandshakeRejected,
|
||||||
|
VidUnavailable,
|
||||||
|
/// A SCSI command failed. `status == SCSI_STATUS_TRANSPORT_FAILURE` with
|
||||||
|
/// `sense: None` is a transport-layer fault; a CHECK CONDITION carries the
|
||||||
|
/// parsed [`ScsiSense`].
|
||||||
|
ScsiError {
|
||||||
|
/// CDB opcode that failed — diagnostic, carried for future logging.
|
||||||
|
opcode: u8,
|
||||||
|
status: u8,
|
||||||
|
sense: Option<ScsiSense>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
/// Stable numeric code (logged). Values are local to this crate.
|
||||||
|
pub fn code(&self) -> u16 {
|
||||||
|
match self {
|
||||||
|
Error::AacsAgidAlloc => 7001,
|
||||||
|
Error::AacsCertRead => 7002,
|
||||||
|
Error::AacsCertRejected => 7003,
|
||||||
|
Error::AacsCertShort => 7004,
|
||||||
|
Error::AacsCertVerify => 7005,
|
||||||
|
Error::AacsDataKey => 7006,
|
||||||
|
Error::AacsKeyRead => 7007,
|
||||||
|
Error::AacsKeyRejected => 7008,
|
||||||
|
Error::AacsKeyVerify => 7009,
|
||||||
|
Error::AacsNoKeys => 7010,
|
||||||
|
Error::AacsVidMac => 7011,
|
||||||
|
Error::AacsVidRead => 7012,
|
||||||
|
Error::HandshakeRejected => 7013,
|
||||||
|
Error::VidUnavailable => 7014,
|
||||||
|
Error::ScsiError { .. } => 7099,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The parsed sense for a CHECK CONDITION SCSI error, else `None`.
|
||||||
|
pub fn scsi_sense(&self) -> Option<ScsiSense> {
|
||||||
|
match self {
|
||||||
|
Error::ScsiError { sense, .. } => *sense,
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True if this is a transport-layer SCSI failure (bus dead).
|
||||||
|
pub fn is_scsi_transport_failure(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
Error::ScsiError { status, sense: None, .. } if *status == SCSI_STATUS_TRANSPORT_FAILURE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A generic transport fault from the SCSI contract converts in (opcode unknown
|
||||||
|
/// at the transport level; sense parsed from the raw buffer when present).
|
||||||
|
impl From<crate::scsi::ScsiError> for Error {
|
||||||
|
fn from(e: crate::scsi::ScsiError) -> Self {
|
||||||
|
Error::ScsiError {
|
||||||
|
opcode: 0,
|
||||||
|
status: e.status,
|
||||||
|
sense: e.sense.map(|s| ScsiSense::from_buf(&s)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
+115
@@ -0,0 +1,115 @@
|
|||||||
|
//! aacs — the AACS host-certificate unlocker (Blu-ray / UHD).
|
||||||
|
//!
|
||||||
|
//! Self-contained module: it owns the cert-handshake EC crypto (the AKE, bus-key
|
||||||
|
//! derivation, P-160 / P-256 curve math) that REMOVES AACS bus encryption. It
|
||||||
|
//! implements [`crate::Unlocker`], learning the Volume ID + AACS 2.x bus key.
|
||||||
|
//! Content-key decryption (unit keys, MKB, VUK) is the consumer's job, not here.
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
mod handshake;
|
||||||
|
|
||||||
|
use aes::Aes128;
|
||||||
|
use aes::cipher::{BlockDecrypt, KeyInit, generic_array::GenericArray};
|
||||||
|
|
||||||
|
use crate::scsi::ScsiTransport;
|
||||||
|
use crate::{DiscKind, UnlockCtx, UnlockError, Unlocked, Unlocker};
|
||||||
|
|
||||||
|
/// AES-128-ECB decrypt a single 16-byte block — used to decrypt the bus key /
|
||||||
|
/// read_data_key the drive returns after the handshake.
|
||||||
|
pub(crate) fn aes_ecb_decrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
|
||||||
|
let cipher = Aes128::new(GenericArray::from_slice(key));
|
||||||
|
let mut block = GenericArray::clone_from_slice(data);
|
||||||
|
cipher.decrypt_block(&mut block);
|
||||||
|
let mut out = [0u8; 16];
|
||||||
|
out.copy_from_slice(&block);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The AACS host-certificate unlocker. Matches a Blu-ray/UHD disc
|
||||||
|
/// (`DiscKind::Aacs`) and runs the cert handshake against the host certs the
|
||||||
|
/// consumer collected (via [`UnlockCtx::host_certs`]), learning the Volume ID
|
||||||
|
/// and — on AACS 2.0 — the bus key.
|
||||||
|
pub struct AacsCert;
|
||||||
|
|
||||||
|
impl AacsCert {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
AacsCert
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AacsCert {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Unlocker for AacsCert {
|
||||||
|
fn matches(&self, ctx: &UnlockCtx) -> bool {
|
||||||
|
ctx.kind == DiscKind::Aacs
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unlock(
|
||||||
|
&self,
|
||||||
|
scsi: &mut dyn ScsiTransport,
|
||||||
|
ctx: &UnlockCtx,
|
||||||
|
) -> std::result::Result<Unlocked, UnlockError> {
|
||||||
|
if ctx.host_certs.is_empty() {
|
||||||
|
// No host cert to authenticate with — the consumer falls back to a
|
||||||
|
// VID-less / keysource path.
|
||||||
|
return Err(UnlockError::NoUsableHostCert);
|
||||||
|
}
|
||||||
|
let h = handshake::run_cert_handshake(scsi, ctx.host_certs)?;
|
||||||
|
Ok(Unlocked {
|
||||||
|
vid: Some(h.volume_id),
|
||||||
|
// Host-cert AKE path: bus removal depends on the bus key, not a
|
||||||
|
// firmware unlock.
|
||||||
|
bus_key: h.read_data_key,
|
||||||
|
drive_unlocked: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn id() -> crate::DriveId {
|
||||||
|
crate::DriveId::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AacsCert matches only `DiscKind::Aacs`.
|
||||||
|
#[test]
|
||||||
|
fn matches_only_aacs_kind() {
|
||||||
|
let id = id();
|
||||||
|
let u = AacsCert::new();
|
||||||
|
for k in [DiscKind::Unknown, DiscKind::Unencrypted, DiscKind::Css] {
|
||||||
|
assert!(
|
||||||
|
!u.matches(&UnlockCtx::new(&id, k, &[])),
|
||||||
|
"must not match {k:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(u.matches(&UnlockCtx::new(&id, DiscKind::Aacs, &[])));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// With no host certs there is nothing to authenticate with → NoUsableHostCert,
|
||||||
|
/// and the transport is never touched.
|
||||||
|
#[test]
|
||||||
|
fn no_host_certs_is_no_usable_host_cert() {
|
||||||
|
struct DeadTransport;
|
||||||
|
impl ScsiTransport for DeadTransport {
|
||||||
|
fn execute(
|
||||||
|
&mut self,
|
||||||
|
_cdb: &[u8],
|
||||||
|
_dir: crate::scsi::DataDirection,
|
||||||
|
_data: &mut [u8],
|
||||||
|
_timeout_ms: u32,
|
||||||
|
) -> crate::scsi::Result<crate::scsi::ScsiResult> {
|
||||||
|
panic!("transport must not be touched with no host certs");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let id = id();
|
||||||
|
let mut t = DeadTransport;
|
||||||
|
let r = AacsCert::new().unlock(&mut t, &UnlockCtx::new(&id, DiscKind::Aacs, &[]));
|
||||||
|
assert_eq!(r.unwrap_err(), UnlockError::NoUsableHostCert);
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-2
@@ -12,8 +12,8 @@
|
|||||||
|
|
||||||
pub mod scsi;
|
pub mod scsi;
|
||||||
|
|
||||||
|
mod aacs;
|
||||||
mod ld;
|
mod ld;
|
||||||
// mod aacs; // stage 2 — AACS host-certificate handshake
|
|
||||||
// mod css; // stage 3 — CSS bus-auth
|
// mod css; // stage 3 — CSS bus-auth
|
||||||
|
|
||||||
use scsi::ScsiTransport;
|
use scsi::ScsiTransport;
|
||||||
@@ -41,8 +41,14 @@ pub enum DiscKind {
|
|||||||
/// these from its key sources and passes them in).
|
/// these from its key sources and passes them in).
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct HostCert {
|
pub struct HostCert {
|
||||||
|
/// AACS 1.0 host private key (20 bytes).
|
||||||
pub private_key: [u8; 20],
|
pub private_key: [u8; 20],
|
||||||
|
/// AACS 1.0 host certificate (92 bytes).
|
||||||
pub certificate: Vec<u8>,
|
pub certificate: Vec<u8>,
|
||||||
|
/// AACS 2.0 host private key (P-256, 32 bytes). `None` for AACS 1.0 only.
|
||||||
|
pub private_key_v2: Option<[u8; 32]>,
|
||||||
|
/// AACS 2.0 host certificate (type 0x11). `None` for AACS 1.0 only.
|
||||||
|
pub certificate_v2: Option<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Context handed to an unlocker: drive identity, disc kind, and (for the cert
|
/// Context handed to an unlocker: drive identity, disc kind, and (for the cert
|
||||||
@@ -114,7 +120,7 @@ pub trait Unlocker: Send + Sync {
|
|||||||
pub fn all_unlockers() -> Vec<Box<dyn Unlocker>> {
|
pub fn all_unlockers() -> Vec<Box<dyn Unlocker>> {
|
||||||
vec![
|
vec![
|
||||||
Box::new(ld::LibreDrive::new()),
|
Box::new(ld::LibreDrive::new()),
|
||||||
// Box::new(aacs::AacsCert::new()), // stage 2
|
Box::new(aacs::AacsCert::new()),
|
||||||
// Box::new(css::Css::new()), // stage 3
|
// Box::new(css::Css::new()), // stage 3
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-4
@@ -41,15 +41,41 @@ pub trait ScsiTransport {
|
|||||||
) -> Result<ScsiResult>;
|
) -> Result<ScsiResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parsed SCSI sense (the diagnostic an unlocker reads off a failed command).
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct ScsiSense {
|
||||||
|
pub sense_key: u8,
|
||||||
|
pub asc: u8,
|
||||||
|
pub ascq: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScsiSense {
|
||||||
|
/// Parse the fixed-format sense buffer (key at byte 2, ASC at 12, ASCQ at 13).
|
||||||
|
pub fn from_buf(sense: &[u8; 32]) -> Self {
|
||||||
|
ScsiSense {
|
||||||
|
sense_key: sense[2] & 0x0F,
|
||||||
|
asc: sense[12],
|
||||||
|
ascq: sense[13],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// ILLEGAL REQUEST (sense key 0x05) — the drive won't honor the command.
|
||||||
|
pub fn is_illegal_request(&self) -> bool {
|
||||||
|
self.sense_key == 0x05
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// SCSI status byte for a transport-layer failure (bridge crash / disconnect).
|
/// SCSI status byte for a transport-layer failure (bridge crash / disconnect).
|
||||||
pub(crate) const SCSI_STATUS_TRANSPORT_FAILURE: u8 = 0xFF;
|
pub(crate) const SCSI_STATUS_TRANSPORT_FAILURE: u8 = 0xFF;
|
||||||
|
/// SCSI status byte CHECK CONDITION (a drive sense is available).
|
||||||
|
pub(crate) const SCSI_STATUS_CHECK_CONDITION: u8 = 0x02;
|
||||||
|
|
||||||
// Common opcodes used by the unlocker modules.
|
// Common opcodes used by the unlocker modules.
|
||||||
pub(crate) const SCSI_READ_CAPACITY: u8 = 0x25;
|
|
||||||
pub(crate) const SCSI_WRITE_BUFFER: u8 = 0x3B;
|
|
||||||
pub(crate) const SCSI_READ_BUFFER: u8 = 0x3C;
|
|
||||||
pub(crate) const SCSI_MODE_SELECT: u8 = 0x55; // MODE SELECT (10)
|
|
||||||
pub(crate) const SCSI_SET_CD_SPEED: u8 = 0xBB;
|
pub(crate) const SCSI_SET_CD_SPEED: u8 = 0xBB;
|
||||||
|
pub(crate) const SCSI_SEND_KEY: u8 = 0xA3;
|
||||||
|
pub(crate) const SCSI_REPORT_KEY: u8 = 0xA4;
|
||||||
|
pub(crate) const SCSI_READ_DISC_STRUCTURE: u8 = 0xAD;
|
||||||
|
/// AACS key class selector used in REPORT/SEND KEY CDBs.
|
||||||
|
pub(crate) const AACS_KEY_CLASS: u8 = 0x02;
|
||||||
|
|
||||||
/// Build a SET CD SPEED (0xBB) CDB requesting `read_speed` (KB/s; 0xFFFF = max).
|
/// Build a SET CD SPEED (0xBB) CDB requesting `read_speed` (KB/s; 0xFFFF = max).
|
||||||
pub(crate) fn build_set_cd_speed(read_speed: u16) -> [u8; 12] {
|
pub(crate) fn build_set_cd_speed(read_speed: u16) -> [u8; 12] {
|
||||||
|
|||||||
Reference in New Issue
Block a user