aacs: redact Debug for ResolvedChain/ResolvedKeys/ProcessingKeyMatch (test-guarded)
These carry raw unit-key / VUK / processing-key bytes on their Debug; manual impls print shape only (unit_keys_len, redacted markers). Each has a red→green test.
This commit is contained in:
+35
-1
@@ -465,7 +465,7 @@ pub enum KeyCandidate {
|
|||||||
/// its declared CPS-unit number); the caller runs
|
/// its declared CPS-unit number); the caller runs
|
||||||
/// `decrypt_unit` + `is_clean_ts` to find which one actually opens the
|
/// `decrypt_unit` + `is_clean_ts` to find which one actually opens the
|
||||||
/// disc. Rungs above the candidate are `None`.
|
/// disc. Rungs above the candidate are `None`.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ResolvedChain {
|
pub struct ResolvedChain {
|
||||||
pub unit_keys: Vec<(u32, [u8; 16])>,
|
pub unit_keys: Vec<(u32, [u8; 16])>,
|
||||||
pub vuk: Option<Vuk>,
|
pub vuk: Option<Vuk>,
|
||||||
@@ -475,6 +475,21 @@ pub struct ResolvedChain {
|
|||||||
pub dk: Option<DeviceKey>,
|
pub dk: Option<DeviceKey>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Redacting `Debug`: `unit_keys` holds raw title-key bytes, never printed. The
|
||||||
|
// other rungs are `types` newtypes that self-redact. Guarded by
|
||||||
|
// `resolved_chain_debug_is_redacted`.
|
||||||
|
impl std::fmt::Debug for ResolvedChain {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("ResolvedChain")
|
||||||
|
.field("unit_keys_len", &self.unit_keys.len())
|
||||||
|
.field("vuk", &self.vuk)
|
||||||
|
.field("mk", &self.mk)
|
||||||
|
.field("pk", &self.pk)
|
||||||
|
.field("dk", &self.dk)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Derive the full AACS key chain from a candidate key of ANY ladder rung.
|
/// Derive the full AACS key chain from a candidate key of ANY ladder rung.
|
||||||
///
|
///
|
||||||
/// Runs the deterministic derivation DOWNWARD to the disc's terminal unit keys:
|
/// Runs the deterministic derivation DOWNWARD to the disc's terminal unit keys:
|
||||||
@@ -568,6 +583,25 @@ mod resolve_candidate_tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::aacs::crypto::aes_ecb_encrypt;
|
use crate::aacs::crypto::aes_ecb_encrypt;
|
||||||
|
|
||||||
|
/// `ResolvedChain.unit_keys` holds raw title-key bytes (the other rungs are
|
||||||
|
/// self-redacting `types` newtypes). `Debug` must not leak the title keys.
|
||||||
|
#[test]
|
||||||
|
fn resolved_chain_debug_is_redacted() {
|
||||||
|
let c = ResolvedChain {
|
||||||
|
unit_keys: vec![(1, [0xD5; 16])],
|
||||||
|
vuk: None,
|
||||||
|
mk: None,
|
||||||
|
pk: None,
|
||||||
|
dk: None,
|
||||||
|
};
|
||||||
|
let dbg = format!("{c:?}");
|
||||||
|
assert!(!dbg.contains("213"), "ResolvedChain leaked unit keys: {dbg}");
|
||||||
|
assert!(
|
||||||
|
dbg.contains("unit_keys_len"),
|
||||||
|
"ResolvedChain missing redaction: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Minimal AACS-1.0 (48-byte stride) `Unit_Key_RO.inf` with `n` encrypted
|
/// Minimal AACS-1.0 (48-byte stride) `Unit_Key_RO.inf` with `n` encrypted
|
||||||
/// unit keys — `parse_unit_key_ro` numbers CPS units 1..=n.
|
/// unit keys — `parse_unit_key_ro` numbers CPS units 1..=n.
|
||||||
fn synth_inf(encs: &[[u8; 16]]) -> Vec<u8> {
|
fn synth_inf(encs: &[[u8; 16]]) -> Vec<u8> {
|
||||||
|
|||||||
+35
-1
@@ -13,7 +13,6 @@ use super::mkb::*;
|
|||||||
// ── Full VUK resolution chain ───────────────────────────────────────────────
|
// ── Full VUK resolution chain ───────────────────────────────────────────────
|
||||||
|
|
||||||
/// Result of resolving a disc's VUK.
|
/// Result of resolving a disc's VUK.
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ResolvedKeys {
|
pub struct ResolvedKeys {
|
||||||
/// Disc hash (SHA1 of Unit_Key_RO.inf)
|
/// Disc hash (SHA1 of Unit_Key_RO.inf)
|
||||||
pub disc_hash: [u8; 20],
|
pub disc_hash: [u8; 20],
|
||||||
@@ -34,6 +33,23 @@ pub struct ResolvedKeys {
|
|||||||
pub key_source: u8,
|
pub key_source: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Redacting `Debug`: `vuk` and `unit_keys` are raw key bytes, never printed.
|
||||||
|
// `disc_hash` is the public per-disc identifier (SHA-1 of the .inf), not secret.
|
||||||
|
// Guarded by `resolved_keys_debug_is_redacted`.
|
||||||
|
impl std::fmt::Debug for ResolvedKeys {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("ResolvedKeys")
|
||||||
|
.field("disc_hash", &self.disc_hash)
|
||||||
|
.field("vuk", &self.vuk.map(|_| "<redacted>"))
|
||||||
|
.field("unit_keys_len", &self.unit_keys.len())
|
||||||
|
.field("title_cps_unit", &self.title_cps_unit)
|
||||||
|
.field("version", &self.version)
|
||||||
|
.field("bus_encryption", &self.bus_encryption)
|
||||||
|
.field("key_source", &self.key_source)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Inputs shared by every classical-path resolver. References only —
|
/// Inputs shared by every classical-path resolver. References only —
|
||||||
/// callers retain ownership of all buffers.
|
/// callers retain ownership of all buffers.
|
||||||
pub struct ResolveContext<'a> {
|
pub struct ResolveContext<'a> {
|
||||||
@@ -467,6 +483,24 @@ mod tests {
|
|||||||
use super::super::types::*;
|
use super::super::types::*;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
/// `ResolvedKeys` carries the disc's VUK and unit keys raw; `Debug` must not
|
||||||
|
/// leak them. Sentinel 213 (0xD5); non-secret fields are not 213.
|
||||||
|
#[test]
|
||||||
|
fn resolved_keys_debug_is_redacted() {
|
||||||
|
let rk = ResolvedKeys {
|
||||||
|
disc_hash: [0u8; 20],
|
||||||
|
vuk: Some([0xD5; 16]),
|
||||||
|
unit_keys: vec![(1, [0xD5; 16])],
|
||||||
|
title_cps_unit: vec![0],
|
||||||
|
version: AacsVersion::V21,
|
||||||
|
bus_encryption: true,
|
||||||
|
key_source: 1,
|
||||||
|
};
|
||||||
|
let dbg = format!("{rk:?}");
|
||||||
|
assert!(!dbg.contains("213"), "ResolvedKeys leaked keys: {dbg}");
|
||||||
|
assert!(dbg.contains("redacted"), "ResolvedKeys missing marker: {dbg}");
|
||||||
|
}
|
||||||
|
|
||||||
/// Audit #5: the `major` / `from_major` mapping is load-bearing for the
|
/// Audit #5: the `major` / `from_major` mapping is load-bearing for the
|
||||||
/// Unit_Key_RO stride, so pin it as a table. V10 ↔ BD; V20/V21 → UHD; any
|
/// Unit_Key_RO stride, so pin it as a table. V10 ↔ BD; V20/V21 → UHD; any
|
||||||
/// non-BD major selects the V20/V21 64-byte stride (V10 is the only 48-byte).
|
/// non-BD major selects the V20/V21 64-byte stride (V10 is the only 48-byte).
|
||||||
|
|||||||
+30
-1
@@ -146,7 +146,7 @@ use super::derive::{calc_pk_from_dk, calc_v_mask};
|
|||||||
/// Outcome of a subset-difference walk against an MKB. Carries the
|
/// Outcome of a subset-difference walk against an MKB. Carries the
|
||||||
/// processing key and the matching `uv` slot — both needed as inputs
|
/// processing key and the matching `uv` slot — both needed as inputs
|
||||||
/// to the variant chain.
|
/// to the variant chain.
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct ProcessingKeyMatch {
|
pub struct ProcessingKeyMatch {
|
||||||
/// Processing Key.
|
/// Processing Key.
|
||||||
pub kp: [u8; 16],
|
pub kp: [u8; 16],
|
||||||
@@ -158,6 +158,20 @@ pub struct ProcessingKeyMatch {
|
|||||||
pub cvalue_index: usize,
|
pub cvalue_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Redacting `Debug`: `kp` (a Processing Key) and `cvalue` are secret, never
|
||||||
|
// printed. `uv` / `cvalue_index` are non-secret coordinates. Guarded by
|
||||||
|
// `processing_key_match_debug_is_redacted`.
|
||||||
|
impl std::fmt::Debug for ProcessingKeyMatch {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("ProcessingKeyMatch")
|
||||||
|
.field("kp", &"<redacted>")
|
||||||
|
.field("uv", &self.uv)
|
||||||
|
.field("cvalue", &"<redacted>")
|
||||||
|
.field("cvalue_index", &self.cvalue_index)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn mkb_find_mk_dv(records: &[MkbRecord]) -> Option<[u8; 16]> {
|
fn mkb_find_mk_dv(records: &[MkbRecord]) -> Option<[u8; 16]> {
|
||||||
let r = records.iter().find(|r| {
|
let r = records.iter().find(|r| {
|
||||||
(r.rec_type == REC_VERIFY_MEDIA_KEY_V1 || r.rec_type == REC_VERIFY_MEDIA_KEY_V2)
|
(r.rec_type == REC_VERIFY_MEDIA_KEY_V1 || r.rec_type == REC_VERIFY_MEDIA_KEY_V2)
|
||||||
@@ -635,6 +649,21 @@ mod tests {
|
|||||||
use super::super::crypto::aesg3;
|
use super::super::crypto::aesg3;
|
||||||
use super::super::derive::calc_pk_from_dk;
|
use super::super::derive::calc_pk_from_dk;
|
||||||
|
|
||||||
|
/// `ProcessingKeyMatch` carries the Processing Key (`kp`) and `cvalue` raw;
|
||||||
|
/// `Debug` must redact both. Non-secret `uv`/`cvalue_index` are not 213.
|
||||||
|
#[test]
|
||||||
|
fn processing_key_match_debug_is_redacted() {
|
||||||
|
let m = ProcessingKeyMatch {
|
||||||
|
kp: [0xD5; 16],
|
||||||
|
uv: 1,
|
||||||
|
cvalue: [0xD5; 16],
|
||||||
|
cvalue_index: 2,
|
||||||
|
};
|
||||||
|
let dbg = format!("{m:?}");
|
||||||
|
assert!(!dbg.contains("213"), "ProcessingKeyMatch leaked kp/cvalue: {dbg}");
|
||||||
|
assert!(dbg.contains("redacted"), "ProcessingKeyMatch missing marker: {dbg}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn calc_pk_from_dk_terminates_on_nonconvergent_mask() {
|
fn calc_pk_from_dk_terminates_on_nonconvergent_mask() {
|
||||||
// Regression for the unbounded-loop hang: pick a (dev_key_v_mask,
|
// Regression for the unbounded-loop hang: pick a (dev_key_v_mask,
|
||||||
|
|||||||
Reference in New Issue
Block a user