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:
Matthew Jackson
2026-06-29 19:33:07 -07:00
parent 0e943c3792
commit 22130bfe12
6 changed files with 2280 additions and 6 deletions
+88
View File
@@ -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)),
}
}
}