diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a1284df..1b0c3b89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,6 +82,8 @@ jobs: working-directory: libfreemkv - run: cargo +stable clippy --all-targets -- -D warnings working-directory: libfreemkv + - run: cargo +stable clippy --all-targets --features keyproof -- -D warnings + working-directory: libfreemkv test: needs: resolve-msrv @@ -107,6 +109,9 @@ jobs: # `--tests` skips doctests; the keys compile_fail doctests enforce spec LK19. - run: cargo test --doc working-directory: libfreemkv + # `keyproof` is off by default, so its oracle compiles and is tested only here. + - run: cargo test --lib --features keyproof aacs::content + working-directory: libfreemkv # The memory orderings in `scsi::fd_handoff` (and `halt`) compile to the same instructions # on x86_64, so the WRONG ones pass every run; loom enumerates the executions diff --git a/src/aacs/content.rs b/src/aacs/content.rs index a5ee87da..8163414b 100644 --- a/src/aacs/content.rs +++ b/src/aacs/content.rs @@ -176,20 +176,19 @@ fn is_clean_ts(unit: &[u8]) -> bool { content == 0 || synced >= content.min(KEY_PROOF_PACKETS) } -/// Does `unit_key` open this encrypted AACS aligned `unit`? Returns one bit — -/// key right or wrong — and NEVER plaintext. The single authoritative answer to -/// "did this candidate key prove out against a sample unit", shared by the mux's -/// key selection/verify and by an offline key-resolver (kdb's `/decode`). BD-TS -/// 6144-byte units take the decrypt-less TS-sync fast reject ([`ts_unit_key_opens`], -/// only the proof blocks are touched); Program-Stream and non-standard lengths -/// fall back to the full [`decrypt_unit`] + [`is_clean`]. +/// Does `unit_key` open this encrypted AACS aligned `unit`? One bit, never plaintext. +/// BD-TS 6144-byte units take the decrypt-less TS-sync check (`ts_unit_key_opens`, only +/// the proof blocks are touched); Program-Stream and other lengths fall back to the full +/// [`decrypt_unit`] + [`is_clean`]. /// -/// Gated behind the `keyproof` feature so the DEFAULT public API still cannot -/// decrypt AACS — this is a key→content ORACLE (one bit), not a decryptor, and -/// it is a private-consumer seam, not part of the open surface -/// (KU §2.2; see `keys::public_api_cannot_decrypt_aacs`). +/// Behind the `keyproof` feature so the default public API still cannot decrypt AACS: a +/// key oracle, not a decryptor (KU §2.2; see `keys::public_api_cannot_decrypt_aacs`). #[cfg(feature = "keyproof")] -pub fn unit_key_opens(unit: &[u8], unit_key: &[u8; 16], format: crate::disc::ContentFormat) -> bool { +pub fn unit_key_opens( + unit: &[u8], + unit_key: &[u8; 16], + format: crate::disc::ContentFormat, +) -> bool { use crate::disc::ContentFormat; if matches!(format, ContentFormat::BdTs) && unit.len() == ALIGNED_UNIT_LEN { return ts_unit_key_opens(unit, unit_key); @@ -200,16 +199,13 @@ pub fn unit_key_opens(unit: &[u8], unit_key: &[u8; 16], format: crate::disc::Con is_clean(&scratch, format) } -/// BD-TS fast path of [`unit_key_opens`]: the SAME verdict as `decrypt_unit` + -/// `is_clean(BdTs)`, from one AES block per source packet instead of the whole -/// 383-block CBC chain — the hot path of a 100k-candidate brute. CBC decrypts -/// any block alone (`P[i] = AES_dec(C[i]) ⊕ C[i-1]`); packet heads sit at 192-byte -/// (block-aligned) offsets ≥ 192, so `C[i-1]` is always real ciphertext. Packet 0 -/// is skipped: its sync byte lives in the clear 16-byte seed and proves nothing. -/// `decrypt_unit` zeroes all-zero-ciphertext (padding) packets, so those are -/// skipped here identically. Verdict mirrors [`is_clean_ts`]: -/// `synced >= min(content, KEY_PROOF_PACKETS)`. `#[allow(dead_code)]` so the -/// equivalence test covers it even in a build without `keyproof`. +/// BD-TS fast path of `unit_key_opens`: the same verdict as `decrypt_unit` + +/// `is_clean(BdTs)` from one AES block per source packet. CBC decrypts any block alone +/// (`P[i] = AES_dec(C[i]) ⊕ C[i-1]`), and packet heads sit at block-aligned offsets ≥ 192. +/// Packet 0 is skipped (its sync byte is in the clear seed), as are all-zero padding +/// packets, which `decrypt_unit` zeroes. Verdict mirrors [`is_clean_ts`]: +/// `synced >= min(content, KEY_PROOF_PACKETS)`. `dead_code` is allowed so the +/// equivalence test covers it without `keyproof`. #[allow(dead_code)] fn ts_unit_key_opens(unit: &[u8], unit_key: &[u8; 16]) -> bool { use aes::cipher::{Array, BlockCipherDecrypt, BlockCipherEncrypt, KeyInit}; @@ -217,7 +213,8 @@ fn ts_unit_key_opens(unit: &[u8], unit_key: &[u8; 16]) -> bool { const PKT: usize = BD_SOURCE_PACKET_BYTES; // 192 const NPKT: usize = ALIGNED_UNIT_LEN / PKT; debug_assert_eq!(unit.len(), ALIGNED_UNIT_LEN); - let block_at = |o: usize| -> Block { Array::try_from(&unit[o..o + 16]).expect("16-byte block") }; + let block_at = + |o: usize| -> Block { Array::try_from(&unit[o..o + 16]).expect("16-byte block") }; // Block key = AES-128E(unit_key, seed) ⊕ seed — `decrypt_unit`'s derivation. let mut bk = block_at(0); diff --git a/src/aacs/content_tests.rs b/src/aacs/content_tests.rs index 9d9d3ec3..8e855e44 100644 --- a/src/aacs/content_tests.rs +++ b/src/aacs/content_tests.rs @@ -1255,11 +1255,8 @@ fn ts_packet_total_for_various_lengths() { assert_eq!(ts_packet_total(&[0u8; ALIGNED_UNIT_LEN]), 32); } -// The decrypt-less BD-TS fast path `ts_unit_key_opens` (the brute's hot reject, -// moved down from kdb) must return the EXACT same verdict as the full -// `decrypt_unit` + `is_clean(BdTs)`, for the RIGHT key and for wrong keys alike — -// or `unit_key_opens` would silently disagree with a real decrypt. This pins the -// two together so a change to either side can't desync them. +// The decrypt-less BD-TS path `ts_unit_key_opens` returns the same verdict as +// `decrypt_unit` + `is_clean(BdTs)` for the right key and wrong keys alike. #[test] fn ts_unit_key_opens_matches_decrypt_unit_then_is_clean() { let fmt = crate::disc::ContentFormat::BdTs; @@ -1284,7 +1281,10 @@ fn ts_unit_key_opens_matches_decrypt_unit_then_is_clean() { is_clean(&u, fmt) }; - assert!(full(&right), "sanity: the right key opens via the full path"); + assert!( + full(&right), + "sanity: the right key opens via the full path" + ); assert_eq!( ts_unit_key_opens(&enc, &right), full(&right),