aacs: unify the SD-walk; derive_media_key_from_dk now exposes the PK
Two byte-identical copies of the subset-difference walk lived in keys.rs and variants.rs. Consolidate the pure helpers (aesg3, calc_v_mask, calc_pk_from_dk) into keys.rs (pub(super)); variants.rs imports them. Add derive_media_key_and_pk_from_dk(mkb, dks) -> Option<(mk, pk)>, which returns the intermediate Processing Key the walk already computes; derive_media_key_from_dk becomes a thin wrapper. This lets callers bank the PK on a DK boil instead of re-deriving it via a second, divergent walk (the classical-vs-variant cvalues order made that miss silently). 216 AACS tests pass (incl. a new (mk,pk) regression); precommit (1.86) green.
This commit is contained in:
+114
-5
@@ -611,7 +611,10 @@ const AESG3_SEED: [u8; 16] = [
|
||||
|
||||
/// AACS-G3: derive a subkey from a parent key.
|
||||
/// seed[15] += inc, then AES-DEC(key, seed) XOR seed.
|
||||
fn aesg3(key: &[u8; 16], inc: u8) -> [u8; 16] {
|
||||
///
|
||||
/// Shared with [`super::variants`] (its variant chain runs the same SD
|
||||
/// tree); a single definition keeps the two walks byte-identical.
|
||||
pub(super) fn aesg3(key: &[u8; 16], inc: u8) -> [u8; 16] {
|
||||
let mut seed = AESG3_SEED;
|
||||
seed[15] = seed[15].wrapping_add(inc);
|
||||
let mut out = aes_ecb_decrypt(key, &seed);
|
||||
@@ -621,8 +624,8 @@ fn aesg3(key: &[u8; 16], inc: u8) -> [u8; 16] {
|
||||
out
|
||||
}
|
||||
|
||||
/// Compute v_mask from a UV value.
|
||||
fn calc_v_mask(uv: u32) -> u32 {
|
||||
/// Compute v_mask from a UV value. Shared with [`super::variants`].
|
||||
pub(super) fn calc_v_mask(uv: u32) -> u32 {
|
||||
let mut v_mask: u32 = 0xFFFF_FFFF;
|
||||
while (uv & !v_mask) == 0 && v_mask != 0 {
|
||||
v_mask <<= 1;
|
||||
@@ -631,7 +634,13 @@ fn calc_v_mask(uv: u32) -> u32 {
|
||||
}
|
||||
|
||||
/// Derive processing key from device key using subset-difference tree traversal.
|
||||
fn calc_pk_from_dk(dk: &[u8; 16], uv: u32, v_mask: u32, dev_key_v_mask: u32) -> [u8; 16] {
|
||||
/// Shared with [`super::variants`].
|
||||
pub(super) fn calc_pk_from_dk(
|
||||
dk: &[u8; 16],
|
||||
uv: u32,
|
||||
v_mask: u32,
|
||||
dev_key_v_mask: u32,
|
||||
) -> [u8; 16] {
|
||||
// Initial derivation: left_child = aesg3(dk, 0), pk = aesg3(dk, 1), right_child = aesg3(dk, 2)
|
||||
let mut left_child = aesg3(dk, 0);
|
||||
let mut pk = aesg3(dk, 1);
|
||||
@@ -675,7 +684,26 @@ fn calc_pk_from_dk(dk: &[u8; 16], uv: u32, v_mask: u32, dev_key_v_mask: u32) ->
|
||||
}
|
||||
|
||||
/// Derive Media Key from MKB using device keys (subset-difference tree).
|
||||
///
|
||||
/// Thin wrapper over [`derive_media_key_and_pk_from_dk`] that drops the
|
||||
/// intermediate Processing Key. Callers that need the PK lineage (e.g.
|
||||
/// the key service banking DK·PK·MK) should call the `_and_pk_` form.
|
||||
pub fn derive_media_key_from_dk(mkb: &[u8], device_keys: &[DeviceKey]) -> Option<[u8; 16]> {
|
||||
derive_media_key_and_pk_from_dk(mkb, device_keys).map(|(mk, _pk)| mk)
|
||||
}
|
||||
|
||||
/// Derive both the Media Key and the intermediate Processing Key from an
|
||||
/// MKB using device keys (subset-difference tree).
|
||||
///
|
||||
/// Identical walk to [`derive_media_key_from_dk`]; this form additionally
|
||||
/// returns the Processing Key `Kp` derived at the matching subset-difference
|
||||
/// node — the value `calc_pk_from_dk` produces immediately before it
|
||||
/// validates into the Media Key. Returns `Some((mk, pk))` for the first DK
|
||||
/// that walks a uv slot whose Processing Key validates against the MKB.
|
||||
pub fn derive_media_key_and_pk_from_dk(
|
||||
mkb: &[u8],
|
||||
device_keys: &[DeviceKey],
|
||||
) -> Option<([u8; 16], [u8; 16])> {
|
||||
let mk_dv = mkb_find_mk_dv(mkb)?;
|
||||
let uvs = mkb_find_subdiff_records(mkb)?;
|
||||
let cvalues = mkb_find_cvalues(mkb)?;
|
||||
@@ -735,7 +763,7 @@ pub fn derive_media_key_from_dk(mkb: &[u8], device_keys: &[DeviceKey]) -> Option
|
||||
if let Some(mk) =
|
||||
validate_processing_key(&pk, cv, &uvs[1 + uvs_idx * 5..], &mk_dv)
|
||||
{
|
||||
return Some(mk);
|
||||
return Some((mk, pk));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2741,4 +2769,85 @@ mod tests {
|
||||
let mkb = [0x05, 0x00, 0x00, 0x04]; // type 0x05, no body
|
||||
assert!(probe::mkb_record_body(&mkb, 0x05).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_media_key_and_pk_from_dk_returns_intermediate_pk() {
|
||||
// Regression: a classical DK boil must yield the intermediate
|
||||
// Processing Key, not just the Media Key. The key service banks the
|
||||
// PK lineage (DK·PK·MK·VUK·UK); before the `_and_pk_` form existed it
|
||||
// recovered the MK here but lost the PK silently.
|
||||
//
|
||||
// Build a minimal classical MKB (no 0x82/0x83) with:
|
||||
// - 0x04 Subset-Difference: u_mask_shift=3, uv=0x00000002
|
||||
// - 0x05 cvalues: one cvalue C planted so AES-D(Kp, C) XOR uv == mk
|
||||
// - 0x86 Verify Media Key: mk_dv = AES-E(mk, magic || pad)
|
||||
// and a DK with node=4, uv=2, u_mask_shift=3 so dev_key_v_mask ==
|
||||
// v_mask: the calc_pk_from_dk loop is a no-op and Kp == aesg3(dk, 1).
|
||||
use super::super::decrypt::aes_ecb_encrypt as enc;
|
||||
|
||||
let dk_bytes: [u8; 16] = [
|
||||
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
|
||||
0xFF, 0x00,
|
||||
];
|
||||
// Expected Processing Key for the no-op walk.
|
||||
let expected_pk = aesg3(&dk_bytes, 1);
|
||||
|
||||
// Plant a known Media Key.
|
||||
let mk: [u8; 16] = [
|
||||
0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD,
|
||||
0xAE, 0xAF,
|
||||
];
|
||||
// uv (big-endian) = 0x00000002; validate XORs uv into mk[12..16].
|
||||
let uv_bytes: [u8; 4] = [0x00, 0x00, 0x00, 0x02];
|
||||
|
||||
// cvalue C = AES-E(Kp, mk_raw) where mk_raw = mk with the uv XOR
|
||||
// pre-undone, so the validate step XORs uv back in and recovers mk.
|
||||
let mut mk_raw = mk;
|
||||
for a in 0..4 {
|
||||
mk_raw[12 + a] ^= uv_bytes[a];
|
||||
}
|
||||
let cvalue = enc(&expected_pk, &mk_raw);
|
||||
|
||||
// mk_dv = AES-E(mk, magic || pad); validate decrypts it under mk and
|
||||
// checks the leading 8 bytes against the verify magic.
|
||||
let mut plaintext_vd = [0u8; 16];
|
||||
plaintext_vd[..8].copy_from_slice(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]);
|
||||
plaintext_vd[8..].copy_from_slice(&[0x11; 8]);
|
||||
let mk_dv = enc(&mk, &plaintext_vd);
|
||||
|
||||
// Assemble the MKB. Type/Version (0x10) header first.
|
||||
let mut mkb = vec![
|
||||
0x10, 0x00, 0x00, 0x0C, 0x48, 0x14, 0x10, 0x03, 0x00, 0x00, 0x00, 0x4D,
|
||||
];
|
||||
// 0x04 Subset-Difference: body = u_mask_shift(0x03) || uv(4 bytes).
|
||||
mkb.extend_from_slice(&[0x04, 0x00, 0x00, 0x09]);
|
||||
mkb.extend_from_slice(&[0x03]);
|
||||
mkb.extend_from_slice(&uv_bytes);
|
||||
// 0x05 cvalues: one 16-byte cvalue (mkb_find_cvalues prefers 0x05).
|
||||
mkb.extend_from_slice(&[0x05, 0x00, 0x00, 0x14]);
|
||||
mkb.extend_from_slice(&cvalue);
|
||||
// 0x86 Verify Media Key: mk_dv.
|
||||
mkb.extend_from_slice(&[0x86, 0x00, 0x00, 0x14]);
|
||||
mkb.extend_from_slice(&mk_dv);
|
||||
|
||||
let dk = DeviceKey {
|
||||
key: dk_bytes,
|
||||
node: 4,
|
||||
uv: 2,
|
||||
u_mask_shift: 3,
|
||||
};
|
||||
|
||||
// The new `_and_pk_` form returns BOTH the MK and the intermediate PK.
|
||||
let dks = [dk];
|
||||
let (got_mk, got_pk) = derive_media_key_and_pk_from_dk(&mkb, &dks)
|
||||
.expect("classical DK boil must derive (mk, pk)");
|
||||
assert_eq!(got_mk, mk, "recovered Media Key must match the planted MK");
|
||||
assert_eq!(
|
||||
got_pk, expected_pk,
|
||||
"returned Processing Key must equal aesg3(dk, 1) for the no-op walk"
|
||||
);
|
||||
|
||||
// And the thin wrapper must still return just the MK.
|
||||
assert_eq!(derive_media_key_from_dk(&mkb, &dks), Some(mk));
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -31,10 +31,10 @@ pub use keydb::{DeviceKey, DiscEntry, HostCert, KeyDb};
|
||||
pub use keys::probe;
|
||||
pub use keys::{
|
||||
AacsVersion, ContentCert, ResolveContext, ResolvedKeys, UnitKeyFile, decrypt_unit_key,
|
||||
derive_media_key_from_dk, derive_media_key_from_pk, derive_media_key_from_pk_walked,
|
||||
derive_vuk, disc_hash, disc_hash_hex, mkb_content_len, mkb_version, parse_content_cert,
|
||||
parse_unit_key_ro, read_mkb_from_drive, resolve_keys_v1, resolve_keys_v2, resolve_keys_v21,
|
||||
trim_mkb,
|
||||
derive_media_key_and_pk_from_dk, derive_media_key_from_dk, derive_media_key_from_pk,
|
||||
derive_media_key_from_pk_walked, derive_vuk, disc_hash, disc_hash_hex, mkb_content_len,
|
||||
mkb_version, parse_content_cert, parse_unit_key_ro, read_mkb_from_drive, resolve_keys_v1,
|
||||
resolve_keys_v2, resolve_keys_v21, trim_mkb,
|
||||
};
|
||||
pub use provider::KeyProvider;
|
||||
pub use variants::{
|
||||
|
||||
+34
-67
@@ -177,66 +177,11 @@ fn aes_g(x1: &[u8; 16], x2: &[u8; 16]) -> [u8; 16] {
|
||||
|
||||
// ── Subset-difference walk that exposes (Kp, uv) ──────────────────────────
|
||||
|
||||
/// AES-G3 seed register initial value.
|
||||
const AESG3_SEED: [u8; 16] = [
|
||||
0x7B, 0x10, 0x3C, 0x5D, 0xCB, 0x08, 0xC4, 0xE5, 0x1A, 0x27, 0xB0, 0x17, 0x99, 0x05, 0x3B, 0xD9,
|
||||
];
|
||||
|
||||
/// AES-G3 single step: AES-G against the seed register at offset `inc`.
|
||||
fn aesg3_step(key: &[u8; 16], inc: u8) -> [u8; 16] {
|
||||
let mut seed = AESG3_SEED;
|
||||
seed[15] = seed[15].wrapping_add(inc);
|
||||
aes_g(key, &seed)
|
||||
}
|
||||
|
||||
fn calc_v_mask(uv: u32) -> u32 {
|
||||
let mut v_mask: u32 = 0xFFFF_FFFF;
|
||||
while (uv & !v_mask) == 0 && v_mask != 0 {
|
||||
v_mask <<= 1;
|
||||
}
|
||||
v_mask
|
||||
}
|
||||
|
||||
fn calc_pk_from_dk(dk: &[u8; 16], uv: u32, v_mask: u32, dev_key_v_mask: u32) -> [u8; 16] {
|
||||
let mut left_child = aesg3_step(dk, 0);
|
||||
let mut pk = aesg3_step(dk, 1);
|
||||
let mut right_child = aesg3_step(dk, 2);
|
||||
let mut current_v_mask = dev_key_v_mask;
|
||||
|
||||
// Bound the walk to the 32-level depth of a u32 subset-difference tree.
|
||||
// `current_v_mask` advances via an arithmetic `>> 1` which sign-extends, so
|
||||
// a disc-supplied v_mask coarser than dev_key_v_mask would otherwise drive
|
||||
// current_v_mask up to 0xFFFF_FFFF and spin forever — a crafted MKB must
|
||||
// not hang the rip thread (this runs before the KCD placeholder gate).
|
||||
let mut steps = 0u32;
|
||||
while current_v_mask != v_mask {
|
||||
if steps >= 32 {
|
||||
break;
|
||||
}
|
||||
steps += 1;
|
||||
let mut bit_pos: i32 = -1;
|
||||
for i in (0..32).rev() {
|
||||
if (current_v_mask & (1u32 << i)) == 0 {
|
||||
bit_pos = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let curr_key = if bit_pos < 0 || (uv & (1u32 << bit_pos as u32)) == 0 {
|
||||
left_child
|
||||
} else {
|
||||
right_child
|
||||
};
|
||||
|
||||
left_child = aesg3_step(&curr_key, 0);
|
||||
pk = aesg3_step(&curr_key, 1);
|
||||
right_child = aesg3_step(&curr_key, 2);
|
||||
|
||||
current_v_mask = ((current_v_mask as i32) >> 1) as u32;
|
||||
}
|
||||
|
||||
pk
|
||||
}
|
||||
// `calc_v_mask` and `calc_pk_from_dk` (and the AES-G3 seed step they ride
|
||||
// on) are shared with the classical walk in [`super::keys`] — a single
|
||||
// definition keeps the variant SD tree byte-identical to the classical one.
|
||||
// (`aesg3` itself is imported separately in the test module.)
|
||||
use super::keys::{calc_pk_from_dk, calc_v_mask};
|
||||
|
||||
/// Outcome of a subset-difference walk against an MKB. Carries the
|
||||
/// processing key and the matching `uv` slot — both needed as inputs
|
||||
@@ -271,6 +216,27 @@ fn mkb_find_mk_dv(records: &[MkbRecord]) -> Option<[u8; 16]> {
|
||||
|
||||
/// Walk an MKB and return the first `(Kp, uv, cvalue)` that
|
||||
/// `device_keys` covers. Returns `None` if no DK walks any uv.
|
||||
///
|
||||
/// This is the AACS-2.1 **variant** walk; the classical walk lives in
|
||||
/// [`super::keys::derive_media_key_and_pk_from_dk`]. The two are kept
|
||||
/// separate on purpose and select MKB records in DELIBERATELY different
|
||||
/// order:
|
||||
///
|
||||
/// - cvalues: this variant walk tries record `0x07`-then-`0x05`; the
|
||||
/// classical walk tries `0x05`-then-`0x07`. On a variant MKB the
|
||||
/// small `0x07` Explicit-Subset-Difference record carries the
|
||||
/// cvalue the Precursor chain consumes, whereas a classical UHD MKB
|
||||
/// keeps its 1:1 cvalue table in the large `0x05` record (see the
|
||||
/// note on [`super::keys::probe::mkb_cvalues`]). They must NOT be
|
||||
/// unified to one order — each is correct for its own MKB shape.
|
||||
/// - finders: this walk operates on parsed [`MkbRecord`]s (needed
|
||||
/// because the variant chain also reads `0x82`/`0x83`); the
|
||||
/// classical walk operates on raw MKB bytes. Same framing, different
|
||||
/// input type.
|
||||
///
|
||||
/// Consequence: do NOT route the classical DK path through this function
|
||||
/// — on a classical MKB the `0x07`-first selection picks the wrong (or
|
||||
/// missing) cvalue and the magic check fails, so it returns `None`.
|
||||
pub fn walk_processing_key(
|
||||
records: &[MkbRecord],
|
||||
device_keys: &[DeviceKey],
|
||||
@@ -544,6 +510,10 @@ pub fn derive_media_key_variant(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
// These three live in `super::keys` now (consolidated SD-walk helpers);
|
||||
// `use super::*` does not re-export the parent module's private `use`
|
||||
// imports, so pull them in directly for the tests below.
|
||||
use super::super::keys::{aesg3, calc_pk_from_dk};
|
||||
|
||||
#[test]
|
||||
fn calc_pk_from_dk_terminates_on_nonconvergent_mask() {
|
||||
@@ -708,7 +678,7 @@ mod tests {
|
||||
/// agreeing with uv on bits 3+ (the u_mask=1 region). dk.uv ==
|
||||
/// MKB.uv and dk.u_mask_shift == MKB.u_mask_shift make
|
||||
/// `dev_key_v_mask == v_mask`, so `calc_pk_from_dk` loops zero
|
||||
/// times — Kp = aesg3_step(dk, 1).
|
||||
/// times — Kp = aesg3(dk, 1).
|
||||
/// - one cvalue in record 0x07 chosen so AES-D(Kp, C) ⊕ uv produces a
|
||||
/// Kmp whose byte-15 is exactly `kmp15`.
|
||||
/// - record 0x82 with a 16-byte body (acts as both Variant Data
|
||||
@@ -731,12 +701,12 @@ mod tests {
|
||||
// Pick a known DK; with dk.uv == MKB.uv (==2) and
|
||||
// dk.u_mask_shift == MKB.u_mask_shift (==3), dev_key_v_mask
|
||||
// equals the MKB's v_mask and the calc_pk_from_dk loop is a
|
||||
// no-op — Kp = aesg3_step(dk, 1).
|
||||
// no-op — Kp = aesg3(dk, 1).
|
||||
let dk_bytes: [u8; 16] = [
|
||||
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
|
||||
0xFF, 0x00,
|
||||
];
|
||||
let kp = aesg3_step(&dk_bytes, 1);
|
||||
let kp = aesg3(&dk_bytes, 1);
|
||||
|
||||
// Plant Kmp with chosen byte-15, then compute C such that
|
||||
// AES-D(Kp, C) ⊕ uv == Kmp. uv=2 → low-4 bytes XOR is 00 00 00 02.
|
||||
@@ -1002,10 +972,7 @@ mod tests {
|
||||
let (recs, dk, planted_kp, _) = synthetic_variant_setup(0x00);
|
||||
let m = walk_processing_key(&recs, &[dk]).expect("variant MKB yields a match");
|
||||
assert_eq!(m.uv, 2, "matched the planted uv");
|
||||
assert_eq!(
|
||||
m.kp, planted_kp,
|
||||
"Kp equals aesg3_step(dk,1) for the no-op walk"
|
||||
);
|
||||
assert_eq!(m.kp, planted_kp, "Kp equals aesg3(dk,1) for the no-op walk");
|
||||
assert_eq!(m.cvalue_index, 0);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user