Matthew Jackson 74fe85cdd2 harden AACS/firmware unlock against forged trust + dead-bus-as-success, and land the real on-curve AACS LA anchors
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).
2026-08-18 22:56:30 -07:00
2026-08-15 08:14:31 -07:00
2026-07-10 12:31:19 -07:00
2026-07-28 15:35:19 -07:00

freemkv-unlock

The unlock layer for the freemkv toolchain.

An unlocker removes a drive-level bus-encryption barrier so the drive serves readable (de-bus'd / de-scrambled) sectors. Content-key decryption is a separate concern — the consumer's job.

This crate defines the Unlocker contract and a generic SCSI transport contract, and holds the self-contained unlocker modules. The consumer (libfreemkv) depends on this crate and dispatches through all_unlockers(); it never names an individual unlocker, and clients of libfreemkv are oblivious to unlockers entirely (as they are to the SCSI layer).

use freemkv_unlock::UnlockError;

// Drive-prep: try each unlocker's feature unlock until one claims the drive.
// `NotApplicable` means "not this unlocker's drive" — move on; a transport
// error means a dead bus — abort. `unlock_bus` follows the same contract for
// removing per-disc bus encryption.
for u in freemkv_unlock::all_unlockers() {
    match u.unlock_features(&mut scsi, &ctx) {
        Ok(unlocked) => return Ok(unlocked),
        Err(UnlockError::NotApplicable) => continue,
        Err(e) => return Err(e),
    }
}

To remove an unlocker, delete its module directory and its one line in all_unlockers() — nothing else changes.

License: MIT.

S
Description
No description provided
Readme MIT
1.3 MiB
Languages
Rust 100%