mirror of
https://github.com/freemkv/freemkv-unlock.git
synced 2026-10-07 18:13:11 +00:00
test: cover aacs/css error paths + aes block
Adds unit tests for aacs::error::Error and css::error::Error (code(), scsi_sense/is_transport_failure branches, From<ScsiError> conversions), a FIPS-197 known-answer test for aacs::aes_ecb_decrypt, and scripted fake-transport tests covering the authenticate_with_agid / read_disc_key error branches in css::mod. Raises line coverage 91.16% -> 92.59%.
This commit is contained in:
@@ -96,3 +96,144 @@ impl From<crate::scsi::ScsiError> for Error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Every variant has its own stable numeric code. Constructing all 16
|
||||
/// arms also exercises the `#[allow(dead_code)]` variants that the
|
||||
/// wired handshake path never builds.
|
||||
#[test]
|
||||
fn code_is_stable_per_variant() {
|
||||
assert_eq!(Error::AacsAgidAlloc.code(), 7001);
|
||||
assert_eq!(Error::AacsCertRead.code(), 7002);
|
||||
assert_eq!(Error::AacsCertRejected.code(), 7003);
|
||||
assert_eq!(Error::AacsCertShort.code(), 7004);
|
||||
assert_eq!(Error::AacsCertVerify.code(), 7005);
|
||||
assert_eq!(Error::AacsDataKey.code(), 7006);
|
||||
assert_eq!(Error::AacsKeyRead.code(), 7007);
|
||||
assert_eq!(Error::AacsKeyRejected.code(), 7008);
|
||||
assert_eq!(Error::AacsKeyVerify.code(), 7009);
|
||||
assert_eq!(Error::AacsNoKeys.code(), 7010);
|
||||
assert_eq!(Error::AacsVidMac.code(), 7011);
|
||||
assert_eq!(Error::AacsVidRead.code(), 7012);
|
||||
assert_eq!(Error::HandshakeRejected.code(), 7013);
|
||||
assert_eq!(Error::VidUnavailable.code(), 7014);
|
||||
assert_eq!(
|
||||
Error::ShortTransfer {
|
||||
opcode: 0xA4,
|
||||
expected: 16,
|
||||
got: 4,
|
||||
}
|
||||
.code(),
|
||||
7015
|
||||
);
|
||||
assert_eq!(
|
||||
Error::Scsi {
|
||||
opcode: 0xA4,
|
||||
status: 0x02,
|
||||
sense: None,
|
||||
}
|
||||
.code(),
|
||||
7099
|
||||
);
|
||||
}
|
||||
|
||||
/// `scsi_sense` returns the parsed sense for a `Scsi` error carrying one,
|
||||
/// `None` when the `Scsi` error has none, and `None` for every non-`Scsi`
|
||||
/// variant.
|
||||
#[test]
|
||||
fn scsi_sense_returns_the_parsed_sense_only_for_scsi_variant() {
|
||||
let sense = ScsiSense {
|
||||
sense_key: 0x05,
|
||||
asc: 0x20,
|
||||
ascq: 0x00,
|
||||
};
|
||||
let with_sense = Error::Scsi {
|
||||
opcode: 0xA4,
|
||||
status: 0x02,
|
||||
sense: Some(sense),
|
||||
};
|
||||
let sc = with_sense.scsi_sense().unwrap();
|
||||
assert_eq!(sc.sense_key, sense.sense_key);
|
||||
assert_eq!(sc.asc, sense.asc);
|
||||
assert_eq!(sc.ascq, sense.ascq);
|
||||
|
||||
let without_sense = Error::Scsi {
|
||||
opcode: 0xA4,
|
||||
status: 0xFF,
|
||||
sense: None,
|
||||
};
|
||||
assert!(without_sense.scsi_sense().is_none());
|
||||
|
||||
assert!(Error::AacsNoKeys.scsi_sense().is_none());
|
||||
}
|
||||
|
||||
/// `is_scsi_transport_failure` is true only for a `Scsi` error with the
|
||||
/// transport-failure status AND no sense; a drive CHECK CONDITION and
|
||||
/// every non-`Scsi` variant are false.
|
||||
#[test]
|
||||
fn is_scsi_transport_failure_true_and_false_paths() {
|
||||
let transport = Error::Scsi {
|
||||
opcode: 0,
|
||||
status: SCSI_STATUS_TRANSPORT_FAILURE,
|
||||
sense: None,
|
||||
};
|
||||
assert!(transport.is_scsi_transport_failure());
|
||||
|
||||
let check_condition = Error::Scsi {
|
||||
opcode: 0xA4,
|
||||
status: 0x02,
|
||||
sense: Some(ScsiSense {
|
||||
sense_key: 0x05,
|
||||
asc: 0x20,
|
||||
ascq: 0x00,
|
||||
}),
|
||||
};
|
||||
assert!(!check_condition.is_scsi_transport_failure());
|
||||
|
||||
assert!(!Error::AacsNoKeys.is_scsi_transport_failure());
|
||||
}
|
||||
|
||||
/// `From<ScsiError>` maps a raw transport-layer error into `Error::Scsi`,
|
||||
/// parsing the sense buffer when present and leaving it `None` otherwise.
|
||||
#[test]
|
||||
fn from_scsi_error_maps_status_and_parses_sense() {
|
||||
let e = Error::from(crate::scsi::ScsiError {
|
||||
status: SCSI_STATUS_TRANSPORT_FAILURE,
|
||||
sense: None,
|
||||
});
|
||||
match e {
|
||||
Error::Scsi {
|
||||
opcode,
|
||||
status,
|
||||
sense,
|
||||
} => {
|
||||
assert_eq!(opcode, 0);
|
||||
assert_eq!(status, SCSI_STATUS_TRANSPORT_FAILURE);
|
||||
assert!(sense.is_none());
|
||||
}
|
||||
_ => panic!("expected Error::Scsi"),
|
||||
}
|
||||
|
||||
let mut buf = [0u8; 32];
|
||||
buf[2] = 0x05;
|
||||
buf[12] = 0x24;
|
||||
buf[13] = 0x01;
|
||||
let e = Error::from(crate::scsi::ScsiError {
|
||||
status: 0x02,
|
||||
sense: Some(buf),
|
||||
});
|
||||
match e {
|
||||
Error::Scsi { status, sense, .. } => {
|
||||
assert_eq!(status, 0x02);
|
||||
let s = sense.expect("sense parsed");
|
||||
assert_eq!(s.sense_key, 0x05);
|
||||
assert_eq!(s.asc, 0x24);
|
||||
assert_eq!(s.ascq, 0x01);
|
||||
}
|
||||
_ => panic!("expected Error::Scsi"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,38 @@ impl Unlocker for AacsCert {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// AES-128-ECB decrypt against the FIPS-197 Appendix B / NIST known-answer
|
||||
/// test vector: decrypting the published ciphertext with the published key
|
||||
/// must recover the published plaintext.
|
||||
#[test]
|
||||
fn aes_ecb_decrypt_matches_fips197_test_vector() {
|
||||
let key: [u8; 16] = [
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
|
||||
0x0E, 0x0F,
|
||||
];
|
||||
let ciphertext: [u8; 16] = [
|
||||
0x69, 0xC4, 0xE0, 0xD8, 0x6A, 0x7B, 0x04, 0x30, 0xD8, 0xCD, 0xB7, 0x80, 0x70, 0xB4,
|
||||
0xC5, 0x5A,
|
||||
];
|
||||
let expected_plaintext: [u8; 16] = [
|
||||
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD,
|
||||
0xEE, 0xFF,
|
||||
];
|
||||
let out = aes_ecb_decrypt(&key, &ciphertext);
|
||||
assert_eq!(out, expected_plaintext);
|
||||
}
|
||||
|
||||
/// Decrypting a different ciphertext under the same key must not produce
|
||||
/// the same plaintext — pins that the function actually decrypts the
|
||||
/// given block rather than returning a constant.
|
||||
#[test]
|
||||
fn aes_ecb_decrypt_varies_with_input() {
|
||||
let key = [0u8; 16];
|
||||
let a = aes_ecb_decrypt(&key, &[0u8; 16]);
|
||||
let b = aes_ecb_decrypt(&key, &[1u8; 16]);
|
||||
assert_ne!(a, b);
|
||||
}
|
||||
|
||||
fn id() -> crate::DriveId {
|
||||
crate::DriveId::default()
|
||||
}
|
||||
|
||||
@@ -89,4 +89,61 @@ mod tests {
|
||||
assert!(matches!(refused, Error::CssAuthFailed));
|
||||
assert_eq!(UnlockError::from(refused), UnlockError::NotApplicable);
|
||||
}
|
||||
|
||||
/// Each variant has its own stable numeric code.
|
||||
#[test]
|
||||
fn code_is_stable_per_variant() {
|
||||
assert_eq!(Error::CssAuthFailed.code(), 7201);
|
||||
assert_eq!(
|
||||
Error::Scsi(ScsiError {
|
||||
status: SCSI_STATUS_CHECK_CONDITION,
|
||||
sense: Some([0u8; 32]),
|
||||
})
|
||||
.code(),
|
||||
7299
|
||||
);
|
||||
}
|
||||
|
||||
/// `is_transport_failure` is false for `CssAuthFailed`, false for a
|
||||
/// `Scsi` error carrying a sense (a drive rejection, not a dead bus) even
|
||||
/// at the transport-failure status, and true only for the genuine
|
||||
/// transport fault (status + no sense).
|
||||
#[test]
|
||||
fn is_transport_failure_false_paths() {
|
||||
assert!(!Error::CssAuthFailed.is_transport_failure());
|
||||
|
||||
let sensed = Error::Scsi(ScsiError {
|
||||
status: SCSI_STATUS_TRANSPORT_FAILURE,
|
||||
sense: Some([0u8; 32]),
|
||||
});
|
||||
assert!(!sensed.is_transport_failure());
|
||||
|
||||
let refusal = Error::Scsi(ScsiError {
|
||||
status: SCSI_STATUS_CHECK_CONDITION,
|
||||
sense: None,
|
||||
});
|
||||
assert!(!refusal.is_transport_failure());
|
||||
|
||||
let dead = Error::Scsi(ScsiError {
|
||||
status: SCSI_STATUS_TRANSPORT_FAILURE,
|
||||
sense: None,
|
||||
});
|
||||
assert!(dead.is_transport_failure());
|
||||
}
|
||||
|
||||
/// `From<ScsiError>` wraps the transport error unchanged into `Error::Scsi`.
|
||||
#[test]
|
||||
fn from_scsi_error_wraps_into_scsi_variant() {
|
||||
let e = Error::from(ScsiError {
|
||||
status: SCSI_STATUS_CHECK_CONDITION,
|
||||
sense: Some([1u8; 32]),
|
||||
});
|
||||
match e {
|
||||
Error::Scsi(inner) => {
|
||||
assert_eq!(inner.status, SCSI_STATUS_CHECK_CONDITION);
|
||||
assert_eq!(inner.sense, Some([1u8; 32]));
|
||||
}
|
||||
_ => panic!("expected Error::Scsi"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1225,4 +1225,69 @@ mod tests {
|
||||
let e = establish_authenticated_session(&mut t).expect_err("short alloc");
|
||||
assert!(matches!(e, Error::CssAuthFailed));
|
||||
}
|
||||
|
||||
// ── authenticate_with_agid step failures ────────────────────────────────
|
||||
|
||||
/// The drive refuses the host-challenge SEND KEY (step 1) — the very first
|
||||
/// command `authenticate_with_agid` issues.
|
||||
#[test]
|
||||
fn host_challenge_send_failure_fails_the_auth() {
|
||||
let mut t =
|
||||
MockTransport::scripted(vec![Reply::illegal_request()], Reply::illegal_request());
|
||||
let e = authenticate_with_agid(&mut t, 0).expect_err("refused host challenge");
|
||||
assert!(matches!(e, Error::CssAuthFailed));
|
||||
assert_eq!(t.calls(), 1, "must not proceed past the first refused step");
|
||||
}
|
||||
|
||||
/// The host challenge SEND KEY succeeds but the Key1 REPORT KEY (step 2) is
|
||||
/// refused.
|
||||
#[test]
|
||||
fn key1_report_failure_fails_the_auth() {
|
||||
let mut t = MockTransport::scripted(
|
||||
vec![Reply::good(vec![]), Reply::illegal_request()],
|
||||
Reply::illegal_request(),
|
||||
);
|
||||
let e = authenticate_with_agid(&mut t, 0).expect_err("refused Key1 report");
|
||||
assert!(matches!(e, Error::CssAuthFailed));
|
||||
assert_eq!(t.calls(), 2);
|
||||
}
|
||||
|
||||
/// Both SCSI steps succeed but the drive's Key1 does not match any of the
|
||||
/// 32 CryptKey variants for the (randomly generated) host challenge — the
|
||||
/// brute-force loop exhausts and `variant.ok_or(CssAuthFailed)` fires.
|
||||
#[test]
|
||||
fn key1_matching_no_variant_fails_the_auth() {
|
||||
let mut t = MockTransport::scripted(
|
||||
vec![Reply::good(vec![]), Reply::good(vec![0xABu8; 12])],
|
||||
Reply::illegal_request(),
|
||||
);
|
||||
let e = authenticate_with_agid(&mut t, 0).expect_err("no variant matches");
|
||||
assert!(matches!(e, Error::CssAuthFailed));
|
||||
assert_eq!(
|
||||
t.calls(),
|
||||
2,
|
||||
"the brute-force loop issues no further SCSI commands"
|
||||
);
|
||||
}
|
||||
|
||||
// ── read_disc_key ────────────────────────────────────────────────────────
|
||||
|
||||
/// The best-effort disc-key REPORT KEY is refused — `read_disc_key` must
|
||||
/// surface the failure to its caller (who treats it as non-fatal), not
|
||||
/// silently return `Ok`.
|
||||
#[test]
|
||||
fn read_disc_key_refused_is_an_error() {
|
||||
let mut t = MockTransport::always(Reply::illegal_request());
|
||||
let e = read_disc_key(&mut t, 0).expect_err("drive refused disc-key read");
|
||||
assert!(matches!(e, Error::CssAuthFailed));
|
||||
}
|
||||
|
||||
/// A transport fault reading the disc key must classify as a transport
|
||||
/// failure, not a generic auth failure.
|
||||
#[test]
|
||||
fn read_disc_key_transport_fault_is_transport_failure() {
|
||||
let mut t = MockTransport::always(Reply::TransportFault);
|
||||
let e = read_disc_key(&mut t, 0).expect_err("dead bus");
|
||||
assert!(e.is_transport_failure());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user