Releases: OxideAV/oxideav-aacs
v0.1.3
Other
- feature-gate MockDrive + AKE self-checks behind test-util (out of default public API)
- SEND DISC STRUCTURE Format 0x84 Write Data Key (Common §4.14.5.1 Table 4-28)
- handshake-level integration tests for Format 0x85 Bus-Encryption Sector Extents
- drop release-plz.toml — use release-plz defaults across the workspace
- READ DISC STRUCTURE Format 0x85 Bus-Encryption Sector Extents (Common §4.14.3.6 Table 4-20)
- READ DISC STRUCTURE Format 0x84 Data Keys sub-payload (Common §4.14.3.5 Table 4-19)
- REPORT KEY Binding Nonce sub-payloads (Common §4.14.2.4 Table 4-10 / §4.14.2.5 Table 4-11)
- typed Type-and-Version accessors (Common §3.2.5.1.1 / Table 3-2)
- Content Revocation List parse + per-segment ECDSA verify + revocation-record lookup (PVB §2.7 / Tables 2-2..2-5)
- signed Content Certificate parse + verify (PVB §2.4/§2.5/§2.6 + BD-Prerecorded Table 2-1)
- AKE/EC runtime self-check entry points (Common §2.3 + §4.3)
- structured ParseReport + fuzz/robustness suite for KEYDB.cfg parser
- fix MKB Subset-Difference walk to match the spec
- bundle the AACS LA root public key as a spec constant
Changed — MockDrive + AKE self-checks moved behind test-util feature
The in-process synthetic-drive test fixture MockDrive and the two
self-checks that drive it — ake_full_self_check and all_self_checks
— are now gated behind a new test-util cargo feature and are no longer
part of the default public API. They remain reachable from the crate's
own tests (the crate enables test-util on itself via a self
dev-dependency). The three pure-math self-checks — curve_self_check,
aacs_la_pub_self_check, and ake_ecdh_self_check — stay public and
ungated, as do DriveCommand, ScsiResponse, DataDirection, and all
real command builders/parsers. External consumers that imported
MockDrive, ake_full_self_check, or all_self_checks should enable
features = ["test-util"] on the oxideav-aacs dependency.
Added — Round 269 SEND DISC STRUCTURE Format 0x84 Write Data Key
(Common §4.14.5 Tables 4-26 / 4-27 + §4.14.5.1 Table 4-28; MMC-6
§6.36.2.1 Table 572 / §6.36.3.2.11 Table 591)
The SEND DISC STRUCTURE (0xBF) command — the host→drive counterpart
of READ DISC STRUCTURE — enters the typed MMC surface with its Format
Code 0x84 (Write Data Key) sub-payload: the command a host issues to
replace the Write Data Key the §4.11 Bus Encryption layer uses to wrap
sectors it writes. Previous rounds covered the full READ-side Format
Code table (0x80..0x85); this round opens the SEND side at the
§4.14.5.1 entry. The remaining Table 4-27 entry — Format 0x85,
Bus-Encryption Sector Extents ingest with the §4.14.5.2 sorted /
non-overlapping / capacity / non-zero-count validation rules — is the
named next step.
SendDiscStructure— typed0xBFCDB builder per Table 4-26:
Media Type in the low nibble of byte 1, bytes 2..6 reserved, Format
Code at byte 7, Parameter List Length at bytes 8..9 (big-endian),
AGID in bits 7..6 of byte 10 (used only for Format0x84per MMC-6
§6.36.2.4), Control at byte 11.cdb()/parse_cdb()inverses
mirror the existingReadDiscStructure/SendKeypattern.SendDiscStructure::aacs_write_data_key(agid)— constructor for
the Format0x84send. Media Type BD, parameter list length 20
(= 4-byte header + 16-byte encrypted Write Data Key).build_send_disc_structure_write_data_key(kwd_encrypted)/
parse_send_disc_structure_write_data_key(buf)— the Table 4-28
parameter list[length:u16=0x0012][reserved:u16][Kwd:16]. The
two-byte Data Length field does not count itself, so its mandated
value is0x0012; the parser rejects any other length field and
truncated buffers. Bytes 4..19 carry the replacement Write Data Key,
encrypted by the Bus Key using AES-128E per §4.14.5.1 paragraph 3 —
the host wraps the plaintext with the existing
aes_128_ecb_encrypt(bus_key, kwd)primitive before building the
list.SEND_DISC_STRUCTURE_OPCODE=0xBFand
FORMAT_AACS_WRITE_DATA_KEY=0x84— named constants for the
new opcode and the SEND-side Format Code (numerically the same value
as the READ-sideFORMAT_AACS_DATA_KEYS, named separately because
Table 4-27 defines it as a distinct data-out payload).
MockDrive gains a SEND_DISC_STRUCTURE_OPCODE dispatcher arm and a
last_write_data_key_sent: Option<[u8; 16]> capture slot holding the
on-wire (still-wrapped) key field. In auth mode the mock unwraps the
incoming key with AES-128D under the established Bus Key and stores
the plaintext in write_data_key, returning the spec-mandated KEY NOT
ESTABLISHED error path (§4.14.5.1 final paragraph) when the auth
slot is armed but no Bus Key has been derived; in static-fixture mode
the wire bytes are adopted verbatim, mirroring the READ-side Format
0x84 behaviour. The §4.14.5.1 INSUFFICIENT PERMISSION branch (host
not authorized to send the Write Data Key) is not modelled — the mock
treats every caller as authorized. The Read Data Key is never touched,
matching the §4.11 "the drive sets Kwd equal to Krd on media insertion
until the host overwrites it" lifecycle.
Companion test suite tests/synth_round269_write_data_key_send.rs
(9 cases): static-mode end-to-end round-trip (wire bytes adopted
verbatim, Krd untouched, response data-in phase empty), CDB byte
layout pin against Table 4-26 (opcode/media-type/reserved-span/format/
length/AGID/control), parameter-list byte layout pin against Table
4-28, wrap-under-planted-Bus-Key (drive stores plaintext, capture slot
stores wrapped bytes), KEY-NOT-ESTABLISHED error path with state
unchanged, read-back coherence (SEND 0x84 then READ 0x84 recovers
the new Kwd and the unchanged Krd under the same Bus Key), a full §4.3
AKE handshake whose host-side Bus Key wraps a replacement Kwd that the
drive's independently-derived Bus Key unwraps to the same plaintext,
malformed-parameter-list rejection (wrong length field + truncated
buffer, state unchanged both times), and a builder/parser
encode→decode→unwrap round-trip. Eight additional unit tests live in
src/mmc.rs alongside the existing CDB-layout cases (including
unknown-Format and wrong-data-direction rejection by the dispatcher
arm).
Added — Round 246 READ DISC STRUCTURE Format 0x85 Bus-Encryption
Sector Extents (Common §4.14.3.6 Table 4-20 / MMC-6 §6.22.3.1.6 Table 389)
The READ DISC STRUCTURE Format Code 0x85 sub-payload — the
variable-length LBA-Extent table the logical unit publishes so the host
can discover which sector ranges are subject to §4.11 Bus Encryption —
is now exposed as a typed CDB constructor + response parser pair.
Previous rounds covered Format Codes 0x80 / 0x81 / 0x82 (IDs),
0x83 (MKB packs), and 0x84 (Data Keys); 0x85 closes the
sub-payload list at the no-authentication entry the spec permits
without the §4.3 AKE (§4.14.3.6 final sentence: "This command does not
require AACS authentication.").
ReadDiscStructure::aacs_bus_encryption_sector_extents()— CDB
constructor for Format0x85. Media Type BD, AGID reserved (no
AACS authentication required), Address + Layer reserved, allocation
length sized for the worst-case 256-extent response (12 + 256 * 16
= 4108 bytes; callers issuing the command against a known smaller
bound may shrinkallocation_lengthafter constructing the CDB).BusEncryptionSectorExtent { start_lba: u32, lba_count: u32 }—
one LBA range. Both fields are 32-bit big-endian on the wire (bytes
12+n16..15+n16 and 16+n16..19+n16 of Table 4-20).BusEncryptionSectorExtentsResponse { maximum: u16, extents: Vec<BusEncryptionSectorExtent> }— decoded variable-length wire
layout[length:u16 = N*16 + 2][reserved:u8][maximum:u8]followed
byN16-byte extent records[reserved:8 || Start LBA:4 || LBA Count:4]. Themaximumfield spans1..=256; the on-wire encoding
represents256as the byte value0per the §4.14.3.6 paragraph 3
sentinel ("The value 256 is denoted by a '0' in the field.").parse_bus_encryption_sector_extents_response(buf)— wire-layout
parser. Decodes the0→256sentinel back to its semantic value;
preserves the on-wire extent order verbatim (per §4.14.3.6 paragraph
3 the extents are sorted bystart_lbaascending and non-overlapping,
but the parser does not enforce the invariant — the SEND DISC
STRUCTURE Format0x85ingest path is where the logical unit
rejects malformed tables per §4.14.5.x). Rejects buffers whose
length field is below2, buffers shorter than2 + length, and
extent sections whose byte count is not a multiple of 16.FORMAT_AACS_BUS_ENCRYPTION_SECTOR_EXTENTS=0x85and
BUS_ENCRYPTION_SECTOR_EXTENT_LEN=16— named constants for
the new Format Code and the per-record wire stride.
MockDrive gains bus_encryption_sector_extents: Vec<BusEncryptionSectorExtent> + max_bus_encryption_sector_extents: u16 slots and a Format 0x85 dispatcher arm that serialises the
table verbatim. The default with_test_fixture constructor pre-loads
two non-overlapping extents in ascending Start LBA order (matching the
§4.14.3.6 paragraph 3 sort rule) so the round-trip test surfaces any
byte-order or stride drift; Default initialises an empty extent list
with a maximum of 1. The empty-table path emits a 4-byte response
(length = 2) per §4.14.3.6 paragraph 2 ("If no Bus-Encryption Sector
Extents are currently defined, the Data Length field shall be 2."); the
256-extent ceiling is encoded as the wire byte 0 per §4.14.3.6
paragraph 3. Because this Format Code does not require AACS
authentication, the dispatcher walks the branch without consulting
MockDrive::auth.
Companion test suite tests/synth_round246_bus_encryption_sector_extents.rs
(12 cases) exercises end-to-end round-trip through MockDrive, pins
the CDB byte layout (Format 0x85, allocation length 0x100C, AGID
field zeroed, Address + Layer Nu...
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(k...
v0.1.1
Other
- Phase C: Drive-Host AKE + ECDSA-secp160r1 + Bus Key (AACS Common 0.953 §4.3)
- Phase B — SCSI MMC drive command layer (REPORT_KEY / SEND_KEY / READ_DISC_STRUCTURE)
- parse |-leader DK / PK / HC / DC / DISCID-scoped records (Phase A)
- disc_id is SHA-1(Unit_Key_RO.inf), not SHA-1(Volume_ID)
- disc_id_for_volume_id — SHA-1(volume_id) for KEYDB.cfg lookup
- fmt + tests: align integration tests with permissive parse
- tolerate sector-padding zeros after the End-of-MKB record
Added — Phase C: Drive-Host Authentication & Key Exchange (AKE)
New ec, ecdsa, and ake modules implementing the AACS Common
Final 0.953 §4.3 "AACS Drive Authentication Algorithm" (Figure 4-9)
end-to-end on top of the Phase B MMC layer. All cryptography is
clean-room from the spec's published math (Table 2-1 curve parameters,
§2.3 ECDSA, §2.1.5 SHA-1, §2.1.6 CMAC); no external crypto-library
source (RustCrypto / OpenSSL / libaacs / …) was consulted. The
openssl CLI was used only as an opaque test-vector oracle, and the
ECDSA path is additionally cross-checked bit-exact against an
independent Python big-int reference.
ecmodule — a 160-bit big-integer (U160, fiveu32limbs)
and short-Weierstrass curve overGF(p)for the AACS curve
(Table 2-1:a = -3, primep, base pointG, ordern). Affine
add/double/is_on_curveplus a Jacobian-coordinate
mul_scalar(single final inversion). 40-byte EC-point encoding
x(20) || y(20).ecdsamodule —AACS_Sign/AACS_Verify(X9.62 / FIPS
186-2) with a clean-room FIPS 180-2 SHA-1 digest; 40-byter || s
signatures. A deterministickhelper (AES-H based, not RFC 6979)
makes the synthetic test handshake reproducible. SHA-1 validated
against the FIPS 180-2abc/ empty / two-block vectors; the full
sign/verify against an independent Python reference vector.aes::aes_128_cmac— AES-128-CMAC (NIST SP 800-38B), validated
against the SP 800-38B Appendix D.1 example MACs (empty / 1-block /
partial / full-message).akemodule:Certificate::parse+verify_signaturefor the 92-byte Drive
(Table 4-1) / Host (Table 4-2) certificates
(Type Flags Length ID Reserved PubKey(40) Sig(40)), with the
Cert_sig = bytes 52..91signed overCert = bytes 0..51.build_signed_certificateto mint synthetic LA-signed certs.host_authenticate— the §4.3 Host state machine driving any
DriveCommandtransport through AGID → Host Cert Challenge →
Drive Cert Challenge → Drive Key → Host Key → Bus Key.DriveAuthState— the §4.3 drive side (verify Host Cert + Hsig,
signDsigoverHn || Dv, deriveDk·Hv), wired into the
Phase BMockDrivevia its newauthfield.bus_key_from_point—BK = [x-coordinate of shared point] lsb_128(§4.3 steps 28/29).read_verified_volume_id— §4.4 Volume ID transfer with
Dm = CMAC(BK, Volume_ID)host-side verification.
- New
AacsErrorvariants:
DriveCertSignatureInvalid,HostCertSignatureInvalid,
DriveSignatureInvalid,HostSignatureInvalid,VolumeIdMacInvalid. - New integration suite
tests/synth_phasec_ake.rs(5 tests): full
synthetic-cert AKE round-trip with matching Host/Drive Bus Keys,
§4.4 Volume ID verification, and rejection of a wrong-LA Drive cert,
a wrong-LA Host cert, and a tampered Volume ID MAC.
Note on the Bus Key KDF. The task brief mentioned a possible
"AES-G / SHA-1 KDF" for the Bus Key; AACS Common Final 0.953 §4.3
steps 28/29 in fact define the Bus Key directly as the least
significant 128 bits of the x-coordinate of the shared ECDH point
(Dk·Hv / Hk·Dv) — no AES-G/SHA-1 post-derivation. This module
implements per the spec; the §4.4+ ID transfers then key AES-CMAC
under that Bus Key.
Added — Phase B: SCSI MMC drive-command wire layer
New mmc module implementing the byte-level encoding/parsing for the
three SCSI MMC commands an AACS host needs to converse with a Licensed
Drive. Wire formats are taken from the publicly-hosted T10 working
drafts now staged at docs/container/aacs/mmc/ (MMC-6 r02g, SPC-3 r23)
cross-referenced against AACS Common Final 0.953 §4.1–§4.14. No
external library source (libaacs / libbluray / etc.) was consulted.
- Typed CDB constructors, each emitting a 12-byte
[u8; 12]block
per MMC-6 Tables 381 / 513 / 599:ReportKey::{aacs_agid, aacs_drive_cert_challenge, aacs_drive_key, aacs_drive_cert, aacs_invalidate_agid}.SendKey::{aacs_host_cert_challenge, aacs_host_key, aacs_invalidate_agid}.ReadDiscStructure::{aacs_volume_id, aacs_media_serial, aacs_media_key_block_pack}.parse_cdb()inverse for each, used by the synthetic mock drive
and by tests.
- AACS sub-payload codecs for the AKE round-trip:
parse_report_key_agid/parse_report_key_drive_cert_chal/
parse_report_key_drive_key/parse_report_key_drive_cert—
drive-to-host responses per AACS Common Tables 4-7, 4-8, 4-9 and
MMC-6 Table 531.parse_volume_id_response— 36-byte READ DISC STRUCTURE
Format 0x80 reply per AACS Common Table 4-15
([u16=0x0022][rsvd:u16][Volume ID:16][MAC:16]).build_send_key_host_cert_chal/build_send_key_host_keyand
theirparse_*inverses — host-to-drive parameter lists per
AACS Common Tables 4-24, 4-25 (Hn || Cert_handHv || Hsig
respectively).
DriveCommandtrait abstracting the SCSI pass-through surface
so platform-specific back-ends (macOSIOSCSITaskDeviceInterface,
LinuxSG_IO, WindowsIOCTL_SCSI_PASS_THROUGH_DIRECT) can be
written against a single contract once Phase C lands. Carries a
DataDirectionenum +ScsiResponse { status, data }.MockDrivein-process fixture implementingDriveCommand,
populated with a deterministic synthetic Drive Certificate /
Volume ID / nonces so tests can assert exact byte layouts.
Public re-exports added to lib.rs (mmc module + the typed
structures and parsers).
Documentation gaps surfaced
- The workspace
docs/container/aacs/mmc/README.md"AACS commands at
a glance" section confuses two different command surfaces: it lists
AACS Volume ID under "REPORT KEY Key Class=0x02 Key Format 0x12",
but per MMC-6 Table 525 the REPORT KEY Key Class 0x02 Key Format
table only defines 0x00 / 0x01 / 0x02 / 0x20 / 0x21 / 0x38 / 0x3F.
The Volume Identifier in fact ships viaREAD_DISC_STRUCTURE
Format Code 0x80 (MMC-6 Table 384 / AACS Common Table 4-15). This
Phase B implementation follows the spec tables; the README would
benefit from a follow-up edit clarifying which list belongs to
which command.
Tests
- 5 new
mmcunit tests pinning the CDB byte layouts (opcode, Key
Class, Allocation/Parameter-List Length packing, AGID bit packing). - 13 new
tests/synth_phaseb_mmc.rsintegration tests round-tripping
the AGID / Drive Cert Challenge / Drive Key / Drive Cert /
Host Cert Challenge / Host Key / Volume ID / Invalidate-AGID flows
throughMockDrive.
Out of scope (deferred to Phase C/D)
- ECDSA-secp160r1 sign/verify primitives needed for the cryptographic
half of the AKE (Common spec §4.3 steps 9, 16, 18, 25, 27). AES_G/ SHA-1-based Bus Key derivation fromHk*Dv/Dk*Hv.- Actual hardware transport: macOS IOKit, Linux SG_IO, Windows IOCTL
back-ends implementingDriveCommandagainst a real/dev/sr0/
IOSCSITaskDeviceInterface/IOCTL_SCSI_PASS_THROUGH_DIRECT. - Phase D: wiring the AKE + Bus-Key-protected reads into
oxideav-blurayfor unencrypted-at-rest disc playback.
Added — Phase A: KEYDB.cfg |-leader header records
KeyDb::parse now recognises the |-leader record form documented in
docs/container/aacs/keydb-cfg-format.md, in addition to the
pre-existing per-disc <DISC_ID>=V <VUK> lines. New record types:
| DK |Device Key (DEVICE_KEY+DEVICE_NODE+KEY_UV+
KEY_U_MASK_SHIFT) — pins a key into the AACS Subset-Difference
tree. Surfaced viaKeyDb::device_keys().| PK |Processing Key (16-byte AES-128) — the SD-tree walk output
for a specific MKB. Surfaced viaKeyDb::processing_keys().| HC |Host Certificate + private key — 20-byte ECDSA-secp160r1
scalar + variable-length signed cert. Parser validates the embedded
lengthfield (cert offset 2..4, big-endian) against the actual
buffer length per AACS Common Final 0.953 §A.1; exposeshost_id(),
cert_type(),declared_length(). Surfaced via
KeyDb::host_certs().| DC |Drive Certificate + private key (drive side of the
Drive-Host auth). Surfaced viaKeyDb::drive_certs().| DISCID |introduces a per-disc record-set scope; subsequent
| VID |,| VUK |,| MEK |,| TK |,| KCD |rows attach to
it. Surfaced asDiscRecordsviaKeyDb::disc_records()/
KeyDb::disc_record(&id).
KeyDb::vuk_for_disc now also looks through the DISCID-scoped
record map, so both legacy and |-leader files yield the same lookup
behaviour.
New AacsError::HeaderParseError(String) variant for malformed
|-leader lines.
Legacy per-disc lines continue to parse byte-identically (a dedicated
legacy_only_file_unchanged unit test pins this).
v0.1.0
Other
- parse the extended libbluray/aacskeys KEYDB.cfg format
- probe ~/Library/Preferences/aacs/KEYDB.cfg on macOS
Added
- macOS-native search path:
KeyDb::load_default()now also probes
$HOME/Library/Preferences/aacs/KEYDB.cfgahead of the XDG fallbacks
ontarget_os = "macos". Matches the convention libbluray + similar
tools use on Apple platforms — users no longer need to set
XDG_CONFIG_HOME(or fall back to~/.config/aacs/) just to be
found.
Added — Round 1 (bootstrap, clean-room AACS Common + BD-Prerecorded 0.953)
Initial pure-Rust AACS decryption library. All spec references are to
the publicly-published AACS LA PDFs in the workspace's
docs/container/aacs/ directory (AACS_Spec_Common_Final_0953.pdf
and AACS_Spec_BD_Prerecorded_Final_0_953.pdf). This is a clean-room
implementation; no libaacs / aacskeys / libbluray / makemkv
source was consulted.
- Crypto primitives (Common spec §2.1):
aes::aes_128_ecb_encrypt/aes_128_ecb_decryptthin wrappers
around the RustCryptoaescrate'sBlockEncrypt/BlockDecrypt
so the rest of the crate doesn't have to import the trait every
time.aes::aes_128_cbc_decrypt/aes_128_cbc_encryptwith explicit
16-byte IV (the AACS default IV constantIV0 = 0BA0F8DDFEA61FB3D8DF9F566A050F78is exposed asaes::IV0_AACS).aes::aes_g(x1, x2) = AES-128D(x1, x2) XOR x2per Common spec
§2.1.3 — the AACS-specific one-way function used to derive child
keys and to mix Volume ID into the VUK.aes::aes_h(data)per Common spec §2.1.4 — AES-G-based hash with
SHA-1-style padding and the AACSh0IV constant
2DC2DF39420321D0CEF1FE2374029D95. Implemented inline (no
external SHA dep).
- Subset-Difference tree (Common spec §3.2.1 — §3.2.4):
subdiff::aes_g3triple-AES generator per Common spec §3.2.2,
Figure 3-3, with the seed register IV constant
s0 = 7B103C5DCB08C4E51A27B01799053BD9. Returns the three
128-bit outputs (left subsidiary key, processing key, right
subsidiary key).subdiff::SubsetDifference { u_mask, uv }and
subdiff::applies_to_device(sd, d_node)per Common spec
§3.2.4 — the(D_node & m_u) == (uv & m_u) && (D_node & m_v) != (uv & m_v)test that picks the subset-difference covering a
given device.subdiff::derive_processing_key(device_key, stored_uv, stored_u_mask, stored_v_mask, target_uv, target_v_mask)per Common spec
§3.2.4 procedure (steps 1 — 4): walk down the tree from the
stored Device Key's u-node toward the target v-node by repeated
AES-G3 left/right child derivation, ending at the appropriate
Device Key for the target subset-difference; return the
Processing Key.subdiff::media_key_from_processing_key(processing_key, target_uv, encrypted_media_key_data)per Common spec §3.2.4 end:
Km = AES-128D(Kp, C) XOR (0 || uv).
- MKB parser (Common spec §3.2.5):
mkb::Mkb::parse(bytes)walks the contiguous record stream and
decodes:0x10Type and Version Record (§3.2.5.1.1) — MKBType + Version
Number.0x21Host Revocation List Record (§3.2.5.1.2) — multi
signature-block layout withRange || HostID8-byte entries.0x20Drive Revocation List Record (§3.2.5.1.3) — identical
layout, different IDs.0x81Verify Media Key Record (§3.2.5.1.4) — 16-byte ciphertext
Vd, used to confirm a derived Media Key.0x04Explicit Subset-Difference Record (§3.2.5.1.5) — 5-byte
(u_mask, uv)entries.0x07Subset-Difference Index Record (§3.2.5.1.6) — speed-up
lookup; 4-byte span + 3-byte offsets.0x05Media Key Data Record (§3.2.5.1.7) — 16-byte entries,
one per explicit subset-difference, in matching order.0x0CMedia Key Variant Data Record (§3.2.5.2.1) — Class II
MKB only.0x0DVariant Number Record (§3.2.5.2.2) — Class II MKB only.0x02End of Media Key Block Record (§3.2.5.1.8) — closes the
block.
- Unknown record types are ignored per spec §3.2.5 ("if a device
encounters a Record with a Record Type field value it does not
recognize, that is not an error; it shall ignore that Record and
skip to the next"). Mkb::verify_media_key(km)cross-checks a derived Media Key
against theVerify Media Key Record.
- Unit Key file parser (BD-Prerecorded spec §3.9.3):
unit_key::UnitKeyFile::parse(bytes)decodes the 32-bit
Unit_Key_Block_start_address, theUnit_Key_File_Header()
(Application_Type, Num_of_BD_Directory, Use_SKB_Unified_MKB_Flag,
per-directory CPS_Unit_number assignments for First Playback /
Top Menu / Titles), and theUnit_Key_Block()(Num_of_CPS_Unit- per-unit
MAC_of_PMSN || MAC_of_DeviceBindingNonce || EncryptedCpsUnitKey).
- per-unit
- Tolerates the 65536-byte alignment and zero-padding requirement
per spec note (*1) / (*2).
- AACS directory walker (BD-Prerecorded spec §3 + Figure 3-5):
volume::AacsVolume::open(disc_root)looks forAACS/MKB_RO.inf
andAACS/Unit_Key_RO.infunder the supplied disc-mount root,
falling back toAACS/DUPLICATE/if the primary copies are
missing.volume::AacsVolume::cps_units()returns the per-CPS-Unit
metadata (encrypted title-key blob), pre-VUK.volume::AacsVolume::unwrap_title_keys(vuk)walks every CPS unit
and decryptsEncryptedCpsUnitKey = AES-128E(Kvu, Kcu)to recover
Kcuper BD-Prerecorded §3.9.3.
- VUK derivation (BD-Prerecorded spec §3.3):
vuk::derive_vuk(media_key, volume_id) = AES-G(Km, IDv)—
AES-128D(Km, IDv) XOR IDv.
- Content scrambling (BD-Prerecorded spec §3.10):
content::decrypt_aligned_unit(cps_unit_key, unit_bytes)decrypts
a 6144-byte Aligned Unit. Computes
BlockKey = AES-128E(Kcu, seed) XOR seedfrom the first 16 bytes
(the cleartext "seed") per Figure 3-8, then AES-128-CBC-decrypts
the remaining 6128 bytes underBlockKeywith the AACS default
IV (IV0).content::encrypt_aligned_unit(cps_unit_key, unit_bytes)
round-trip companion (used by the test suite to construct
fixtures from known plaintext).
- KEYDB.cfg parser: the community-format VUK key database used by
libbluray / similar OSS tools. Format implemented from the de-facto
public description (the line layoutDISC_ID = V VUK | labelplus
;-comments and blank lines); no source from those projects was
consulted.keydb::KeyDb::parse(text)accepts a string.keydb::KeyDb::load_from(path)reads from a file.keydb::KeyDb::load_default()walks the XDG search order:
OXIDEAV_AACS_KEYDBenv override first, then
$XDG_CONFIG_HOME/aacs/KEYDB.cfg, then each entry in
$XDG_CONFIG_DIRS(:-split), then~/.config/aacs/KEYDB.cfg
as the conventional fallback.keydb::KeyDb::vuk_for_disc(&[u8; 20])looks up a VUK by Disc
ID; case-insensitive on the hex.
- Volume integration:
volume::AacsVolume::resolve_vuk_from_keydb(&KeyDb)— convenience
for the KEYDB.cfg-based flow that doesn't need an active MKB walk.volume::AacsVolume::derive_vuk_from_device_key(&DeviceKey)—
full MKB walk using a Device Key from a manually-loaded key set.
Test fixtures (synthetic only, no real disc keys)
tests/synth_round1_keydb.rs— KEYDB.cfg parser positive +
negative cases (comments, blank lines, lowercase hex, malformed
lines).tests/synth_round1_mkb.rs— hand-crafted Type-3 MKB built record
by record per spec §3.2.5; verifies type tag, version, every record
is round-tripped through the parser,verify_media_key()accepts
the correct Km and rejects a flipped bit.tests/synth_round1_subdiff.rs— minimal Subset-Difference tree
walk: synthetic Device Key + uv path, single AES-G3 step, asserts
the derived Processing Key matches the spec equation.tests/synth_round1_content.rs— round-trip of
encrypt_aligned_unit->decrypt_aligned_unitwith a randomly
generated CPS Unit Key + random seed + random plaintext payload;
also asserts that a single-bit flip in the ciphertext changes the
decryption.tests/synth_round1_unit_key.rs— hand-crafted Unit_Key_RO.inf
built per spec §3.9.3 with 2 CPS units; verifies header decode and
thatunwrap_title_keys(vuk)recovers the matching Kcu values.tests/synth_round1_volume.rs— syntheticAACS/directory layout
(MKB_RO.inf + Unit_Key_RO.inf) under atempdir(); verifies
AacsVolume::openfinds both, that VUK from KEYDB.cfg unwraps the
title keys, and thatdecrypt_uniton a freshly-encrypted aligned
unit recovers the plaintext.
Documentation gaps surfaced
- The Common spec doesn't include a worked numerical example for any
of: AES-G3 with the publisheds0, Subset-Difference tree walk,
Verify Media Key Record cross-check, or AES-G as
AES-128D(x1, x2) XOR x2. Tests roundtrip our own
generate/parse/derive paths but cannot cross-check against a
third-party reference vector. A docs-collaborator-supplied test
vector for AES-G3 (e.g., a known Device Key and the resulting
Processing Key it derives) would close this gap. - KEYDB.cfg is a de-facto community format; AACS LA does not specify
it. The exact whitespace tolerance / comment grammar implemented
here is what the parser accepts, and may diverge from what
libbluray would accept in obscure edge cases.
Out of scope
- Bus-encryption (BD-Prerecorded §3.7) — applies only to the SCSI
bus between a Licensed Drive and PC Host; irrelevant when reading
decrypted-at-rest disc images. - Drive / Host authentication (Common spec ch. 4) — same reason.
- ECDSA signature verification of the MKB / HRL / DRL
(AACS_Verify(AACS_LA_pub, ...)) — the spec defines these but we
don't need them to derive Km. Could be added later if validation
becomes important. - Content Hash Table verification (BD-Prerecorded §2.3) — SHA-1...
v0.0.1
chore: Release package oxideav-aacs version 0.0.1