v0.1.2
Other
- Content Hash Table parse + Hash-Unit integrity verify (BD-Prerecorded §2.3)
- AACS_Verify integration for §3.2.5.1.2/.3/.8 signature records
- paraphrase external-implementation citations in src/
- READ_DISC_STRUCTURE Format 0x81/0x82/0x83 sub-payload constructors + parsers (AACS Common §4.14.3.2 / §4.14.3.3 / §4.14.3.4)
- Phase D: Type-4 MKB + Key Conversion Data post-processing (AACS Common 0.953 §3.2.5.1.4 + BD-Prerecorded §3.8)
Added — Content Hash Table parsing + Hash-Unit integrity verification (BD-Prerecorded §2.3)
New cht module implementing the Content Hash Table (CHT) integrity
layer per AACS BD-Prerecorded Final 0.953 §2.3 — the per-Hash-Unit
SHA-1 check a Licensed Player runs over \BDMV\STREAM Clip AV data:
cht::ContentHashTable::parse(bytes, number_of_digests, number_of_hash_units) -> Result<ContentHashTable>— parses a
ContentHash00N.tblper Table 2-2: a header ofNumber_of_Digests
12-byteClipDescriptorrecords (Starting_HU_Num/Clip_Num/
HU_Offset_in_Clip, all 32-bituimsbf) followed by
Number_of_HashUnits8-byte (bslbf) Hash Values. The two counts
are NOT stored in the table file — they come from the per-layer
Content Certificate (Table 2-1), so the parser takes them as
arguments. Trailing00-padding is tolerated (authoring/mastering
rule).cht::ContentHashTable::verify_hash_unit(index, hash_unit_bytes) -> Result<()>— recomputesHash_Value = [SHA-1(Hash_Unit)]_lsb_64(§2.3.2.1, least-significant 8 bytes of
the 20-byte SHA-1 digest) and compares it to the stored Hash Value.
Per §2.3.2.1 the encrypted on-disc bytes are hashed, so this
verifies integrity without a Title Key.cht::hash_value_of_unit(hash_unit) -> [u8; 8]— the standalone
[SHA-1(·)]_lsb_64primitive (e.g. for a CHT author).cht::ContentHashTable::{len, is_empty}— a zero-byte CHT is
valid for a layer with no Clip AV file ≥ 96 Logical Sectors.- Size constants:
HASH_UNIT_SIZE(96 × 2048 = 196608 bytes =
exactly 32 Aligned Units, and 7 of them = the spec's 1344 KB
minimum),LOGICAL_SECTOR_SIZE(2048),LOGICAL_SECTORS_PER_HASH_UNIT
(96),HASH_VALUE_SIZE(8).
New error variants:
AacsError::BadHashUnitLength(usize)— supplied Hash Unit was not
exactly 196608 bytes.AacsError::ContentHashMismatch { index }— recomputed
[SHA-1(Hash_Unit)]_lsb_64did not match the stored Hash Value.
Tests: 9 cht unit tests (size arithmetic, lsb-64 extraction,
header/body roundtrip, tamper / wrong-length / out-of-range / short-
buffer rejection, trailing-padding tolerance, zero-byte table) + 4
tests/synth_cht.rs integration tests (incl. one that builds a Hash
Unit from 32 freshly-encrypted Aligned Units and verifies the
encrypted bytes with no Title Key, then detects a single-byte flip).
All synthetic — no disc-derived material.
No docs gap: §2.3 (Table 2-2 syntax, §2.3.1 Hash-Unit geometry,
§2.3.2.1 [SHA-1(Hash_Unit)]_lsb_64) and the §2 "Logical Sector =
2048 bytes" definition in AACS_Spec_BD_Prerecorded_Final_0_953.pdf
are unambiguous. Standalone (--no-default-features) build still
passes.
Added — AACS LA signature verification on MKB records (§3.2.5.1.2 / §3.2.5.1.3 / §3.2.5.1.8)
Wires the existing ecdsa::verify (AACS_Verify) primitive into the
MKB parser so callers that hold the AACS LA public key can check the
three MKB signatures the spec defines:
Mkb::verify_end_of_block_signature(&self, original_bytes, aacs_la_pub) -> Result<()>— verifies the End-of-Media-Key-Block
Record signature per Common spec §3.2.5.1.8
(AACS_Verify(AACS_LApub, Signature Data, MKB), whereMKBis the
byte range up to but not including the End-of-MKB record).Mkb::verify_host_revocation_list(&self, original_bytes, aacs_la_pub) -> Result<()>+ the parallel
verify_drive_revocation_list— verify the per-signature-block
ECDSA signatures inside the HRL / DRL records (Common spec
§3.2.5.1.2 / §3.2.5.1.3). Each block's signed-data range is the
cumulative prefix "Type-and-Version Record || HRL/DRL record bytes
up to the byte immediately preceding this signature", so block N's
signature transitively also covers blocks 1..=N−1.Mkb::end_of_block_signature: Option<[u8; 40]>+ the new
RevocationSignatureBlock { entries_in_block, entries, signature }struct surfaced onMkb::host_revocation_blocks/
drive_revocation_blocks— both expose the raw 40-byte ECDSA
signature(s) for callers that want to feed them to an external
verifier.Mkb::type_and_version_raw: Vec<u8>— the on-wire bytes of the
mandatory Type-and-Version Record (header included), preserved by
the parser because §3.2.5.1.2 requires it as the first portion of
the HRL / DRL signed data.
The parser still tolerates revocation blocks whose trailing signature
field is truncated (per §3.2.5.1.2 final paragraph — "hosts are
required to store only the data being signed for the first signature
block, but not required to store the signature itself"); the verifier
surfaces AacsError::MkbSignatureMissing rather than panicking on a
None-signature block.
This crate ships no AACS LA public key. AACS LA distributes
AACS_LApub to licensees only, so the verifier takes a &Point
parameter; callers obtain the key out-of-band. A non-licensed
deployment can still call the verifiers against a self-issued LA
identity (e.g. for end-to-end MKB-authoring tests).
New error variants:
AacsError::MkbSignatureMissing— the requested signature record
was absent or its payload was not the expected 40 bytes.AacsError::MkbSignatureInvalid— the signature was present but
AACS_Verifyrejected it under the supplied public key.
Tests:
- 8 new
mkbunit tests covering: End-of-MKB sign-then-verify
roundtrip, wrong-key rejection, prefix-tamper rejection, no-record
→ MkbSignatureMissing, non-40-byte payload → MkbSignatureMissing,
single-block HRL verify roundtrip, DRL wrong-key rejection,
no-signature-block-stored truncation tolerance, and a two-block
cumulative-prefix HRL chain. - 2 new integration tests in
tests/synth_round1_mkb_sig.rsthat
build a full Type-and-Version + HRL + Verify-Media-Key +
End-of-MKB byte stream through the public API and confirm both
verifiers accept the legitimate signatures + reject a tampered
buffer.
No docs gap; the spec text for all three signed-data ranges
(docs/container/aacs/AACS_Spec_Common_Final_0953.pdf §3.2.5.1.2
final paragraph, §3.2.5.1.3 second paragraph, §3.2.5.1.8 second
paragraph) is unambiguous. Standalone (--no-default-features)
build still passes.
Added — READ_DISC_STRUCTURE Format 0x81/0x82/0x83 sub-payloads
Closes the §4.14.3.2 (Pre-recorded Media Serial Number / PMSN),
§4.14.3.3 (Media Identifier), and §4.14.3.4 (Media Key Block Pack)
sub-payload gap that Phase B left as Format-Code constants without
matching CDB constructors, response decoders, or MockDrive service.
ReadDiscStructure::aacs_media_id(agid)— CDB builder for
Format Code0x82(Media Identifier) per AACS Common §4.14.3.3
Table 4-17. Same on-wire layout as the Volume Identifier and PMSN
reads: 36-byte response with[length=0x0022:u16][reserved:u16] [Media ID:16][MAC:16].parse_media_serial_response+MediaSerialNumberResponse
— Format0x81decoder per Table 4-16. The MAC byte field is
Dm = CMAC(BK, PMSN)per §4.5 step 3.parse_media_id_response+MediaIdentifierResponse—
Format0x82decoder per Table 4-17 / Table 4-15 layout. The MAC
isDm = CMAC(BK, MediaID)per §4.6 step 3.parse_mkb_pack_response+MkbPackResponse— Format
0x83decoder per Table 4-18, with variable-length pack body up
to 32,768 bytes. Returns thetotal_packscount alongside the
pack data. (Per §4.14.3.4 the MKB itself is not bus-encrypted —
the spec note is explicit about this.)MockDrivegrows four new fields —media_serial_number,
media_serial_mac,media_identifier,media_id_mac— populated
with deterministic patterns bywith_test_fixture(). The
READ_DISC_STRUCTURE_OPCODEdispatch arm now serves Format
0x81and0x82with the same auth-vs-static MAC selection the
Volume ID path already uses: whenauth.bus_keyisSome, the
mock recomputesDm = CMAC(BK, ID)per §4.5 / §4.6; otherwise it
returns the fixture-MAC bytes.
New tests:
- 7 new unit tests in
mmc.rs: PMSN/Media-ID CDB-byte-layout
invariants, response-parser round trips, length-field /
truncated-payload rejection, and the MKB-pack length-counting
semantics. - 4 new integration tests in
tests/synth_phaseb_mmc.rs: PMSN +
Media-IDMockDriveend-to-end round trips, an unknown-format
rejection, and an MKB-pack hand-built-wire round trip (since the
MKB body is not bus-encrypted, hand-stuffed wire bytes are the
right level for the Phase B sub-payload layer).
No new dependencies, no oxideav-core API change, no docs-gap (all
three sub-payloads are in docs/container/aacs/
AACS_Spec_Common_Final_0953.pdf Tables 4-16 / 4-17 / 4-18). The
crate's standalone (no-default features) build still passes.
Added — Phase D: Type-4 MKB + Key Conversion Data post-processing
Wires the AACS Common spec §3.2.5.1.4 + BD-Prerecorded §3.8 "Type-4
MKB / Media Key Precursor" path into the high-level AacsVolume
pipeline. Type-4 MKBs emit a Media Key Precursor K_mp from the
Subset-Difference walk rather than the Media Key directly; devices
that are required to use Key Conversion Data combine K_mp with the
disc's 16-byte KCD payload via K_m = AES-G(K_mp, KCD) before VUK
derivation. The KCD itself is sourced out-of-band (from the BD-ROM
KCD-Mark, surfaced in oxideav-aacs via the | KCD | row of a
KEYDB.cfg file → DiscRecords::kcd).
subdiff::apply_key_conversion_data(kmp, kcd)— the
K_m = AES-G(K_mp, KCD)primitive. Equivalent to
aes::aes_g(kmp, kcd); named separately for readability at the
call site. Re-exported from the crate root.AacsVolume::derive_vuk_from_device_key_with_kcd(device, vol_id, kcd)— Type-4-aware VUK derivation implementing the spec's
"verify-then-apply-KCD" decision tree:- SD-walk to obtain the raw precursor (or Media Key for Type-3).
- If that value already verifies under the Verify Media Key Record
(the spec's "old MKB" rule, §3.2.5.1.4 final paragraph), adopt
it asK_mand skip KCD application — even if a KCD was
supplied. - Otherwise apply
AES-G(K_mp, KCD)and re-verify; failure
surfacesMediaKeyVerificationFailed.
AacsVolume::derive_media_key_from_device_key(device)—
exposed cryptographic primitive returning the raw SD-walk output
(precursor for Type-4, Media Key for Type-3). Lets callers make
the verify/KCD decision themselves rather than going through the
higher-level_with_kcdhelper.Mkb::is_verified_media_key(km) -> bool— boolean variant of
the existingverify_media_key, returningfalse(rather than
MissingVerifyMediaKeyRecord) when the0x81record is absent.
The Type-4 decision path needs this distinction.MkbType::requires_kcd()+MkbType::as_u32()— predicate- wire-format inverse of the parser's
from_u32.
- wire-format inverse of the parser's
derive_vuk_from_device_key is refactored on top of
derive_media_key_from_device_key + the existing
Mkb::verify_media_key so the Type-3 path is byte-identical to the
prior implementation.
New tests/synth_phased_kcd.rs (7 tests) pins:
- Type-4 + valid KCD round-trip → matching VUK.
- Type-4 + wrong KCD →
MediaKeyVerificationFailed. - Type-4 "old MKB" precursor-verifies-directly fallback (KCD ignored
even when supplied). - Type-4 without KCD when KCD was needed → error.
- Type-3 with a stray KCD argument → KCD ignored.
KCDrecord loads from a syntheticKEYDB.cfg.- Synthetic SD configuration sanity check.
Plus 5 new mkb unit tests covering MkbType::requires_kcd,
MkbType::as_u32 round-trip, and is_verified_media_key edges.
No new external dependencies, no docs-gap (Common spec §3.2.5.1.1 +
§3.2.5.1.4 + BD-Prerecorded §3.8 are all in docs/container/aacs/),
no oxideav-core API change. The crate's standalone (no-default
features) build still passes.