audit: cap the sparse-PTS reorder buffer, FMTS key state, zero KCD

Round-1 findings from the 10-phase release audit:

- SparsePtsReorder buffered its current GOP with no bound, draining only on
  a keyframe — an open-GOP or crafted program stream that never signals one
  could hold the whole title in RAM. Force-complete the GOP at
  MAX_GOP_FRAMES, matching the MPEG-2 parser's backstop.
- inject_unit_keys labelled a 2.1 FMTS disc as AACS 1.0 / bus-encryption
  off; FMTS is UHD-family, so synthesize the UHD version + bus encryption.
- The compiled Key Correction Data was a non-zero 16-byte constant fed into
  the Media Key derivation. Per the no-compiled-keys rule it is now all-zero;
  the chain still cannot complete on a real disc (documented), so this is
  behaviour-neutral — all variant tests pass unchanged.
- Fix stale doc references (broken `super::variants` intra-doc links, and
  `aacs::keys` comments) left by the module rename.
This commit is contained in:
Matthew Jackson
2026-07-09 14:14:20 -07:00
parent 14c4227292
commit a94f78d090
7 changed files with 51 additions and 23 deletions
+1 -1
View File
@@ -89,7 +89,7 @@ pub(crate) const AESG3_SEED: [u8; 16] = [
/// left=`D(k,s0)⊕s0` inc 0, pk=`D(k,s0+1)⊕(s0+1)` inc 1, right=`D(k,s0+2)⊕(s0+2)` inc 2).
/// seed[15] += inc, then AES-DEC(key, seed) XOR seed.
///
/// Shared with [`super::variants`] (its variant chain runs the same SD
/// Shared with [`super::variant`] (its variant chain runs the same SD
/// tree); a single definition keeps the two walks byte-identical.
pub(crate) fn aesg3(key: &[u8; 16], inc: u8) -> [u8; 16] {
let mut seed = AESG3_SEED;
+2 -2
View File
@@ -105,7 +105,7 @@ pub(crate) fn validate_processing_key(
None
}
/// Compute v_mask from a UV value. [C] §3.2.3. Shared with [`super::variants`].
/// Compute v_mask from a UV value. [C] §3.2.3. Shared with [`super::variant`].
pub(super) fn calc_v_mask(uv: u32) -> u32 {
let mut v_mask: u32 = 0xFFFF_FFFF;
while (uv & !v_mask) == 0 && v_mask != 0 {
@@ -115,7 +115,7 @@ pub(super) fn calc_v_mask(uv: u32) -> u32 {
}
/// Derive processing key from device key using subset-difference tree traversal.
/// [C] §3.2.4 (device-tree descent, MSB-branch, terminal PK). Shared with [`super::variants`].
/// [C] §3.2.4 (device-tree descent, MSB-branch, terminal PK). Shared with [`super::variant`].
pub(super) fn calc_pk_from_dk(
dk: &[u8; 16],
uv: u32,
+1 -1
View File
@@ -68,7 +68,7 @@ pub fn walk_mkb(mkb: &[u8]) -> Vec<MkbRecord> {
/// then the body — stopping at the `00 000000` end marker or a
/// malformed/out-of-bounds length. Lazy (no body clone), so a find-one-record
/// caller never materialises the multi-MB cvalue table. [`walk_mkb`] and every
/// MKB record walk in `aacs::keys` are built on this, so the framing rules — and
/// MKB record walk in `aacs::resolve`/`aacs::derive` are built on this, so the framing rules — and
/// any future fix to them — live in exactly one place (they had drifted across
/// six hand-rolled copies).
pub(crate) fn mkb_records(mkb: &[u8]) -> impl Iterator<Item = (usize, u8, usize)> + '_ {
+1 -1
View File
@@ -42,7 +42,7 @@ use super::types::{DeviceKey, DiscEntry, HostCert};
/// Source of AACS key material.
///
/// Implementors return raw material only — the resolver in
/// `aacs::keys` owns all the crypto (DK→PK walking, PK validation,
/// `aacs::resolve` and `aacs::derive` own the crypto (DK→PK walking, PK validation,
/// MK→VUK→TK derivation). See module docs for method semantics.
pub trait KeyProvider: Send + Sync {
/// Device keys (top-of-tree, walked by the resolver).
+9 -11
View File
@@ -62,19 +62,17 @@ use super::types::DeviceKey;
// ── Public constants ──────────────────────────────────────────────────────
/// AACS 2.1 Key Correction Data.
/// AACS 2.1 Key Correction Data — a zero placeholder, NOT real key material.
///
/// **KCD is PER-LICENSEE** (per player manufacturer) — there is no single
/// universal value, so this one constant cannot be correct across discs. We do
/// NOT have the real per-manufacturer KCDs coded, and won't: libfreemkv compiles
/// in no AACS key material (keydb.cfg is the single source of truth). The bytes
/// below only let the chain's SHAPE exercise against synthetic fixtures; on a
/// real variant disc they yield a wrong Media Key that the final
/// Verify-Media-Key gate rejects. So the variant chain cannot complete on a real
/// disc today — a key-acquisition gap, not a code gap.
const KEY_CORRECTION_DATA: [u8; 16] = [
0x3b, 0x62, 0x8a, 0x78, 0x29, 0x00, 0xca, 0x2f, 0xdb, 0xe7, 0x7a, 0x49, 0xfe, 0x22, 0xd6, 0x6e,
];
/// universal value. libfreemkv compiles in no AACS key material (keydb.cfg is
/// the single source of truth), so this stays all-zero: the chain's SHAPE still
/// runs, but on a real variant disc the derivation yields a wrong Media Key that
/// the final Verify-Media-Key gate rejects. The variant chain therefore cannot
/// complete on a real disc today — a key-acquisition gap, not a code gap. If a
/// real per-licensee KCD is ever available it must come from keydb.cfg, never a
/// compiled constant.
const KEY_CORRECTION_DATA: [u8; 16] = [0u8; 16];
// ── MKB record walking ────────────────────────────────────────────────────