[RFC PATCH v2 02/10] drm/vino: add the clean-room HDCP 2.2 AKE/LC/SKE handshake
From: Mike Lothian
Date: Thu Jul 02 2026 - 23:12:32 EST
crypto.rs wraps the crypto bindings (AES-128, AES-CMAC) for the KDF this
driver needs; hdcp.rs holds the HDCP 2.2 message-ID constants and wire
structs (reusing <drm/display/drm_hdcp.h> where it overlaps); ake.rs
builds/parses the AKE_Init/AKE_Send_Cert/AKE_No_Stored_km/
AKE_Send_H_prime/AKE_Send_Pairing_Info, LC_Init/LC_Send_L_prime and
SKE_Send_Eks messages plus the RepeaterAuth exchange, all verified
byte-exact against the reference daemon's wire traffic; golden.rs holds
a small captured plaintext capability-announce skeleton the handshake
fills with live session values.
This is a clean-room implementation of the standard HDCP 2.2 AKE from
the published spec and the driver's own captured wire traffic -- no
daemon code or binary content is reused.
Signed-off-by: Mike Lothian <mike@xxxxxxxxxxxxxx>
Assisted-by: Claude:claude-sonnet-5 [Claude-Code]
---
drivers/gpu/drm/vino/ake.rs | 173 +++++++++++++++++++++++++++++++++
drivers/gpu/drm/vino/crypto.rs | 48 +++++++++
drivers/gpu/drm/vino/golden.rs | 69 +++++++++++++
drivers/gpu/drm/vino/hdcp.rs | 167 +++++++++++++++++++++++++++++++
4 files changed, 457 insertions(+)
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
diff --git a/drivers/gpu/drm/vino/ake.rs b/drivers/gpu/drm/vino/ake.rs
new file mode 100644
index 000000000000..38919624cb2f
--- /dev/null
+++ b/drivers/gpu/drm/vino/ake.rs
@@ -0,0 +1,173 @@
+// 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.
+///
+/// `type0` (CP_STREAM_TYPE0 experiment, 2026-06-23): zero the two DL3 stream-type bytes
+/// (0x04/0x05 -> 0x00) so the dock is asked for a single Type-0 (unrestricted) stream
+/// instead of the replicated DLM stream set. Default DLM-exact (`type0 == false`); the M
+/// computation in `wait_cap_complete` zeroes the matching `m_data` bytes under the same flag.
+pub(super) fn repeater_auth_stream_manage(hdcp_seq: u32, seq: u32, type0: bool) -> 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)
+ let (s0, s1) = if type0 { (0x00, 0x00) } else { (0x04, 0x05) };
+ b[36..40].copy_from_slice(&[s0, 0, 0, 0]); // StreamID_Type[0]
+ b[43] = s1; // StreamID_Type[1]
+ 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..83e988ded726
--- /dev/null
+++ b/drivers/gpu/drm/vino/crypto.rs
@@ -0,0 +1,48 @@
+// 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::*;
+
+/// An AES-128 key prepared once for single-block encryption; re-exported so the
+/// AES-CTR keystream loops in [`cp`](super::cp) can expand the key once and reuse
+/// it across every block (rather than re-expanding per block).
+pub(super) use kernel::crypto::Aes128;
+
+/// `AES_ECB(key, block)` -- one 16-byte AES-128 block. Convenience one-shot for
+/// callers that encrypt a single block (e.g. HDCP dKey derivation); the AES-CTR
+/// paths build an [`Aes128`] once and call [`Aes128::encrypt_block`] in a loop.
+pub(super) fn aes128_ecb(key: &[u8; 16], block: &[u8; 16]) -> Result<[u8; 16]> {
+ Ok(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) via the in-tree AES-CMAC library.
+/// 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]) -> [u8; 16] {
+ kernel::crypto::aes_cmac(key, data)
+}
+
+/// `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)
+}
--
2.55.0