Files
freemkv-unlock/docs/aacs-handshake.md
T
Matthew Jackson 74936ad892 release: 1.7.0
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).
2026-09-02 12:27:57 -07:00

9.8 KiB
Raw Permalink Blame History

AACS bus authentication handshake

Flow

  1. Invalidate AGIDs → allocate fresh AGID
  2. Send host certificate + nonce
  3. Receive drive certificate + nonce
  4. Receive drive key point + signature, verify
  5. Sign host key point, send
  6. ECDH: host_priv × drive_key_point → bus key (low 128 bits of x)
  7. Read VID or Read Data Keys (encrypted with bus key)

Why scsi_read/scsi_write check status at the seam

The contract this crate is built on (ScsiTransport::execute) returns Ok on a SCSI sense and Err ONLY on a transport-layer fault. These two helpers used to propagate the ? and nothing else — so a CHECK CONDITION (Ok, non-zero status) handed the caller back its own ZERO-FILLED buffer and every REPORT/SEND KEY step consumed zeros as valid drive data: a drive certificate of zeros, a key point of zeros, a Volume ID of zeros, all reported as success. It has not fired in production only because the sole transport implementor violates the contract in the opposite direction (it returns Err for any non-zero status). Plugging in a CONFORMING transport would have turned the whole handshake into a zero-buffer success, so the status and the transferred length are checked here, at the seam.

Why scsi_write shares this reasoning

Same contract reasoning as scsi_read: a drive that REFUSES the host certificate answers with Ok + CHECK CONDITION, and treating that as a successful send carried the handshake on against a drive that never accepted it.

ec_mul design notes

Constant-time tradeoff: this branches on scalar.bit(0) and clones BigUints per iteration, so its timing is data-dependent on the secret scalar (the long-term host private key in ecdsa_sign, the ephemeral key in ECDH). This is deliberate: the handshake runs once per disc against a local optical drive, so throughput and the narrow local-timing surface do not justify pulling in a vetted constant-time backend. Revisit if this ever signs in a remote/shared context.

Cofactor: both AACS curves used here have cofactor 1, so a point that lies on the curve is automatically in the prime-order subgroup — no small-subgroup defense / n·P == O check is required for the inputs this is called with.

verify_cert_p256 — provisional AACS 2.0 cert layout

Verifies an AACS 2.0 drive certificate (type 0x11) against an AACS 2.0 LA key. PROVISIONAL LAYOUT — the byte offsets below are NOT yet confirmed against a genuine AACS 2.0 cert, and no such cert exists on this machine to pin them (an exhaustive hunt found only 2.0 content certs, a different format; a real host/drive AKE cert needs a live USB/SCSI capture). What IS confirmed: first, the P-256/SHA-256 ECDSA primitive and curve constants are correct (ecdsa_verify_p256_verifies_a_genuine_aacs2_content_cert verifies a real 2.0 content cert under the published CC key); second, the 132-byte 4-byte-header framing MATCHES the step-6 drive-key message (x[4..36] / y[36..68] / r[68..100] / s[100..132]), the natural 20→32-byte scaling of the established AACS 1.0 84-byte key-message framing.

