Expose the LibreDrive profile catalog as a curated public API on `ld`: `ld::profiles()` returns the `Profiles` object (with `.get(drive_id)`) and `ld::profile(filter)` finds one — the catalog of supported drives, queried without unlocking. The unlock mechanism (firmware blobs, upload sequence, CDB wire format) stays private; `LibreDrive` is pub(crate), reached only via `all_unlockers()`. The unlock-handshake wire format the bdemu test-emulator needs to impersonate a drive (`UNLOCK_MARKER`, `is_unlock_read_buffer`) is behind a non-default `emulation` feature; the `cdb` module compiles only under it. Rename the `ScsiError` error variant to `Scsi` (matches css), gate the test-only `load_bundled` and `SCSI_STATUS_CHECK_CONDITION`. Clippy-clean in both feature configs; 86/89 tests pass.
2034 lines
74 KiB
Rust
2034 lines
74 KiB
Rust
//! AACS bus authentication handshake — ECDH key agreement + bus key derivation.
|
||
//!
|
||
//! Implements the AACS SCSI authentication protocol to obtain:
|
||
//! - Volume ID (VID) — needed for VUK derivation
|
||
//! - Read Data Key — needed for AACS 2.0 (UHD) bus decryption
|
||
//!
|
||
//! Flow:
|
||
//! 1. Invalidate AGIDs → allocate fresh AGID
|
||
//! 2. Send host certificate + nonce
|
||
//! 3. Receive drive certificate + nonce
|
||
//! 4. Receive drive key point + signature, verify
|
||
//! 5. Sign host key point, send
|
||
//! 6. ECDH: host_priv × drive_key_point → bus key (low 128 bits of x)
|
||
//! 7. Read VID or Read Data Keys (encrypted with bus key)
|
||
//!
|
||
//! Supports:
|
||
//! - AACS 1.0: custom 160-bit curve, SHA-1, 20-byte keys
|
||
//! - AACS 2.0: drives accept AACS 1.0 host certs for backward compatibility
|
||
//! (full P-256/SHA-256 AACS 2.0 handshake prepared but rarely needed)
|
||
|
||
#![allow(dead_code)] // complete AACS 1.0 + 2.0 crypto; the P-256/v2 path is prepared, not yet wired into run_cert_handshake
|
||
use crate::aacs::error::{Error, Result};
|
||
use crate::scsi::{DataDirection, ScsiTransport};
|
||
use num_bigint::BigUint;
|
||
use num_traits::{One, Zero};
|
||
use sha1::{Digest, Sha1};
|
||
|
||
/// Map a SCSI-layer error from a handshake step onto a cert/key-specific
|
||
/// code — but only when the failure is *not* a transport-layer wedge.
|
||
///
|
||
/// A SEND KEY / REPORT KEY step can fail because the drive genuinely
|
||
/// rejected the host certificate or key (a real `Aacs*` condition), or
|
||
/// because the transport died mid-handshake (bridge wedge / USB
|
||
/// disconnect). Collapsing the latter into a cert/key code tells the
|
||
/// operator the drive rejected their credentials, sending them down a
|
||
/// keydb/host-cert rabbit hole for what is actually a replug/power-cycle
|
||
/// situation. Preserve the transport error so the true root cause is
|
||
/// surfaced; otherwise substitute the handshake-specific code.
|
||
fn handshake_err(err: Error, fallback: Error) -> Error {
|
||
if err.is_scsi_transport_failure() {
|
||
err
|
||
} else {
|
||
fallback
|
||
}
|
||
}
|
||
|
||
/// Execute a SCSI command that reads data from the device.
|
||
fn scsi_read(session: &mut dyn ScsiTransport, cdb: &[u8], len: usize) -> Result<Vec<u8>> {
|
||
let mut buf = vec![0u8; len];
|
||
session.execute(cdb, DataDirection::FromDevice, &mut buf, 5_000)?;
|
||
Ok(buf)
|
||
}
|
||
|
||
/// Execute a SCSI command that writes data to the device.
|
||
fn scsi_write(session: &mut dyn ScsiTransport, cdb: &[u8], data: &[u8]) -> Result<()> {
|
||
let mut buf = data.to_vec();
|
||
session.execute(cdb, DataDirection::ToDevice, &mut buf, 5_000)?;
|
||
Ok(())
|
||
}
|
||
|
||
// ── AACS 1.0 elliptic curve parameters (160-bit) ───────────────────────────
|
||
|
||
const EC_P: [u8; 20] = [
|
||
0x9D, 0xC9, 0xD8, 0x13, 0x55, 0xEC, 0xCE, 0xB5, 0x60, 0xBD, 0xB0, 0x9E, 0xF9, 0xEA, 0xE7, 0xC4,
|
||
0x79, 0xA7, 0xD7, 0xDF,
|
||
];
|
||
const EC_A: [u8; 20] = [
|
||
0x9D, 0xC9, 0xD8, 0x13, 0x55, 0xEC, 0xCE, 0xB5, 0x60, 0xBD, 0xB0, 0x9E, 0xF9, 0xEA, 0xE7, 0xC4,
|
||
0x79, 0xA7, 0xD7, 0xDC,
|
||
];
|
||
const EC_B: [u8; 20] = [
|
||
0x40, 0x2D, 0xAD, 0x3E, 0xC1, 0xCB, 0xCD, 0x16, 0x52, 0x48, 0xD6, 0x8E, 0x12, 0x45, 0xE0, 0xC4,
|
||
0xDA, 0xAC, 0xB1, 0xD8,
|
||
];
|
||
const EC_N: [u8; 20] = [
|
||
0x9D, 0xC9, 0xD8, 0x13, 0x55, 0xEC, 0xCE, 0xB5, 0x60, 0xBD, 0xC4, 0x4F, 0x54, 0x81, 0x7B, 0x2C,
|
||
0x7F, 0x5A, 0xB0, 0x17,
|
||
];
|
||
const EC_GX: [u8; 20] = [
|
||
0x2E, 0x64, 0xFC, 0x22, 0x57, 0x83, 0x51, 0xE6, 0xF4, 0xCC, 0xA7, 0xEB, 0x81, 0xD0, 0xA4, 0xBD,
|
||
0xC5, 0x4C, 0xCE, 0xC6,
|
||
];
|
||
const EC_GY: [u8; 20] = [
|
||
0x09, 0x14, 0xA2, 0x5D, 0xD0, 0x54, 0x42, 0x88, 0x9D, 0xB4, 0x55, 0xC7, 0xF2, 0x3C, 0x9A, 0x07,
|
||
0x07, 0xF5, 0xCB, 0xB9,
|
||
];
|
||
|
||
// ── AACS 2.0 elliptic curve parameters (P-256 / secp256r1 / NIST prime256v1)
|
||
|
||
const P256_P: [u8; 32] = [
|
||
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||
];
|
||
const P256_A: [u8; 32] = [
|
||
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC,
|
||
];
|
||
const P256_B: [u8; 32] = [
|
||
0x5A, 0xC6, 0x35, 0xD8, 0xAA, 0x3A, 0x93, 0xE7, 0xB3, 0xEB, 0xBD, 0x55, 0x76, 0x98, 0x86, 0xBC,
|
||
0x65, 0x1D, 0x06, 0xB0, 0xCC, 0x53, 0xB0, 0xF6, 0x3B, 0xCE, 0x3C, 0x3E, 0x27, 0xD2, 0x60, 0x4B,
|
||
];
|
||
const P256_N: [u8; 32] = [
|
||
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||
0xBC, 0xE6, 0xFA, 0xAD, 0xA7, 0x17, 0x9E, 0x84, 0xF3, 0xB9, 0xCA, 0xC2, 0xFC, 0x63, 0x25, 0x51,
|
||
];
|
||
const P256_GX: [u8; 32] = [
|
||
0x6B, 0x17, 0xD1, 0xF2, 0xE1, 0x2C, 0x42, 0x47, 0xF8, 0xBC, 0xE6, 0xE5, 0x63, 0xA4, 0x40, 0xF2,
|
||
0x77, 0x03, 0x7D, 0x81, 0x2D, 0xEB, 0x33, 0xA0, 0xF4, 0xA1, 0x39, 0x45, 0xD8, 0x98, 0xC2, 0x96,
|
||
];
|
||
const P256_GY: [u8; 32] = [
|
||
0x4F, 0xE3, 0x42, 0xE2, 0xFE, 0x1A, 0x7F, 0x9B, 0x8E, 0xE7, 0xEB, 0x4A, 0x7C, 0x0F, 0x9E, 0x16,
|
||
0x2B, 0xCE, 0x33, 0x57, 0x6B, 0x31, 0x5E, 0xCE, 0xCB, 0xB6, 0x40, 0x68, 0x37, 0xBF, 0x51, 0xF5,
|
||
];
|
||
|
||
/// AACS 2.0 LA public key for cert verification (P-256).
|
||
/// From AACS2 specification — used to verify type 0x11 drive certificates.
|
||
const AACS2_LA_PUB_X: [u8; 32] = [
|
||
0xF9, 0x57, 0xBC, 0x1F, 0xD7, 0xE6, 0x09, 0x7E, 0xCA, 0xCC, 0x35, 0x23, 0x4C, 0x9C, 0x66, 0xC3,
|
||
0x42, 0xEB, 0x3D, 0xB7, 0x2B, 0x41, 0x06, 0xF4, 0x04, 0x9C, 0x6A, 0x88, 0x70, 0x00, 0xAA, 0x2C,
|
||
];
|
||
const AACS2_LA_PUB_Y: [u8; 32] = [
|
||
0x39, 0x55, 0x0B, 0x41, 0x02, 0x27, 0xEA, 0x7B, 0x1A, 0x53, 0xF8, 0x67, 0x8C, 0x5A, 0x91, 0x6F,
|
||
0xFC, 0x7C, 0x78, 0x01, 0x3E, 0x89, 0x15, 0xE3, 0xF0, 0x81, 0xD3, 0xE9, 0x3E, 0x17, 0x55, 0x0B,
|
||
];
|
||
|
||
// ── AACS 1.0 LA (Licensing Administrator) public key for cert verification ──
|
||
|
||
const AACS_LA_PUB_X: [u8; 20] = [
|
||
0x01, 0xF3, 0x5D, 0xAB, 0xD8, 0xAE, 0x5F, 0x40, 0x56, 0x5E, 0x30, 0xC8, 0x8A, 0x60, 0x42, 0x82,
|
||
0x07, 0x61, 0xDF, 0x93,
|
||
];
|
||
const AACS_LA_PUB_Y: [u8; 20] = [
|
||
0x44, 0x87, 0xB5, 0xAC, 0x07, 0x10, 0x8D, 0x10, 0x5B, 0xA5, 0xB9, 0xE3, 0x2F, 0x3B, 0xBB, 0xFC,
|
||
0x0C, 0x2C, 0xBC, 0xD1,
|
||
];
|
||
|
||
// ── Elliptic curve arithmetic over GF(p) ───────────────────────────────────
|
||
|
||
#[derive(Clone, Debug)]
|
||
struct EcPoint {
|
||
x: BigUint,
|
||
y: BigUint,
|
||
infinity: bool,
|
||
}
|
||
|
||
impl EcPoint {
|
||
fn infinity() -> Self {
|
||
EcPoint {
|
||
x: BigUint::zero(),
|
||
y: BigUint::zero(),
|
||
infinity: true,
|
||
}
|
||
}
|
||
|
||
fn new(x: BigUint, y: BigUint) -> Self {
|
||
EcPoint {
|
||
x,
|
||
y,
|
||
infinity: false,
|
||
}
|
||
}
|
||
|
||
fn from_bytes(x_bytes: &[u8], y_bytes: &[u8]) -> Self {
|
||
EcPoint::new(
|
||
BigUint::from_bytes_be(x_bytes),
|
||
BigUint::from_bytes_be(y_bytes),
|
||
)
|
||
}
|
||
}
|
||
|
||
/// Modular inverse using extended Euclidean algorithm.
|
||
fn mod_inv(a: &BigUint, m: &BigUint) -> Option<BigUint> {
|
||
use num_bigint::BigInt;
|
||
use num_traits::Signed;
|
||
|
||
let a = BigInt::from(a.clone());
|
||
let m = BigInt::from(m.clone());
|
||
|
||
let (mut old_r, mut r) = (a, m.clone());
|
||
let (mut old_s, mut s) = (BigInt::one(), BigInt::zero());
|
||
|
||
while !r.is_zero() {
|
||
let q = &old_r / &r;
|
||
let temp_r = r.clone();
|
||
r = old_r - &q * &r;
|
||
old_r = temp_r;
|
||
let temp_s = s.clone();
|
||
s = old_s - &q * &s;
|
||
old_s = temp_s;
|
||
}
|
||
|
||
if old_r != BigInt::one() {
|
||
return None;
|
||
}
|
||
|
||
if old_s.is_negative() {
|
||
old_s += &m;
|
||
}
|
||
Some(old_s.to_biguint().unwrap())
|
||
}
|
||
|
||
/// EC point addition on curve y² = x³ + ax + b (mod p).
|
||
fn ec_add(p1: &EcPoint, p2: &EcPoint, a: &BigUint, p: &BigUint) -> EcPoint {
|
||
if p1.infinity {
|
||
return p2.clone();
|
||
}
|
||
if p2.infinity {
|
||
return p1.clone();
|
||
}
|
||
|
||
if p1.x == p2.x {
|
||
if p1.y == p2.y && !p1.y.is_zero() {
|
||
return ec_double(p1, a, p);
|
||
}
|
||
return EcPoint::infinity();
|
||
}
|
||
|
||
// λ = (y2 - y1) / (x2 - x1) mod p
|
||
let dy = if p2.y >= p1.y {
|
||
(&p2.y - &p1.y) % p
|
||
} else {
|
||
(p - (&p1.y - &p2.y) % p) % p
|
||
};
|
||
let dx = if p2.x >= p1.x {
|
||
(&p2.x - &p1.x) % p
|
||
} else {
|
||
(p - (&p1.x - &p2.x) % p) % p
|
||
};
|
||
|
||
let dx_inv = match mod_inv(&dx, p) {
|
||
Some(v) => v,
|
||
None => return EcPoint::infinity(),
|
||
};
|
||
let lam = (&dy * &dx_inv) % p;
|
||
|
||
// x3 = λ² - x1 - x2 mod p
|
||
let x3 = {
|
||
let lam2 = (&lam * &lam) % p;
|
||
let sum = (&p1.x + &p2.x) % p;
|
||
if lam2 >= sum {
|
||
(lam2 - sum) % p
|
||
} else {
|
||
(p - (sum - lam2) % p) % p
|
||
}
|
||
};
|
||
|
||
// y3 = λ(x1 - x3) - y1 mod p
|
||
let y3 = {
|
||
let diff = if p1.x >= x3 {
|
||
(&p1.x - &x3) % p
|
||
} else {
|
||
(p - (&x3 - &p1.x) % p) % p
|
||
};
|
||
let prod = (&lam * &diff) % p;
|
||
if prod >= p1.y {
|
||
(prod - &p1.y) % p
|
||
} else {
|
||
(p - (&p1.y - prod) % p) % p
|
||
}
|
||
};
|
||
|
||
EcPoint::new(x3, y3)
|
||
}
|
||
|
||
/// EC point doubling.
|
||
fn ec_double(pt: &EcPoint, a: &BigUint, p: &BigUint) -> EcPoint {
|
||
if pt.infinity || pt.y.is_zero() {
|
||
return EcPoint::infinity();
|
||
}
|
||
|
||
// λ = (3x² + a) / (2y) mod p
|
||
let three = BigUint::from(3u32);
|
||
let two = BigUint::from(2u32);
|
||
|
||
let numerator = (&three * &pt.x * &pt.x + a) % p;
|
||
let denominator = (&two * &pt.y) % p;
|
||
let denom_inv = match mod_inv(&denominator, p) {
|
||
Some(v) => v,
|
||
None => return EcPoint::infinity(),
|
||
};
|
||
let lam = (&numerator * &denom_inv) % p;
|
||
|
||
// x3 = λ² - 2x mod p
|
||
let x3 = {
|
||
let lam2 = (&lam * &lam) % p;
|
||
let two_x = (&two * &pt.x) % p;
|
||
if lam2 >= two_x {
|
||
(lam2 - two_x) % p
|
||
} else {
|
||
(p - (two_x - lam2) % p) % p
|
||
}
|
||
};
|
||
|
||
// y3 = λ(x - x3) - y mod p
|
||
let y3 = {
|
||
let diff = if pt.x >= x3 {
|
||
(&pt.x - &x3) % p
|
||
} else {
|
||
(p - (&x3 - &pt.x) % p) % p
|
||
};
|
||
let prod = (&lam * &diff) % p;
|
||
if prod >= pt.y {
|
||
(prod - &pt.y) % p
|
||
} else {
|
||
(p - (&pt.y - prod) % p) % p
|
||
}
|
||
};
|
||
|
||
EcPoint::new(x3, y3)
|
||
}
|
||
|
||
/// Scalar multiplication using double-and-add.
|
||
///
|
||
/// NOTE (constant-time tradeoff): this branches on `scalar.bit(0)` and
|
||
/// clones BigUints per iteration, so its timing is data-dependent on the
|
||
/// secret scalar (the long-term host private key in `ecdsa_sign`, the
|
||
/// ephemeral key in ECDH). This is a deliberate tradeoff: the handshake
|
||
/// runs once per disc against a local optical drive, so throughput and
|
||
/// the narrow local-timing surface do not justify pulling in a vetted
|
||
/// constant-time backend. Revisit if this ever signs in a remote/shared
|
||
/// context.
|
||
///
|
||
/// NOTE (cofactor): both AACS curves used here have cofactor 1, so a
|
||
/// point that lies on the curve is automatically in the prime-order
|
||
/// subgroup — no small-subgroup defense / `n·P == O` check is required
|
||
/// for the inputs this is called with.
|
||
fn ec_mul(k: &BigUint, pt: &EcPoint, a: &BigUint, p: &BigUint) -> EcPoint {
|
||
if k.is_zero() {
|
||
return EcPoint::infinity();
|
||
}
|
||
|
||
let mut result = EcPoint::infinity();
|
||
let mut base = pt.clone();
|
||
let mut scalar = k.clone();
|
||
|
||
while !scalar.is_zero() {
|
||
if scalar.bit(0) {
|
||
result = ec_add(&result, &base, a, p);
|
||
}
|
||
base = ec_double(&base, a, p);
|
||
scalar >>= 1;
|
||
}
|
||
|
||
result
|
||
}
|
||
|
||
/// True if the point (x, y) satisfies y² ≡ x³ + ax + b (mod p) and lies
|
||
/// in the field (x, y < p). Guards the ECDH multiply against the classic
|
||
/// invalid-curve attack: a drive that supplies an off-curve key point can
|
||
/// otherwise steer the scalar multiply onto a weak curve and leak the host
|
||
/// scalar. Caller must reject the point when this returns false.
|
||
fn point_on_curve(x: &BigUint, y: &BigUint, a: &BigUint, b: &BigUint, p: &BigUint) -> bool {
|
||
if x >= p || y >= p {
|
||
return false;
|
||
}
|
||
let lhs = (y * y) % p;
|
||
let rhs = (((x * x) % p) * x + a * x + b) % p;
|
||
lhs == rhs
|
||
}
|
||
|
||
/// Convert BigUint to fixed-size big-endian bytes, zero-padded.
|
||
fn to_bytes_be_padded(n: &BigUint, len: usize) -> Vec<u8> {
|
||
let bytes = n.to_bytes_be();
|
||
if bytes.len() >= len {
|
||
bytes[bytes.len() - len..].to_vec()
|
||
} else {
|
||
let mut padded = vec![0u8; len - bytes.len()];
|
||
padded.extend_from_slice(&bytes);
|
||
padded
|
||
}
|
||
}
|
||
|
||
// ── ECDSA ───────────────────────────────────────────────────────────────────
|
||
|
||
/// ECDSA sign: sign SHA-1(data) with private key on AACS curve.
|
||
/// Returns (r, s) each 20 bytes.
|
||
fn ecdsa_sign(priv_key: &[u8; 20], data: &[u8]) -> ([u8; 20], [u8; 20]) {
|
||
let p = BigUint::from_bytes_be(&EC_P);
|
||
let a = BigUint::from_bytes_be(&EC_A);
|
||
let n = BigUint::from_bytes_be(&EC_N);
|
||
let g = EcPoint::from_bytes(&EC_GX, &EC_GY);
|
||
let d = BigUint::from_bytes_be(priv_key);
|
||
|
||
// Hash the data
|
||
let hash = Sha1::digest(data);
|
||
let z = BigUint::from_bytes_be(&hash);
|
||
|
||
loop {
|
||
// Generate random k via rejection sampling. Reducing raw RNG bytes
|
||
// modulo n would bias k toward small values (n is not a power of
|
||
// two); a biased ECDSA nonce is a known key-recovery weakness, so
|
||
// we reject and redraw any candidate >= n instead.
|
||
let mut k_bytes = [0u8; 20];
|
||
use rand::RngCore;
|
||
rand::thread_rng().fill_bytes(&mut k_bytes);
|
||
let k = BigUint::from_bytes_be(&k_bytes);
|
||
if k.is_zero() || k >= n {
|
||
continue;
|
||
}
|
||
|
||
// R = k × G
|
||
let r_point = ec_mul(&k, &g, &a, &p);
|
||
let r = &r_point.x % &n;
|
||
if r.is_zero() {
|
||
continue;
|
||
}
|
||
|
||
// s = k⁻¹(z + r·d) mod n
|
||
let k_inv = match mod_inv(&k, &n) {
|
||
Some(v) => v,
|
||
None => continue,
|
||
};
|
||
let s = (&k_inv * ((&z + &r * &d) % &n)) % &n;
|
||
if s.is_zero() {
|
||
continue;
|
||
}
|
||
|
||
let r_bytes = to_bytes_be_padded(&r, 20);
|
||
let s_bytes = to_bytes_be_padded(&s, 20);
|
||
|
||
let mut r_out = [0u8; 20];
|
||
let mut s_out = [0u8; 20];
|
||
r_out.copy_from_slice(&r_bytes);
|
||
s_out.copy_from_slice(&s_bytes);
|
||
|
||
return (r_out, s_out);
|
||
}
|
||
}
|
||
|
||
/// ECDSA verify: verify signature (r, s) against SHA-1(data) using public key.
|
||
fn ecdsa_verify(
|
||
pub_x: &[u8; 20],
|
||
pub_y: &[u8; 20],
|
||
sig_r: &[u8; 20],
|
||
sig_s: &[u8; 20],
|
||
data: &[u8],
|
||
) -> bool {
|
||
let p = BigUint::from_bytes_be(&EC_P);
|
||
let a = BigUint::from_bytes_be(&EC_A);
|
||
let n = BigUint::from_bytes_be(&EC_N);
|
||
let g = EcPoint::from_bytes(&EC_GX, &EC_GY);
|
||
let q = EcPoint::from_bytes(pub_x, pub_y);
|
||
|
||
let r = BigUint::from_bytes_be(sig_r);
|
||
let s = BigUint::from_bytes_be(sig_s);
|
||
|
||
if r.is_zero() || r >= n || s.is_zero() || s >= n {
|
||
return false;
|
||
}
|
||
|
||
let hash = Sha1::digest(data);
|
||
let z = BigUint::from_bytes_be(&hash);
|
||
|
||
let s_inv = match mod_inv(&s, &n) {
|
||
Some(v) => v,
|
||
None => return false,
|
||
};
|
||
|
||
let u1 = (&z * &s_inv) % &n;
|
||
let u2 = (&r * &s_inv) % &n;
|
||
|
||
let p1 = ec_mul(&u1, &g, &a, &p);
|
||
let p2 = ec_mul(&u2, &q, &a, &p);
|
||
let r_point = ec_add(&p1, &p2, &a, &p);
|
||
|
||
if r_point.infinity {
|
||
return false;
|
||
}
|
||
|
||
&r_point.x % &n == r
|
||
}
|
||
|
||
// ── P-256 ECDSA (SHA-256) for AACS 2.0 ─────────────────────────────────────
|
||
|
||
/// ECDSA sign with P-256/SHA-256. Returns (r, s) each 32 bytes.
|
||
fn ecdsa_sign_p256(priv_key: &[u8; 32], data: &[u8]) -> ([u8; 32], [u8; 32]) {
|
||
use sha2::{Digest as Sha2Digest, Sha256};
|
||
|
||
let p = BigUint::from_bytes_be(&P256_P);
|
||
let a = BigUint::from_bytes_be(&P256_A);
|
||
let n = BigUint::from_bytes_be(&P256_N);
|
||
let g = EcPoint::from_bytes(&P256_GX, &P256_GY);
|
||
let d = BigUint::from_bytes_be(priv_key);
|
||
|
||
let hash = Sha256::digest(data);
|
||
let z = BigUint::from_bytes_be(&hash);
|
||
|
||
loop {
|
||
// Rejection sampling for the nonce — see ecdsa_sign for rationale
|
||
// (avoid the modulo bias that reducing raw RNG bytes mod n would
|
||
// introduce).
|
||
let mut k_bytes = [0u8; 32];
|
||
use rand::RngCore;
|
||
rand::thread_rng().fill_bytes(&mut k_bytes);
|
||
let k = BigUint::from_bytes_be(&k_bytes);
|
||
if k.is_zero() || k >= n {
|
||
continue;
|
||
}
|
||
|
||
let r_point = ec_mul(&k, &g, &a, &p);
|
||
let r = &r_point.x % &n;
|
||
if r.is_zero() {
|
||
continue;
|
||
}
|
||
|
||
let k_inv = match mod_inv(&k, &n) {
|
||
Some(v) => v,
|
||
None => continue,
|
||
};
|
||
let s = (&k_inv * ((&z + &r * &d) % &n)) % &n;
|
||
if s.is_zero() {
|
||
continue;
|
||
}
|
||
|
||
let r_bytes = to_bytes_be_padded(&r, 32);
|
||
let s_bytes = to_bytes_be_padded(&s, 32);
|
||
|
||
let mut r_out = [0u8; 32];
|
||
let mut s_out = [0u8; 32];
|
||
r_out.copy_from_slice(&r_bytes);
|
||
s_out.copy_from_slice(&s_bytes);
|
||
|
||
return (r_out, s_out);
|
||
}
|
||
}
|
||
|
||
/// ECDSA verify with P-256/SHA-256.
|
||
fn ecdsa_verify_p256(pub_x: &[u8], pub_y: &[u8], sig_r: &[u8], sig_s: &[u8], data: &[u8]) -> bool {
|
||
use sha2::{Digest as Sha2Digest, Sha256};
|
||
|
||
let p = BigUint::from_bytes_be(&P256_P);
|
||
let a = BigUint::from_bytes_be(&P256_A);
|
||
let n = BigUint::from_bytes_be(&P256_N);
|
||
let g = EcPoint::from_bytes(&P256_GX, &P256_GY);
|
||
let q = EcPoint::new(BigUint::from_bytes_be(pub_x), BigUint::from_bytes_be(pub_y));
|
||
|
||
let r = BigUint::from_bytes_be(sig_r);
|
||
let s = BigUint::from_bytes_be(sig_s);
|
||
|
||
if r.is_zero() || r >= n || s.is_zero() || s >= n {
|
||
return false;
|
||
}
|
||
|
||
let hash = Sha256::digest(data);
|
||
let z = BigUint::from_bytes_be(&hash);
|
||
|
||
let s_inv = match mod_inv(&s, &n) {
|
||
Some(v) => v,
|
||
None => return false,
|
||
};
|
||
|
||
let u1 = (&z * &s_inv) % &n;
|
||
let u2 = (&r * &s_inv) % &n;
|
||
|
||
let p1 = ec_mul(&u1, &g, &a, &p);
|
||
let p2 = ec_mul(&u2, &q, &a, &p);
|
||
let r_point = ec_add(&p1, &p2, &a, &p);
|
||
|
||
if r_point.infinity {
|
||
return false;
|
||
}
|
||
|
||
&r_point.x % &n == r
|
||
}
|
||
|
||
/// Verify an AACS 2.0 certificate (type 0x11) against the AACS 2.0 LA key.
|
||
///
|
||
/// Layout: type(1) + flags(1) + padding(2) + serial(6) + pub_x(32) +
|
||
/// pub_y(32) + sig_r(32) + sig_s(32) = 138 bytes. The signature covers
|
||
/// the first 74 bytes (everything up to and including the public key).
|
||
///
|
||
/// The full P-256 certificate is 138 bytes, so the entire 138-byte
|
||
/// length must be present before any signature slice is taken — checking
|
||
/// `>= 138` up front (rather than the old `>= 132`, which left the
|
||
/// `cert[106..138]` slice able to panic on a 132-byte input) keeps this
|
||
/// safe against the truncated 132-byte cert the handshake actually
|
||
/// passes in (`&response[24..156]`).
|
||
fn verify_cert_p256(cert: &[u8]) -> bool {
|
||
if cert.len() < 138 {
|
||
return false;
|
||
}
|
||
let sig_r = &cert[74..106];
|
||
let sig_s = &cert[106..138];
|
||
ecdsa_verify_p256(&AACS2_LA_PUB_X, &AACS2_LA_PUB_Y, sig_r, sig_s, &cert[..74])
|
||
}
|
||
|
||
/// Extract public key from an AACS 2.0 certificate (32-byte x,y).
|
||
///
|
||
/// Returns a zeroed key pair if `cert` is too short to hold the fixed
|
||
/// offsets (matches the `>= 138` guard in `verify_cert_p256`), so a
|
||
/// short/hostile cert cannot panic on the slice index.
|
||
fn cert_pub_key_p256(cert: &[u8]) -> ([u8; 32], [u8; 32]) {
|
||
let mut x = [0u8; 32];
|
||
let mut y = [0u8; 32];
|
||
if cert.len() < 74 {
|
||
return (x, y);
|
||
}
|
||
x.copy_from_slice(&cert[10..42]);
|
||
y.copy_from_slice(&cert[42..74]);
|
||
(x, y)
|
||
}
|
||
|
||
/// Compute bus key via ECDH on P-256 curve.
|
||
fn compute_bus_key_p256(
|
||
host_priv: &[u8; 32],
|
||
drive_key_point_x: &[u8],
|
||
drive_key_point_y: &[u8],
|
||
) -> Option<[u8; 16]> {
|
||
let p = BigUint::from_bytes_be(&P256_P);
|
||
let a = BigUint::from_bytes_be(&P256_A);
|
||
let b = BigUint::from_bytes_be(&P256_B);
|
||
|
||
let d = BigUint::from_bytes_be(host_priv);
|
||
let dx = BigUint::from_bytes_be(drive_key_point_x);
|
||
let dy = BigUint::from_bytes_be(drive_key_point_y);
|
||
|
||
// Reject an off-curve drive point before the multiply (invalid-curve attack).
|
||
if !point_on_curve(&dx, &dy, &a, &b, &p) {
|
||
return None;
|
||
}
|
||
let dkp = EcPoint::new(dx, dy);
|
||
|
||
let shared = ec_mul(&d, &dkp, &a, &p);
|
||
|
||
// Bus key = lowest 128 bits of x-coordinate
|
||
let x_bytes = to_bytes_be_padded(&shared.x, 32);
|
||
let mut bus_key = [0u8; 16];
|
||
bus_key.copy_from_slice(&x_bytes[16..32]);
|
||
Some(bus_key)
|
||
}
|
||
|
||
// ── AACS certificate handling ───────────────────────────────────────────────
|
||
|
||
/// Verify an AACS certificate (92 bytes) against the AACS LA public key.
|
||
fn verify_cert(cert: &[u8]) -> bool {
|
||
if cert.len() < 92 {
|
||
return false;
|
||
}
|
||
// Certificate format: type(1) + flags(1) + padding(2) + serial(6) + pub_x(20) + pub_y(20) + sig_r(20) + sig_s(20)
|
||
// Signature is over the first 52 bytes
|
||
let mut sig_r = [0u8; 20];
|
||
let mut sig_s = [0u8; 20];
|
||
sig_r.copy_from_slice(&cert[52..72]);
|
||
sig_s.copy_from_slice(&cert[72..92]);
|
||
|
||
ecdsa_verify(&AACS_LA_PUB_X, &AACS_LA_PUB_Y, &sig_r, &sig_s, &cert[..52])
|
||
}
|
||
|
||
/// Extract public key from certificate.
|
||
///
|
||
/// Returns a zeroed key pair if `cert` is too short to hold the fixed
|
||
/// offsets (matches the `>= 92` guard in `verify_cert`), so a
|
||
/// short/hostile cert cannot panic on the slice index.
|
||
fn cert_pub_key(cert: &[u8]) -> ([u8; 20], [u8; 20]) {
|
||
let mut x = [0u8; 20];
|
||
let mut y = [0u8; 20];
|
||
if cert.len() < 52 {
|
||
return (x, y);
|
||
}
|
||
x.copy_from_slice(&cert[12..32]);
|
||
y.copy_from_slice(&cert[32..52]);
|
||
(x, y)
|
||
}
|
||
|
||
// ── Bus key derivation (ECDH) ───────────────────────────────────────────────
|
||
|
||
/// Compute bus key via ECDH: bus_key = low 128 bits of (host_priv × drive_key_point).x
|
||
fn compute_bus_key(
|
||
host_priv: &[u8; 20],
|
||
drive_key_point_x: &[u8; 20],
|
||
drive_key_point_y: &[u8; 20],
|
||
) -> Option<[u8; 16]> {
|
||
let p = BigUint::from_bytes_be(&EC_P);
|
||
let a = BigUint::from_bytes_be(&EC_A);
|
||
let b = BigUint::from_bytes_be(&EC_B);
|
||
|
||
let d = BigUint::from_bytes_be(host_priv);
|
||
let dx = BigUint::from_bytes_be(drive_key_point_x);
|
||
let dy = BigUint::from_bytes_be(drive_key_point_y);
|
||
|
||
// Reject an off-curve drive point before the multiply (invalid-curve attack).
|
||
if !point_on_curve(&dx, &dy, &a, &b, &p) {
|
||
return None;
|
||
}
|
||
let dkp = EcPoint::new(dx, dy);
|
||
|
||
let shared = ec_mul(&d, &dkp, &a, &p);
|
||
|
||
// Bus key = lowest 128 bits (last 16 bytes) of x-coordinate
|
||
let x_bytes = to_bytes_be_padded(&shared.x, 20);
|
||
let mut bus_key = [0u8; 16];
|
||
bus_key.copy_from_slice(&x_bytes[4..20]); // last 16 of 20
|
||
Some(bus_key)
|
||
}
|
||
|
||
/// Generate ephemeral host key pair: (private_key, public_point_x, public_point_y).
|
||
/// Generate P-256 ephemeral key pair for AACS 2.0.
|
||
fn generate_host_key_pair_p256() -> ([u8; 32], [u8; 32], [u8; 32]) {
|
||
let p_mod = BigUint::from_bytes_be(&P256_P);
|
||
let a = BigUint::from_bytes_be(&P256_A);
|
||
let n = BigUint::from_bytes_be(&P256_N);
|
||
let g = EcPoint::from_bytes(&P256_GX, &P256_GY);
|
||
|
||
let (d, q) = loop {
|
||
let mut priv_bytes = [0u8; 32];
|
||
use rand::RngCore;
|
||
rand::thread_rng().fill_bytes(&mut priv_bytes);
|
||
// d == 0 (prob ~1/n) would yield the point at infinity / an
|
||
// all-zero key and degenerate the bus key — reject and retry,
|
||
// matching the AACS 1.0 sibling generate_host_key_pair.
|
||
let d = BigUint::from_bytes_be(&priv_bytes) % &n;
|
||
if d.is_zero() {
|
||
continue;
|
||
}
|
||
let q = ec_mul(&d, &g, &a, &p_mod);
|
||
break (d, q);
|
||
};
|
||
|
||
let mut key = [0u8; 32];
|
||
let mut pub_x = [0u8; 32];
|
||
let mut pub_y = [0u8; 32];
|
||
key.copy_from_slice(&to_bytes_be_padded(&d, 32));
|
||
pub_x.copy_from_slice(&to_bytes_be_padded(&q.x, 32));
|
||
pub_y.copy_from_slice(&to_bytes_be_padded(&q.y, 32));
|
||
|
||
(key, pub_x, pub_y)
|
||
}
|
||
|
||
/// Generate AACS 1.0 ephemeral key pair.
|
||
fn generate_host_key_pair() -> ([u8; 20], [u8; 20], [u8; 20]) {
|
||
let p_mod = BigUint::from_bytes_be(&EC_P);
|
||
let a = BigUint::from_bytes_be(&EC_A);
|
||
let n = BigUint::from_bytes_be(&EC_N);
|
||
let g = EcPoint::from_bytes(&EC_GX, &EC_GY);
|
||
|
||
let (d, q) = loop {
|
||
let mut priv_bytes = [0u8; 20];
|
||
use rand::RngCore;
|
||
rand::thread_rng().fill_bytes(&mut priv_bytes);
|
||
let d = BigUint::from_bytes_be(&priv_bytes) % &n;
|
||
if d.is_zero() {
|
||
continue;
|
||
}
|
||
let q = ec_mul(&d, &g, &a, &p_mod);
|
||
break (d, q);
|
||
};
|
||
|
||
let d_bytes = to_bytes_be_padded(&d, 20);
|
||
let qx = to_bytes_be_padded(&q.x, 20);
|
||
let qy = to_bytes_be_padded(&q.y, 20);
|
||
|
||
let mut key = [0u8; 20];
|
||
let mut pub_x = [0u8; 20];
|
||
let mut pub_y = [0u8; 20];
|
||
key.copy_from_slice(&d_bytes);
|
||
pub_x.copy_from_slice(&qx);
|
||
pub_y.copy_from_slice(&qy);
|
||
|
||
(key, pub_x, pub_y)
|
||
}
|
||
|
||
// ── AES-CMAC (for MAC verification) ────────────────────────────────────────
|
||
|
||
/// AES-128-CMAC, single-complete-block case ONLY.
|
||
///
|
||
/// Implements just the exactly-16-byte message path: it derives subkey
|
||
/// K1 and XORs the one full block. It does NOT derive K2 or apply the
|
||
/// `0x80` 10*-padding, so it is correct only for a 16-byte input — the
|
||
/// `&[u8; 16]` signature enforces that at compile time. Do NOT generalize
|
||
/// this to multi-block or short-final-block messages without adding K2 +
|
||
/// padding.
|
||
fn aes_cmac_16(data: &[u8; 16], key: &[u8; 16]) -> [u8; 16] {
|
||
use aes::Aes128;
|
||
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
|
||
|
||
let cipher = Aes128::new(GenericArray::from_slice(key));
|
||
|
||
// For single-block CMAC:
|
||
// 1. Generate subkey K1
|
||
let mut l = GenericArray::clone_from_slice(&[0u8; 16]);
|
||
cipher.encrypt_block(&mut l);
|
||
|
||
let mut k1 = [0u8; 16];
|
||
let carry = (l[0] >> 7) & 1;
|
||
for i in 0..15 {
|
||
k1[i] = (l[i] << 1) | (l[i + 1] >> 7);
|
||
}
|
||
k1[15] = l[15] << 1;
|
||
if carry == 1 {
|
||
k1[15] ^= 0x87; // Rb for AES-128
|
||
}
|
||
|
||
// 2. XOR data with K1, encrypt
|
||
let mut block = [0u8; 16];
|
||
for i in 0..16 {
|
||
block[i] = data[i] ^ k1[i];
|
||
}
|
||
let mut ga = GenericArray::clone_from_slice(&block);
|
||
cipher.encrypt_block(&mut ga);
|
||
|
||
let mut mac = [0u8; 16];
|
||
mac.copy_from_slice(&ga);
|
||
mac
|
||
}
|
||
|
||
// ── SCSI command builders ───────────────────────────────────────────────────
|
||
|
||
/// Build REPORT KEY CDB (0xA4).
|
||
fn cdb_report_key(agid: u8, format: u8, len: u16) -> [u8; 12] {
|
||
let mut cdb = [0u8; 12];
|
||
cdb[0] = crate::scsi::SCSI_REPORT_KEY;
|
||
cdb[7] = crate::scsi::AACS_KEY_CLASS;
|
||
cdb[8] = (len >> 8) as u8;
|
||
cdb[9] = (len & 0xFF) as u8;
|
||
cdb[10] = (agid << 6) | (format & 0x3F);
|
||
cdb
|
||
}
|
||
|
||
/// Build SEND KEY CDB (0xA3).
|
||
fn cdb_send_key(agid: u8, format: u8, len: u16) -> [u8; 12] {
|
||
let mut cdb = [0u8; 12];
|
||
cdb[0] = crate::scsi::SCSI_SEND_KEY;
|
||
cdb[7] = crate::scsi::AACS_KEY_CLASS;
|
||
cdb[8] = (len >> 8) as u8;
|
||
cdb[9] = (len & 0xFF) as u8;
|
||
cdb[10] = (agid << 6) | (format & 0x3F);
|
||
cdb
|
||
}
|
||
|
||
/// Build REPORT DISC STRUCTURE CDB (0xAD).
|
||
fn cdb_report_disc_structure(agid: u8, format: u8, len: u16) -> [u8; 12] {
|
||
let mut cdb = [0u8; 12];
|
||
cdb[0] = crate::scsi::SCSI_READ_DISC_STRUCTURE;
|
||
cdb[1] = 0x01; // Blu-ray
|
||
cdb[7] = format;
|
||
cdb[8] = (len >> 8) as u8;
|
||
cdb[9] = (len & 0xFF) as u8;
|
||
cdb[10] = agid << 6;
|
||
cdb
|
||
}
|
||
|
||
// ── High-level handshake ────────────────────────────────────────────────────
|
||
|
||
/// Result of a successful AACS authentication handshake.
|
||
///
|
||
/// `Debug` is implemented manually so the session key material
|
||
/// (`bus_key`, `volume_id`, `read_data_key`) is never rendered into logs
|
||
/// or `dbg!` output — only its presence is reported.
|
||
pub struct AacsAuth {
|
||
/// Bus key (16 bytes) — derived from ECDH
|
||
pub bus_key: [u8; 16],
|
||
/// AGID used for this session
|
||
pub agid: u8,
|
||
/// Volume ID (16 bytes) — read after auth
|
||
pub volume_id: Option<[u8; 16]>,
|
||
/// Read data key (16 bytes) — for AACS 2.0 bus decryption
|
||
pub read_data_key: Option<[u8; 16]>,
|
||
/// Drive certificate (first 92 bytes of the drive's certificate;
|
||
/// an AACS 2.0 P-256 cert is 132 bytes and is truncated to fit this
|
||
/// fixed-size field — see [`aacs2_authenticate_p256`]).
|
||
pub drive_cert: [u8; 92],
|
||
}
|
||
|
||
// Manual Debug: bus_key, volume_id, and read_data_key are key material (the
|
||
// VID feeds VUK derivation), so they are redacted — a `dbg!`/tracing of
|
||
// AacsAuth must never dump them in plaintext.
|
||
impl std::fmt::Debug for AacsAuth {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
f.debug_struct("AacsAuth")
|
||
.field("bus_key", &"[redacted]")
|
||
.field("agid", &self.agid)
|
||
.field("volume_id", &self.volume_id.map(|_| "[redacted]"))
|
||
.field("read_data_key", &self.read_data_key.map(|_| "[redacted]"))
|
||
.field("drive_cert", &self.drive_cert)
|
||
.finish()
|
||
}
|
||
}
|
||
|
||
/// Perform the full AACS authentication handshake.
|
||
///
|
||
/// Requires a host private key (20 bytes) and host certificate (92 bytes)
|
||
/// from the KEYDB.cfg HC entry.
|
||
pub fn aacs_authenticate(
|
||
session: &mut dyn ScsiTransport,
|
||
host_priv_key: &[u8; 20],
|
||
host_cert: &[u8],
|
||
) -> Result<AacsAuth> {
|
||
if host_cert.len() < 92 {
|
||
return Err(Error::AacsCertShort);
|
||
}
|
||
|
||
// Step 1: Invalidate all AGIDs
|
||
for agid in 0..4u8 {
|
||
let cdb = cdb_report_key(agid, 0x3F, 2);
|
||
let _ = scsi_read(session, &cdb, 2);
|
||
}
|
||
|
||
// Step 2: Allocate AGID
|
||
let cdb = cdb_report_key(0, 0x00, 8);
|
||
let response =
|
||
scsi_read(session, &cdb, 8).map_err(|e| handshake_err(e, Error::AacsAgidAlloc))?;
|
||
let agid = (response[7] >> 6) & 0x03;
|
||
|
||
// Step 3: Generate host nonce and ephemeral key pair
|
||
let mut host_nonce = [0u8; 20];
|
||
use rand::RngCore;
|
||
rand::thread_rng().fill_bytes(&mut host_nonce);
|
||
let (host_key, host_key_point_x, host_key_point_y) = generate_host_key_pair();
|
||
|
||
// Step 4: Send host certificate + nonce (SEND KEY format 0x01)
|
||
let mut send_buf = [0u8; 116];
|
||
send_buf[1] = 0x72; // data length
|
||
send_buf[4..24].copy_from_slice(&host_nonce);
|
||
send_buf[24..116].copy_from_slice(&host_cert[..92]);
|
||
|
||
let cdb = cdb_send_key(agid, 0x01, 116);
|
||
scsi_write(session, &cdb, &send_buf).map_err(|e| handshake_err(e, Error::AacsCertRejected))?;
|
||
|
||
// Step 5: Read drive certificate + nonce (REPORT KEY format 0x01)
|
||
let cdb = cdb_report_key(agid, 0x01, 116);
|
||
let response =
|
||
scsi_read(session, &cdb, 116).map_err(|e| handshake_err(e, Error::AacsCertRead))?;
|
||
|
||
let mut drive_nonce = [0u8; 20];
|
||
let mut drive_cert = [0u8; 92];
|
||
drive_nonce.copy_from_slice(&response[4..24]);
|
||
drive_cert.copy_from_slice(&response[24..116]);
|
||
|
||
// Verify drive certificate. `is_aacs20` tracks the 2.0 cert type so the
|
||
// step-6 key-signature verify below is skipped too (see there).
|
||
let is_aacs20 = drive_cert[0] == 0x11;
|
||
if drive_cert[0] == 0x01 {
|
||
// AACS 1.0 certificate
|
||
if !verify_cert(&drive_cert) {
|
||
return Err(Error::AacsCertVerify);
|
||
}
|
||
} else if is_aacs20 {
|
||
// AACS 2.0 certificate — verification intentionally skipped here.
|
||
// Reason: backward compatibility. AACS 2.0 drives accept AACS 1.0 host
|
||
// certs, so we proceed with the AACS 1.0 flow regardless. The P-256
|
||
// LA public key needed to verify 2.0 certs is not always available, and
|
||
// failing here would break handshakes with drives that work fine otherwise.
|
||
// The 2.0 cert lays out its public key and signature at different byte
|
||
// offsets than the 1.0 cert, so the step-6 verify below (which reads
|
||
// 1.0 offsets) cannot validate a 2.0 cert and is skipped for it.
|
||
}
|
||
|
||
// Step 6: Read drive key point + signature (REPORT KEY format 0x02)
|
||
let cdb = cdb_report_key(agid, 0x02, 84);
|
||
let response =
|
||
scsi_read(session, &cdb, 84).map_err(|e| handshake_err(e, Error::AacsKeyRead))?;
|
||
|
||
let mut drive_key_point = [0u8; 40]; // x(20) + y(20)
|
||
let mut drive_key_sig = [0u8; 40]; // r(20) + s(20)
|
||
drive_key_point.copy_from_slice(&response[4..44]);
|
||
drive_key_sig.copy_from_slice(&response[44..84]);
|
||
|
||
// Verify drive key signature: sign(drive_nonce=host_nonce || drive_key_point).
|
||
// Skipped for an AACS 2.0 (type 0x11) cert: `cert_pub_key` reads the public
|
||
// key at AACS-1.0 byte offsets, which don't apply to a 2.0 cert, so the
|
||
// verify would be meaningless (it would reject every 2.0 drive). Mirrors the
|
||
// cert-verify skip above; the ECDH key exchange still proceeds.
|
||
if !is_aacs20 {
|
||
let (drive_pub_x, drive_pub_y) = cert_pub_key(&drive_cert);
|
||
let mut verify_data = [0u8; 60];
|
||
verify_data[..20].copy_from_slice(&host_nonce);
|
||
verify_data[20..60].copy_from_slice(&drive_key_point);
|
||
|
||
let mut sig_r = [0u8; 20];
|
||
let mut sig_s = [0u8; 20];
|
||
sig_r.copy_from_slice(&drive_key_sig[..20]);
|
||
sig_s.copy_from_slice(&drive_key_sig[20..40]);
|
||
|
||
if !ecdsa_verify(&drive_pub_x, &drive_pub_y, &sig_r, &sig_s, &verify_data) {
|
||
return Err(Error::AacsKeyVerify);
|
||
}
|
||
}
|
||
|
||
// Step 7: Sign host key point (ECDSA over drive_nonce || host_key_point)
|
||
let mut sign_data = [0u8; 60];
|
||
sign_data[..20].copy_from_slice(&drive_nonce);
|
||
sign_data[20..40].copy_from_slice(&host_key_point_x);
|
||
sign_data[40..60].copy_from_slice(&host_key_point_y);
|
||
|
||
let (host_sig_r, host_sig_s) = ecdsa_sign(host_priv_key, &sign_data);
|
||
|
||
// Step 8: Send host key point + signature (SEND KEY format 0x02)
|
||
let mut send_buf = [0u8; 84];
|
||
send_buf[1] = 0x52;
|
||
send_buf[4..24].copy_from_slice(&host_key_point_x);
|
||
send_buf[24..44].copy_from_slice(&host_key_point_y);
|
||
send_buf[44..64].copy_from_slice(&host_sig_r);
|
||
send_buf[64..84].copy_from_slice(&host_sig_s);
|
||
|
||
let cdb = cdb_send_key(agid, 0x02, 84);
|
||
scsi_write(session, &cdb, &send_buf).map_err(|e| handshake_err(e, Error::AacsKeyRejected))?;
|
||
|
||
// Step 9: Compute bus key via ECDH
|
||
let mut dkp_x = [0u8; 20];
|
||
let mut dkp_y = [0u8; 20];
|
||
dkp_x.copy_from_slice(&drive_key_point[..20]);
|
||
dkp_y.copy_from_slice(&drive_key_point[20..40]);
|
||
|
||
let bus_key = compute_bus_key(&host_key, &dkp_x, &dkp_y).ok_or(Error::AacsKeyVerify)?;
|
||
|
||
Ok(AacsAuth {
|
||
bus_key,
|
||
agid,
|
||
volume_id: None,
|
||
read_data_key: None,
|
||
drive_cert,
|
||
})
|
||
}
|
||
|
||
/// Full AACS 2.0 authentication using P-256/SHA-256.
|
||
///
|
||
/// Used when both host and drive support AACS 2.0 natively.
|
||
/// Falls back to aacs_authenticate (AACS 1.0) if AACS 2.0 host credentials
|
||
/// are not available.
|
||
pub fn aacs2_authenticate(
|
||
session: &mut dyn ScsiTransport,
|
||
host_priv_key_v1: &[u8; 20],
|
||
host_cert_v1: &[u8],
|
||
host_priv_key_v2: Option<&[u8; 32]>,
|
||
host_cert_v2: Option<&[u8]>,
|
||
) -> Result<AacsAuth> {
|
||
// Try AACS 1.0 first (backward compatible with all drives)
|
||
match aacs_authenticate(session, host_priv_key_v1, host_cert_v1) {
|
||
Ok(auth) => return Ok(auth),
|
||
Err(_) => {
|
||
// AACS 1.0 rejected — try native P-256 if we have v2 credentials
|
||
}
|
||
}
|
||
|
||
// AACS 2.0 native P-256 handshake. Absent v2 credentials are "no AACS
|
||
// 2.0 keys configured" (AacsNoKeys), distinct from a malformed/too-short
|
||
// cert (AacsCertShort) — so callers can tell "not provided" from "bad".
|
||
let host_priv_v2 = host_priv_key_v2.ok_or(Error::AacsNoKeys)?;
|
||
let host_cert_v2 = host_cert_v2.ok_or(Error::AacsNoKeys)?;
|
||
|
||
aacs2_authenticate_p256(session, host_priv_v2, host_cert_v2)
|
||
}
|
||
|
||
/// Native AACS 2.0 handshake using P-256/SHA-256.
|
||
/// Same SCSI protocol, larger payloads (32-byte keys, 132-byte certs).
|
||
fn aacs2_authenticate_p256(
|
||
session: &mut dyn ScsiTransport,
|
||
host_priv_key: &[u8; 32],
|
||
host_cert: &[u8],
|
||
) -> Result<AacsAuth> {
|
||
if host_cert.len() < 132 {
|
||
return Err(Error::AacsCertShort);
|
||
}
|
||
|
||
// Step 1: Invalidate all AGIDs
|
||
for agid in 0..4u8 {
|
||
let cdb = cdb_report_key(agid, 0x3F, 2);
|
||
let _ = scsi_read(session, &cdb, 2);
|
||
}
|
||
|
||
// Step 2: Allocate AGID
|
||
let cdb = cdb_report_key(0, 0x00, 8);
|
||
let response =
|
||
scsi_read(session, &cdb, 8).map_err(|e| handshake_err(e, Error::AacsAgidAlloc))?;
|
||
let agid = (response[7] >> 6) & 0x03;
|
||
|
||
// Step 3: Generate host nonce + P-256 ephemeral key pair
|
||
let mut host_nonce = [0u8; 20];
|
||
use rand::RngCore;
|
||
rand::thread_rng().fill_bytes(&mut host_nonce);
|
||
let (host_eph_key, host_eph_pub_x, host_eph_pub_y) = generate_host_key_pair_p256();
|
||
|
||
// Step 4: Send AACS 2.0 host certificate + nonce
|
||
// AACS 2.0: cert is 132 bytes, total payload = 4 + 20 + 132 = 156
|
||
let mut send_buf = vec![0u8; 156];
|
||
send_buf[1] = 0x9a; // data length (154)
|
||
send_buf[4..24].copy_from_slice(&host_nonce);
|
||
send_buf[24..156].copy_from_slice(&host_cert[..132]);
|
||
|
||
let cdb = cdb_send_key(agid, 0x01, 156);
|
||
scsi_write(session, &cdb, &send_buf).map_err(|e| handshake_err(e, Error::AacsCertRejected))?;
|
||
|
||
// Step 5: Read drive certificate + nonce
|
||
// AACS 2.0 drive cert is also 132 bytes
|
||
let cdb = cdb_report_key(agid, 0x01, 156);
|
||
let response =
|
||
scsi_read(session, &cdb, 156).map_err(|e| handshake_err(e, Error::AacsCertRead))?;
|
||
|
||
let mut drive_nonce = [0u8; 20];
|
||
drive_nonce.copy_from_slice(&response[4..24]);
|
||
let drive_cert = &response[24..156];
|
||
|
||
// Verify drive certificate with AACS 2.0 LA key.
|
||
// Verification failure is intentionally non-fatal: some drive firmware
|
||
// uses certificate formats that differ from the spec, and rejecting them
|
||
// would break otherwise working drives. The drive is still authenticated
|
||
// through the ECDH key exchange and P-256 signature verification below.
|
||
// The outcome is surfaced as a trace event rather than discarded so the
|
||
// trust decision is observable (and so the call is not dead code).
|
||
if drive_cert[0] == 0x11 && !verify_cert_p256(drive_cert) {
|
||
tracing::debug!(
|
||
target: "freemkv::disc",
|
||
phase = "aacs2_cert_verify_skipped",
|
||
"drive cert failed P-256 LA verification; proceeding for backward compat"
|
||
);
|
||
}
|
||
|
||
// Step 6: Read drive key point + signature (P-256: 64+64 = 128 bytes)
|
||
let cdb = cdb_report_key(agid, 0x02, 132);
|
||
let response =
|
||
scsi_read(session, &cdb, 132).map_err(|e| handshake_err(e, Error::AacsKeyRead))?;
|
||
|
||
let drive_key_x = &response[4..36];
|
||
let drive_key_y = &response[36..68];
|
||
let drive_sig_r = &response[68..100];
|
||
let drive_sig_s = &response[100..132];
|
||
|
||
// Verify drive key signature
|
||
let (drive_pub_x, drive_pub_y) = cert_pub_key_p256(drive_cert);
|
||
let mut verify_data = Vec::with_capacity(84);
|
||
verify_data.extend_from_slice(&host_nonce);
|
||
verify_data.extend_from_slice(drive_key_x);
|
||
verify_data.extend_from_slice(drive_key_y);
|
||
|
||
if !ecdsa_verify_p256(
|
||
&drive_pub_x,
|
||
&drive_pub_y,
|
||
drive_sig_r,
|
||
drive_sig_s,
|
||
&verify_data,
|
||
) {
|
||
return Err(Error::AacsKeyVerify);
|
||
}
|
||
|
||
// Step 7: Sign host key point
|
||
let mut sign_data = Vec::with_capacity(84);
|
||
sign_data.extend_from_slice(&drive_nonce);
|
||
sign_data.extend_from_slice(&host_eph_pub_x);
|
||
sign_data.extend_from_slice(&host_eph_pub_y);
|
||
|
||
let (host_sig_r, host_sig_s) = ecdsa_sign_p256(host_priv_key, &sign_data);
|
||
|
||
// Step 8: Send host key point + signature (P-256: 64+64 = 128 bytes payload)
|
||
let mut send_buf = vec![0u8; 132];
|
||
send_buf[1] = 0x82; // data length
|
||
send_buf[4..36].copy_from_slice(&host_eph_pub_x);
|
||
send_buf[36..68].copy_from_slice(&host_eph_pub_y);
|
||
send_buf[68..100].copy_from_slice(&host_sig_r);
|
||
send_buf[100..132].copy_from_slice(&host_sig_s);
|
||
|
||
let cdb = cdb_send_key(agid, 0x02, 132);
|
||
scsi_write(session, &cdb, &send_buf).map_err(|e| handshake_err(e, Error::AacsKeyRejected))?;
|
||
|
||
// Step 9: Compute bus key via P-256 ECDH
|
||
let bus_key = compute_bus_key_p256(&host_eph_key, drive_key_x, drive_key_y)
|
||
.ok_or(Error::AacsKeyVerify)?;
|
||
|
||
Ok(AacsAuth {
|
||
bus_key,
|
||
agid,
|
||
volume_id: None,
|
||
read_data_key: None,
|
||
drive_cert: {
|
||
let mut dc = [0u8; 92];
|
||
dc.copy_from_slice(&drive_cert[..92.min(drive_cert.len())]);
|
||
dc
|
||
},
|
||
})
|
||
}
|
||
|
||
/// Read Volume ID after successful authentication.
|
||
pub fn read_volume_id(session: &mut dyn ScsiTransport, auth: &mut AacsAuth) -> Result<[u8; 16]> {
|
||
// REPORT DISC STRUCTURE format 0x80
|
||
let cdb = cdb_report_disc_structure(auth.agid, 0x80, 36);
|
||
let response =
|
||
scsi_read(session, &cdb, 36).map_err(|e| handshake_err(e, Error::AacsVidRead))?;
|
||
|
||
let mut vid = [0u8; 16];
|
||
let mut mac = [0u8; 16];
|
||
vid.copy_from_slice(&response[4..20]);
|
||
mac.copy_from_slice(&response[20..36]);
|
||
|
||
// Verify MAC: AES-CMAC(VID, bus_key) should equal mac
|
||
let calc_mac = aes_cmac_16(&vid, &auth.bus_key);
|
||
if calc_mac != mac {
|
||
return Err(Error::AacsVidMac);
|
||
}
|
||
|
||
auth.volume_id = Some(vid);
|
||
Ok(vid)
|
||
}
|
||
|
||
/// Read data keys after successful authentication (for AACS 2.0 bus encryption).
|
||
pub fn read_data_keys(
|
||
session: &mut dyn ScsiTransport,
|
||
auth: &mut AacsAuth,
|
||
) -> Result<([u8; 16], [u8; 16])> {
|
||
// REPORT DISC STRUCTURE format 0x84
|
||
let cdb = cdb_report_disc_structure(auth.agid, 0x84, 36);
|
||
let response =
|
||
scsi_read(session, &cdb, 36).map_err(|e| handshake_err(e, Error::AacsDataKey))?;
|
||
|
||
let mut enc_rdk = [0u8; 16];
|
||
let mut enc_wdk = [0u8; 16];
|
||
enc_rdk.copy_from_slice(&response[4..20]);
|
||
enc_wdk.copy_from_slice(&response[20..36]);
|
||
|
||
// Decrypt with bus key (AES-ECB)
|
||
let read_data_key = crate::aacs::aes_ecb_decrypt(&auth.bus_key, &enc_rdk);
|
||
let write_data_key = crate::aacs::aes_ecb_decrypt(&auth.bus_key, &enc_wdk);
|
||
|
||
auth.read_data_key = Some(read_data_key);
|
||
Ok((read_data_key, write_data_key))
|
||
}
|
||
|
||
// ── Cert-handshake orchestration (shared by the in-tree path + the external
|
||
// freemkv-unlock-aacs plugin) ─────────────────────────────────────────────
|
||
|
||
/// What a completed AACS host-certificate handshake learned: the Volume ID, the
|
||
/// AACS 2.x bus key (`read_data_key`) when the drive served one, and — when the
|
||
/// bus-key read was attempted and FAILED — its numeric error code (so the
|
||
/// downstream bus-key gate can log WHY the bus key is missing).
|
||
pub struct CertHandshake {
|
||
pub volume_id: [u8; 16],
|
||
pub read_data_key: Option<[u8; 16]>,
|
||
pub read_data_key_err: Option<u16>,
|
||
}
|
||
|
||
/// Run the host-certificate mutual-auth handshake over `scsi` against the given
|
||
/// host certs (already collected — see [`collect_host_certs`]) and, on success,
|
||
/// read the Volume ID + `read_data_key`. This is the cert "remove bus
|
||
/// encryption" primitive, shared by the in-tree path and the external
|
||
/// `freemkv-unlock-aacs` plugin. Wedge-guarded: caps attempts, sleeps between,
|
||
/// and bails on the drive's ILLEGAL_REQUEST sense. Every no-VID outcome is a
|
||
/// structured [`crate::UnlockError`].
|
||
pub fn run_cert_handshake(
|
||
scsi: &mut dyn ScsiTransport,
|
||
host_certs: &[crate::HostCert],
|
||
) -> std::result::Result<CertHandshake, crate::UnlockError> {
|
||
use crate::UnlockError;
|
||
|
||
let host_cert_count = host_certs.len();
|
||
tracing::debug!(
|
||
target: "freemkv::disc",
|
||
phase = "handshake_start",
|
||
host_cert_count,
|
||
"handshake starting"
|
||
);
|
||
|
||
// Cert-attempt wedge guard. An earlier version fired up to 16 AACS
|
||
// authenticate attempts back-to-back with no pause — 80-160 SCSI
|
||
// REPORT_KEY/SEND_KEY commands in a few hundred ms, which can drive
|
||
// consumer optical drives into a fast-fail firmware wedge (every CDB
|
||
// returns ILLEGAL_REQUEST until power-cycled). Defense-in-depth: cap
|
||
// attempts, sleep between, bail early on the drive's wedge sense.
|
||
const MAX_CERT_ATTEMPTS: usize = 3;
|
||
const PER_CERT_BACKOFF_MS: u64 = 1000;
|
||
let mut last_err_code: Option<u16> = None;
|
||
for (idx, hc) in host_certs.iter().take(MAX_CERT_ATTEMPTS).enumerate() {
|
||
if idx > 0 {
|
||
std::thread::sleep(std::time::Duration::from_millis(PER_CERT_BACKOFF_MS));
|
||
}
|
||
match aacs_authenticate(scsi, &hc.private_key, &hc.certificate) {
|
||
Ok(mut auth) => {
|
||
let volume_id = match read_volume_id(scsi, &mut auth) {
|
||
Ok(vid) => vid,
|
||
Err(e) => {
|
||
tracing::warn!(
|
||
target: "freemkv::disc",
|
||
phase = "handshake_vid_read_failed",
|
||
cert_index = idx,
|
||
error_code = e.code(),
|
||
"auth ok but volume ID read failed"
|
||
);
|
||
return Err(UnlockError::VidUnavailable);
|
||
}
|
||
};
|
||
let (read_data_key, read_data_key_err) = match read_data_keys(scsi, &mut auth) {
|
||
Ok((rdk, _)) => (Some(rdk), None),
|
||
Err(e) => {
|
||
tracing::debug!(
|
||
target: "freemkv::disc",
|
||
phase = "handshake_read_data_key_failed",
|
||
cert_index = idx,
|
||
error_code = e.code(),
|
||
"auth + VID read OK, but the drive served no read_data_key (bus key); \
|
||
a bus-encrypted disc stays undecryptable until it does"
|
||
);
|
||
(None, Some(e.code()))
|
||
}
|
||
};
|
||
tracing::debug!(
|
||
target: "freemkv::disc",
|
||
phase = "handshake_ok",
|
||
cert_index = idx,
|
||
has_volume_id = volume_id != [0u8; 16],
|
||
has_read_data_key = read_data_key.is_some(),
|
||
"AACS bus-auth handshake complete"
|
||
);
|
||
return Ok(CertHandshake {
|
||
volume_id,
|
||
read_data_key,
|
||
read_data_key_err,
|
||
});
|
||
}
|
||
Err(e) => {
|
||
last_err_code = Some(e.code());
|
||
// Read the wedge sense off the structured ScsiSense, NOT
|
||
// `e.code()` (a flat constant for every ScsiError). On
|
||
// ILLEGAL_REQUEST the drive is signalling it won't talk to us
|
||
// — trying more certs worsens the wedge, so bail immediately.
|
||
let sense = e.scsi_sense();
|
||
if sense.map(|s| s.is_illegal_request()).unwrap_or(false) {
|
||
tracing::warn!(
|
||
target: "freemkv::disc",
|
||
phase = "handshake_wedge_detected",
|
||
cert_index = idx,
|
||
sense_key = sense.map(|s| s.sense_key),
|
||
asc = sense.map(|s| s.asc),
|
||
ascq = sense.map(|s| s.ascq),
|
||
"drive returned ILLEGAL_REQUEST during auth; bailing out to avoid wedge"
|
||
);
|
||
return Err(UnlockError::HandshakeRejected);
|
||
}
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
tracing::info!(
|
||
target: "freemkv::disc",
|
||
phase = "vid_cert_rejected",
|
||
host_cert_count,
|
||
tried = host_cert_count.min(MAX_CERT_ATTEMPTS),
|
||
last_error_code = last_err_code,
|
||
"The drive rejected the AACS host certificate, so no Volume ID was obtained."
|
||
);
|
||
Err(UnlockError::HandshakeRejected)
|
||
}
|
||
|
||
// ── Tests ───────────────────────────────────────────────────────────────────
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn handshake_err_preserves_transport_failure() {
|
||
use crate::scsi::{SCSI_STATUS_CHECK_CONDITION, SCSI_STATUS_TRANSPORT_FAILURE};
|
||
|
||
// A transport wedge mid-handshake must NOT be reported as a cert/key
|
||
// rejection — the operator needs to see the real (replug) cause, not
|
||
// be sent down a keydb/host-cert rabbit hole.
|
||
let transport = Error::Scsi {
|
||
opcode: 0xA3, // SEND KEY
|
||
status: SCSI_STATUS_TRANSPORT_FAILURE,
|
||
sense: None,
|
||
};
|
||
let mapped = handshake_err(transport, Error::AacsCertRejected);
|
||
assert!(
|
||
mapped.is_scsi_transport_failure(),
|
||
"transport failure must be preserved, not collapsed to a cert code"
|
||
);
|
||
|
||
// A genuine SCSI rejection (CHECK CONDITION) IS the drive saying no, so
|
||
// it maps to the handshake-specific code as before.
|
||
let rejected = Error::Scsi {
|
||
opcode: 0xA3,
|
||
status: SCSI_STATUS_CHECK_CONDITION,
|
||
sense: Some(crate::scsi::ScsiSense {
|
||
sense_key: 0x05, // ILLEGAL REQUEST
|
||
asc: 0x24,
|
||
ascq: 0x00,
|
||
}),
|
||
};
|
||
let mapped = handshake_err(rejected, Error::AacsCertRejected);
|
||
assert!(matches!(mapped, Error::AacsCertRejected));
|
||
assert!(!mapped.is_scsi_transport_failure());
|
||
}
|
||
|
||
#[test]
|
||
fn test_ec_curve_generator_on_curve() {
|
||
// Verify G is on the curve: y² = x³ + ax + b (mod p)
|
||
let p = BigUint::from_bytes_be(&EC_P);
|
||
let a = BigUint::from_bytes_be(&EC_A);
|
||
let b = BigUint::from_bytes_be(&EC_B);
|
||
let gx = BigUint::from_bytes_be(&EC_GX);
|
||
let gy = BigUint::from_bytes_be(&EC_GY);
|
||
|
||
let lhs = (&gy * &gy) % &p;
|
||
let rhs = (&gx * &gx * &gx + &a * &gx + &b) % &p;
|
||
assert_eq!(lhs, rhs, "Generator point is not on the curve");
|
||
}
|
||
|
||
#[test]
|
||
fn test_ec_mul_identity() {
|
||
let p = BigUint::from_bytes_be(&EC_P);
|
||
let a = BigUint::from_bytes_be(&EC_A);
|
||
let g = EcPoint::from_bytes(&EC_GX, &EC_GY);
|
||
|
||
// 1 × G = G
|
||
let result = ec_mul(&BigUint::one(), &g, &a, &p);
|
||
assert_eq!(result.x, g.x);
|
||
assert_eq!(result.y, g.y);
|
||
}
|
||
|
||
#[test]
|
||
fn test_ec_mul_order() {
|
||
// n × G = O (point at infinity)
|
||
let p = BigUint::from_bytes_be(&EC_P);
|
||
let a = BigUint::from_bytes_be(&EC_A);
|
||
let n = BigUint::from_bytes_be(&EC_N);
|
||
let g = EcPoint::from_bytes(&EC_GX, &EC_GY);
|
||
|
||
let result = ec_mul(&n, &g, &a, &p);
|
||
assert!(result.infinity, "n × G should be point at infinity");
|
||
}
|
||
|
||
#[test]
|
||
fn test_ecdsa_sign_verify() {
|
||
// Generate a key pair and test sign/verify
|
||
let (priv_key, pub_x, pub_y) = generate_host_key_pair();
|
||
let data = b"test data for AACS ECDSA";
|
||
|
||
let (sig_r, sig_s) = ecdsa_sign(&priv_key, data);
|
||
assert!(
|
||
ecdsa_verify(&pub_x, &pub_y, &sig_r, &sig_s, data),
|
||
"ECDSA signature should verify"
|
||
);
|
||
|
||
// Verify with wrong data fails
|
||
assert!(
|
||
!ecdsa_verify(&pub_x, &pub_y, &sig_r, &sig_s, b"wrong data"),
|
||
"ECDSA should fail with wrong data"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_ecdh_shared_secret() {
|
||
// Two parties should derive the same shared point
|
||
let _p = BigUint::from_bytes_be(&EC_P);
|
||
let _a = BigUint::from_bytes_be(&EC_A);
|
||
let _g = EcPoint::from_bytes(&EC_GX, &EC_GY);
|
||
|
||
let (priv_a, pub_ax, pub_ay) = generate_host_key_pair();
|
||
let (priv_b, pub_bx, pub_by) = generate_host_key_pair();
|
||
|
||
// A computes: priv_a × pub_B
|
||
let shared_a = compute_bus_key(&priv_a, &pub_bx, &pub_by)
|
||
.expect("on-curve generated point must be accepted");
|
||
// B computes: priv_b × pub_A
|
||
let shared_b = compute_bus_key(&priv_b, &pub_ax, &pub_ay)
|
||
.expect("on-curve generated point must be accepted");
|
||
|
||
assert_eq!(shared_a, shared_b, "ECDH shared secrets should match");
|
||
}
|
||
|
||
#[test]
|
||
fn test_p256_generator_on_curve() {
|
||
let p = BigUint::from_bytes_be(&P256_P);
|
||
let a = BigUint::from_bytes_be(&P256_A);
|
||
let b = BigUint::from_bytes_be(&P256_B);
|
||
let gx = BigUint::from_bytes_be(&P256_GX);
|
||
let gy = BigUint::from_bytes_be(&P256_GY);
|
||
|
||
let lhs = (&gy * &gy) % &p;
|
||
let rhs = (&gx * &gx * &gx + &a * &gx + &b) % &p;
|
||
assert_eq!(lhs, rhs, "P-256 generator not on curve");
|
||
}
|
||
|
||
#[test]
|
||
fn test_p256_mul_order() {
|
||
let p = BigUint::from_bytes_be(&P256_P);
|
||
let a = BigUint::from_bytes_be(&P256_A);
|
||
let n = BigUint::from_bytes_be(&P256_N);
|
||
let g = EcPoint::from_bytes(&P256_GX, &P256_GY);
|
||
|
||
let result = ec_mul(&n, &g, &a, &p);
|
||
assert!(
|
||
result.infinity,
|
||
"n × G should be point at infinity on P-256"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_p256_ecdsa_sign_verify() {
|
||
let p = BigUint::from_bytes_be(&P256_P);
|
||
let a = BigUint::from_bytes_be(&P256_A);
|
||
let n = BigUint::from_bytes_be(&P256_N);
|
||
let g = EcPoint::from_bytes(&P256_GX, &P256_GY);
|
||
|
||
// Generate random P-256 key pair
|
||
let mut priv_bytes = [0u8; 32];
|
||
use rand::RngCore;
|
||
rand::thread_rng().fill_bytes(&mut priv_bytes);
|
||
let d = BigUint::from_bytes_be(&priv_bytes) % &n;
|
||
let priv_key: [u8; 32] = to_bytes_be_padded(&d, 32).try_into().unwrap();
|
||
|
||
let pub_point = ec_mul(&d, &g, &a, &p);
|
||
let pub_x: Vec<u8> = to_bytes_be_padded(&pub_point.x, 32);
|
||
let pub_y: Vec<u8> = to_bytes_be_padded(&pub_point.y, 32);
|
||
|
||
let data = b"AACS 2.0 P-256 ECDSA test";
|
||
let (sig_r, sig_s) = ecdsa_sign_p256(&priv_key, data);
|
||
assert!(ecdsa_verify_p256(&pub_x, &pub_y, &sig_r, &sig_s, data));
|
||
assert!(!ecdsa_verify_p256(&pub_x, &pub_y, &sig_r, &sig_s, b"wrong"));
|
||
}
|
||
|
||
#[test]
|
||
fn test_p256_ecdh() {
|
||
let p = BigUint::from_bytes_be(&P256_P);
|
||
let a = BigUint::from_bytes_be(&P256_A);
|
||
let n = BigUint::from_bytes_be(&P256_N);
|
||
let g = EcPoint::from_bytes(&P256_GX, &P256_GY);
|
||
|
||
let mut pa = [0u8; 32];
|
||
let mut pb = [0u8; 32];
|
||
use rand::RngCore;
|
||
rand::thread_rng().fill_bytes(&mut pa);
|
||
rand::thread_rng().fill_bytes(&mut pb);
|
||
let da = BigUint::from_bytes_be(&pa) % &n;
|
||
let db = BigUint::from_bytes_be(&pb) % &n;
|
||
let priv_a: [u8; 32] = to_bytes_be_padded(&da, 32).try_into().unwrap();
|
||
let priv_b: [u8; 32] = to_bytes_be_padded(&db, 32).try_into().unwrap();
|
||
|
||
let pub_a = ec_mul(&da, &g, &a, &p);
|
||
let pub_b = ec_mul(&db, &g, &a, &p);
|
||
|
||
let key_a = compute_bus_key_p256(
|
||
&priv_a,
|
||
&to_bytes_be_padded(&pub_b.x, 32),
|
||
&to_bytes_be_padded(&pub_b.y, 32),
|
||
)
|
||
.expect("on-curve generated point must be accepted");
|
||
let key_b = compute_bus_key_p256(
|
||
&priv_b,
|
||
&to_bytes_be_padded(&pub_a.x, 32),
|
||
&to_bytes_be_padded(&pub_a.y, 32),
|
||
)
|
||
.expect("on-curve generated point must be accepted");
|
||
|
||
assert_eq!(key_a, key_b, "P-256 ECDH shared secrets should match");
|
||
}
|
||
|
||
#[test]
|
||
fn test_aes_cmac_deterministic() {
|
||
// Same (data, key) must always produce the same MAC.
|
||
let key = [
|
||
0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf,
|
||
0x4f, 0x3c,
|
||
];
|
||
let data = [0u8; 16];
|
||
let mac1 = aes_cmac_16(&data, &key);
|
||
let mac2 = aes_cmac_16(&data, &key);
|
||
assert_eq!(mac1, mac2);
|
||
assert_ne!(mac1, [0u8; 16]); // shouldn't be all zeros
|
||
}
|
||
|
||
#[test]
|
||
fn test_aes_cmac_nist_kat_full_block() {
|
||
// NIST SP 800-38B Appendix D.1, Example 2 (Mlen = 128):
|
||
// K = 2b7e1516 28aed2a6 abf71588 09cf4f3c
|
||
// M = 6bc1bee2 2e409f96 e93d7e11 7393172a
|
||
// T = 070a16b4 6b4d4144 f79bdd9d d04a287c
|
||
let key = [
|
||
0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf,
|
||
0x4f, 0x3c,
|
||
];
|
||
let data = [
|
||
0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93,
|
||
0x17, 0x2a,
|
||
];
|
||
let expected = [
|
||
0x07, 0x0a, 0x16, 0xb4, 0x6b, 0x4d, 0x41, 0x44, 0xf7, 0x9b, 0xdd, 0x9d, 0xd0, 0x4a,
|
||
0x28, 0x7c,
|
||
];
|
||
let mac = aes_cmac_16(&data, &key);
|
||
assert_eq!(mac, expected, "AES-CMAC-128 must match NIST SP 800-38B KAT");
|
||
}
|
||
|
||
#[test]
|
||
fn test_vid_mac_verify_roundtrip() {
|
||
// Simulate the drive-side: pick a (bus_key, vid), compute the MAC, and
|
||
// verify the host-side check accepts it. Then mutate VID and MAC each
|
||
// in turn and verify both mutations cause a mismatch (the path that
|
||
// would yield Error::AacsVidMac in read_volume_id).
|
||
let bus_key = [
|
||
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54,
|
||
0x32, 0x10,
|
||
];
|
||
let vid = [
|
||
0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
|
||
0x77, 0x88,
|
||
];
|
||
|
||
// Drive returns vid + mac where mac == AES-CMAC-128(bus_key, vid).
|
||
let drive_mac = aes_cmac_16(&vid, &bus_key);
|
||
let calc_mac = aes_cmac_16(&vid, &bus_key);
|
||
assert_eq!(calc_mac, drive_mac, "honest drive: MACs must match");
|
||
|
||
// Mutate the MAC: a malicious drive that swapped VID but returned its
|
||
// original MAC would produce a mismatch here.
|
||
let mut bad_mac = drive_mac;
|
||
bad_mac[0] ^= 0x01;
|
||
assert_ne!(calc_mac, bad_mac, "mutated MAC must be rejected");
|
||
|
||
// Mutate the VID: even one bit of VID drift produces a wildly different
|
||
// CMAC (this is what catches a substituted VID with a stale MAC).
|
||
let mut bad_vid = vid;
|
||
bad_vid[15] ^= 0x01;
|
||
let calc_for_bad_vid = aes_cmac_16(&bad_vid, &bus_key);
|
||
assert_ne!(
|
||
calc_for_bad_vid, drive_mac,
|
||
"MAC over mutated VID must not match original MAC"
|
||
);
|
||
|
||
// Wrong bus key (e.g. handshake replayed against the wrong session)
|
||
// also produces a different MAC over the same VID.
|
||
let mut wrong_key = bus_key;
|
||
wrong_key[0] ^= 0xff;
|
||
let calc_with_wrong_key = aes_cmac_16(&vid, &wrong_key);
|
||
assert_ne!(
|
||
calc_with_wrong_key, drive_mac,
|
||
"MAC under wrong bus key must not match"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_vid_mac_all_zero_mac_rejected() {
|
||
// Defensive: a buggy or hostile drive that returns all-zero MAC must
|
||
// be rejected (the real MAC over any non-trivial VID is nearly never
|
||
// 0...0). This guards against a class of "drive returned garbage"
|
||
// failures masquerading as success.
|
||
let bus_key = [
|
||
0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf,
|
||
0x4f, 0x3c,
|
||
];
|
||
let vid = [
|
||
0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93,
|
||
0x17, 0x2a,
|
||
];
|
||
let calc_mac = aes_cmac_16(&vid, &bus_key);
|
||
assert_ne!(calc_mac, [0u8; 16], "real CMAC must not be all zeros");
|
||
}
|
||
|
||
#[test]
|
||
fn test_verify_cert_p256_short_cert_no_panic() {
|
||
// Regression: verify_cert_p256 used to slice cert[106..138] after only
|
||
// a `len < 132` guard. The drive cert the handshake passes in is
|
||
// exactly 132 bytes (&response[24..156]), so the slice panicked OOB.
|
||
// It must now return false (cannot verify) rather than panic.
|
||
let cert_132 = [0x11u8; 132];
|
||
assert!(
|
||
!verify_cert_p256(&cert_132),
|
||
"132-byte cert must be rejected, not panic"
|
||
);
|
||
// Boundary lengths around the slice requirement.
|
||
for len in [0usize, 73, 74, 105, 106, 131, 137] {
|
||
let cert = vec![0x11u8; len];
|
||
assert!(!verify_cert_p256(&cert), "len {len} must not panic");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_compute_bus_key_rejects_off_curve_point() {
|
||
// An off-curve drive point must be rejected (invalid-curve guard),
|
||
// while an on-curve point (here the generator G) is accepted.
|
||
let (host_priv, _, _) = generate_host_key_pair();
|
||
|
||
// On-curve: G itself.
|
||
assert!(
|
||
compute_bus_key(&host_priv, &EC_GX, &EC_GY).is_some(),
|
||
"on-curve point must be accepted"
|
||
);
|
||
|
||
// Off-curve: G with y flipped by one bit almost never stays on the curve.
|
||
let mut bad_y = EC_GY;
|
||
bad_y[19] ^= 0x01;
|
||
assert!(
|
||
compute_bus_key(&host_priv, &EC_GX, &bad_y).is_none(),
|
||
"off-curve point must be rejected"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_compute_bus_key_p256_rejects_off_curve_point() {
|
||
let (host_priv, _, _) = generate_host_key_pair_p256();
|
||
|
||
assert!(
|
||
compute_bus_key_p256(&host_priv, &P256_GX, &P256_GY).is_some(),
|
||
"on-curve P-256 point must be accepted"
|
||
);
|
||
|
||
let mut bad_y = P256_GY;
|
||
bad_y[31] ^= 0x01;
|
||
assert!(
|
||
compute_bus_key_p256(&host_priv, &P256_GX, &bad_y).is_none(),
|
||
"off-curve P-256 point must be rejected"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_verify_host_cert_from_keydb() {
|
||
// Exercise verify_cert against a real AACS 1.0 host certificate.
|
||
//
|
||
// libfreemkv no longer parses keydb.cfg (the parser lives in
|
||
// freemkv-keysources), so the cert bytes are read from a raw 92-byte
|
||
// certificate file named by HOST_CERT_PATH instead of being pulled
|
||
// from a parsed KeyDb. This keeps verify_cert (private to this module,
|
||
// so it cannot move to keysources) covered against genuine LA-signed
|
||
// bytes without re-introducing a keydb dependency here. Inert in CI
|
||
// (env unset), matching the prior KEYDB_PATH gating.
|
||
let cert_path = match std::env::var("HOST_CERT_PATH").ok() {
|
||
Some(p) => std::path::PathBuf::from(p),
|
||
None => return,
|
||
};
|
||
if !cert_path.exists() {
|
||
return;
|
||
}
|
||
let certificate = match std::fs::read(&cert_path) {
|
||
Ok(b) => b,
|
||
Err(_) => return,
|
||
};
|
||
|
||
// Direct HostCert construction — no parser. Only `certificate` feeds
|
||
// verify_cert; the other fields are inert placeholders.
|
||
let hc = crate::HostCert {
|
||
private_key: [0u8; 20],
|
||
certificate,
|
||
private_key_v2: None,
|
||
certificate_v2: None,
|
||
};
|
||
let valid = verify_cert(&hc.certificate);
|
||
eprintln!(
|
||
"Host cert verification: {}",
|
||
if valid { "PASS" } else { "FAIL" }
|
||
);
|
||
// Note: a revoked cert should still carry a valid LA signature.
|
||
// If it doesn't verify, the LA public key might be wrong.
|
||
if !valid {
|
||
eprintln!(" (cert may use different LA key or format)");
|
||
}
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// Hardening additions
|
||
// ════════════════════════════════════════════════════════════════════
|
||
|
||
// ── EC curve invariants: a, b chosen so 4a³+27b² != 0 (nonsingular) ────
|
||
|
||
#[test]
|
||
fn aacs1_curve_is_nonsingular() {
|
||
// A valid Weierstrass curve requires discriminant 4a³ + 27b² ≠ 0
|
||
// (mod p). A typo in EC_A or EC_B that singularised the curve would be
|
||
// caught here.
|
||
let p = BigUint::from_bytes_be(&EC_P);
|
||
let a = BigUint::from_bytes_be(&EC_A);
|
||
let b = BigUint::from_bytes_be(&EC_B);
|
||
let four = BigUint::from(4u32);
|
||
let twenty_seven = BigUint::from(27u32);
|
||
let disc = (&four * &a % &p * &a % &p * &a % &p + &twenty_seven * &b % &p * &b % &p) % &p;
|
||
assert!(!disc.is_zero(), "AACS 1.0 curve must be nonsingular");
|
||
}
|
||
|
||
#[test]
|
||
fn p256_curve_is_nonsingular() {
|
||
let p = BigUint::from_bytes_be(&P256_P);
|
||
let a = BigUint::from_bytes_be(&P256_A);
|
||
let b = BigUint::from_bytes_be(&P256_B);
|
||
let four = BigUint::from(4u32);
|
||
let twenty_seven = BigUint::from(27u32);
|
||
let disc = (&four * &a % &p * &a % &p * &a % &p + &twenty_seven * &b % &p * &b % &p) % &p;
|
||
assert!(!disc.is_zero(), "P-256 curve must be nonsingular");
|
||
}
|
||
|
||
// ── mod_inv ────────────────────────────────────────────────────────────
|
||
|
||
#[test]
|
||
fn mod_inv_round_trips() {
|
||
// a * a⁻¹ ≡ 1 (mod m). Pin against the AACS prime.
|
||
let m = BigUint::from_bytes_be(&EC_N);
|
||
let a = BigUint::from(123456789u64);
|
||
let inv = mod_inv(&a, &m).expect("inverse exists for a coprime to prime n");
|
||
assert_eq!((&a * &inv) % &m, BigUint::one());
|
||
}
|
||
|
||
#[test]
|
||
fn mod_inv_of_one_is_one() {
|
||
let m = BigUint::from(97u32);
|
||
assert_eq!(mod_inv(&BigUint::one(), &m), Some(BigUint::one()));
|
||
}
|
||
|
||
// ── to_bytes_be_padded ─────────────────────────────────────────────────
|
||
|
||
#[test]
|
||
fn to_bytes_be_padded_left_pads_short_values() {
|
||
// A small number must be left-zero-padded to the fixed width (keys are
|
||
// fixed-size big-endian; a short value left unpadded would shift bytes).
|
||
let n = BigUint::from(0x1234u32);
|
||
assert_eq!(to_bytes_be_padded(&n, 20), {
|
||
let mut v = vec![0u8; 18];
|
||
v.extend_from_slice(&[0x12, 0x34]);
|
||
v
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn to_bytes_be_padded_truncates_to_low_bytes_when_longer() {
|
||
// When the encoding is longer than len, the low `len` bytes are kept
|
||
// (the function slices the tail) — this is how the 256-bit ECDH x is
|
||
// reduced to the low 128 bits for the bus key.
|
||
let n = BigUint::from(0x0102030405u64); // 5 bytes
|
||
assert_eq!(to_bytes_be_padded(&n, 2), vec![0x04, 0x05]);
|
||
}
|
||
|
||
// ── point_on_curve (via compute_bus_key acceptance) ────────────────────
|
||
// point_on_curve is private; exercise it through compute_bus_key, which
|
||
// calls it as the invalid-curve guard.
|
||
|
||
#[test]
|
||
fn off_curve_x_out_of_field_is_rejected() {
|
||
// A coordinate >= p is outside the field and must be rejected before
|
||
// the multiply (the `x >= p || y >= p` guard). Use x = p (== modulus).
|
||
let (host_priv, _, _) = generate_host_key_pair();
|
||
// EC_P itself as the x coordinate → x == p → out of field.
|
||
assert!(
|
||
compute_bus_key(&host_priv, &EC_P, &EC_GY).is_none(),
|
||
"x == p is out of field and must be rejected"
|
||
);
|
||
}
|
||
|
||
// ── CDB builders: REPORT KEY / SEND KEY / REPORT DISC STRUCTURE ────────
|
||
|
||
#[test]
|
||
fn cdb_report_key_layout() {
|
||
// 0xA4 opcode; AACS key class at byte 7; BE16 length at 8/9;
|
||
// (agid<<6)|format at byte 10. Pin the exact bit packing.
|
||
let cdb = cdb_report_key(0b10, 0x02, 0x0054);
|
||
assert_eq!(cdb[0], crate::scsi::SCSI_REPORT_KEY);
|
||
assert_eq!(cdb[7], crate::scsi::AACS_KEY_CLASS);
|
||
assert_eq!(cdb[8], 0x00);
|
||
assert_eq!(cdb[9], 0x54);
|
||
// agid=2 → bits 7:6 = 10b = 0x80; format 0x02 in low 6 bits.
|
||
assert_eq!(cdb[10], 0x80 | 0x02);
|
||
}
|
||
|
||
#[test]
|
||
fn cdb_report_key_format_masked_to_6_bits() {
|
||
// The format field is `format & 0x3F`; a value with bits 6/7 set must
|
||
// not bleed into the AGID field. 0xFF & 0x3F == 0x3F.
|
||
let cdb = cdb_report_key(0, 0xFF, 2);
|
||
assert_eq!(cdb[10], 0x3F, "format must be masked to its low 6 bits");
|
||
}
|
||
|
||
#[test]
|
||
fn cdb_send_key_layout() {
|
||
let cdb = cdb_send_key(0b11, 0x01, 116);
|
||
assert_eq!(cdb[0], crate::scsi::SCSI_SEND_KEY);
|
||
assert_eq!(cdb[7], crate::scsi::AACS_KEY_CLASS);
|
||
assert_eq!(cdb[8], (116u16 >> 8) as u8);
|
||
assert_eq!(cdb[9], (116u16 & 0xFF) as u8);
|
||
assert_eq!(cdb[10], (0b11 << 6) | 0x01);
|
||
}
|
||
|
||
#[test]
|
||
fn cdb_report_disc_structure_layout() {
|
||
// 0xAD opcode; byte 1 = 0x01 (Blu-ray); format at byte 7; BE16 length;
|
||
// agid<<6 at byte 10 (no format bits here).
|
||
let cdb = cdb_report_disc_structure(0b01, 0x80, 36);
|
||
assert_eq!(cdb[0], crate::scsi::SCSI_READ_DISC_STRUCTURE);
|
||
assert_eq!(cdb[1], 0x01);
|
||
assert_eq!(cdb[7], 0x80);
|
||
assert_eq!(cdb[8], 0x00);
|
||
assert_eq!(cdb[9], 36);
|
||
assert_eq!(cdb[10], 0b01 << 6);
|
||
}
|
||
|
||
// ── verify_cert (AACS 1.0): length guard ───────────────────────────────
|
||
|
||
#[test]
|
||
fn verify_cert_v1_rejects_short_cert_no_panic() {
|
||
// < 92 bytes → false (the sig slices cert[52..72]/[72..92] would
|
||
// otherwise panic). Sweep the boundary.
|
||
for len in [0usize, 51, 52, 71, 72, 91] {
|
||
assert!(!verify_cert(&vec![0u8; len]), "len {len} must not panic");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn cert_pub_key_v1_zeroes_when_too_short() {
|
||
// < 52 bytes → zeroed (x,y) rather than an OOB slice on cert[12..52].
|
||
let (x, y) = cert_pub_key(&[0u8; 40]);
|
||
assert_eq!(x, [0u8; 20]);
|
||
assert_eq!(y, [0u8; 20]);
|
||
}
|
||
|
||
#[test]
|
||
fn cert_pub_key_v1_extracts_offsets_12_32_52() {
|
||
// pub_x at [12..32], pub_y at [32..52]. Build a 92-byte cert with
|
||
// distinct x/y regions.
|
||
let mut cert = vec![0u8; 92];
|
||
for b in &mut cert[12..32] {
|
||
*b = 0xA1;
|
||
}
|
||
for b in &mut cert[32..52] {
|
||
*b = 0xB2;
|
||
}
|
||
let (x, y) = cert_pub_key(&cert);
|
||
assert_eq!(x, [0xA1u8; 20]);
|
||
assert_eq!(y, [0xB2u8; 20]);
|
||
}
|
||
|
||
#[test]
|
||
fn cert_pub_key_p256_extracts_offsets_10_42_74() {
|
||
// AACS 2.0: pub_x at [10..42], pub_y at [42..74].
|
||
let mut cert = vec![0u8; 138];
|
||
for b in &mut cert[10..42] {
|
||
*b = 0xC3;
|
||
}
|
||
for b in &mut cert[42..74] {
|
||
*b = 0xD4;
|
||
}
|
||
let (x, y) = cert_pub_key_p256(&cert);
|
||
assert_eq!(x, [0xC3u8; 32]);
|
||
assert_eq!(y, [0xD4u8; 32]);
|
||
}
|
||
|
||
#[test]
|
||
fn cert_pub_key_p256_zeroes_when_too_short() {
|
||
// < 74 bytes → zeroed, matching the verify_cert_p256 >= 138 guard's
|
||
// safety contract (no OOB on cert[10..74]).
|
||
let (x, y) = cert_pub_key_p256(&[0u8; 73]);
|
||
assert_eq!(x, [0u8; 32]);
|
||
assert_eq!(y, [0u8; 32]);
|
||
}
|
||
|
||
// ── ECDSA sign produces 20/32-byte fixed-width outputs ─────────────────
|
||
|
||
#[test]
|
||
fn ecdsa_sign_outputs_are_fixed_width_and_verify() {
|
||
// Sign/verify already covered; here assert the (r,s) are full-width
|
||
// (the to_bytes_be_padded path must not emit short arrays — a fixed
|
||
// [u8;20] return enforces width, but verify the values are non-trivial
|
||
// and round-trip).
|
||
let (priv_key, px, py) = generate_host_key_pair();
|
||
let (r, s) = ecdsa_sign(&priv_key, b"payload");
|
||
assert_ne!(r, [0u8; 20]);
|
||
assert_ne!(s, [0u8; 20]);
|
||
assert!(ecdsa_verify(&px, &py, &r, &s, b"payload"));
|
||
}
|
||
|
||
#[test]
|
||
fn ecdsa_verify_rejects_out_of_range_signature_components() {
|
||
// r or s == 0, or >= n, must be rejected up front (standard ECDSA
|
||
// range check). Use r = 0.
|
||
let (_priv, px, py) = generate_host_key_pair();
|
||
let zero = [0u8; 20];
|
||
let some = [0x01u8; 20];
|
||
assert!(
|
||
!ecdsa_verify(&px, &py, &zero, &some, b"d"),
|
||
"r == 0 must be rejected"
|
||
);
|
||
assert!(
|
||
!ecdsa_verify(&px, &py, &some, &zero, b"d"),
|
||
"s == 0 must be rejected"
|
||
);
|
||
// r == n must be rejected (>= n).
|
||
assert!(!ecdsa_verify(&px, &py, &EC_N, &some, b"d"));
|
||
}
|
||
|
||
// ── ec_add / ec_double identities ──────────────────────────────────────
|
||
|
||
#[test]
|
||
fn ec_add_with_infinity_is_identity() {
|
||
let p = BigUint::from_bytes_be(&EC_P);
|
||
let a = BigUint::from_bytes_be(&EC_A);
|
||
let g = EcPoint::from_bytes(&EC_GX, &EC_GY);
|
||
let inf = EcPoint::infinity();
|
||
let r1 = ec_add(&g, &inf, &a, &p);
|
||
let r2 = ec_add(&inf, &g, &a, &p);
|
||
assert_eq!((r1.x, r1.y), (g.x.clone(), g.y.clone()));
|
||
assert_eq!((r2.x, r2.y), (g.x, g.y));
|
||
}
|
||
|
||
#[test]
|
||
fn ec_add_point_and_its_negation_is_infinity() {
|
||
// P + (-P) = O. -P has y' = p - y. Same x, different y → infinity.
|
||
let p = BigUint::from_bytes_be(&EC_P);
|
||
let a = BigUint::from_bytes_be(&EC_A);
|
||
let g = EcPoint::from_bytes(&EC_GX, &EC_GY);
|
||
let neg_y = (&p - &g.y) % &p;
|
||
let neg_g = EcPoint::new(g.x.clone(), neg_y);
|
||
let sum = ec_add(&g, &neg_g, &a, &p);
|
||
assert!(sum.infinity, "P + (-P) must be the point at infinity");
|
||
}
|
||
|
||
#[test]
|
||
fn ec_mul_two_g_equals_g_plus_g() {
|
||
// 2·G via scalar mul equals ec_double(G) and ec_add(G,G).
|
||
let p = BigUint::from_bytes_be(&EC_P);
|
||
let a = BigUint::from_bytes_be(&EC_A);
|
||
let g = EcPoint::from_bytes(&EC_GX, &EC_GY);
|
||
let two = BigUint::from(2u32);
|
||
let mul2 = ec_mul(&two, &g, &a, &p);
|
||
let dbl = ec_double(&g, &a, &p);
|
||
let add = ec_add(&g, &g, &a, &p);
|
||
assert_eq!((mul2.x.clone(), mul2.y.clone()), (dbl.x, dbl.y));
|
||
assert_eq!((mul2.x, mul2.y), (add.x, add.y));
|
||
}
|
||
|
||
// ── AES-CMAC subkey: K1 doubling with Rb=0x87 ──────────────────────────
|
||
|
||
#[test]
|
||
fn aes_cmac_full_block_changes_with_one_input_bit() {
|
||
// A single-bit flip in the message must change the MAC (the K1 XOR +
|
||
// encrypt is sensitive to all input bits). Pairs with the NIST KAT.
|
||
let key = [0x2bu8; 16];
|
||
let m1 = [0x00u8; 16];
|
||
let mut m2 = m1;
|
||
m2[7] ^= 0x01;
|
||
assert_ne!(aes_cmac_16(&m1, &key), aes_cmac_16(&m2, &key));
|
||
}
|
||
|
||
// ── verify_cert_p256 boundary at exactly 138 ───────────────────────────
|
||
|
||
#[test]
|
||
fn verify_cert_p256_accepts_138_byte_length_without_panic() {
|
||
// 138 bytes is the minimum that satisfies the guard; the slices
|
||
// cert[74..106]/[106..138] are all in-bounds. The signature won't
|
||
// verify (random bytes) but it must NOT panic and must return false.
|
||
let cert = vec![0x00u8; 138];
|
||
assert!(!verify_cert_p256(&cert));
|
||
}
|
||
}
|