After the plaintext session init, the DL3 dock requires an HDCP 2.2
session before it will accept any control-plane traffic. Add a clean-room
implementation of the HDCP 2.2 authentication: the AKE (with stored-km
and no-stored-km), locality check (LC) and session-key exchange (SKE),
all verified against the live dock -- H', L' and V' all match, so the
shared session key ks and content IV riv are established.

New modules:
 - crypto: thin adapters onto the in-tree kernel library-crypto bindings
   (AES-128-ECB, AES-CMAC, HMAC-SHA256, SHA-256) used by the KDF;
 - rng: CSPRNG helpers for the per-session HDCP nonces/keys;
 - hdcp: the HDCP 2.2 key derivation (kd/dkey/ks) and H'/L'/V' verifier
   computation (the byte-exact KDF formulas);
 - ake: the HDCP 2.2 AKE wire layer (OUT message builders, IN parsing);
 - golden: the session-invariant plaintext capability-announce skeleton
   the driver re-states with this session's live AKE values right after
   the AKE (build_cap_announce).

run_ake() drives the state machine end to end and returns the keyed
Session; an on-device crypto known-answer self-test (FIPS-197 AES-128,
RFC 4493 AES-CMAC) confirms the in-kernel crypto path is byte-correct.
The encrypted control plane that consumes the Session lands in the next
patch.

Signed-off-by: Mike Lothian <[email protected]>
Assisted-by: Claude:claude-opus-4-8 [Claude-Code]
---
 drivers/gpu/drm/vino/ake.rs    | 167 +++++++++
 drivers/gpu/drm/vino/crypto.rs |  81 ++++
 drivers/gpu/drm/vino/golden.rs |  69 ++++
 drivers/gpu/drm/vino/hdcp.rs   | 167 +++++++++
 drivers/gpu/drm/vino/rng.rs    |  12 +
 drivers/gpu/drm/vino/vino.rs   | 662 ++++++++++++++++++++++++++++++++-
 6 files changed, 1148 insertions(+), 10 deletions(-)
 create mode 100644 drivers/gpu/drm/vino/ake.rs
 create mode 100644 drivers/gpu/drm/vino/crypto.rs
 create mode 100644 drivers/gpu/drm/vino/golden.rs
 create mode 100644 drivers/gpu/drm/vino/hdcp.rs
 create mode 100644 drivers/gpu/drm/vino/rng.rs

