mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
spec, test_util: specification quote registry, spec guards, test-util fixtures
libfreemkv::spec holds the verbatim specification quotes the code cites, with guards tying each governed site to its quote. A test-util feature exposes shared fixtures to downstream tests. M2TS output derives DTS for reordered video and rounds to whole 90 kHz ticks.
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
//! Spec guards (keys-upfront-design §7.8): behaviour that is correct by the quoted
|
||||
//! spec and must not change. Each asserts against the `libfreemkv::spec` const text
|
||||
//! and the behaviour, and is written to fail on a plausible wrong change. Guards
|
||||
//! that need crate-private items (or the unit decrypt, which leaves the public API)
|
||||
//! live next to their code, named the same.
|
||||
|
||||
use aes::Aes128;
|
||||
use aes::cipher::{Array, BlockCipherDecrypt, KeyInit};
|
||||
use libfreemkv::ContentFormat;
|
||||
use libfreemkv::aacs::content::{ALIGNED_UNIT_LEN, ALIGNED_UNIT_SECTORS, aacs_unit_encrypted};
|
||||
use libfreemkv::aacs::derive::derive_vuk;
|
||||
use libfreemkv::aacs::inf::parse_unit_key_ro;
|
||||
use libfreemkv::aacs::mkb::AacsVersion;
|
||||
use libfreemkv::spec::keys::*;
|
||||
|
||||
fn aes_128d(key: &[u8; 16], block: &[u8; 16]) -> [u8; 16] {
|
||||
let mut b: Array<u8, _> = (*block).into();
|
||||
Aes128::new(&(*key).into()).decrypt_block(&mut b);
|
||||
b.into()
|
||||
}
|
||||
|
||||
// A Unit_Key_RO.inf with the key block at `start` (Num_of_CPS_Unit = `declared`) and
|
||||
// room for `slots` keys at `stride`; every other byte is a non-zero filler.
|
||||
fn inf(start: usize, declared: u16, slots: usize, stride: usize) -> Vec<u8> {
|
||||
let mut v = vec![0xEEu8; start + 48 + stride * slots];
|
||||
v[..4].copy_from_slice(&(start as u32).to_be_bytes());
|
||||
v[24..26].copy_from_slice(&0u16.to_be_bytes()); // Num_of_Title
|
||||
v[start..start + 2].copy_from_slice(&declared.to_be_bytes());
|
||||
v
|
||||
}
|
||||
|
||||
/// per spec; do not change without a spec citation — KS-2 [BD] §3.10.1:
|
||||
/// "An Aligned Unit consists of 32 MPEG source packets … 6144 bytes … 3 logical sectors."
|
||||
#[test]
|
||||
fn aligned_unit_is_6144_bytes_3_sectors_32_packets() {
|
||||
let t = KS_2_ALIGNED_UNIT.text;
|
||||
assert!(t.contains("32 MPEG source packets") && t.contains("TP_extra_header (4 bytes)"));
|
||||
assert!(t.contains("(188 bytes)") && t.contains("6144 bytes") && t.contains("3 logical"));
|
||||
assert_eq!(ALIGNED_UNIT_LEN, 6144);
|
||||
assert_eq!(ALIGNED_UNIT_SECTORS, 3);
|
||||
assert_eq!(ALIGNED_UNIT_LEN / (4 + 188), 32);
|
||||
assert_eq!(ALIGNED_UNIT_LEN, ALIGNED_UNIT_SECTORS as usize * 2048);
|
||||
}
|
||||
|
||||
fn unit_with_cpi(bits: u8) -> Vec<u8> {
|
||||
let mut u = vec![0u8; ALIGNED_UNIT_LEN];
|
||||
u[0] = bits << 6 | 0x15; // the low 6 bits are Arrival_time_stamp, never the flag
|
||||
u
|
||||
}
|
||||
|
||||
/// per spec; do not change without a spec citation — KS-5 [BD] §3.10.2:
|
||||
/// "shall be set to 00₂ if the data is not encrypted" (KS-24: libaacs agrees).
|
||||
#[test]
|
||||
fn cpi_00_is_clear() {
|
||||
assert!(
|
||||
KS_5_CPI
|
||||
.text
|
||||
.contains("set to 00₂ if the data is not encrypted")
|
||||
);
|
||||
assert!(
|
||||
KS_24_LIBAACS_CPI_CLEAR_UNIT
|
||||
.text
|
||||
.contains("if (!(buf[0] & 0xc0))")
|
||||
);
|
||||
assert!(!aacs_unit_encrypted(
|
||||
&unit_with_cpi(0b00),
|
||||
ContentFormat::BdTs
|
||||
));
|
||||
}
|
||||
|
||||
/// per spec; do not change without a spec citation — KS-5 [BD] §3.10.2:
|
||||
/// "Copy_permission_indicator shall be set to 11₂ if the data is encrypted".
|
||||
#[test]
|
||||
fn cpi_11_is_encrypted() {
|
||||
assert!(
|
||||
KS_5_CPI
|
||||
.text
|
||||
.contains("set to 11₂ if the data is encrypted")
|
||||
);
|
||||
assert!(aacs_unit_encrypted(
|
||||
&unit_with_cpi(0b11),
|
||||
ContentFormat::BdTs
|
||||
));
|
||||
}
|
||||
|
||||
/// per spec; do not change without a spec citation — KS-5 [BD] §3.10.2:
|
||||
/// "set to 10₂ or 01₂, the data shall be considered encrypted" (not `== 0xC0`).
|
||||
#[test]
|
||||
fn cpi_10_and_01_are_considered_encrypted() {
|
||||
assert!(
|
||||
KS_5_CPI
|
||||
.text
|
||||
.contains("10₂ or 01₂, the data shall be considered encrypted")
|
||||
);
|
||||
assert!(aacs_unit_encrypted(
|
||||
&unit_with_cpi(0b10),
|
||||
ContentFormat::BdTs
|
||||
));
|
||||
assert!(aacs_unit_encrypted(
|
||||
&unit_with_cpi(0b01),
|
||||
ContentFormat::BdTs
|
||||
));
|
||||
}
|
||||
|
||||
/// per spec; do not change without a spec citation — KS-12 [BD] §3.9.3:
|
||||
/// "Unit_Key_Block_start_address field (32 bits) indicates the start address".
|
||||
#[test]
|
||||
fn unit_key_block_start_address_is_bytes_0_to_4_be() {
|
||||
assert!(KS_12_UNIT_KEY_BLOCK_START.text.contains("field (32 bits)"));
|
||||
assert!(
|
||||
KS_12_UNIT_KEY_BLOCK_START
|
||||
.text
|
||||
.contains("from the first byte of CPS Unit Key")
|
||||
);
|
||||
// Start 0x0130: big-endian bytes 00 00 01 30 (little-endian would read 0x3001_0000).
|
||||
let mut v = inf(0x130, 2, 2, 48);
|
||||
v[16] = 1; // Application_Type: the header begins at byte 16, after 96 reserved bits
|
||||
let ukf = parse_unit_key_ro(&v, AacsVersion::V10).expect("parses");
|
||||
assert_eq!(
|
||||
ukf.encrypted_keys.len(),
|
||||
2,
|
||||
"Num_of_CPS_Unit read at the BE start address"
|
||||
);
|
||||
assert_eq!(ukf.app_type, 1);
|
||||
v[..4].copy_from_slice(&0x130u32.to_le_bytes());
|
||||
assert!(
|
||||
parse_unit_key_ro(&v, AacsVersion::V10).is_none(),
|
||||
"LE is out of range"
|
||||
);
|
||||
}
|
||||
|
||||
/// per spec; do not change without a spec citation — KS-13 [BD] Table 3-13:
|
||||
/// "Application_Type (= 01₁₆) 8 … Num_of_Title#I 16 … (reserved) 16 … CPS_Unit_number …".
|
||||
#[test]
|
||||
fn header_fields_and_title_table_layout() {
|
||||
let t = KS_13_UNIT_KEY_FILE_HEADER.text;
|
||||
assert!(t.contains("First Playback#I 16 … CPS_Unit_number for Top Menu#I 16"));
|
||||
assert!(t.contains("Num_of_Title#I 16 … (reserved) 16 … CPS_Unit_number for Title#J"));
|
||||
let mut v = inf(0x40, 3, 3, 48);
|
||||
v[16] = 1; // Application_Type @16
|
||||
v[17] = 1; // Num_of_BD_Directory @17
|
||||
v[18..20].copy_from_slice(&[0, 0]); // flag + reserved
|
||||
v[20..22].copy_from_slice(&2u16.to_be_bytes()); // First Playback @20
|
||||
v[22..24].copy_from_slice(&3u16.to_be_bytes()); // Top Menu @22
|
||||
v[24..26].copy_from_slice(&2u16.to_be_bytes()); // Num_of_Title @24
|
||||
v[28..30].copy_from_slice(&1u16.to_be_bytes()); // Title 1: 2 reserved (0xEE) + CPS
|
||||
v[32..34].copy_from_slice(&3u16.to_be_bytes()); // Title 2
|
||||
let ukf = parse_unit_key_ro(&v, AacsVersion::V10).expect("parses");
|
||||
assert_eq!((ukf.app_type, ukf.num_bdmv_dir), (1, 1));
|
||||
assert_eq!(
|
||||
ukf.title_cps_unit,
|
||||
vec![1, 2, 0, 2],
|
||||
"FP, TM, titles (0-based)"
|
||||
);
|
||||
}
|
||||
|
||||
/// per spec; do not change without a spec citation — KS-14 [BD] §3.9.3:
|
||||
/// "Num_of_CPS_Unit field (16 bits) indicates the number of CPS Units on the disc."
|
||||
#[test]
|
||||
fn num_of_cps_unit_is_the_declared_count() {
|
||||
assert!(
|
||||
KS_14_UNIT_KEY_BLOCK
|
||||
.text
|
||||
.contains("Num_of_CPS_Unit field (16 bits) indicates")
|
||||
);
|
||||
for declared in [1u16, 2, 5] {
|
||||
let v = inf(0x40, declared, declared as usize, 48);
|
||||
let ukf = parse_unit_key_ro(&v, AacsVersion::V10).expect("parses");
|
||||
let nums: Vec<u32> = ukf.encrypted_keys.iter().map(|k| k.0).collect();
|
||||
assert_eq!(
|
||||
nums,
|
||||
(1..=declared as u32).collect::<Vec<_>>(),
|
||||
"declared {declared}"
|
||||
);
|
||||
}
|
||||
// A file that declares 3 units but holds only 2 key slots is malformed, never "2 units".
|
||||
let mut short = inf(0x40, 3, 2, 48);
|
||||
short.truncate(0x40 + 48 + 48 + 16);
|
||||
assert!(parse_unit_key_ro(&short, AacsVersion::V10).is_none());
|
||||
}
|
||||
|
||||
/// per spec; do not change without a spec citation — KS-14 [BD] Table 3-15:
|
||||
/// "MAC of PMSN#I 128 … MAC of Device Binding Nonce#I 128 … Encrypted CPS Unit Key … 128".
|
||||
#[test]
|
||||
fn v10_key_entry_is_48_bytes_encrypted_key_last() {
|
||||
let t = KS_14_UNIT_KEY_BLOCK.text;
|
||||
assert!(t.contains("Num_of_CPS_Unit 16 … (reserved) 112 … MAC of PMSN#I 128"));
|
||||
assert!(t.contains("MAC of Device Binding Nonce#I 128 … Encrypted CPS Unit Key"));
|
||||
let start = 0x40;
|
||||
let mut v = inf(start, 2, 2, 48);
|
||||
for i in 0..2 {
|
||||
let entry = start + 16 + 48 * i;
|
||||
v[entry..entry + 16].fill(0xA0 + i as u8); // MAC of PMSN
|
||||
v[entry + 16..entry + 32].fill(0xB0 + i as u8); // MAC of Device Binding Nonce
|
||||
v[entry + 32..entry + 48].fill(0xC0 + i as u8); // Encrypted CPS Unit Key
|
||||
}
|
||||
let ukf = parse_unit_key_ro(&v, AacsVersion::V10).expect("parses");
|
||||
assert_eq!(ukf.encrypted_keys, vec![(1, [0xC0; 16]), (2, [0xC1; 16])]);
|
||||
}
|
||||
|
||||
/// per spec; do not change without a spec citation — KS-11 [BD] §3.9.2:
|
||||
/// "CPS_Unit_number values are defined in ascending order, starting from one."
|
||||
#[test]
|
||||
fn cps_unit_numbers_are_one_based() {
|
||||
assert!(KS_11_CPS_NUMBER_FROM_ONE.text.contains("starting from one"));
|
||||
let mut v = inf(0x40, 3, 3, 48);
|
||||
v[20..22].copy_from_slice(&1u16.to_be_bytes()); // FP: CPS 1 → index 0
|
||||
v[22..24].copy_from_slice(&3u16.to_be_bytes()); // TM: CPS n → index n-1
|
||||
v[24..26].copy_from_slice(&2u16.to_be_bytes());
|
||||
v[28..30].copy_from_slice(&0u16.to_be_bytes()); // 0: not a CPS number → clamped to 0
|
||||
v[32..34].copy_from_slice(&4u16.to_be_bytes()); // > n: out of range → clamped to 0
|
||||
let ukf = parse_unit_key_ro(&v, AacsVersion::V10).expect("parses");
|
||||
assert_eq!(ukf.title_cps_unit, vec![0, 2, 0, 0]);
|
||||
let first = ukf.encrypted_keys.first().expect("keys");
|
||||
assert_eq!(first.0, 1, "the first key is CPS unit number 1");
|
||||
}
|
||||
|
||||
/// per spec; do not change without a spec citation — KS-16 [PV] §3.3 "Kvu = AES-G(Km, IDv)";
|
||||
/// KS-17 [CM] §2.1.3 "AES-G(x1, x2) = AES-128D(x1, x2) ⊕ x2."
|
||||
#[test]
|
||||
fn kvu_is_aes_g_of_km_and_vid() {
|
||||
assert_eq!(KS_16_KVU.text, "Kvu = AES-G(Km, IDv)");
|
||||
assert_eq!(KS_17_AES_G.text, "AES-G(x1, x2) = AES-128D(x1, x2) ⊕ x2.");
|
||||
let km: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(29) ^ 0x5C);
|
||||
let vid: [u8; 16] = core::array::from_fn(|i| 0xF0 - i as u8);
|
||||
let mut want = aes_128d(&km, &vid);
|
||||
for (w, x) in want.iter_mut().zip(vid) {
|
||||
*w ^= x;
|
||||
}
|
||||
assert_eq!(derive_vuk(&km, &vid), want);
|
||||
assert_ne!(
|
||||
derive_vuk(&km, &vid),
|
||||
aes_128d(&km, &vid),
|
||||
"the ⊕ IDv step is there"
|
||||
);
|
||||
assert_ne!(derive_vuk(&vid, &km), want, "Km is the key, IDv the data");
|
||||
}
|
||||
|
||||
/// per evidence (no public spec); do not change without evidence proving otherwise —
|
||||
/// KS-25: "the 64-byte Unit_Key_RO.inf stride" of AACS 2.x (qa UHD fixture).
|
||||
#[test]
|
||||
fn v20_stride_64_evidence() {
|
||||
assert_eq!(
|
||||
KS_25_AACS2_EVIDENCE.kind,
|
||||
libfreemkv::spec::QuoteKind::Evidence
|
||||
);
|
||||
assert!(
|
||||
KS_25_AACS2_EVIDENCE
|
||||
.text
|
||||
.contains("64-byte Unit_Key_RO.inf stride")
|
||||
);
|
||||
let start = 0x40;
|
||||
let mut v = inf(start, 2, 2, 64);
|
||||
v[start + 48..start + 64].fill(0xC0);
|
||||
v[start + 48 + 64..start + 64 + 64].fill(0xC1);
|
||||
for version in [AacsVersion::V20, AacsVersion::V21] {
|
||||
let ukf = parse_unit_key_ro(&v, version).expect("parses");
|
||||
assert_eq!(ukf.encrypted_keys, vec![(1, [0xC0; 16]), (2, [0xC1; 16])]);
|
||||
}
|
||||
let v10 = parse_unit_key_ro(&v, AacsVersion::V10).expect("parses");
|
||||
assert_ne!(
|
||||
v10.encrypted_keys[1].1, [0xC1; 16],
|
||||
"48 is the AACS 1.0 stride only"
|
||||
);
|
||||
}
|
||||
|
||||
/// per evidence (no public spec); do not change without evidence proving otherwise —
|
||||
/// KS-27: the HD DVD book was "removed from this AACS website due to inactivity".
|
||||
#[test]
|
||||
fn hddvd_flag_offset_is_unverified() {
|
||||
assert_eq!(
|
||||
KS_27_HDDVD_EVIDENCE.kind,
|
||||
libfreemkv::spec::QuoteKind::Evidence
|
||||
);
|
||||
let src = include_str!("../src/aacs/content.rs");
|
||||
let lines: Vec<&str> = src.lines().collect();
|
||||
let at = lines
|
||||
.iter()
|
||||
.position(|l| l.starts_with("const PS_SCRAMBLE_OFF"))
|
||||
.expect("PS_SCRAMBLE_OFF is defined in aacs/content.rs");
|
||||
let context = lines[at.saturating_sub(3)..at].join("\n");
|
||||
assert!(
|
||||
context.contains("UNVERIFIED"),
|
||||
"the UNVERIFIED marker left PS_SCRAMBLE_OFF"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user