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 Number reserved), pins the response wire
layout (length field encoding N*16 + 2, per-record stride 16, Start
LBA at offset 8 of each record, LBA Count at offset 12), pins the
256 ↔ wire-byte 0 sentinel for the Maximum field both directions,
exercises the empty-table path explicitly, walks a three-extent
hand-stuffed wire payload byte-by-byte to catch any silent u32-field
swap, and rejects malformed inputs (length field below 2, truncated
buffer, misaligned stride). Nine additional unit tests live in
src/mmc.rs alongside the existing CDB-layout cases.
Added — Round 243 READ DISC STRUCTURE Format 0x84 Data Keys
(Common §4.14.3.5 Table 4-19)
The READ DISC STRUCTURE Format Code 0x84 sub-payload — the encrypted
Read/Write Data Key pair the Bus Encryption layer (§4.11) uses to wrap
sector payloads — is now exposed as a typed CDB constructor + response
parser pair. Previous rounds covered Format Codes 0x80 / 0x81 / 0x82
(IDs) and 0x83 (MKB packs); 0x84 brings the sub-payload list level
with the §4.11 protocol the surrounding aes + ake modules already
implement.
ReadDiscStructure::aacs_data_keys(agid)— CDB constructor for
Format0x84. Media Type BD, allocation length 36 (= 4-byte header- 16-byte Read Data Key + 16-byte Write Data Key), AGID in bits
7..6 of byte 10; bytes 2..6 reserved per the §4.14.3.5 table.
- 16-byte Read Data Key + 16-byte Write Data Key), AGID in bits
DataKeysResponse { read_data_key_encrypted: [u8; 16], write_data_key_encrypted: [u8; 16] }— decoded 36-byte wire
layout[length:u16=0x0022][reserved:u16][Krd:16][Kwd:16]per
Table 4-19. Both Data Keys are on the wire wrapped under the Bus
Key with AES-128E per §4.11.DataKeysResponse::decrypt_read_data_key(bus_key)/
decrypt_write_data_key(bus_key)— host-side unwrap helpers
(AES-128D under the same Bus Key) recovering plaintextKrd/
Kwd.parse_data_keys_response(buf)— wire-layout parser; rejects
buffers whose length field is not0x0022and truncated payloads.FORMAT_AACS_DATA_KEYS=0x84andDATA_KEY_LEN=16—
named constants for the new Format Code and key-field width.
MockDrive gains read_data_key: [u8; 16] / write_data_key: [u8; 16] plaintext slots and a last_data_keys_read: bool capture flag so
tests can assert the §4.14.3.5 branch ran. In auth mode the mock
wraps each Data Key with aes_128_ecb_encrypt(bus_key, key) per §4.11
("the Bus Key is used to protect the Data Keys using AES-128E") before
serialising the response; in static-fixture mode the plaintext bytes
go out verbatim, mirroring the existing Volume-ID / PMSN / Media-ID
behaviour. When the auth slot is set but the Bus Key has not yet
been derived, the dispatcher returns the spec-mandated
KEY-NOT-ESTABLISHED error path (§4.14.3.5 final paragraph), surfaced
as AacsError::InvalidValue { what: "READ_DISC_STRUCTURE Format 0x84 without Bus Key", … }.
Companion test suite tests/synth_round243_data_keys.rs (9 cases)
covers end-to-end round-trip through MockDrive in both static and
synthetic-Bus-Key modes, pins the CDB byte layout (Format byte 0x84,
allocation length 0x0024, AGID packing, zero Reserved/Address), pins
the response wire layout (length field 0x0022, 36-byte total, Krd
at bytes 4..19 and Kwd at bytes 20..35), pins the AES-128E ↔
AES-128D wrap/unwrap round-trip property, and rejects malformed
inputs (wrong length field, truncated payload). Six additional unit
tests live in src/mmc.rs alongside the existing CDB-layout cases.
The previous read_disc_structure_unknown_format_is_rejected test
case (which had used 0x84 as a placeholder "not modelled" Format
Code) moves to 0x87 so the rejection diagnostic still surfaces from
the dispatcher's catch-all arm.
Added — Round 240 REPORT KEY Binding Nonce sub-payloads
(Common §4.14.2.4 Table 4-10 / §4.14.2.5 Table 4-11)
The two AACS Key Class 0x02 Key Format codes the spec defines
alongside the previously-implemented AGID / Drive Cert Challenge /
Drive Key / Drive Cert / Invalidate-AGID set — 0x20 (Binding Nonce —
generated in drive) and 0x21 (Binding Nonce — read from medium) —
were declared as named constants since round 93 but had no typed CDB
constructor or response parser. Round 240 fills that hole:
ReportKey::aacs_binding_nonce_gen(agid, starting_lba, block_count)—
CDB constructor for the generate-and-store variant. The LBA Extent
identified by(starting_lba, block_count)lands in CDB bytes 2..5
(big-endian) and byte 6 per AACS Common §4.14.2 final paragraph; the
rest of the CDB follows Table 513.ReportKey::aacs_binding_nonce_read(agid, starting_lba, block_count)—
CDB constructor for the read-from-medium variant. Same wire layout;
the only difference is the Key Format field (0x21vs0x20),
which selects the §4.7.2 read protocol over §4.7.1 generate.BindingNonceResponse { binding_nonce: [u8; 16], mac: [u8; 16] }—
decoded response. Both Key Formats share the 36-byte response wire
layout (Tables 4-10 and 4-11 are identical):
[length:u16=0x0022][reserved:u16][nonce:16][mac:16].parse_report_key_binding_nonce(buf)— parser for that wire
layout. A single parser covers both Key Formats.BINDING_NONCE_LEN= 16 andBINDING_NONCE_MAC_LEN= 16 —
named constants for the two payload fields.
The 16-byte MAC the response carries is Dm = CMAC(BK, Nonce) under
the §4.3-derived Bus Key per the §4.7.1 / §4.7.2 transferred-binding-
nonce protocol; the caller validates it against its own
Hm = CMAC(BK, Nonce) after deriving the Bus Key from the §4.3 AKE.
MockDrive now dispatches both Key Format codes and stores the
(key_format, starting_lba, block_count) triple from the most recent
Binding Nonce CDB in a new last_binding_nonce_op: Option<(u8, u32, u8)> field, so a test can confirm the AACS Common §4.14.2 LBA-Extent
CDB packing. In auth mode the mock recomputes the MAC with
aes_128_cmac(bus_key, binding_nonce) per §4.7; in static mode it
returns the fixture binding_nonce_mac verbatim. Static-fixture
binding_nonce / binding_nonce_mac slots use the same
index-pattern-tagging idiom as the existing Volume-ID / PMSN / Media-ID
fixture bytes.
Companion test suite tests/synth_round240_binding_nonce.rs
(7 cases) covers both Key Formats end-to-end through MockDrive,
asserts the 36-byte response length matches Table 4-10, pins the LBA
Extent CDB packing (bytes 2..5 big-endian + byte 6), distinguishes
0x20 from 0x21 via the CDB Key Format field, and rejects malformed
inputs (wrong length field, truncated payload). Five additional unit
tests live in src/mmc.rs alongside the existing CDB-layout cases.
Added — Round 236 MKB Type-and-Version typed accessors
(Common §3.2.5.1.1 / Table 3-2)
Three small, surface-level typed accessors on the parsed Mkb that
expose Type-and-Version Record fields a downstream consumer would
otherwise have to extract by hand from the existing mkb_type / raw
version fields:
-
MkbType::has_aacs_marker() -> bool— verifies the low 16 bits
of the on-wire MKBType field match the spec-mandated0x1003
marker (Table 3-2's000x_1003₁₆layout). The three named variants
(Type3/Type4/Type10) always returntrue; the
Other(u32)catch-all returnstrueiff the low 16 bits actually
match. The parser does not reject non-marker values
(manufacturer-specific behaviour per §3.2.5 final paragraph for an
improperly formatted MKB), so the predicate lets a caller assert
well-formedness explicitly. -
MkbType::generation() -> Option<u8>— returns the high-byte
of the on-wire MKBType field when the0x1003marker is present
(soType3 → Some(3),Type4 → Some(4),Type10 → Some(10),
Other(0x0007_1003) → Some(7)for forward-compat with a
hypothetical generation the spec might define later), andNone
when the marker doesn't match. -
Mkb::generation() -> Option<u8>— convenience wrapper that
forwards tomkb_type.and_then(|t| t.generation()). ReturnsNone
on a hand-constructedMkbwith no Type-and-Version Record (the
parser would have errored out withMissingTypeAndVersionRecord
before returning such anMkb). -
Mkb::is_test_mkb() -> bool— Common §3.2.5.1.1 reads "The
Version Numbers begin at 1; 0 is a special value used for test
Media Key Blocks." This predicate surfaces that sentinel so a
Licensed Player that wants to refuse test MKBs in production can
gate on it before any further processing.
Pure surface additions: no behavioural change to Mkb::parse, no new
parse-error variants, no fixture changes. One new test
(mkb_generation_and_is_test_mkb_render_from_parsed_type_record)
parses synthetic MKBs covering every spec-defined generation, a
forward-compat generation, a malformed non-1003 field, the
version == 0 test sentinel, and a default-constructed Mkb, and
pins the accessors' contract for each case. Two additional unit
tests cover MkbType::has_aacs_marker and MkbType::generation on
the enum variants directly.
Added — Round 229 Content Revocation List parse + per-segment
ECDSA verify + revocation-record lookup (PVB §2.7 / Tables 2-2..2-5)
New module crl implements the AACS-LA-signed Content Revocation List
(ContentRevocation.lst stored under the \AACS\ and
\AACS\DUPLICATE\ directories) — the signed list of revoked Content
Certificate IDs, Managed Copy Server Certificate IDs, and Recordable
Media Revocation Records (RMRR) a Licensed Player consults before
honouring the Content Certificate the round-222 layer parses /
verifies.
ContentRevocationList::parse(bytes)— decodes a
ContentRevocation.lstblob into:- The 4-byte CRL Header (
List Type4 bits + reserved nibble, 2-byte
big-endianList Version, 1-byteNumber of Segments). NCrlSegment { segment_size, records, signature }records, each
starting with a 4-byteSegment Sizefield, then a Revocation
Record Set of decodedRevocationRecords, then a trailing 40-byte
Entity Signature. The Segment Size #1 cap (≤ 128 KiB − CRL Header
per PVB §2.7) is enforced; trailing0x00padding bytes after the
last segment are tolerated per the PVB §2.2 mastering rule.
- The 4-byte CRL Header (
RevocationRecordenum — structured decode of every spec-defined
Record Type plus a forward-compatibilityUnknownvariant:ContentCertificateId { range, id }(PVB Table 2-3,
Record_Type == 0x0) — 4-bit Record_Type + 12-bit Range + 6-byte
Content Certificate ID.ManagedCopyServerCertificateId { range, id }(PVB Table 2-4,
Record_Type == 0x1).RecordableMedia(RecordableMediaRevocation)(PVB Table 2-5, the
three contiguous0x2 / 0x3 / 0x4records folded on parse into one
high-level record carrying theICCIDflag, 3-bitRecordable Media Type, 6-byte Content Certificate ID, and 16-byte Media ID).
A 3-record run that doesn't begin withRecord_Type == 0x2or
whose0x3/0x4continuation is missing is preserved verbatim
asUnknownso adjacent valid records still apply.Unknown { record_type, bytes }per PVB §2.7 ("If a Licensed
Product encounters a Revocation Record with a Record_Type value
it does not recognize, the record shall be ignored.") — preserves
the raw 8 bytes so a forward-compatibility record doesn't cause
adjacent valid records to be silently dropped.
ContentRevocationList::verify_segment_signature(k, aacs_la_pub)verify_last_segment_signature(aacs_la_pub)+
verify_all_segments(aacs_la_pub)— run
AACS_Verify(AACS_LApub, CRL_Segsig, CRL_Seg)with the spec's
cumulative-prefix signed range: segmentk's signed payload is
"CRL Header || segment 0 bytes (size + records + signature) || … ||
segment k bytes (size + records)" — everything before segmentk's
own 40-byte signature. Theverify_last_segment_signaturehelper
encodes the PVB §2.7 spec optimisation ("when reading more than one
CRL Segment, only the signature of the last Segment shall be
checked since that signature includes all previous fields including
previous Segments and the CRL Header").
ContentRevocationList::is_content_certificate_id_revoked(id)is_managed_copy_server_id_revoked(id)— global revocation
queries that walk every segment's records and apply the spec
range-match semantics; the Content-Certificate query additionally
surfaces the embedded Content Certificate IDs of anyRecordableMedia
records whose ICCID flag is0(PVB §2.7.2 step 4 from the Prepared
Video book).
ContentRevocationList::recordable_media_revoked(media_type, media_id, content_certificate_id)— the four-step §2.7.2
applicability check for a(type, media_id, content_certificate_id)
triple. Returnstrueonly when an RMRR matches both the media
type and the media ID and either its ICCID flag is1or its
embedded Content Certificate ID matches the queried CCID.ContentRevocationList::to_bytes— byte-exact round-trip with
parsefor any value that parsed cleanly (used by the test fixture
builder to mint signed synthetic CRLs).- New types
ManagedCopyServerCertificateId(pub [u8; 6]),
RecordableMediaRevocation { iccid, media_type, content_certificate_id, media_id }, andRecordableMediaType
(BdRecordable/HdDvdRecordable/DvdRecordable/
PlusRecordable/Reserved(u8)for forward-compatibility). - Public constants
LIST_TYPE_FIRST_GEN,CRL_HEADER_LEN,
REVOCATION_RECORD_LEN,SEGMENT_SIGNATURE_LEN,
SEGMENT_1_SIZE_MAX, and the fiveRECORD_TYPE_*tag values.
No new error variants — the module reuses AacsError::Truncated,
OversizedRecord, InvalidValue, and MkbSignatureInvalid so the
consumer can fold all AACS_Verify failures through the same branch
that already handles the MKB-side and Content-Certificate-side
ECDSA verify failures.
Companion integration test tests/synth_round229_crl.rs (10 cases)
mints a synthetic AACS LA Entity ECDSA key pair, builds a fully
populated three-segment CRL (segment 0: 4 CCID records including a
12-ID range; segment 1: 2 MCS records; segment 2: 1 RMRR with
ICCID=0), then pins: byte-exact round trip, per-segment + last-segment
- all-segment signature verification, cumulative-prefix signed-range
lengths, content-certificate range semantics, MCS range semantics,
RMRR with ICCID=1 (matches by media only), RMRR with ICCID=0 (also
revokes by CCID), wrong public key rejects every segment, in-place
record tampering breaks the last-segment signature, trailing
0x00-padding tolerance, andUnknown-record preservation.
Lib-side test module (18 cases) covers single-segment round trips for
each record type, the RMRR bit-layout (ICCID at bit 3, media type at
bits 2..=0 of byte 0 of record 1), the 16-byte Media ID
reassembly across the three on-wire records, the Range-match
semantics for both CCID and MCS records, multi-segment cumulative
prefix verification, malformed RMRR-1-without-2-or-3 preservation as
Unknown, segment-size-too-small rejection, the
first-segment 128 KiB cap, zero-Number_of_Segments rejection,
truncated-buffer rejection, and trailing-non-zero-byte rejection.
Added — Round 222 signed Content Certificate parse + verify
(PVB §2.4 / §2.5 / §2.6, BD-Prerecorded Table 2-1)
New module content_certificate implements the AACS-LA-signed Table
2-1 wrapper that binds a BD-ROM physical layer's revocation parameters,
BDMV usage-rules hashes, and per-Content-Hash-Table digests into one
ECDSA-signed blob — the Pre-recorded Video Book §2.6 verification the
Content Hash Table integrity check (already implemented in round 188)
depends on.
ContentCertificate::parse(bytes)— decodes one
Content00N.cerfile into the generic PVB Table 2-1 header
(Certificate Type, BEE flag,Total_Number_of_HashUnits,
Total_Number_of_Layers,Layer_Number,Number_of_HashUnits,
Number_of_Digests,Applicant ID,Content Sequence Number,
Minimum CRL Version), the variable-lengthFormat_Specific_Section
(with theLength_Format_Specific_Section4-byte-alignment rule
enforced andL = 0accepted as the spec's alignment-pad case), the
Number_of_Digests8-byteContent Hash Table Digests, and the
trailing 40-byteSignature Datafield.ContentCertificate::verify_signature(aacs_cc_pub)—
AACS_Verify(AACS_CC_pub, Signature_Data, CC)over the certificate
bytes up to but excluding the trailing 40-byte signature (PVB §2.5
step 4 / §2.6 step 5). The caller supplies the AACS LA Content
Certificate public key; AACS LA distributes it only to licensees so
the crate ships no key material.ContentCertificate::content_hash_table_digest(cht_bytes)+
verify_content_hash_table_digest(digest_index, cht_bytes)—
recomputeCHT_d = [SHA-1(CHT)]_lsb_64(PVB §2.5 step 3 / §2.6
step 3) over the raw on-disc Content Hash Table bytes and match
against the per-layer digest stored in the certificate.ContentCertificate::content_certificate_id()— returns the
6-byte Content Certificate ID =Applicant_ID || Content Sequence Numberper PVB §2.4; this is the lookup key for PVB Table 2-3
Revocation Records.ContentCertificate::bd_format_specific_section()/
BdFormatSpecificSection::parse(bytes)— decode the
BD-Prerecorded Final 0.953 Table 2-1 Format-Specific Section
(Hash_Value_of_MC_Manifest_File(20) +Hash_Value_of_BDJ_Root_Cert
(20) +Num_of_CPS_Unit(2) +J × Hash_Value_of_CPS_Unit_Usage_File
(20·J)) out of the generic variable-length region.ContentSequenceNumber— structured 6-bit CCSS ID / 15-bit
Timestamp / 11-bit Sequence Number (=Sequence Number 1(4) || Sequence Number 2(7)) decoder + re-encoder for the bit layout in
BD-Prerecorded Table 2-1 bytes 16..=19.usage_rules_hash(bytes)— exposes theC_ur = SHA-1(Usage_Rules)primitive from PVB §2.6 so the caller can apply
it to whichever usage-rules artefact the adaptation book specifies
(the BD spec uses the MC Manifest File and the per-CPS-Unit Usage
Files).
Reused error variants only: AacsError::Truncated /
AacsError::OversizedRecord (mirror the existing parser surface),
AacsError::InvalidValue (unknown Certificate Type or violated length
alignment), AacsError::MkbSignatureInvalid (signature AACS_Verify
rejected; reused so consumers can fold all AACS_Verify failures
through one branch), and AacsError::ContentHashMismatch (recomputed
CHT_d did not match a stored digest).
Companion integration test tests/synth_round222_content_certificate.rs
(6 cases) mints a synthetic AACS_CC ECDSA key pair, builds a
fully-populated layer-0 certificate over two synthetic Content Hash
Tables, and asserts that: the canonical to_bytes form round-trips
through parse byte-exact; the verify_signature check passes against
the synthetic public key; tampering one stored CHT digest breaks
verify_signature; tampering the on-disc CHT bytes breaks the
per-digest match; a wrong public key rejects the signature; CHT_d is
exactly 8 bytes; and usage_rules_hash is plain SHA-1 and
deterministic.
Lib-side test module (16 cases) covers
ContentSequenceNumber::{from,to}_be_bytes round-tripping across
boundary values, BD Format-Specific Section parse / round-trip /
truncated-prefix / short-trailer rejection, full-certificate round-trip,
unknown-Certificate Type rejection, the (L + 2) ≡ 0 (mod 4) length
alignment rule, signature-payload range, and the L = 0 alignment-pad
path.
Added — Round 211 AKE/EC runtime self-check entry points
New module self_check exposes runtime-callable self-check entry points
for the AACS 160-bit curve (Common Final 0.953 §2.3 Table 2-1) + the
§4.3 Drive-Host AKE state machine. A downstream consumer (e.g.
oxideav-bluray) can now validate the in-tree cryptographic primitives
before issuing a real SCSI command to a Licensed Drive, without itself
needing AACS LA key material or a real disc.
Four checks, callable independently or as a single cascade:
curve_self_check()— Table 2-1 identity round-trip: generator
Gis on the curve,n·G == O,G.double() == G + G,
(a + b)·G == a·G + b·Gfor two independent scalars, scalar
a · a⁻¹ ≡ 1 (mod n), and the affine→bytes→affine round-trip is
identity.aacs_la_pub_self_check()— the bundledAACS_LA_PUB_X/
AACS_LA_PUB_Ycoordinates form a valid on-curve secp160r1 point, and
theaacs_la_pub_point()helper agrees.ake_ecdh_self_check()— synthetic ECDH:Dv = dk·G,Hv = hk·G,
thenlsb_128(x(hk·Dv)) == lsb_128(x(dk·Hv))per §4.3 step 28/29; the
agreed Bus Key is also checked non-degenerate (not all-zero).ake_full_self_check()— full §4.3 AKE end-to-end against a
synthetic-LA-rooted in-processMockDrive: mints a synthetic AACS LA
root, signs synthetic Drive + Host certificates, runs
host_authenticatethrough an authenticatingDriveAuthState, and
asserts both sides derive the same 128-bit Bus Key. Exercises every
Phase B + Phase C path in a single call.all_self_checks()— convenience wrapper running all four in
sequence and short-circuiting on the first failure.
Failures surface as a new AacsError::SelfCheckFailed { what: &'static str } variant carrying a tag for the failing identity (e.g.
"n·G != point at infinity", "ECDH bus keys disagree", "host and drive Bus Keys disagree after full §4.3 AKE").
Companion integration test tests/synth_round211_self_check.rs (6
cases) pins the per-function pass on a clean build and asserts the
cascade is idempotent across repeated invocations (no hidden RNG, no
stashed state). All values are synthesised in-source from constants — no
real AACS LA key material, no disc fixtures.
Added — structured ParseReport for KEYDB.cfg + fuzz/robustness suite
The KEYDB.cfg parser already skipped malformed lines individually
rather than aborting the whole load, but the only way to find out
what was skipped was the OXIDEAV_AACS_DEBUG=1 stderr toggle. Round
200 surfaces that information programmatically:
KeyDb::parse_with_report(&str) -> Result<(KeyDb, ParseReport)>
— same tolerant per-line semantics asKeyDb::parse; in addition,
every non-empty / non-comment line the parser couldn't interpret is
captured in aParseReport. The report exposes:skipped: Vec<SkippedLine>— 1-basedline_number, 80-byte
UTF-8-safesnippetof the offending line, and the
Display-formattedAacsErrorreasonreturned by the per-line
parser.is_clean()/skipped_count()accessors.
KeyDb::load_from_with_report(path)— same shape, reading from
a filesystem path. Useful for diagnostic tooling that wants to show
a "loaded N records, skipped M lines for the following reasons"
summary instead of silently dropping records.KeyDb::parse(text)is unchanged behaviourally — it's now a
thin discard of the report and continues to honour
OXIDEAV_AACS_DEBUG=1for stderr mirroring.- A new
truncate_excerpthelper consolidates the UTF-8-boundary-safe
snippet logic shared byKeyDbParseError/HeaderParseErrorso
multi-byte codepoints can never be split by the 80-byte cap.
New public types oxideav_aacs::{ParseReport, SkippedLine}. No
behavioural change to any other crate API.
A companion integration test suite,
tests/synth_round200_keydb_fuzz.rs (27 cases, all synthetic),
provides "fuzz-equivalent" coverage by enumerating every structurally
distinct failure mode the parser exposes:
- Per-record-type malformations: short / long / odd-length /
non-0x-prefixed hex literals, each required field missing, each
named field at the wrong byte count, declared-vs-actual length
mismatches forHCcertificates. - Scope rules:
VID/VUK/MEK/TK/KCDrows outside any
DISCIDscope are rejected; a malformedDISCIDinvalidates the
subsequent scoped rows. - Lexical robustness: mixed CRLF / LF / lone-CR line endings, the
printable-ASCII byte range as the first character of a line,
multi-byte UTF-8 in record bodies (snippet truncation must not
split codepoints), and a 10 KiB malformed line (the snippet must
stay under 80 bytes). - Composite:
ParseReport.skippedline numbers come back in source
order; comment-only / whitespace-only lines never appear in the
report; a file of 26 unknown leaders all skip without aborting.
The suite pins three invariants: the parser never panics, never aborts
the whole load because of one bad line, and every skip is surfaced
through ParseReport exactly once.