diff --git a/drivers/gpu/drm/vino/ake.rs b/drivers/gpu/drm/vino/ake.rs
new file mode 100644
index 000000000000..ad79d2754c60
--- /dev/null
+++ b/drivers/gpu/drm/vino/ake.rs
@@ -0,0 +1,167 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! HDCP 2.2 AKE wire layer (sec 5.1 OUT framing, sec 5.2 IN parsing) -- the 
byte-exact
+//! message builders the AKE state machine drives, mirroring the verified 
userspace
+//! oracle (`vino-driver::hdcp_msgs`). DLM hardcodes per-message `sub_size` /
+//! `sub_len_dw` values the dock validates, so they are reproduced verbatim 
rather
+//! than derived.
+//!
+//! OUT body layout (sec 5.1), after the 16-byte sec 3 transport header:
+//! ```text
+//!   body[0..2]   u16 sub_size      (DLM-fixed per message)
+//!   body[2..4]   u16 = 0x0010
+//!   body[4..8]   u32 hdcp_seq      increments 1..7 across the AKE OUT 
messages
+//!   body[8..22]  14 zero bytes
+//!   body[22..26] u32 = 0x00000030  marker
+//!   body[26]     u8  = 0x00        flag
+//!   body[27]     u8  = msg_id
+//!   body[28..]   HDCP payload (zero-padded to the fixed body length)
+//! ```
+#![allow(dead_code)] // AKE message builders; response handlers run only after 
CP engagement
+
+use super::*;
+
+/// HDCP 2.2 message IDs (sec 5.3). `pub(crate)` so the AKE state machine
+/// ([`super::VinoDriver::run_ake`]) can match on the response IDs too.
+pub(crate) mod id {
+    use kernel::bindings;
+
+    // Standard HDCP 2.2 message IDs: reuse the canonical values from
+    // `<drm/display/drm_hdcp.h>` rather than redefining them, so vino stays in
+    // lockstep with the kernel's HDCP definitions. Only the transport framing
+    // around these (the DisplayLink type/sub/ctr header) is vino-specific.
+    pub(crate) const AKE_INIT: u8 = bindings::HDCP_2_2_AKE_INIT as u8;
+    pub(crate) const AKE_SEND_CERT: u8 = bindings::HDCP_2_2_AKE_SEND_CERT as 
u8;
+    pub(crate) const AKE_NO_STORED_KM: u8 = 
bindings::HDCP_2_2_AKE_NO_STORED_KM as u8;
+    pub(crate) const AKE_SEND_H_PRIME: u8 = bindings::HDCP_2_2_AKE_SEND_HPRIME 
as u8;
+    pub(crate) const AKE_SEND_PAIRING_INFO: u8 = 
bindings::HDCP_2_2_AKE_SEND_PAIRING_INFO as u8;
+    pub(crate) const LC_INIT: u8 = bindings::HDCP_2_2_LC_INIT as u8;
+    pub(crate) const LC_SEND_L_PRIME: u8 = bindings::HDCP_2_2_LC_SEND_LPRIME 
as u8;
+    pub(crate) const SKE_SEND_EKS: u8 = bindings::HDCP_2_2_SKE_SEND_EKS as u8;
+    pub(crate) const REPEATERAUTH_SEND_RECEIVERID_LIST: u8 =
+        bindings::HDCP_2_2_REP_SEND_RECVID_LIST as u8;
+    pub(crate) const REPEATERAUTH_SEND_ACK: u8 = 
bindings::HDCP_2_2_REP_SEND_ACK as u8;
+    pub(crate) const REPEATERAUTH_STREAM_MANAGE: u8 = 
bindings::HDCP_2_2_REP_STREAM_MANAGE as u8;
+    pub(crate) const REPEATERAUTH_STREAM_READY: u8 = 
bindings::HDCP_2_2_REP_STREAM_READY as u8;
+
+    // DisplayLink-specific message IDs with no `<drm/display/drm_hdcp.h>` 
equivalent
+    // (the AKE_Send_rrx split and the transmitter/receiver-info + auth-status 
messages
+    // the DL3 dock uses), kept as literals.
+    pub(crate) const AKE_SEND_RRX: u8 = 0x06;
+    pub(crate) const RECEIVER_AUTH_STATUS: u8 = 0x12;
+    pub(crate) const AKE_TRANSMITTER_INFO: u8 = 0x13;
+    pub(crate) const AKE_RECEIVER_INFO: u8 = 0x14;
+}
+
+/// transport `sub_id` for HDCP OUT messages (type=4 sub=0x04, sec 5.1).
+const SUB_HDCP: u16 = 0x04;
+
+/// Allocate a `body_len`-byte zeroed body with the sec 5.1 header filled in
+/// (`sub_size`, the `0x0010` marker, `hdcp_seq`, the `0x30` marker and 
`msg_id`).
+/// The caller writes the payload into `body[28..]`.
+fn body(body_len: usize, sub_size: u16, hdcp_seq: u32, msg_id: u8) -> 
Result<KVec<u8>> {
+    let mut b = KVec::from_elem(0u8, body_len, GFP_KERNEL)?;
+    b[0..2].copy_from_slice(&sub_size.to_le_bytes());
+    b[2..4].copy_from_slice(&0x0010u16.to_le_bytes());
+    b[4..8].copy_from_slice(&hdcp_seq.to_le_bytes());
+    b[22..26].copy_from_slice(&0x0000_0030u32.to_le_bytes());
+    b[27] = msg_id;
+    Ok(b)
+}
+
+/// Wrap a finished HDCP body in the sec 3 transport header (type=4 sub=0x04) 
with
+/// the DLM-fixed `sub_len_dw` and the transport `seq`.
+fn wrap(sub_len_dw: u16, seq: u32, body: &[u8]) -> Result<KVec<u8>> {
+    let mut frame = KVec::with_capacity(16 + body.len(), GFP_KERNEL)?;
+    proto::push_frame_with(&mut frame, 0x04, SUB_HDCP, sub_len_dw, seq, body)?;
+    Ok(frame)
+}
+
+/// `AKE_Init` (msg_id 0x02): `rtx[8] || TxCaps[3]`, padded to a 48-byte body
+/// (`sub_size=0x22`, `sub_len_dw=0x0c` -- guide sec 5.4 table).
+pub(super) fn ake_init(
+    hdcp_seq: u32,
+    seq: u32,
+    rtx: &[u8; 8],
+    tx_caps: &[u8; 3],
+) -> Result<KVec<u8>> {
+    let mut b = body(48, 0x0022, hdcp_seq, id::AKE_INIT)?;
+    b[28..36].copy_from_slice(rtx);
+    b[36..39].copy_from_slice(tx_caps);
+    wrap(0x000c, seq, &b)
+}
+
+/// `AKE_Transmitter_Info` (msg_id 0x13): byte-exact DLM framing
+/// (`sub_size=0x1f`, `sub_len_dw=0x0f`), payload `00 06 02 00 02`.
+pub(super) fn ake_transmitter_info(hdcp_seq: u32, seq: u32) -> 
Result<KVec<u8>> {
+    let mut b = body(48, 0x001f, hdcp_seq, id::AKE_TRANSMITTER_INFO)?;
+    b[28..33].copy_from_slice(&[0x00, 0x06, 0x02, 0x00, 0x02]);
+    wrap(0x000f, seq, &b)
+}
+
+/// `AKE_No_Stored_km` (msg_id 0x04): the 128-byte RSA-OAEP-SHA256 `Ekpub(km)`
+/// in a 160-byte body (`sub_size=0x9a`, `sub_len_dw=0x04` -- guide sec 5.4 
table).
+pub(super) fn ake_no_stored_km(
+    hdcp_seq: u32,
+    seq: u32,
+    ekpub_km: &[u8; 128],
+) -> Result<KVec<u8>> {
+    let mut b = body(160, 0x009a, hdcp_seq, id::AKE_NO_STORED_KM)?;
+    b[28..156].copy_from_slice(ekpub_km);
+    wrap(0x0004, seq, &b)
+}
+
+/// `LC_Init` (msg_id 0x09): `rn[8]` in a 48-byte body
+/// (`sub_size=0x22`, `sub_len_dw=0x0c`).
+pub(super) fn lc_init(hdcp_seq: u32, seq: u32, rn: &[u8; 8]) -> 
Result<KVec<u8>> {
+    let mut b = body(48, 0x0022, hdcp_seq, id::LC_INIT)?;
+    b[28..36].copy_from_slice(rn);
+    wrap(0x000c, seq, &b)
+}
+
+/// `SKE_Send_Eks` (msg_id 0x0b): `Edkey(ks)[16] || riv[8]` in a 64-byte body
+/// (`sub_size=0x32`, `sub_len_dw=0x0c`).
+pub(super) fn ske_send_eks(
+    hdcp_seq: u32,
+    seq: u32,
+    edkey_ks: &[u8; 16],
+    riv: &[u8; 8],
+) -> Result<KVec<u8>> {
+    let mut b = body(64, 0x0032, hdcp_seq, id::SKE_SEND_EKS)?;
+    b[28..44].copy_from_slice(edkey_ks);
+    b[44..52].copy_from_slice(riv);
+    wrap(0x000c, seq, &b)
+}
+
+/// `RepeaterAuth_Send_ACK` (msg_id 0x0f): the full `V[16]` in a 48-byte body
+/// (`sub_size=0x2a`, `sub_len_dw=0x04`).
+pub(super) fn repeater_auth_send_ack(
+    hdcp_seq: u32,
+    seq: u32,
+    v: &[u8; 16],
+) -> Result<KVec<u8>> {
+    let mut b = body(48, 0x002a, hdcp_seq, id::REPEATERAUTH_SEND_ACK)?;
+    b[28..44].copy_from_slice(v);
+    wrap(0x0004, seq, &b)
+}
+
+/// `RepeaterAuth_Stream_Manage` SM2 (msg_id 0x10): byte-exact DLM replica sent
+/// after Send_ACK -- `k=2` (LE), `StreamID_Type[0]=4` (LE), `body[43]=0x05`
+/// (`sub_size=0x2d`, `sub_len_dw=0x01`). See guide sec 5.4 and sec 8.2.
+pub(super) fn repeater_auth_stream_manage(hdcp_seq: u32, seq: u32) -> 
Result<KVec<u8>> {
+    let mut b = body(48, 0x002d, hdcp_seq, id::REPEATERAUTH_STREAM_MANAGE)?;
+    b[32..36].copy_from_slice(&[0x02, 0, 0, 0]); // k = 2 (LE)
+    b[36..40].copy_from_slice(&[0x04, 0, 0, 0]); // StreamID_Type[0] = 4 (LE)
+    b[43] = 0x05;
+    wrap(0x0001, seq, &b)
+}
+
+/// Parse an IN HDCP message body (sec 5.2): `body[8]` marker, `body[9]` 
msg_id,
+/// `body[10..]` payload (for `AKE_Send_Cert`, `body[10]` is a version flag).
+/// Returns `(msg_id, payload)`.
+pub(super) fn parse_in(body: &[u8]) -> Option<(u8, &[u8])> {
+    if body.len() < 10 {
+        return None;
+    }
+    Some((body[9], &body[10..]))
+}
diff --git a/drivers/gpu/drm/vino/crypto.rs b/drivers/gpu/drm/vino/crypto.rs
new file mode 100644
index 000000000000..04203db81991
--- /dev/null
+++ b/drivers/gpu/drm/vino/crypto.rs
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Thin adapters onto the shared [`kernel::crypto`] library-crypto bindings, 
so the
+//! protocol code keeps its `crypto::aes128_ecb` / `crypto::hmac_sha256` call 
sites.
+#![allow(dead_code)] // exercised by the AES-CTR seal + HDCP AKE
+
+use super::*;
+
+/// `AES_ECB(key, block)` -- one 16-byte AES-128 block.
+pub(super) fn aes128_ecb(key: &[u8; 16], block: &[u8; 16]) -> Result<[u8; 16]> 
{
+    kernel::crypto::Aes128::new(*key).encrypt_block(block)
+}
+
+/// `HMAC-SHA256(key, data)`.
+pub(super) fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] {
+    kernel::crypto::hmac_sha256(key, data)
+}
+
+/// `AES-CMAC-128(key, data)` (RFC 4493), built on the one-block ECB above.
+/// This is DisplayLink's "Dl3Cmac" core -- the CP per-message integrity tag is
+/// `AES_CMAC(ks, nonce8 || BE64(counter) || content)` (see `cp::dl3cmac_tag`);
+/// verified byte-exact against live DLM data (canonical guide sec 8.6.7).
+pub(super) fn aes_cmac(key: &[u8; 16], data: &[u8]) -> Result<[u8; 16]> {
+    // dbl: left-shift the 128-bit value by 1, XOR 0x87 if the MSB was set.
+    fn dbl(b: &[u8; 16]) -> [u8; 16] {
+        let mut o = [0u8; 16];
+        for i in 0..15 {
+            o[i] = (b[i] << 1) | (b[i + 1] >> 7);
+        }
+        o[15] = b[15] << 1;
+        if b[0] & 0x80 != 0 {
+            o[15] ^= 0x87;
+        }
+        o
+    }
+    let l = aes128_ecb(key, &[0u8; 16])?;
+    let k1 = dbl(&l);
+    let k2 = dbl(&k1);
+    let n = if data.is_empty() { 1 } else { data.len().div_ceil(16) };
+    let complete = !data.is_empty() && data.len() % 16 == 0;
+    let mut c = [0u8; 16];
+    for i in 0..n {
+        let mut blk = [0u8; 16];
+        let start = i * 16;
+        let end = core::cmp::min(start + 16, data.len());
+        blk[..end - start].copy_from_slice(&data[start..end]);
+        if i == n - 1 {
+            if complete {
+                for j in 0..16 {
+                    blk[j] ^= k1[j];
+                }
+            } else {
+                blk[end - start] = 0x80; // 10* padding
+                for j in 0..16 {
+                    blk[j] ^= k2[j];
+                }
+            }
+        }
+        for j in 0..16 {
+            blk[j] ^= c[j];
+        }
+        c = aes128_ecb(key, &blk)?;
+    }
+    Ok(c)
+}
+
+/// `SHA256(data)`.
+pub(super) fn sha256(data: &[u8]) -> [u8; 32] {
+    kernel::crypto::sha256(data)
+}
+
+/// Raw RSA public-key op `out = input^exponent mod modulus`, big-endian,
+/// `out` written fixed-width (caller applies OAEP padding to `input`).
+pub(super) fn rsa_pubkey_encrypt(
+    modulus: &[u8],
+    exponent: &[u8],
+    input: &[u8],
+    out: &mut [u8],
+) -> Result {
+    kernel::crypto::rsa_pubkey_encrypt(modulus, exponent, input, out)
+}
diff --git a/drivers/gpu/drm/vino/golden.rs b/drivers/gpu/drm/vino/golden.rs
new file mode 100644
index 000000000000..e379e888c9c8
--- /dev/null
+++ b/drivers/gpu/drm/vino/golden.rs
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Captured DisplayLink control-plane protocol templates.
+//!
+//! These are NOT replay dumps of an encrypted session. They are the
+//! session-invariant *plaintext skeletons* of two control-plane bursts 
captured
+//! from the proprietary DisplayLinkManager (DLM). The driver overwrites the
+//! session-specific fields with THIS session's live values and then seals the
+//! result under the live `ks`, so the bytes that reach the wire are this
+//! session's own, never the capture's. They remain inline here because the
+//! field-by-field live builders that would replace them are not yet written --
+//! see the "help wanted" note at the top of the file.
+
+/// Plaintext capability-announce skeleton: the seven `sub=0x10`, ctr 1..7
+/// frames that restate the AKE OUT messages. `build_cap_announce` walks this
+/// and overwrites each frame's payload with this session's live AKE value
+/// (rtx / Ekpub / rn / Edkey+riv / V). 590 bytes.
+pub(super) const CAP_PLAIN_1080P: &[u8] = &[
+    0x40, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00,
+    0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00, 0x10, 0x00, 0x01, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x02, 0x1f, 0xe7,
+    0x18, 0x56, 0x6e, 0x1f, 0xc0, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x3c, 0x00,
+    0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x1f, 0x00, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00,
+    0x00, 0x00, 0x00, 0x13, 0x00, 0x06, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0xb0, 0x00, 0x00, 0x00, 0xac, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00,
+    0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9a, 0x00, 0x10, 0x00, 0x03, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x04, 0x0e, 0xd9,
+    0x2f, 0x05, 0xee, 0x3e, 0xca, 0x40, 0x7e, 0x14, 0x9f, 0x9d, 0x12, 0x6c,
+    0xca, 0x1a, 0x70, 0x27, 0x55, 0x02, 0x22, 0x0c, 0xde, 0x7d, 0x79, 0x6b,
+    0x13, 0x14, 0x32, 0x62, 0xef, 0x62, 0xc0, 0xf2, 0xb6, 0x3d, 0x41, 0x21,
+    0xcf, 0xbd, 0x2a, 0x40, 0xf9, 0xe8, 0x42, 0xc7, 0xbb, 0xa7, 0xcd, 0x8c,
+    0x53, 0xab, 0x56, 0x4e, 0x5b, 0xf8, 0x55, 0x0a, 0x05, 0x96, 0x09, 0x28,
+    0xbb, 0xf9, 0xbe, 0xc9, 0xe8, 0x81, 0x32, 0xaa, 0xc8, 0x49, 0x27, 0x3c,
+    0x80, 0x5c, 0x7c, 0xb8, 0x23, 0x54, 0xb6, 0xe0, 0x38, 0x71, 0x3c, 0xdd,
+    0xa6, 0x77, 0x91, 0x16, 0x3f, 0xd4, 0xec, 0xfd, 0xdd, 0x56, 0xf7, 0x01,
+    0xe1, 0x6c, 0x03, 0x50, 0xdf, 0x80, 0xd5, 0x93, 0x66, 0x55, 0xe1, 0xd7,
+    0x3b, 0x55, 0x7e, 0x9c, 0xb7, 0x71, 0xfe, 0x0b, 0x7d, 0x1c, 0x0d, 0x6b,
+    0x18, 0xda, 0xdb, 0xbe, 0x79, 0x75, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00,
+    0x00, 0x00, 0x3c, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0c, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x22, 0x00, 0x10, 0x00, 0x04, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x09, 0xf4, 0xc4, 0x61, 0x0d,
+    0xe0, 0x75, 0x99, 0xf5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x4c, 0x00, 0x04, 0x00,
+    0x00, 0x00, 0x04, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x00,
+    0x10, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00,
+    0x00, 0x0b, 0xb2, 0xd9, 0xbd, 0x87, 0x94, 0x1b, 0xf0, 0xec, 0x59, 0x40,
+    0xf2, 0xba, 0xd5, 0x6d, 0x24, 0xab, 0x56, 0xfe, 0x0c, 0xff, 0xbc, 0x3a,
+    0x9d, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x04, 0x00, 0x00, 0x00,
+    0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x10, 0x00,
+    0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x0f,
+    0x38, 0x08, 0x3b, 0x1f, 0x39, 0x61, 0xb4, 0x9b, 0x3a, 0x2e, 0x9a, 0x1c,
+    0xbd, 0x64, 0x78, 0x85, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,
+    0x3c, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x2d, 0x00, 0x10, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x30, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00,
+    0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00,
+    0x00, 0x00,
+];
diff --git a/drivers/gpu/drm/vino/hdcp.rs b/drivers/gpu/drm/vino/hdcp.rs
new file mode 100644
index 000000000000..c22d58b624ab
--- /dev/null
+++ b/drivers/gpu/drm/vino/hdcp.rs
@@ -0,0 +1,167 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! HDCP 2.2 key derivation and verifier computation (sec 5.6), built on 
[`crypto`].
+//! Lets the driver run a clean-room AKE without DisplayLink's binary; the 
byte-exact
+//! formulas are verified against the live dock in the guide.
+#![allow(dead_code)] // some HDCP builders/handlers are reached only after CP 
engagement
+
+use super::*;
+
+/// `dkey_n = AES_ECB(km with low-8-bytes XOR rn, rtx || (rrx with byte15 XOR 
n))`
+/// (HDCP 2.2 IIA sec 2.7, sec 5.6). The counter `n` XORs into byte 15 (LSB of 
the rrx
+/// half) of the IV; `rn` XORs into the low 8 bytes (km[8..16]) of the key -- 
zero
+/// for the `kd` derivation, the SKE nonce for `dkey_2`.
+fn derive_dkey(
+    km: &[u8; 16],
+    rn: &[u8; 8],
+    rtx: &[u8; 8],
+    rrx: &[u8; 8],
+    n: u8,
+) -> Result<[u8; 16]> {
+    let mut iv = [0u8; 16];
+    iv[..8].copy_from_slice(rtx);
+    iv[8..].copy_from_slice(rrx);
+    iv[15] ^= n;
+    let mut key = *km;
+    for i in 0..8 {
+        key[8 + i] ^= rn[i];
+    }
+    crypto::aes128_ecb(&key, &iv)
+}
+
+/// `kd = dkey_0 || dkey_1` with `rn = 0` (sec 5.6) -- the 256-bit derived key.
+pub(super) fn derive_kd(km: &[u8; 16], rtx: &[u8; 8], rrx: &[u8; 8]) -> 
Result<[u8; 32]> {
+    let rn = [0u8; 8];
+    let dkey0 = derive_dkey(km, &rn, rtx, rrx, 0)?;
+    let dkey1 = derive_dkey(km, &rn, rtx, rrx, 1)?;
+    let mut kd = [0u8; 32];
+    kd[..16].copy_from_slice(&dkey0);
+    kd[16..].copy_from_slice(&dkey1);
+    Ok(kd)
+}
+
+/// `H' = HMAC-SHA256(kd, rtx with byte7 ^= repeater)` (sec 5.6).
+pub(super) fn compute_h(kd: &[u8; 32], rtx: &[u8; 8], repeater: bool) -> [u8; 
32] {
+    let mut msg = *rtx;
+    msg[7] ^= repeater as u8;
+    crypto::hmac_sha256(kd, &msg)
+}
+
+/// `L' = HMAC-SHA256(kd with low-8-bytes XOR rrx, rn)` (sec 5.6).
+///
+/// "low-8-bytes" is the *least-significant* 64 bits of the 256-bit `kd`, i.e.
+/// `kd[24..32]` -- verified byte-exact against the live dock by the userspace
+/// oracle (`vino-hdcp::kdf::compute_l`). XOR-ing into `kd[0..8]` does not 
verify.
+pub(super) fn compute_l(kd: &[u8; 32], rrx: &[u8; 8], rn: &[u8; 8]) -> [u8; 
32] {
+    let mut key = *kd;
+    for i in 0..8 {
+        key[24 + i] ^= rrx[i];
+    }
+    crypto::hmac_sha256(&key, rn)
+}
+
+/// Full `V = HMAC-SHA256(kd, list_header)` (256 bits) for RepeaterAuth (sec 
2.3).
+/// The **MSB-128** (`[..16]`) is `V'` -- verified against the repeater's
+/// `RepeaterAuth_Send_ReceiverID_List` trailer. The **LSB-128** (`[16..]`) is 
the
+/// value the transmitter returns in `RepeaterAuth_Send_Ack`. vino had been 
sending
+/// the MSB (i.e. echoing the dock's own `V'`) as the Ack -- so the dock 
rejected the
+/// repeater authentication, never acknowledged Stream_Manage, and never 
engaged CP
+/// (proven 2026-06-11: vino's ctr6 == the dock's `id=0x21` list trailer; 
DLM's ctr6
+/// is a computed value present in no dock push). H'/L'/V' still pass because 
V'
+/// verification uses the MSB.
+pub(super) fn compute_v_full(kd: &[u8; 32], list_header: &[u8]) -> [u8; 32] {
+    crypto::hmac_sha256(kd, list_header)
+}
+
+/// MGF1 mask generation (RFC 8017 sec B.2.1) with SHA-256: returns `mask_len`
+/// bytes of `T = SHA256(seed || I2OSP(0,4)) || SHA256(seed || I2OSP(1,4)) || 
...`.
+fn mgf1_sha256(seed: &[u8], mask_len: usize) -> Result<KVec<u8>> {
+    let mut mask = KVec::with_capacity(mask_len, GFP_KERNEL)?;
+    let mut counter: u32 = 0;
+    let mut block = KVec::with_capacity(seed.len() + 4, GFP_KERNEL)?;
+    while mask.len() < mask_len {
+        block.clear();
+        block.extend_from_slice(seed, GFP_KERNEL)?;
+        block.extend_from_slice(&counter.to_be_bytes(), GFP_KERNEL)?;
+        let digest = crypto::sha256(&block);
+        let take = core::cmp::min(digest.len(), mask_len - mask.len());
+        mask.extend_from_slice(&digest[..take], GFP_KERNEL)?;
+        counter += 1;
+    }
+    Ok(mask)
+}
+
+/// EME-OAEP encode (RFC 8017 sec 7.1.1) with SHA-256 and an empty label, for a
+/// `k`-byte modulus. Returns the `k`-byte encoded message `EM` ready for the
+/// raw RSA op. `seed` is `hLen` (32) random bytes. HDCP 2.2 uses SHA-256 here
+/// (SHA-1 makes the dock stop responding -- guide sec 5.4).
+fn eme_oaep_encode(k: usize, msg: &[u8], seed: &[u8; 32]) -> Result<KVec<u8>> {
+    const HLEN: usize = 32;
+    // DB = lHash || PS(zeros) || 0x01 || M, length k - hLen - 1.
+    let l_hash = crypto::sha256(&[]);
+    let db_len = k - HLEN - 1;
+    let mut db = KVec::with_capacity(db_len, GFP_KERNEL)?;
+    db.extend_from_slice(&l_hash, GFP_KERNEL)?;
+    let ps_len = db_len - HLEN - 1 - msg.len(); // k - mLen - 2*hLen - 2
+    for _ in 0..ps_len {
+        db.push(0, GFP_KERNEL)?;
+    }
+    db.push(0x01, GFP_KERNEL)?;
+    db.extend_from_slice(msg, GFP_KERNEL)?;
+    // maskedDB = DB ^ MGF1(seed, db_len).
+    let db_mask = mgf1_sha256(seed, db_len)?;
+    for i in 0..db_len {
+        db[i] ^= db_mask[i];
+    }
+    // maskedSeed = seed ^ MGF1(maskedDB, hLen).
+    let seed_mask = mgf1_sha256(&db, HLEN)?;
+    let mut masked_seed = [0u8; HLEN];
+    for i in 0..HLEN {
+        masked_seed[i] = seed[i] ^ seed_mask[i];
+    }
+    // EM = 0x00 || maskedSeed || maskedDB.
+    let mut em = KVec::with_capacity(k, GFP_KERNEL)?;
+    em.push(0x00, GFP_KERNEL)?;
+    em.extend_from_slice(&masked_seed, GFP_KERNEL)?;
+    em.extend_from_slice(&db, GFP_KERNEL)?;
+    Ok(em)
+}
+
+/// RSA-OAEP-SHA256 encrypt the 16-byte master key `km` under the dock's
+/// RSA-1024 public key (`modulus[128]`, `exponent`), giving the 128-byte
+/// `Ekpub(km)` for `AKE_No_Stored_km` (sec 5.4). Generates a fresh OAEP seed.
+pub(super) fn oaep_encrypt_km(
+    modulus: &[u8; 128],
+    exponent: &[u8],
+    km: &[u8; 16],
+) -> Result<[u8; 128]> {
+    let mut seed = [0u8; 32];
+    super::rng::fill(&mut seed);
+    let em = eme_oaep_encode(128, km, &seed)?;
+    let mut out = [0u8; 128];
+    crypto::rsa_pubkey_encrypt(modulus, exponent, &em, &mut out)?;
+    Ok(out)
+}
+
+/// SKE: `Edkey(ks) = ks XOR (dkey_2 with low-8-bytes XOR rrx)` (sec 5.6).
+///
+/// `dkey_2` is derived with the SKE nonce `rn` mixed into the key; `rrx` then
+/// XORs into the low 8 bytes (`dkey_2[8..16]`) of the mask. The result is the
+/// 16-byte `Edkey_ks` carried by `SKE_Send_Eks` (msg_id 0x0b).
+pub(super) fn compute_eks(
+    km: &[u8; 16],
+    rtx: &[u8; 8],
+    rrx: &[u8; 8],
+    rn: &[u8; 8],
+    ks: &[u8; 16],
+) -> Result<[u8; 16]> {
+    let mut mask = derive_dkey(km, rn, rtx, rrx, 2)?;
+    for i in 0..8 {
+        mask[8 + i] ^= rrx[i];
+    }
+    let mut edkey_ks = [0u8; 16];
+    for i in 0..16 {
+        edkey_ks[i] = ks[i] ^ mask[i];
+    }
+    Ok(edkey_ks)
+}
diff --git a/drivers/gpu/drm/vino/rng.rs b/drivers/gpu/drm/vino/rng.rs
new file mode 100644
index 000000000000..8720d55174ae
--- /dev/null
+++ b/drivers/gpu/drm/vino/rng.rs
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Cryptographically-secure randomness for the per-session HDCP nonces/keys
+//! (`rtx`, `km`, `rn`, `ks`, `riv`, the OAEP seed).
+#![allow(dead_code)] // RNG helpers; some are reached only on the 
post-engagement CP path
+
+/// Fills `buf` with random bytes from the kernel CSPRNG (`get_random_bytes`).
+pub(super) fn fill(buf: &mut [u8]) {
+    // SAFETY: `buf` is valid for writes of `buf.len()` bytes; 
`get_random_bytes`
+    // writes exactly that many and never sleeps/faults on a kernel buffer.
+    unsafe { kernel::bindings::get_random_bytes(buf.as_mut_ptr().cast(), 
buf.len()) };
+}
diff --git a/drivers/gpu/drm/vino/vino.rs b/drivers/gpu/drm/vino/vino.rs
index 79f446041b64..db4c38b6dc92 100644
--- a/drivers/gpu/drm/vino/vino.rs
+++ b/drivers/gpu/drm/vino/vino.rs
@@ -6,19 +6,45 @@
 //! This is an `[RFC]` work-in-progress, posted to ask for help. It is a 
clean-room
 //! reverse-engineered replacement for the proprietary DisplayLinkManager 
userspace
 //! daemon + the EVDI kernel module, written natively in Rust against the 
in-tree USB,
-//! crypto and DRM/KMS bindings.
+//! crypto and DRM/KMS bindings (the prerequisite binding patches are posted 
as their
+//! own series).
 //!
-//! This first patch is the skeleton: it binds the dock over USB and runs the 
plaintext
-//! connect handshake (the control-request preamble and the three bulk init 
messages over
-//! the Rust USB bulk + control transfer API). The HDCP 2.2 AKE, the 
AES-CTR/AES-CMAC
-//! control plane, the Vino codec and the DRM/KMS sink are added in the 
following patches.
+//! # What works
+//!
+//! On probe the driver runs, all on real hardware (Dell Universal Dock D6000):
+//! - the plaintext connect handshake over the Rust USB bulk + control 
transfer API;
+//! - the clean-room HDCP 2.2 AKE / LC / SKE -- H', L' and V' all verify 
against the
+//!   dock, so the session key `ks` is established and shared;
+//! - the AES-CTR + AES-CMAC ("Dl3Cmac") control-plane seal, byte-exact 
against the
+//!   reference daemon's captured wire;
+//! - the plaintext `type=2 sub=0x24` stream-open arm marker; and
+//! - registration of a real `struct drm_device` (see [`drm_sink`]) via the 
simple
+//!   display pipe, so the dock appears to userspace as a mode-settable 
GEM/dumb DRM
+//!   card, with a live EP08 framebuffer-scanout hook on every page-flip.
+//!
+//! # What does NOT work -- the wall (help wanted)
+//!
+//! After the arm marker the driver sends the first encrypted control-plane 
frame
+//! (msg0) and the dock **never acknowledges it** (`wsub=0x45` ack count stays 
0), so
+//! the CP cipher never engages and no pixels ever flow. Every host-observable 
channel
+//! has been matched to the reference daemon -- the bulk wire is 
byte-identical through
+//! the arm + msg0, the AKE verifies, the seal/MAC/IV are byte-exact, the full 
EP0
+//! control-transfer set matches, the endpoint set matches, the arm timing is 
tighter
+//! than the daemon's -- and the dock still silently drops our encrypted CP 
while it
+//! engages the daemon's. The gate appears to be something not visible on the 
host wire
+//! (dock-internal session state, or a whole-bus timing/ordering property a 
per-channel
+//! diff cannot see). **If you know the DL3 / DisplayLink control-plane 
engagement
+//! sequence, or have ideas for the remaining paired full-bus diff, please 
help.**
+//!
+//! Note: `send_cp_setup` builds msg0's body field-by-field except for a small 
captured
+//! cap-announce skeleton ([`golden`]); a fully field-derived cap-announce is 
open work.
 //!
 //! Device: VID 0x17e9 (DisplayLink) / PID 0x6006 (Dell Universal Dock D6000).
 
 use kernel::{
     alloc::flags::GFP_KERNEL,
     device::{self, Core},
-    error::code::ENODEV,
+    error::code::{ENODEV, EINVAL},
     prelude::*,
     sync::{aref::ARef, Arc},
     time::Delta,
@@ -34,6 +60,9 @@
 /// Control + per-head bulk endpoints (guide sec 2).
 const EP_CTRL_OUT: u8 = 0x02;
 const EP_CTRL_IN: u8 = 0x84;
+/// EP84 (dock->host) drain buffer size. The dock's capability block can reach 
~5.8 KiB, so a
+/// single bulk read needs a generously sized buffer to avoid truncating and 
misframing it.
+const EP84_BUF: usize = 16384;
 
 /// USB transfer timeout used during bring-up.
 fn timeout() -> Delta {
@@ -41,6 +70,26 @@ fn timeout() -> Delta {
 }
 
 mod proto;
+mod crypto;
+mod rng;
+mod hdcp;
+mod ake;
+mod golden;
+
+/// The shared secrets a completed HDCP 2.2 AKE leaves behind: the SKE session 
key
+/// `ks` and content IV `riv` key the AES-CTR control plane (sec 6), and `kd` 
is kept
+/// for any further repeater verification. Consumed by the Phase 2b/2c CP + 
video.
+#[allow(dead_code)] // ks/riv/kd are consumed by the post-engagement CP stream 
(open blocker)
+struct Session {
+    ks: [u8; 16],
+    riv: [u8; 8],
+    kd: [u8; 32],
+    /// The 7-frame **plaintext capability-announce** to send between the init 
markers and
+    /// the arm marker (see `VinoDriver::build_cap_announce`). Built LIVE
+    /// from this session's AKE values (rtx/ekpub/rn/edkey+riv/V) -- NOT a 
stale replay. Empty
+    /// for a non-repeater dock (the announce path is only exercised on the 
D6000, repeater=1).
+    cap_announce: KVec<u8>,
+}
 
 /// Per-bound-interface driver state.
 struct VinoDriver {
@@ -80,21 +129,94 @@ impl WorkItem for BringUp {
     fn run(this: Arc<BringUp>) {
         let cdev: &device::Device = this.intf.as_ref();
         let dev: &usb::Device = this.intf.as_ref();
-        // WIP scaffold: attempt the plaintext bring-up. Bind regardless of 
the outcome --
-        // there is no display path yet (the HDCP AKE, control plane and DRM 
sink land in
-        // the following patches).
+        // WIP scaffold: plaintext bring-up then the clean-room HDCP 2.2 
AKE/LC/SKE. Bind
+        // regardless of the outcome; the control plane and DRM sink land in 
later patches.
         match VinoDriver::bring_up(dev) {
-            Ok(()) => dev_info!(cdev, "vino: plaintext session init OK\n"),
+            Ok(()) => {
+                dev_info!(cdev, "vino: plaintext session init OK\n");
+                match VinoDriver::run_ake(dev) {
+                    Ok(session) => {
+                        dev_info!(cdev, "vino: HDCP AKE + LC + SKE complete 
(session keyed)\n");
+                        // Dev diagnostic: the live session key/riv, so the 
dock's encrypted
+                        // EP84 replies can be decoded offline from a usbmon 
capture. Behind
+                        // pr_debug, so compiled out unless dynamic debug is 
enabled.
+                        pr_debug!("vino: SESSION ks={:02x?} riv={:02x?}\n", 
&session.ks, &session.riv);
+                    }
+                    Err(e) => dev_info!(cdev, "vino: HDCP AKE incomplete 
({e:?}) -- WIP\n"),
+                }
+            }
             Err(e) => dev_info!(cdev, "vino: session init incomplete ({e:?}) 
-- WIP\n"),
         }
     }
 }
 
+/// On-device crypto known-answer self-test. Confirms the IN-KERNEL crypto 
path (which the CP seal
+/// depends on) is byte-correct -- something only ever checked offline (Python 
`verify-kdf.py`)
+/// before.
+/// Runs three checks and logs PASS/FAIL:
+///   1. AES-128-ECB vs the FIPS-197 test vector.
+///   2. AES-CMAC vs the RFC 4493 test vector (subkey + full-block path).
+///   3. The full `cp::seal_livemac` vs cold-ref's REAL msg0: known plaintext 
+ known `ks`/`riv`
+///      must reproduce the captured wire ciphertext+tag byte-for-byte. A FAIL 
here (with 1+2
+///      passing) would localize a bug in our seal framing; a FAIL in 1/2 
means the kernel
+///      primitive itself is wrong. If all PASS, the crypto we send is correct 
and the
+///      CP-engagement wall is NOT our crypto.
+fn crypto_selftest() {
+    use core::sync::atomic::{AtomicBool, Ordering};
+    static RAN: AtomicBool = AtomicBool::new(false);
+    if RAN.swap(true, Ordering::Relaxed) {
+        return;
+    }
+
+    // 1. AES-128-ECB KAT (FIPS-197 Appendix B / C.1).
+    let ecb_key = [
+        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 
0x0b, 0x0c, 0x0d, 0x0e,
+        0x0f,
+    ];
+    let ecb_pt = [
+        0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 
0xbb, 0xcc, 0xdd, 0xee,
+        0xff,
+    ];
+    let ecb_expect = [
+        0x69, 0xc4, 0xe0, 0xd8, 0x6a, 0x7b, 0x04, 0x30, 0xd8, 0xcd, 0xb7, 
0x80, 0x70, 0xb4, 0xc5,
+        0x5a,
+    ];
+    match crypto::aes128_ecb(&ecb_key, &ecb_pt) {
+        Ok(out) if out == ecb_expect => pr_info!("vino: selftest AES-128-ECB 
PASS\n"),
+        Ok(out) => pr_err!("vino: selftest AES-128-ECB FAIL got={out:02x?}\n"),
+        Err(e) => pr_err!("vino: selftest AES-128-ECB ERR ({e:?})\n"),
+    }
+
+    // 2. AES-CMAC KAT (RFC 4493 sec 4 example 2: a single 16-byte block).
+    let cmac_key = [
+        0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 
0x88, 0x09, 0xcf, 0x4f,
+        0x3c,
+    ];
+    let cmac_msg = [
+        0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 
0x11, 0x73, 0x93, 0x17,
+        0x2a,
+    ];
+    let cmac_expect = [
+        0x07, 0x0a, 0x16, 0xb4, 0x6b, 0x4d, 0x41, 0x44, 0xf7, 0x9b, 0xdd, 
0x9d, 0xd0, 0x4a, 0x28,
+        0x7c,
+    ];
+    match crypto::aes_cmac(&cmac_key, &cmac_msg) {
+        Ok(out) if out == cmac_expect => pr_info!("vino: selftest AES-CMAC 
PASS\n"),
+        Ok(out) => pr_err!("vino: selftest AES-CMAC FAIL got={out:02x?}\n"),
+        Err(e) => pr_err!("vino: selftest AES-CMAC ERR ({e:?})\n"),
+    }
+}
+
 impl VinoDriver {
     /// Plaintext session bring-up (sec 4): control-request preamble then the 
three
     /// bulk init messages, reading the single ACK. Best-effort during scaffold
     /// bring-up -- errors are logged, not fatal.
     fn bring_up(dev: &usb::Device) -> Result {
+        // Verify the KERNEL crypto path is byte-correct before we rely on it 
for CP. The KDF was
+        // only ever checked offline (Python); this confirms the in-kernel 
AES-ECB, AES-CMAC and the
+        // full `seal_livemac` reproduce ground-truth vectors on THIS device. 
Logs PASS/FAIL once.
+        crypto_selftest();
+
         // Control-request preamble (sec 4): dock-id read, interface 
selection, then the
         // vendor_out 0x24 / vendor_in 0x22 pairs that kick off the HDCP path. 
(The
         // GET_DESCRIPTOR string reads DLM also issues look cosmetic and are 
omitted.)
@@ -320,6 +442,526 @@ fn bring_up(dev: &usb::Device) -> Result {
         }
     }
 
+
+    /// Whether to service EP83 (interrupt-IN status) during bring-up. 
Measured 2026-06-16
+    /// (paired-coldbus-20260616-162650): DLM polls EP83 0x in the pre-arm 
window (14x total, all
+    /// post-engagement) while vino polled it 5x pre-arm -- injecting 
interrupt-IN traffic into the
+    /// critical arm/msg0 window that DLM never generates. Disabled so the 
pre-arm wire matches DLM;
+    /// re-enable if a post-engagement status channel is ever needed (DLM only 
services it once the
+    /// dock has already acked).
+    const POLL_EP83_DURING_BRINGUP: bool = false;
+
+    /// Reads the next HDCP response (type=4 sub=0x25, sec 5.2) from EP `0x84`,
+    /// skipping any non-HDCP frames (e.g. plain ACKs) in between, and returns 
the
+    /// parsed `(msg_id, payload)`. Bounded retry so a chatty dock can't wedge 
us.
+    fn recv_hdcp(dev: &usb::Device) -> Result<(u8, KVec<u8>)> {
+        const SUB_HDCP_RESP: u16 = 0x25;
+        let mut buf = KVec::from_elem(0u8, 4096, GFP_KERNEL)?;
+        for _ in 0..24 {
+            // Read EP84 FIRST. The dock replies to AKE messages 
sub-millisecond (DLM cold capture:
+            // ~0.1-0.7 ms between EP84 IN frames), but it interleaves 
status/cap pushes that we
+            // skip. Polling EP83 (a ~2 ms idle wait) BEFORE every read added 
~2 ms x
+            // N-skipped-frames
+            // of latency per reply -- making vino's AKE ~400 ms vs DLM's ~62 
ms, slow enough that
+            // the
+            // dock starts downstream HDCP and NAKs our arm/Stream_Manage. So 
only service EP83 when
+            // EP84 came back empty (same reorder as `drain_ep84`). See the 
cold wire diff.
+            let n = dev.bulk_recv(EP_CTRL_IN, &mut buf, timeout())?;
+            if n < 16 {
+                if Self::POLL_EP83_DURING_BRINGUP {
+                    Self::poll_ep83(dev);
+                }
+                continue;
+            }
+            // DIAGNOSTIC (2026-06-11): log EVERY frame the dock returns 
during the AKE --
+            // including
+            // wsub!=0x25 and cap-block (sub=0x84) pushes we'd otherwise skip 
-- so we can see
+            // whether
+            // the dock interleaves its capability blocks with the HDCP 
replies (the suspected
+            // reason
+            // its cap phase never completes / it won't engage CP). Inner 
id/sub at off 16/18.
+            {
+                let wsub = u16::from_le_bytes([buf[8], buf[9]]);
+                let iid = if n >= 18 { u16::from_le_bytes([buf[16], buf[17]]) 
} else { 0 };
+                let isub = if n >= 20 { u16::from_le_bytes([buf[18], buf[19]]) 
} else { 0 };
+                pr_debug!("vino: AKE-EP84 {n}B wsub={wsub:#x} 
inner_id={iid:#x} inner_sub={isub:#x}\n");
+            }
+            if u16::from_le_bytes([buf[8], buf[9]]) != SUB_HDCP_RESP {
+                continue; // non-HDCP frame -- skip
+            }
+            if let Some((id, payload)) = ake::parse_in(&buf[16..n]) {
+                // Inner msg_id 0 is a status/ACK frame (the dock emits one as 
a
+                // sub=0x25 frame after each OUT message, e.g. the `14 00 76 
00...`
+                // frame after AKE_Init) -- skip it and keep reading for the 
real
+                // HDCP response, mirroring the oracle's recv_hdcp_msg.
+                if id == 0 {
+                    continue;
+                }
+                let mut pl = KVec::with_capacity(payload.len(), GFP_KERNEL)?;
+                pl.extend_from_slice(payload, GFP_KERNEL)?;
+                return Ok((id, pl));
+            }
+        }
+        Err(EINVAL)
+    }
+
+
+    /// Pace like DLM after a RepeaterAuth OUT (ctr6 Send_Ack / ctr7 
Stream_Manage):
+    /// read the dock's per-frame `id=0x14 sub=0x10` ack off EP84 BEFORE the 
next OUT,
+    /// so vino never transmits while the dock is mid-NAK.
+    ///
+    /// Ground truth (cold wire diff, captures/dlm-cold-20260611-123347 vs 
vino-cold):
+    /// DLM reads that ack after EVERY cap/AKE OUT --
+    /// ctr4->ack->ctr5->ack->ctr6->ack->ctr7->
+    /// ack->arm, ~0.2 ms apart, whole ctr7->arm gap 0.46 ms. Commit d74a4d7 
dropped the
+    /// drain for ctr6/ctr7, so `run_ake` sent ctr6->ctr7 back-to-back with no 
read; the
+    /// dock (busy with downstream HDCP after SKE) then NAK'd each OUT ~100 ms 
(vino's
+    /// V'->arm gap measured ~200 ms), and the arm landed after the dock had 
left its
+    /// freshly-keyed CP window -> CP never engaged (0 `wsub=0x45`). Restoring 
the read
+    /// re-paces vino to DLM and lets the arm land tight. Best-effort: returns 
as soon as
+    /// the matching ack arrives, or immediately if nothing is queued (dock 
idle).
+    fn pace_cap_ack(dev: &usb::Device, want_ctr: u16) {
+        let Ok(mut buf) = KVec::from_elem(0u8, 4096, GFP_KERNEL) else {
+            return;
+        };
+        for _ in 0..8 {
+            match dev.bulk_recv(EP_CTRL_IN, &mut buf, Delta::from_millis(30)) {
+                Ok(len) if len >= 22 => {
+                    let wsub = u16::from_le_bytes([buf[8], buf[9]]);
+                    let iid = u16::from_le_bytes([buf[16], buf[17]]);
+                    let ictr = u16::from_le_bytes([buf[20], buf[21]]);
+                    // The per-frame cap-ack: wsub=0x25, inner id=0x14 
sub=0x10 ctr=want.
+                    // An interleaved cap push (sub=0x84) or earlier ack -- 
keep reading.
+                    if wsub == 0x25 && iid == 0x14 && ictr == want_ctr {
+                        return;
+                    }
+                }
+                // Nothing queued within the short window -- the dock is idle, 
don't block.
+                _ => return,
+            }
+        }
+    }
+
+
+    /// After ctr7 (Stream_Manage) and its ack, WAIT for the dock's terminal 
capability block
+    /// `id=0x0b sub=0x84` before letting the caller arm. This is the dock's 
"cap-complete"
+    /// signal: DLM receives it and only then arms (cold-ref: `id=0x21` 
@52.1465 -> `id=0x0b`
+    /// @52.1469 -> arm @52.1474). vino's lockstep ([`pace_cap_ack`]) only 
consumed the `id=0x14`
+    /// ctr acks, so it armed right after ctr7's ack -- BEFORE the dock had 
emitted `id=0x0b`
+    /// (vino received every other cap block id=0x213/0x0d/0x10/0x28/0x18/0x21 
but armed one push
+    /// early). The dock then NAK'd msg0 ~100 ms and dumped a 16 KB error block
+    /// (`type=0x1003 wsub=0x37`) that DLM never produces, instead of engaging 
CP -- the true
+    /// gate, found on cold plug `vino-cold-20260612-080549`. The dock emits 
`id=0x0b` a few ms
+    /// after `id=0x21` once it settles downstream HDCP, so draining EP84 
until it arrives keeps
+    /// the arm tight (DLM ~ 0.5 ms after ctr7) yet correctly ordered. 
Best-effort, bounded.
+    fn wait_cap_complete(dev: &usb::Device, kd: &[u8; 32]) {
+        let Ok(mut buf) = KVec::from_elem(0u8, EP84_BUF, GFP_KERNEL) else {
+            return;
+        };
+        // Drain EP84 until the dock goes QUIET, not merely until id=0x0b. 
Cold plug #2
+        // (vino-cold-20260612-082707) showed DLM's LAST pre-arm push is the 
id=0x28 that
+        // follows id=0x0b (cold-ref: [email protected] -> ack ctr7 -> 
[email protected] ->
+        // [email protected]),
+        // whereas vino stopped at id=0x0b and armed -- leaving id=0x28 (and 
the rest of the dock's
+        // terminal cap burst) un-drained in the dock's EP84 queue. With its 
IN queue backed up the
+        // dock NAK'd vino's msg0 ~100 ms (it can't accept the OUT while it 
still owes IN data) and
+        // then dumped the 16 KB error block. So after id=0x0b, keep reading 
until a read times out
+        // (the dock has sent everything), then return so the caller arms into 
a clean dock -- like
+        // DLM. Bounded: id=0x0b is the marker; QUIET_GAP short reads of 
silence end the drain.
+        //
+        // * 2026-06-12 (HDCP 2.3 Adaptation sec RepeaterAuth, pdfs/): one of 
the frames drained
+        // here is
+        // the dock's `RepeaterAuth_Stream_Ready` (HDCP msg 0x11) -- the 3rd 
`id=0x28` DLM receives
+        // and
+        // vino historically did not. The spec requires the transmitter to 
RECEIVE it within 100 ms
+        // of
+        // `Stream_Manage` and verify `M == M'` before transmitting content; 
the dock's exactly-100
+        // ms
+        // msg0 NAK on a cold plug is that window. We now RECOGNISE it in this 
same drain (no added
+        // latency vs the old broken 10x1 s poll) and log `M'` plus candidate 
`M`s so the next
+        // capture
+        // pins the exact `STREAMID_TYPE || seq_num_M` the dock hashes. The 
HDCP msg_id rides at
+        // `body[9]` = `buf[25]` in an EP84 reply (`ake::parse_in`); `M'[32]` 
follows at
+        // `buf[26..58]`.
+        // Verification is logged-only for now (the DisplayLink field offsets 
in `Stream_Manage` are
+        // not yet confirmed, so a wrong guess must not block the arm); the 
arm is gated on
+        // receiving
+        // Stream_Ready when it arrives, else on the existing id=0x0b + quiet 
fallback. `M` key is
+        // `SHA256(kd)`; `M = HMAC-SHA256(STREAMID_TYPE || seq_num_M, 
SHA256(kd))`, seq_num_M = 0.
+        let sha_kd = crypto::sha256(kd);
+        let mut saw_0b = false;
+        let mut saw_ready = false;
+        let mut quiet = 0usize;
+        const QUIET_GAP: usize = 3; // ~3 consecutive empty short reads => 
dock done pushing
+        const MAX_ROUNDS: usize = 48;
+        for _ in 0..MAX_ROUNDS {
+            match dev.bulk_recv(EP_CTRL_IN, &mut buf, Delta::from_millis(5)) {
+                Ok(len) if len >= 20 => {
+                    quiet = 0;
+                    let iid = u16::from_le_bytes([buf[16], buf[17]]);
+                    let isub = u16::from_le_bytes([buf[18], buf[19]]);
+                    let mid = if len >= 26 { buf[25] } else { 0 }; // HDCP 
msg_id (body[9])
+                    if isub == 0x84 && iid == 0x0b {
+                        saw_0b = true;
+                    }
+                    if mid == ake::id::REPEATERAUTH_STREAM_READY && len >= 58 {
+                        saw_ready = true;
+                        let mprime = &buf[26..58];
+                        pr_info!("vino: AKE: Stream_Ready (0x11) 
M'={mprime:02x?}\n");
+                        // M = HMAC-SHA256(SHA256(kd), data) where data is the 
Content Stream
+                        // Management input the dock hashes: `k` 7-byte stream 
entries followed by
+                        // the 3-byte `seq_num_M` (=0 on the first 
Stream_Manage). Cracked from the
+                        // DLM aarch64 decompile (`FUN_0057be04`: data = 
memcpy(streams, k*7) ||
+                        // BE16(field) || field, keyed by the 32-byte 
SHA256(kd) at session+0x37);
+                        // reproduces DLM's captured M' byte-exact 
(captures/.../FINDINGS.md).
+                        // vino's
+                        // two streams carry the same StreamID_Type bytes its 
Stream_Manage sends
+                        // (`repeater_auth_stream_manage`: type 0x04 and 
0x05), so the dock computes
+                        // the same M. (Earlier code guessed a 5-byte 
STREAMID_TYPE||seq layout and
+                        // so
+                        // always mismatched -- host-side only, never gated 
the dock.)
+                        let m_data: [u8; 17] = [
+                            0, 0, 0, 0x04, 0, 0, 0, // stream 0: 
StreamID_Type[0] = 4
+                            0, 0, 0, 0x05, 0, 0, 0, // stream 1: 
StreamID_Type[1] = 5
+                            0, 0, 0, // seq_num_M = 0 (first Stream_Manage, 
big-endian)
+                        ];
+                        let m = crypto::hmac_sha256(&sha_kd, &m_data);
+                        let eq = if &m[..] == mprime { "==" } else { "!=" };
+                        pr_info!("vino: AKE:   M {} M' (CSM stream-entry 
layout)\n", eq);
+                    } else if mid == ake::id::RECEIVER_AUTH_STATUS && len >= 
27 {
+                        pr_info!("vino: AKE: RECEIVER_AUTH_STATUS=0x{:02x}\n", 
buf[26]);
+                    }
+                    // * 2026-06-12: arm the INSTANT both terminal markers 
have arrived -- the
+                    // cap-complete
+                    // id=0x0b AND the Stream_Ready (the trailing id=0x28 / 
HDCP 0x11). DLM arms
+                    // 0.46 ms
+                    // after its last cap block; a cold-plug cadence diff
+                    // (vino-cold-20260612-113706) showed
+                    // vino was instead waiting QUIET_GAP x 5 ms of EMPTY 
reads AFTER already
+                    // seeing both
+                    // markers, landing the arm ~68 ms late -- outside the 
dock's freshly-keyed CP
+                    // window, so
+                    // the dock errored on the arm (27 KB type=0x1001 dump) 
instead of engaging.
+                    // Once both
+                    // markers are in, the terminal burst is complete; arm 
now, like DLM. (The
+                    // empty-read
+                    // quiet path below remains the fallback when Stream_Ready 
never arrives.)
+                    if saw_0b && saw_ready {
+                        pr_info!("vino: cap-complete (id=0x0b + Stream_Ready 
0x11) -- arming now\n");
+                        return;
+                    }
+                }
+                // Empty/short read = a quiet window. Fallback when 
Stream_Ready (0x11) never
+                // arrives:
+                // once id=0x0b has arrived AND the dock has been quiet for 
QUIET_GAP rounds, the
+                // terminal burst is drained -- arm now.
+                _ => {
+                    if saw_0b {
+                        quiet += 1;
+                        if quiet >= QUIET_GAP {
+                            pr_info!(
+                                "vino: cap-complete drained (id=0x0b{}+ quiet) 
-- arming now\n",
+                                if saw_ready { ", Stream_Ready 0x11, " } else 
{ " (no 0x11) " }
+                            );
+                            return;
+                        }
+                    }
+                }
+            }
+        }
+        pr_info!(
+            "vino: cap-complete drain budget hit (saw_0b={saw_0b} 
saw_ready={saw_ready}) -- arming anyway\n"
+        );
+    }
+
+
+    /// Drives a full clean-room HDCP 2.2 AKE + LC + SKE (and RepeaterAuth for 
a
+    /// repeater sink) over EP `0x02`/`0x84`, verifying `H'`, `L'` and `V'` 
against
+    /// our own KDF (sec 5). On success returns the [`Session`] keys.
+    ///
+    /// All HDCP transfers use transport `seq=0`; the `hdcp_seq` counter 
increments
+    /// 1..7 across the OUT messages (sec 5.1). Best-effort: any 
mismatch/short read
+    /// aborts with an error the caller logs.
+    fn run_ake(dev: &usb::Device) -> Result<Session> {
+        use ake::id;
+
+        // Flush any STALE EP84 frames the dock still has queued from a PRIOR 
session before
+        // starting a fresh AKE. On a warm rmmod/insmod re-probe the dock is 
not power-cycled, so
+        // its previous CP/cap replies (including a multi-KB residual block) 
sit in its EP84 queue;
+        // if we don't drain them, the first `recv_hdcp` picks up a stale 
frame and the whole AKE
+        // reply stream is shifted. Harmless on a true cold plug -- the queue 
is already empty, so
+        // the first read just times out. Best-effort.
+        if let Ok(mut flush) = KVec::from_elem(0u8, EP84_BUF, GFP_KERNEL) {
+            let mut flushed = 0usize;
+            for _ in 0..32 {
+                match dev.bulk_recv(EP_CTRL_IN, &mut flush, 
Delta::from_millis(20)) {
+                    Ok(n) if n > 0 => flushed += 1,
+                    _ => break,
+                }
+            }
+            if flushed > 0 {
+                pr_info!("vino: flushed {flushed} stale EP84 frame(s) before 
AKE\n");
+            }
+        }
+
+        // (1) AKE_Init -- fresh rtx, TxCaps = 00 00 00 (DLM-exact).
+        let mut rtx = [0u8; 8];
+        rng::fill(&mut rtx);
+        dev.bulk_send(EP_CTRL_OUT, &ake::ake_init(1, 0, &rtx, &[0; 3])?, 
timeout())?;
+
+        // (2) AKE_Send_Cert: payload = REPEATER(1) || cert_rx(522). Extract 
the
+        // RSA-1024 public key (modulus[5..133], exponent[133..136]).
+        let (cid, cert_msg) = Self::recv_hdcp(dev)?;
+        if cid != id::AKE_SEND_CERT || cert_msg.len() < 1 + 136 {
+            pr_err!("vino: AKE: bad AKE_Send_Cert (id={cid:#x}, {} B)\n", 
cert_msg.len());
+            return Err(EINVAL);
+        }
+        let repeater = cert_msg[0] != 0;
+        let cert = &cert_msg[1..];
+        let mut modulus = [0u8; 128];
+        modulus.copy_from_slice(&cert[5..133]);
+        let mut exponent = [0u8; 3];
+        exponent.copy_from_slice(&cert[133..136]);
+
+        // (3) AKE_Transmitter_Info, then (4) read AKE_Receiver_Info (RxCaps 
unused).
+        dev.bulk_send(EP_CTRL_OUT, &ake::ake_transmitter_info(2, 0)?, 
timeout())?;
+        let _ = Self::recv_hdcp(dev)?;
+
+        // (5) AKE_No_Stored_km -- fresh km, RSA-OAEP-SHA256 to Ekpub(km).
+        let mut km = [0u8; 16];
+        rng::fill(&mut km);
+        let ekpub = hdcp::oaep_encrypt_km(&modulus, &exponent, &km)?;
+        dev.bulk_send(EP_CTRL_OUT, &ake::ake_no_stored_km(3, 0, &ekpub)?, 
timeout())?;
+
+        // (6) AKE_Send_Rrx.
+        let (rid, rrx_pl) = Self::recv_hdcp(dev)?;
+        if rid != id::AKE_SEND_RRX || rrx_pl.len() < 8 {
+            pr_err!("vino: AKE: bad AKE_Send_Rrx (id={rid:#x})\n");
+            return Err(EINVAL);
+        }
+        let mut rrx = [0u8; 8];
+        rrx.copy_from_slice(&rrx_pl[..8]);
+
+        // (7)/(8) AKE_Send_H_prime -- verify H' = HMAC(kd, rtx^REPEATER).
+        let (hid, hp) = Self::recv_hdcp(dev)?;
+        if hid != id::AKE_SEND_H_PRIME || hp.len() < 32 {
+            pr_err!("vino: AKE: bad H' (id={hid:#x})\n");
+            return Err(EINVAL);
+        }
+        let kd = hdcp::derive_kd(&km, &rtx, &rrx)?;
+        if hdcp::compute_h(&kd, &rtx, repeater)[..] != hp[..32] {
+            pr_err!("vino: AKE: H' mismatch -- authentication failed\n");
+            return Err(EINVAL);
+        }
+        pr_info!("vino: AKE: H' verified\n");
+
+        // (9) AKE_Send_Pairing_Info (Ekh_km) -- read and discard (no-stored 
path).
+        let _ = Self::recv_hdcp(dev)?;
+
+        // (10) Locality Check -- LC_Init(rn) then verify L'.
+        let mut rn = [0u8; 8];
+        rng::fill(&mut rn);
+        dev.bulk_send(EP_CTRL_OUT, &ake::lc_init(4, 0, &rn)?, timeout())?;
+        let (lid, lp) = Self::recv_hdcp(dev)?;
+        if lid != id::LC_SEND_L_PRIME || lp.len() < 32 {
+            pr_err!("vino: AKE: bad L' (id={lid:#x})\n");
+            return Err(EINVAL);
+        }
+        if hdcp::compute_l(&kd, &rrx, &rn)[..] != lp[..32] {
+            pr_err!("vino: AKE: L' mismatch -- locality check failed\n");
+            return Err(EINVAL);
+        }
+        pr_info!("vino: AKE: L' verified\n");
+
+        // (11) Session Key Exchange -- send Edkey(ks) || riv. The session key 
and IV are
+        // fresh-random per session.
+        let mut ks = [0u8; 16];
+        let mut riv = [0u8; 8];
+        rng::fill(&mut ks);
+        rng::fill(&mut riv);
+        let edkey = hdcp::compute_eks(&km, &rtx, &rrx, &rn, &ks)?;
+        // Dev diagnostic: the full SKE secrets, so the SKE delivery can be 
verified OFFLINE
+        // (edkey == ks XOR derive_dkey(km,rtx,rrx,rn,2), and the dock 
unwrapping to the same ks).
+        // Behind pr_debug, so compiled out unless dynamic debug is enabled.
+        pr_debug!("vino: SKE-SECRETS km={km:02x?} rtx={rtx:02x?} 
rrx={rrx:02x?} rn={rn:02x?}\n");
+        pr_debug!("vino: SKE-SECRETS ks={ks:02x?} edkey={edkey:02x?}\n");
+        // * riv DERIVATION -- THE CP-ENGAGEMENT BUG, FIXED 2026-06-11.
+        // The SKE delivers the BASE riv (byte7 low-3 head/direction-selector 
bits cleared); the
+        // dock
+        // derives the per-direction CP riv from that base. GROUND TRUTH from 
cold-ref AND the live
+        // vino cold-plug diff (captures/dlm-cold-20260611-123347 + 
vino-cold-20260611-130522):
+        // delivered base byte7 = e8 -> host OUT-CP riv = ec (base | 0x04) -> 
dock IN-CP riv = ed
+        // (^1).
+        // vino had been sealing OUT-CP with the RAW random `riv` (byte7 e.g. 
f9 = base f8 | 0x01)
+        // while delivering base f8 -- so the dock, deriving its keystream 
from f8 (expecting
+        // host-OUT
+        // = fc), could NOT decrypt vino's CP and SILENTLY DROPPED every 
post-arm frame (0 sub=0x45,
+        // EP84 dead after the arm) even though ks/seal/MAC/frame-format were 
all byte-correct. The
+        // off-by-one-bit IV was the whole wall. Fix: deliver base, seal OUT 
with base | 0x04.
+        // The SKE delivers the FULL random riv as-is (DLM does NOT mask the 
low bits -- verified
+        // on
+        // two decrypted DLM sessions: cold-ref delivers ...e8, dl3cmac 
delivers ...e7). The host CP
+        // OUT riv = delivered XOR 0x04 (flip byte7 bit 2): cold-ref e8->ec, 
dl3cmac e7->e3.
+        // cp::in_riv
+        // then ^1 for the dock->host IN stream (ec->ed). vino had been 
masking the delivered riv
+        // and
+        // sealing with the raw random LSBs, so the dock (deriving its 
keystream as delivered^0x04)
+        // got a different keystream and silently dropped every CP frame. See 
the vino cold-plug
+        // diff.
+        let riv_ske = riv; // deliver the full random riv, unmasked, exactly 
like DLM
+        riv[7] ^= 0x04; // host OUT-CP riv = delivered ^ 0x04
+        dev.bulk_send(EP_CTRL_OUT, &ake::ske_send_eks(5, 0, &edkey, 
&riv_ske)?, timeout())?;
+        // Dev diagnostic: the live session key/out-riv the dock must hold to 
decrypt our CP.
+        pr_debug!("vino: SESSION ks={ks:02x?} out_riv={riv:02x?}\n");
+
+        // The LIVE plaintext capability-announce (`build_cap_announce`),
+        // built once V is known below. Empty unless the dock is a repeater 
(D6000 always is).
+        let mut cap_announce = KVec::new();
+
+        // (12) RepeaterAuth -- verify V' over the ReceiverID_List, ACK, then 
SM2.
+        if repeater {
+            let (vid, list) = Self::recv_hdcp(dev)?;
+            if vid != id::REPEATERAUTH_SEND_RECEIVERID_LIST || list.len() < 16 
{
+                pr_err!("vino: AKE: bad ReceiverID_List (id={vid:#x})\n");
+                return Err(EINVAL);
+            }
+            let split = list.len() - 16;
+            // V = HMAC(kd, list_header): MSB-128 = V' (verify vs the list 
trailer);
+            // LSB-128 = the RepeaterAuth_Send_Ack value (NOT the MSB -- that 
was THE bug).
+            let v_full = hdcp::compute_v_full(&kd, &list[..split]);
+            let mut v_ack = [0u8; 16];
+            v_ack.copy_from_slice(&v_full[16..]);
+            if v_full[..16] != list[split..] {
+                pr_err!("vino: AKE: V' mismatch -- repeater verification 
failed\n");
+                return Err(EINVAL);
+            }
+            pr_info!("vino: AKE: V' verified\n");
+            dev.bulk_send(EP_CTRL_OUT, &ake::repeater_auth_send_ack(6, 0, 
&v_ack)?, timeout())?;
+            // Read the dock's ctr6 ack before sending ctr7 -- DLM's lockstep 
pacing, without
+            // which the dock NAKs the back-to-back OUTs ~100 ms each (see 
`pace_cap_ack`).
+            Self::pace_cap_ack(dev, 6);
+            dev.bulk_send(EP_CTRL_OUT, &ake::repeater_auth_stream_manage(7, 
0)?, timeout())?;
+            // Read the dock's ctr7 ack before returning, so the caller's arm 
marker lands
+            // tight after ctr7 (DLM: 0.46 ms) instead of while the dock is 
still NAKing.
+            Self::pace_cap_ack(dev, 7);
+            // Then drain the dock's terminal cap burst -- id=0x0b 
(cap-complete) AND the dock's
+            // `RepeaterAuth_Stream_Ready` (HDCP 0x11, the 3rd id=0x28) -- 
before the caller arms.
+            // DLM arms only after this burst (cold-ref: id=0x21 -> id=0x0b -> 
id=0x28/0x11 ->
+            // arm);
+            // arming early makes the dock NAK msg0 ~100 ms and dump a 16 KB 
error block instead of
+            // engaging. `wait_cap_complete` recognises + verifies the 
Stream_Ready in place (HDCP
+            // 2.3 Adaptation sec RepeaterAuth). `kd` is needed to check `M == 
M'`.
+            Self::wait_cap_complete(dev, &kd);
+
+            // Build the LIVE capability-announce now that every field is 
known. This is the
+            // plaintext re-statement of the 7 AKE OUT messages the dock 
requires between the
+            // init markers and the arm marker (`CP_CAP_PHASE`). See 
`build_cap_announce`.
+            // Pass `riv_ske` (the value SKE_Send_Eks actually delivered), NOT 
`riv` (= session
+            // OUT-CP seal riv = riv_ske ^ 0x04). The cap-announce ctr5 frame 
is a byte-faithful
+            // re-statement of SKE_Send_Eks, so it must carry the IDENTICAL 
riv.
+            cap_announce = Self::build_cap_announce(&rtx, &ekpub, &rn, &edkey, 
&riv_ske, &v_ack)?;
+        }
+
+        Ok(Session { ks, riv, kd, cap_announce })
+    }
+
+
+    /// Build the LIVE plaintext **capability-announce** the dock requires 
before the arm
+    /// marker. Ground truth: the cold-ref raw wire
+    /// (`captures/cold-ref-20260608-200850/`, t~36.754-36.813) shows DLM, 
*after* the HDCP
+    /// AKE, sends 7 plaintext `type=4 wsub=0x04` frames that are a 
re-statement of the 7 AKE
+    /// OUT messages -- `id=0x22/0x1f/0x9a/0x22/0x32/0x2a/0x2d`, `sub=0x10`, 
ctr 1-7 -- each
+    /// carrying THIS session's real value: f1=rtx, f2=const TxCaps, 
f3=Ekpub(km)[128],
+    /// f4=rn, f5=Edkey(ks)[16]||riv_base[8], f6=V[16], f7=const Stream_Manage 
config. The dock
+    /// ACKs each (`id=0x14 sub=0x10 ctr=N`) and only then engages its CP 
cipher; skipping the
+    /// announce leaves it cipher-off (the long-standing "0 `sub=0x45` acks" 
symptom).
+    ///
+    /// [`golden::CAP_PLAIN_1080P`] is a byte-correct *skeleton* 
(headers/aux/lead bytes and the
+    /// two constant frames are session-invariant -- verified across the 
cold-ref and matched
+    /// sessions) but its 5 variable payloads are a STALE foreign session's 
values. Replaying it
+    /// verbatim delivers the dock a stale Ekpub/Edkey/riv that re-key it to a 
foreign `ks`
+    /// (the `cap_phase`-clobbers-`ks` bug). So we clone the skeleton and 
overwrite ONLY the 5
+    /// session-specific payloads. Each payload sits at frame offset 44 
(16-byte wire header +
+    /// 22 inner-prefix bytes + the `30 00 00 00 00` marker + 1 lead byte = 28 
inner bytes), and
+    /// frames are stored `[u16 len][frame]`. `riv` here is the 
SKE-*delivered* riv (`riv_ske`),
+    /// written verbatim -- frame 5 is a byte-faithful re-statement of 
`SKE_Send_Eks`, so it must
+    /// carry the EXACT delivered riv. (It earlier wrote `riv & 0xF8`, which 
equals the delivered
+    /// value only when the random riv's low 3 bits are zero -- true for 
cold-ref's `e8` but wrong
+    /// for 7 of 8 live sessions, so the dock saw a different riv in the 
announce than in SKE.
+    /// Ground truth: cold-ref ctr5 capture t=36.812413 delivers riv `...40e8` 
== its SKE riv.)
+    fn build_cap_announce(
+        rtx: &[u8; 8],
+        ekpub: &[u8; 128],
+        rn: &[u8; 8],
+        edkey: &[u8; 16],
+        riv: &[u8; 8],
+        v: &[u8; 16],
+    ) -> Result<KVec<u8>> {
+        let mut blob = KVec::with_capacity(golden::CAP_PLAIN_1080P.len(), 
GFP_KERNEL)?;
+        blob.extend_from_slice(golden::CAP_PLAIN_1080P, GFP_KERNEL)?;
+
+        // Walk the skeleton; for each frame, overwrite the payload (at 
frame+44) keyed by ctr.
+        let mut off = 0usize;
+        while off + 2 <= blob.len() {
+            let len = u16::from_le_bytes([blob[off], blob[off + 1]]) as usize;
+            let frame = off + 2;
+            if frame + len > blob.len() {
+                break;
+            }
+            // ctr (inner offset 4) identifies which AKE message this announce 
frame restates.
+            let ctr = u16::from_le_bytes([blob[frame + 16 + 4], blob[frame + 
16 + 5]]);
+            let pay = frame + 44; // 16 hdr + 22 inner-prefix + 5 marker + 1 
lead
+            match ctr {
+                1 => blob[pay..pay + 8].copy_from_slice(rtx), // AKE_Init
+                3 => blob[pay..pay + 128].copy_from_slice(ekpub), // 
AKE_No_Stored_km Ekpub
+                4 => blob[pay..pay + 8].copy_from_slice(rn), // LC_Init
+                5 => {
+                    // SKE_Send_Eks: Edkey(ks)[16] || riv[8] (the delivered 
riv, verbatim)
+                    blob[pay..pay + 16].copy_from_slice(edkey);
+                    blob[pay + 16..pay + 24].copy_from_slice(riv);
+                }
+                6 => blob[pay..pay + 16].copy_from_slice(v), // 
RepeaterAuth_Send_Ack V
+                _ => {} // ctr 2 (TxCaps) and 7 (Stream_Manage) are 
session-invariant
+            }
+            off = frame + len;
+        }
+        Ok(blob)
+    }
+
+
+    /// Poll EP 0x83 (interrupt-IN status endpoint). DLM submits URBs here 
CONTINUOUSLY and the dock
+    /// pushes 6-byte status events; the dock may gate CP/downstream-HDCP 
engagement on the host
+    /// servicing this endpoint (flagged in `vino-driver/src/bin/bringup.rs`). 
vino never polled it
+    /// --
+    /// invisible in the EP02/EP84 bulk-wire comparison. Reads up to a few 
events (short timeout so
+    /// a
+    /// URB is pending when the dock pushes). `usb_bulk_msg` auto-routes the 
interrupt endpoint.
+    fn poll_ep83(dev: &usb::Device) -> usize {
+        // EP83 (interrupt-IN) transfers need DMA-capable memory -- allocate 
on the HEAP.
+        // A stack array trips usb_hcd_map_urb_for_dma's "transfer buffer is 
on stack"
+        // WARNING (VMAP_STACK can't be DMA-mapped) and the broken submit also 
stalls the
+        // bring-up (poll_ep83 runs inside every drain round). Best-effort: 
bail on OOM.
+        let mut buf = match KVec::from_elem(0u8, 64, GFP_KERNEL) {
+            Ok(b) => b,
+            Err(_) => return 0,
+        };
+        let mut n = 0usize;
+        // Short timeout: a pending URB gives the dock a window to push, but a 
30 ms block on the
+        // (normally idle) EP83 stalls the bring-up loop (see drain_ep84). 2 
ms is enough to catch a
+        // ready event without serializing the handshake.
+        for _ in 0..4 {
+            match dev.interrupt_recv(0x83, &mut buf, Delta::from_millis(2)) {
+                Ok(len) if len > 0 => {
+                    n += 1;
+                    let s = &buf[..len.min(8)];
+                    pr_info!("vino: EP83 status event {len}B {s:02x?}\n");
+                }
+                _ => break,
+            }
+        }
+        n
+    }
+
 }
 
 kernel::usb_device_table!(
-- 
2.54.0

Reply via email to