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:
Matthew Jackson
2026-09-28 00:50:09 -07:00
parent 3207856c17
commit cbd3db9d0e
27 changed files with 5543 additions and 57 deletions
+286
View File
@@ -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"
);
}
+149
View File
@@ -0,0 +1,149 @@
//! `spec_quotes_match_registry`: every `libfreemkv::spec` quote (all prefixes)
//! against `tests/spec_quotes.txt`. It proves const == registry only; registry ==
//! source document is a PR-review item (the reviewer opens the PDF page or URL).
use libfreemkv::spec::{self, SpecQuote};
use std::collections::{HashMap, HashSet};
const REGISTRY: &str = include_str!("spec_quotes.txt");
const PREFIXES: &[&str] = &["KS-", "SS-"];
const ELISION: &str = " … ";
// `<id>\t<passage>[\t// comment]` per line; `#` lines are the header. Panics on a
// malformed line or a duplicate ID.
fn registry() -> HashMap<&'static str, &'static str> {
let mut out = HashMap::new();
for line in REGISTRY
.lines()
.filter(|l| !l.starts_with('#') && !l.is_empty())
{
let (id, rest) = line
.split_once('\t')
.expect("registry line is `<id>\\t<passage>`");
let passage = rest.split("\t// ").next().unwrap_or(rest);
assert!(!passage.trim().is_empty(), "{id}: empty registry passage");
assert!(
out.insert(id, passage).is_none(),
"{id}: two registry lines"
);
}
out
}
// Each " … "-separated fragment of `text` occurs in `passage`, in order.
fn fragments_in_order(text: &str, passage: &str) -> Result<(), String> {
let mut rest = passage;
for frag in text.split(ELISION) {
match rest.find(frag) {
Some(at) => rest = &rest[at + frag.len()..],
None => return Err(format!("fragment {frag:?} not found (in order)")),
}
}
Ok(())
}
// A known prefix, then a number from 1 (the const name agreeing is checked below).
fn check_id(q: &SpecQuote) {
let prefix = PREFIXES.iter().find(|p| q.id.starts_with(**p));
let prefix = prefix.unwrap_or_else(|| panic!("{}: prefix not in {PREFIXES:?}", q.id));
let n: u32 = q.id[prefix.len()..]
.parse()
.unwrap_or_else(|_| panic!("{}: no number after the prefix", q.id));
assert!(n >= 1, "{}: IDs start at 1", q.id);
}
#[test]
fn spec_quotes_match_registry() {
let reg = registry();
let mut seen = HashSet::new();
let mut n = 0;
for q in spec::quotes() {
n += 1;
check_id(q);
assert!(seen.insert(q.id), "{}: ID used twice", q.id);
assert!(!q.section.is_empty(), "{}: empty section", q.id);
assert!(!q.locator.is_empty(), "{}: empty locator", q.id);
assert!(!q.source.is_empty(), "{}: empty source", q.id);
assert!(!q.text.is_empty(), "{}: empty text", q.id);
let passage = reg
.get(q.id)
.unwrap_or_else(|| panic!("{}: no registry line", q.id));
if let Err(e) = fragments_in_order(q.text, passage) {
panic!("{}: const text differs from the registry: {e}", q.id);
}
}
let orphans: Vec<_> = reg.keys().filter(|id| !seen.contains(*id)).collect();
assert!(
orphans.is_empty(),
"registry lines with no const: {orphans:?}"
);
assert!(n > 0, "spec::ALL is empty");
}
/// Every `pub const <PREFIX>_<n>_<SHORT>: SpecQuote` in `src/spec/` carries `id: "<PREFIX>-<n>"`
/// (KU design §3.6.1 rule 3), and the source defines exactly the quotes `spec::ALL` lists.
#[test]
fn const_names_agree_with_ids() {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/spec");
let mut consts = 0;
for entry in std::fs::read_dir(&dir).unwrap() {
let src = std::fs::read_to_string(entry.unwrap().path()).unwrap();
let mut lines = src.lines();
while let Some(line) = lines.next() {
let Some(name) = line
.strip_prefix("pub const ")
.and_then(|l| l.strip_suffix(": SpecQuote = SpecQuote {"))
else {
continue;
};
let id_line = lines.next().unwrap_or_default().trim();
let mut parts = name.splitn(3, '_');
let (prefix, n) = (parts.next().unwrap(), parts.next().unwrap_or_default());
assert_eq!(id_line, format!("id: \"{prefix}-{n}\","), "const {name}");
consts += 1;
}
}
assert_eq!(
consts,
spec::quotes().count(),
"a quote const missing from spec::ALL"
);
}
/// `KS-1`…`KS-29` exist, in order, with no gap (KU design §3.6.1 rule 4).
#[test]
fn keys_quotes_are_ks_1_to_29_in_order() {
let ids: Vec<&str> = spec::keys::ALL.iter().map(|q| q.id).collect();
let want: Vec<String> = (1..=29).map(|n| format!("KS-{n}")).collect();
assert_eq!(ids, want);
}
/// The checker itself: in-order fragments pass; a changed, missing or reordered one fails.
#[test]
fn fragment_check_rejects_drift() {
let passage = "alpha beta gamma delta";
assert!(fragments_in_order("alpha beta … delta", passage).is_ok());
assert!(fragments_in_order("alpha beta gamma delta", passage).is_ok());
assert!(fragments_in_order("alpha … epsilon", passage).is_err());
assert!(fragments_in_order("delta … alpha", passage).is_err());
assert!(fragments_in_order("alpha beta", passage).is_err());
}
/// Registry == source for the libaacs rows (KS-22…KS-24), whitespace-collapsed. Needs a
/// libaacs checkout at `55be92be`: `LIBAACS_AACS_C=…/src/libaacs/aacs.c cargo test
/// --test spec_quotes -- --ignored`. Ignored in CI, which has no libaacs checkout.
#[test]
#[ignore = "needs LIBAACS_AACS_C pointing at libaacs src/libaacs/aacs.c @55be92be"]
fn libaacs_quotes_match_source() {
let path = std::env::var("LIBAACS_AACS_C").expect("set LIBAACS_AACS_C");
let collapse = |s: &str| s.split_whitespace().collect::<Vec<_>>().join(" ");
let src = collapse(&std::fs::read_to_string(path).expect("read aacs.c"));
let reg = registry();
for id in ["KS-22", "KS-23", "KS-24"] {
let passage = collapse(reg[id]);
assert!(
src.contains(&passage),
"{id}: registry passage not in aacs.c: {passage}"
);
}
}
+36
View File
@@ -0,0 +1,36 @@
# The spec-quote registry: one line per `libfreemkv::spec` quote, `<id>\t<passage>`.
# The passage is the unelided, rendered source text a const's " … " fragments come
# from (radix subscripts as ₂/₁₆, ⊕ for XOR, curly quotes as printed). A trailing
# `\t// …` comment is not part of the passage: `test-only reference` (rule 9), or
# where a quote is cited later. `spec_quotes_match_registry` checks const == registry.
# Registry == source is a PR-review item (open the page); for KS-22…KS-24 run the
# ignored `libaacs_quotes_match_source` with LIBAACS_AACS_C=<aacs.c @55be92be>.
KS-1 When AACS encryption is applied to Clip AV stream files under the “\BDMV” directory, encryption is applied to every Aligned Unit in the file.
KS-2 An Aligned Unit consists of 32 MPEG source packets: Each MPEG source packet consists of the TP_extra_header (4 bytes) and an MPEG Transport packet (188 bytes). The total size of an Aligned Unit is 6144 bytes, which is equal to the size of 3 logical sectors.
KS-3 The final 6128 bytes of each Aligned Unit is encrypted using the Block Key and AES-128CBCE. A new CBC cipher chain is started for each Aligned Unit (see Figure 3-7).
KS-4 The first 16 bytes of each Aligned Unit is used as the seed for calculating the Block Key. Calculation method for the Block key is described in Figure 3-8.
KS-5 Copy_permission_indicator shall be set to 11₂ if the data is encrypted, or shall be set to 00₂ if the data is not encrypted. If the Licensed Player encounters the packet with Copy_permission_indicator set to 10₂ or 01₂, the data shall be considered encrypted.
KS-6 Table 3-34 TP_extra_header: Syntax No. of bits Mnemonic TP_extra_header { Copy_permission_indicator 2 uimsbf Arrival_time_stamp 30 uimsbf } // cited later by SG9 (KU-L1) and the §3.6.2 CPI-clear site
KS-7 Each physical sector in an Aligned Unit shall be allocated contiguously on the BD-ROM disc.
KS-8 All the extents of each Clip AV stream file shall be allocated with ascending order in physical layer.
KS-9 Within the Stereoscopic Interleaved file, segments of the two Clip AV stream files are recorded alternately. The boundary of these segments shall be always aligned to Aligned Unit boundary, so that there is no impact on the encryption scheme of the Stereoscopic Interleaved file.
KS-10 All AV stream files that are referred to by one Title are included in the same CPS Unit. If First Playback, Top Menu and/or a Title share one or more Clips, they shall be included in the same CPS Unit, i.e. the same Unit Key shall be assigned to First Playback, Top Menu and/or the Title. If multiple Titles share one or more Clips, these Titles shall be included in the same CPS Unit, i.e. the same Unit Key shall be assigned to these Titles. // cited later by SG23 (KU-L2) and the §3.6.2 playlist-borrowing site
KS-11 Each CPS Unit has a CPS_Unit_number. CPS_Unit_number values are defined in ascending order, starting from one. So, the maximum value of CPS_Unit_number shall be the same as the number of CPS Units that are assigned to First Playback, Top Menu, and/or Titles.
KS-12 Unit_Key_Block_start_address field (32 bits) indicates the start address of Unit_Key_Block() in the relative byte number from the first byte of CPS Unit Key File. The value of Unit_Key_Block_start_address field shall be a multiple of 16.
KS-13 Table 3-13: Unit_Key_File_Header(){ Application_Type (= 01₁₆) 8 Uimsbf Num_of_BD_Directory (= 01₁₆) 8 Uimsbf Use_SKB_Unified_MKB_Flag 1 Bslbf (reserved) 15 Bslbf For(I=0; I < Num_of_BD_Directory; I++){ CPS_Unit_number for First Playback#I 16 Uimsbf CPS_Unit_number for Top Menu#I 16 Uimsbf Num_of_Title#I 16 Uimsbf For(J=1; J < Num_of_Title+1; J++){ (reserved) 16 Bslbf CPS_Unit_number for Title#J in Directory #I 16 Uimsbf } } }
KS-14 Table 3-15: Unit_Key_Block(){ Num_of_CPS_Unit 16 Uimsbf (reserved) 112 Bslbf For(I=1; I < Num_of_CPS_Unit+1; I++){ MAC of PMSN#I 128 Bslbf MAC of Device Binding Nonce#I 128 Bslbf Encrypted CPS Unit Key for CPS Unit#I 128 bslbf } } Num_of_CPS_Unit field (16 bits) indicates the number of CPS Units on the disc.
KS-15 Encrypted CPS Unit Key field contains the 16 bytes of the encrypted CPS Unit Key (Kcu) for each CPS Unit. The CPS Unit Key is encrypted as follows: AES-128E( Kvu, Kcu ) where Kvu denotes a Volume Unique Key defined in Section 3.3 of the AACS Pre-recorded Video Book of this specification.
KS-16 The Licensed Replicator chooses a Volume ID (IDv) to be placed on the pre-recorded media and calculates a Volume Unique Key (Kvu) as follows: Kvu = AES-G(Km, IDv) where AES-G represents the AES-based one-way function defined in the Introduction and Common Cryptographic Elements book.
KS-17 The AES-based One-way Function result is calculated as AES-G(x1, x2) = AES-128D(x1, x2) ⊕ x2.
KS-18 If the Bus Encryption Enabled (BEE) flag in the Content Certificate is set to 1₂, the BEF shall be set to 1₂ for all the sectors that correspond to the Aligned Unit with Copy_permission_indicator set to 11₂ of the Clip AV stream files under “\BDMV\STREAM” directory. Otherwise, the BEF shall be set to 0₂.
KS-19 Unless otherwise specified, the Initialization Vector used at the beginning of a CBC encryption or decryption chain is a constant, iv0, which is: 0BA0F8DDFEA61FB3D8DF9F566A050F78₁₆
KS-20 The 30 least significant bits of this field shall be interpreted as a 30-bit unsigned binary number specifying the length of the extent in bytes. Unless otherwise specified, the length shall be an integral multiple of the logical block size. The 2 most significant bits shall be interpreted as a 2-bit unsigned binary number specifying the type of the extent as described in figure 4/42. If the 30 least significant bits are set to ZERO, the two most significant bits shall also be set to ZERO.
KS-21 Value Interpretation / 0 Extent recorded and allocated / 1 Extent not recorded but allocated / 2 Extent not recorded and not allocated / 3 The extent is the next extent of allocation descriptors (see 4/12) / Figure 42 - Extent interpretation
KS-22 static int _verify_ts(uint8_t *buf) { int i; for (i = 0; i < ALIGNED_UNIT_LEN; i += 192) { if (BD_UNLIKELY(buf[i + 4] != 0x47)) { return 0; } /* Clear copy_permission_indicator bits */ buf[i] &= ~0xc0; } return 1; } // cited later by SG9 (KU-L1) and the §3.6.2 CPI-clear site
KS-23 crypto_error = crypto_aes128e(aacs->uk->uk[curr_uk].key, out_buf, key); if (crypto_error) { LOG_CRYPTO_ERROR(DBG_AACS, "unit key derivation failed", crypto_error); return 1; } for (a = 0; a < 16; a++) { key[a] ^= out_buf[a]; /* here out_buf is plain data from in_buf */ } // test-only reference
KS-24 /* plain ? */ if (!(buf[0] & 0xc0)) { // TP_extra_header Copy_permission_indicator == 0, unit is not encrypted return 1; } // test-only reference
KS-25 AACS 2.x (UHD): the 64-byte Unit_Key_RO.inf stride and the FMTS IndividualSegment.tbl / .fmts model have no public book. They are pinned by the qa UHD fixture and aacs-unit-grid.md §2.
KS-26 FMTS anchor contract: the decode server returns the whole set only for an index-1 anchor, and 422 for the wrong phase (kdb decode-server main.rs:1640-1688)
KS-27 Please note: The AACS Specifications books for HD DVD and CBHD have been removed from this AACS website due to inactivity.
KS-28 File-anchored grid on real discs: 32/32 sync on the file grid vs 0–1/32 on the disc-LBA grid (aacs-unit-grid.md §2 table). This corroborates KS-1.
KS-29 The Licensed Drive reads Volume ID (Volume_ID) from the media and calculates a message authentication code (Dm) from the Volume ID and the Bus Key (BK) calculated in step 26 in Section 4.3. // test-only reference