What is NOT confirmed: that a certificate shares that framing. No known implementation verifies 2.0 certs against a confirmed layout (the published spec draft leaves that format ambiguous), and the AACS 2.0 draft architecture lists the 2.0 host cert (type 0x12) with a 6-byte Host ID + a NEW 4-byte paired-Device-Key-Set field before the 64-byte public key — implying a ~140–144-byte cert with the pubkey near offset 12–16 and a signed range of ~76–80 bytes, NOT 68. The drive cert (type 0x11) has no paired-DKS field (~140 bytes, pubkey ~[12..76], signed ~first 76). So the offsets HERE (pubkey [4..68], signed [..68]) are a best-effort self-consistent placeholder, chosen to match the confirmed key message; they will almost certainly move when a real cert lands (see the #[ignore] real-cert slot). This matters for USEFULNESS, not SAFETY: a wrong signed range makes verification FAIL, and since the caller treats a verify failure as FATAL, the path fails CLOSED (rejects) rather than trusting anything — never the reverse.

The >= 132 guard keeps the cert[100..132] slice safe while letting a full-length cert reach verification (the old >= 138 guard rejected every 132-byte input unconditionally).

The LA anchor is a PARAMETER, not the compiled-in constant, so a unit test can drive the whole chain under a test LA keypair (sign a synthetic drive cert, prove it is accepted / a bad-sig cert is rejected) without the real anchor's private half, which no one has. Production always threads AACS2_LA_PUB_X/Y from aacs2_authenticate_p256.

Why an unknown drive-cert type must be rejected

This used to be an if / else if with NO else, so an unknown type (e.g. 0x02) fell straight through: is_aacs20 stayed false, so the step-6 key-signature check still ran — against cert_pub_key(drive_cert), the drive's OWN key lifted out of a cert that was never checked against the LA. A rogue USB bridge could set byte 0 to 0x02, present its own keypair, sign host_nonce || drive_key_point, and the handshake would return Ok with an attacker-chosen bus key. The chain of trust must not appear to have run when it did not.

AACS 2.0 chain-of-trust gate (aacs2_authenticate_p256_with_agid)

The SAME shape the AACS 1.0 sibling (aacs_authenticate_with_agid) enforces, now MANDATORY because this path is live. Two distinct rejections, both BEFORE any drive key is trusted:

(a) Cert TYPE gate. An AACS 2.0 drive cert is type 0x11. Anything else is unexpected on the native P-256 AKE and is REJECTED here, never allowed to fall through. The earlier if type==0x11 && !verify form left NO branch for a non-0x11 type: a rogue bridge could send a type-0x10 (or any) cert, skip verification entirely, and still reach the step-6 key check against cert_pub_key_p256(drive_cert) — its OWN key, lifted from a cert the LA never signed — winning an attacker-chosen bus key. The chain of trust must not merely LOOK like it ran.

(b) Cert VERIFY is FATAL. A type-0x11 cert whose P-256/SHA-256 signature does not verify under the real 2.0 LA anchor (AACS2_LA_PUB, on-curve — see la_anchor_keys_are_on_curve) is REJECTED, not logged-and-continued. ecdsa_verify_p256 range/curve-checks every point (point_on_curve), so a forged or off-curve cert key cannot slip past. The previous "non-fatal for backward compat" behaviour was the exact hole the SECURITY note demanded be closed before wiring.

run_cert_handshake — cert-agnostic AKE selection

The AACS 1.0 host cert is always present and is tried FIRST: an AACS 2.0 drive accepts a 1.0 host cert for backward compatibility, which is enough to learn the Volume ID. If the drive REFUSES the 1.0 cert (a plain cert-level rejection — NOT a dead or wedged bus) and THIS host cert also carries AACS 2.0 (P-256) credentials, fall back to the native P-256 AKE.

This does NOT branch on "do we possess 2.0 certs?" as a feature switch: it consumes whatever the supplied HostCert carries, exactly as the 1.0 path consumes hc.certificate. A host cert without v2 credentials simply has nothing more to present and keeps its 1.0 error; a host cert WITH them drives the P-256 AKE the moment the drive asks for it. When a real 2.0 host cert is supplied at runtime, the P-256 handshake runs — no code change, no possession gate.

A transport fault on the 1.0 attempt short-circuits the fallback: re-running the P-256 AKE against a bus that is gone re-issues AGID invalidation + SEND/REPORT KEY for nothing and buries the real (replug) cause. The Err arm below still classifies it as Transport.

AACS 2.0 (P-256) host-certificate handshake — live-path test plumbing

No genuine 2.0 host cert exists for us to sign with (a side agent is hunting one; see the #[ignore] slot at the end). So the tests below prove the PLUMBING under a self-generated test LA keypair: a 132-byte drive cert signed by the test LA is threaded through the SAME code the production anchor uses, only the anchor differs (a parameter, not a recompile). The layout mirrored is the code's self-consistent 4-byte-header framing — header(4)+pub_x(32)+pub_y(32)+sig_r(32)+sig_s(32)=132, LA signs SHA-256 over cert[..68] — identical to the step-6 drive-key message framing and the 156-byte SEND KEY host-cert payload (4+20 nonce+132 cert).

ecdsa_verify_p256_verifies_a_genuine_aacs2_content_cert — why it matters

SANITY: ecdsa_verify_p256 + the P-256 curve constants + SHA-256 are correct against GENUINE AACS 2.0 material. No real 2.0 host/drive AKE cert exists anywhere on this machine (an exhaustive hunt found none — it needs a live USB/SCSI AKE capture), but a real AACS 2.0 CONTENT certificate does (research/aacs/iso-aacs/CivilWar/Content000.cer, type 0x10, 232 bytes). It is signed by the AACS 2.0 Content Cert key (P-256/SHA-256, 64-byte signature over the first signed_len bytes) — a DIFFERENT key for a DIFFERENT purpose than the AKE LA anchor, but it exercises the EXACT same ecdsa_verify_p256 primitive verify_cert_p256 relies on; only the pubkey and the offsets differ. Proving it verifies here pins that the P-256 ECDSA/SHA-256 math and the point_on_curve gate are right, so when a genuine 2.0 host/drive cert (and its correct signed range) lands, only the offsets — not the crypto — remain to be fixed.

The CC pubkey is the published AACS 2.0 Content Certificate public key (P-256). Corrupting any P256_* constant, or removing SHA-256, makes this real-vector verification go red.

Cert support

  • AACS 1.0: custom 160-bit curve, SHA-1, 20-byte keys
  • AACS 2.0: an AACS 2.0 drive accepts an AACS 1.0 host cert for backward compatibility (that yields the Volume ID), and the native P-256/SHA-256 AACS 2.0 AKE is the fallback when the drive refuses the 1.0 cert AND the supplied host cert carries AACS 2.0 (P-256) credentials. Both paths are LIVE and share the same chain-of-trust hardening; see run_cert_handshake for the cert-agnostic dispatch.