Skip to content

v0.1.3

Latest

Choose a tag to compare

@MagicalTux MagicalTux released this 10 Jun 11:56
· 5 commits to master since this release
e0f3d24

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 — typed 0xBF CDB 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 Format 0x84 per MMC-6
    §6.36.2.4), Control at byte 11. cdb() / parse_cdb() inverses
    mirror the existing ReadDiscStructure / SendKey pattern.
  • SendDiscStructure::aacs_write_data_key(agid) — constructor for
    the Format 0x84 send. 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 is 0x0012; 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 = 0xBF and
    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-side FORMAT_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 Format 0x85. 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 shrink allocation_length after 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
    by N 16-byte extent records [reserved:8 || Start LBA:4 || LBA Count:4]. The maximum field spans 1..=256; the on-wire encoding
    represents 256 as the byte value 0 per 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 the 0256 sentinel back to its semantic value;
    preserves the on-wire extent order verbatim (per §4.14.3.6 paragraph
    3 the extents are sorted by start_lba ascending and non-overlapping,
    but the parser does not enforce the invariant — the SEND DISC
    STRUCTURE Format 0x85 ingest path is where the logical unit
    rejects malformed tables per §4.14.5.x). Rejects buffers whose
    length field is below 2, buffers shorter than 2 + length, and
    extent sections whose byte count is not a multiple of 16.
  • FORMAT_AACS_BUS_ENCRYPTION_SECTOR_EXTENTS = 0x85 and
    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
    Format 0x84. 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.
  • 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 plaintext Krd /
    Kwd.
  • parse_data_keys_response(buf) — wire-layout parser; rejects
    buffers whose length field is not 0x0022 and truncated payloads.
  • FORMAT_AACS_DATA_KEYS = 0x84 and DATA_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 (0x21 vs 0x20),
    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 and BINDING_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-mandated 0x1003
    marker (Table 3-2's 000x_1003₁₆ layout). The three named variants
    (Type3 / Type4 / Type10) always return true; the
    Other(u32) catch-all returns true iff 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 the 0x1003 marker is present
    (so Type3 → 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), and None
    when the marker doesn't match.

  • Mkb::generation() -> Option<u8> — convenience wrapper that
    forwards to mkb_type.and_then(|t| t.generation()). Returns None
    on a hand-constructed Mkb with no Type-and-Version Record (the
    parser would have errored out with MissingTypeAndVersionRecord
    before returning such an Mkb).

  • 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.lst blob into:
    • The 4-byte CRL Header (List Type 4 bits + reserved nibble, 2-byte
      big-endian List Version, 1-byte Number of Segments).
    • N CrlSegment { segment_size, records, signature } records, each
      starting with a 4-byte Segment Size field, then a Revocation
      Record Set of decoded RevocationRecords, then a trailing 40-byte
      Entity Signature. The Segment Size #1 cap (≤ 128 KiB − CRL Header
      per PVB §2.7) is enforced; trailing 0x00 padding bytes after the
      last segment are tolerated per the PVB §2.2 mastering rule.
  • RevocationRecord enum — structured decode of every spec-defined
    Record Type plus a forward-compatibility Unknown variant:
    • 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 contiguous 0x2 / 0x3 / 0x4 records folded on parse into one
      high-level record carrying the ICCID flag, 3-bit Recordable Media Type, 6-byte Content Certificate ID, and 16-byte Media ID).
      A 3-record run that doesn't begin with Record_Type == 0x2 or
      whose 0x3 / 0x4 continuation is missing is preserved verbatim
      as Unknown so 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: segment k's signed payload is
      "CRL Header || segment 0 bytes (size + records + signature) || … ||
      segment k bytes (size + records)" — everything before segment k's
      own 40-byte signature. The verify_last_segment_signature helper
      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 any RecordableMedia
      records whose ICCID flag is 0 (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. Returns true only when an RMRR matches both the media
    type and the media ID and either its ICCID flag is 1 or its
    embedded Content Certificate ID matches the queried CCID.
  • ContentRevocationList::to_bytes — byte-exact round-trip with
    parse for 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 }, and RecordableMediaType
    (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 five RECORD_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, and Unknown-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.cer file 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-length Format_Specific_Section
    (with the Length_Format_Specific_Section 4-byte-alignment rule
    enforced and L = 0 accepted as the spec's alignment-pad case), the
    Number_of_Digests 8-byte Content Hash Table Digests, and the
    trailing 40-byte Signature Data field.
  • 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)
    recompute CHT_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 Number per 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 the C_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
    G is on the curve, n·G == O, G.double() == G + G,
    (a + b)·G == a·G + b·G for two independent scalars, scalar
    a · a⁻¹ ≡ 1 (mod n), and the affine→bytes→affine round-trip is
    identity.
  • aacs_la_pub_self_check() — the bundled AACS_LA_PUB_X /
    AACS_LA_PUB_Y coordinates form a valid on-curve secp160r1 point, and
    the aacs_la_pub_point() helper agrees.
  • ake_ecdh_self_check() — synthetic ECDH: Dv = dk·G, Hv = hk·G,
    then lsb_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-process MockDrive: mints a synthetic AACS LA
    root, signs synthetic Drive + Host certificates, runs
    host_authenticate through an authenticating DriveAuthState, 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 as KeyDb::parse; in addition,
    every non-empty / non-comment line the parser couldn't interpret is
    captured in a ParseReport. The report exposes:
    • skipped: Vec<SkippedLine> — 1-based line_number, 80-byte
      UTF-8-safe snippet of the offending line, and the
      Display-formatted AacsError reason returned 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=1 for stderr mirroring.
  • A new truncate_excerpt helper consolidates the UTF-8-boundary-safe
    snippet logic shared by KeyDbParseError / HeaderParseError so
    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 for HC certificates.
  • Scope rules: VID / VUK / MEK / TK / KCD rows outside any
    DISCID scope are rejected; a malformed DISCID invalidates 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.skipped line 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.