find_by_drive_id gated the product-id-blind four-field fallback behind an empty
product_id in 1.7.0, so a drive reporting a specific product_id (e.g. an LG BU40N
reporting "BD-RE BU40N") no longer matched its catalog entry (generic "BD-RE"),
silently skipping the LibreDrive firmware unlocker — the rip fell through to the
AACS host-cert path (LD: no, AACS: yes).
Run the four-field fallback (vendor/revision/vendor_specific/firmware_date)
unconditionally but ambiguity-guarded: bind a UNIQUE catalog match regardless of
the reported product_id (the real-drive case), and require the exact product_id
only when catalogued siblings genuinely share the four-field identity — preserving
the anti-mis-bind protection the 1.7.0 gate intended. Regression test added.
Bump to 1.7.1.
identify() requested a 32-byte data-in allocation while the firmware commits
KNOCK_RESP_LEN (64) bytes on every subfn. The short allocation desyncs the first
command's data-in phase and wedges the drive, so the later bare 0xAD VID read
aborts (CHECK CONDITION) — the "Raw Read GOOD but no VID" failure that left the
keyserver with vid=none. Request KNOCK_RESP_LEN like every other knock; update the
CDB-shape tests. Also sync Cargo.lock to the 1.7.0 manifest.
Every unlocker now implements one method — unlock(&self, scsi, ctx) ->
Result<Option<Unlocked>> — with identical workflow and error handling,
differing only in CDBs/checks. Drops the unlock_features/unlock_bus split,
per-unlocker result state, and the drive_unlocked flag ("unlocked" is just
Some). Only a dead bus is Err; everything else falls through as Ok(None).
- VID read is best-effort everywhere via the shared crate::vid reader: a
VID miss no longer discards an unlock that already removed the bus — the
bug that dropped a freemkv-unlocked drive to the cert path and sent the
keyserver vid=none.
- AACS: host certs injected at construction (AacsUnlocker::new(certs)), not
passed per-call; a successful cert AKE is a real unlock.
- Renesas/Pioneer: the vendor open enables raw reads (LibreDrive parity),
then reads the VID via the same 0xAD reader — no host cert, matching the
firmware routes.
- Rename the MT1959 unlocker to LD (matches the ld/ module); the MediaTek
MT1959 platform driver keeps its name.
- Consumer assembles its own dispatch list from the exported types; no
central factory. Adds a uniform dead-bus contract test over all unlockers.
Fixed
- ld/profile: find_by_drive_id no longer mis-binds a drive whose reported
product_id is real but uncatalogued to a sibling model's profile (and its wrong
LibreDrive microcode); the product-id-blind loose match is gated on an empty id.
Docs / CI
- Third-party NOTICE for the LibreDrive microcode bundled in profiles.json;
cargo-deny gate (#15) and audience-based comment-guard (#17).
Matches the ld/ platform-folder convention (MediaTek lives in src/ld/); the
module/struct spelling is normalized renesis->renesas to match the drives it
detects. Pure move + identifier rename, no behavior change.
Declared rust-version and the ci.yml/qa.yml toolchain pins both sit at 1.88 (the
edition-2024 floor once zip 8 and let-chains are accounted for), so CI builds on
exactly the MSRV: an accidental bump to a newer toolchain fails CI by design and
moving the floor takes a deliberate pin edit. The uninlined_format_args allow
reconciles 1.88's clippy with the allow-by-default it became in 1.97+.
Extends the fake ScsiTransport in css::mod to run the full CSS bus-auth
happy path (variant brute-force, drive-challenge/Key2 exchange, best-effort
disc-key REPORT KEY, DvdUnlocker::unlock_bus success) instead of only its
failure branches.
Covers mt1959's run_init retry loop (signature-mismatch abort, generic
do_unlock failure with firmware-reload fallback, firmware-reload transport
fault), run_probe's disc-type detection (capacity sense fallback, UHD vs BD
init address, pass-2 early-end on a rejected probe), and the PlatformDriver
guard clauses.
Covers aacs/handshake's EC math edge cases (mod_inv/ec_add/ec_double/ec_mul
defensive branches, off-curve pubkey rejection), the Debug redaction impls,
the AACS 1.0 type-0x01 cert-verify gate, the P-256 thin wrapper + short-cert
guard + step-6 bad-signature rejection, and drives run_cert_handshake to its
full Ok(CertHandshake) success path via an extended drive emulator.
Adds unit tests for aacs::error::Error and css::error::Error (code(),
scsi_sense/is_transport_failure branches, From<ScsiError> conversions),
a FIPS-197 known-answer test for aacs::aes_ecb_decrypt, and scripted
fake-transport tests covering the authenticate_with_agid / read_disc_key
error branches in css::mod. Raises line coverage 91.16% -> 92.59%.
Add an additive, non-required coverage job to CI that runs cargo
llvm-cov and uploads lcov to Codecov (tokenless, fail_ci_if_error
false). Mirrors the test job's environment and pinned 1.90.0 toolchain;
--locked is omitted to match this crate's gitignored-lockfile policy.
Adds codecov.yml (informational, comment off) and a README badge.
Bisected the true floor: 1.85 fails to compile (needs is_multiple_of,
stabilized 1.87, and let-chains, stabilized 1.88), 1.90 builds/clippy/tests
clean. Repin Cargo.toml rust-version and every CI dtolnay/rust-toolchain to
1.90.0, and note the change in CHANGELOG.md. Also removed a redundant
"// Hash the data" comment and a stale duplicate doc line on
generate_host_key_pair_p256 found during a pass over the crate's comments.
Adds a 'changelog' job that fails when Cargo.toml's version has no matching
'## [X.Y.Z]' heading in CHANGELOG.md, hard on main and PRs into main, warning
on dev/qa — so the unified release can't ship a crate with a blank changelog.
The 2.0 host-cert handshake is now live and cert-agnostic (works when a
cert is supplied), with the same chain-of-trust hardening as 1.0; the
SECURITY note is resolved by fixing, not by leaving dead.
- Remove the module-level #[allow(dead_code)]; the P-256/v2 AKE is now
reachable from run_cert_handshake.
- run_cert_handshake selects the P-256 AKE per host cert: the 1.0 cert is
tried first (an AACS 2.0 drive accepts it for the VID); if the drive
refuses it AND the supplied HostCert carries AACS 2.0 (P-256)
credentials, fall back to the native P-256 AKE. No possession/feature
gate — it consumes whatever the HostCert carries, exactly as the 1.0
path consumes hc.certificate. A 1.0-attempt transport fault
short-circuits the fallback so a dead bus is not hammered.
- Chain-of-trust hardening on the now-live v2 path, mirroring 1.0:
drive-cert TYPE gate (only 0x11 accepted, else rejected), cert verify
is FATAL (verify_cert_p256 under the real 2.0 LA anchor), ECDSA-P256
point validation on every attacker-supplied point (point_on_curve in
ecdsa_verify_p256 + compute_bus_key_p256), and AGID release on every
exit.
- The 2.0 cert byte layout/signed-range is documented as PROVISIONAL: no
genuine 2.0 host/drive AKE cert exists to pin it, and the draft spec
implies extra Host-ID/paired-DKS fields. A wrong signed range fails
CLOSED (fatal verify rejects), never trusts. An #[ignore] slot awaits a
real cert.
- Tests: synthetic test-LA-signed 2.0 certs prove accept + bus-key
derivation, bad-signature fatal reject, unknown-type reject, and
off-curve/identity rejection (each red-before-green per guard); a
genuine AACS 2.0 content cert verifies under the published CC key,
proving the P-256/SHA-256 primitive against real material.
Two coupled classes. The chain-of-trust hardening makes a genuine AACS
LA public key load-bearing (the new ECDSA public-key validation rejects
every off-curve key), so the anchor replacement ships in the same commit
— with the old off-curve constants and the new guard, verify_cert would
reject every certificate.
=== Chain of trust (forged-trust class) ===
- aacs_authenticate: reject an unknown drive-cert type instead of falling
through. The if/else-if had no else, so a type that was neither 0x01
(AACS 1.0) nor 0x11 (AACS 2.0) skipped LA verification yet still ran the
step-6 key-signature check against the drive's OWN key lifted from a cert
that was never LA-verified — a rogue bridge could inject an attacker-chosen
bus key. Reject at the cert-type gate, before any key is trusted.
- ecdsa_verify / ecdsa_verify_p256: validate the public key Q (on-curve,
in-field, not the identity) before use. A Q of (0,0) collapsed verification
to a form forgeable with no private key.
- verify_cert_p256: guard on the real 132-byte P-256 cert length (was 138,
which the 132-byte caller never satisfied, so it returned false for 100%
of AACS 2.0 drives) and read the signature at the real offsets.
=== Failure-that-looks-like-success class (dead bus reported as unlocked) ===
- run_cert_handshake read_data_keys arm: classify a transport fault like its
read_volume_id sibling. A bus that died during the data-key read returned
Ok(volume_id: Some, read_data_key: None) — a dead bus rendered as success.
- LibreDrive::firmware_unlock: a transport fault in probe_disc now aborts with
Transport. For the 140/206 profiles without a read_vid_cdb the bus is never
touched again, so this swallowed fault was the only dead-bus signal and
warn-and-continue reported Ok(drive_unlocked: true) on a dead bus.
- mt1959 run_probe pass-1 and pass-2 speed-probe loops: preserve the transport
classification instead of flattening to UnlockFailed (pass-1) or silently
returning Ok(()) (pass-2 bare break).
=== AGID + key-material redaction ===
- aacs2_authenticate_p256: release the AGID on every fallible exit (the v1
twin was fixed; the v2 path still leaked).
- Unlocked, DriveProfile: hand-written redacting Debug. Unlocked is the public
carrier for bus_key/vid; DriveProfile (reachable via the public catalog)
carried the drive firmware image and per-drive vendor CDBs — a derived Debug
printed all of it. This crate is unpublished precisely because of that
firmware.
=== Validated AACS Licensing Administrator anchors ===
Replace the compiled-in AACS 1.0 and 2.0 LA public keys, which were
OFF-CURVE (so the new Q-validation rejected every cert signed by them),
with the real published keys. Both confirmed on-curve; the 1.0 key also
verifies a genuine LA-signed host certificate. The native P-256 handshake
stays #[allow(dead_code)] and is NOT wired — a security note documents the
cert-type gate + fatal verification that must be added first, once the 2.0
wire layout is confirmed against a real drive cert.
Tests: every behavioural fix is pinned with a red-before-green test naming
its mutation, including transport-abort coverage for validate, both firmware
upload variants, both probe passes, and the probe-disc dead-bus path; the
anchor landing is pinned by an on-curve assertion and a genuine-cert verify
(both go red if an anchor reverts to the old off-curve value).