cp.rs implements the post-AKE control-plane wire format: the AES-CTR
seal (K_dl3 = ks, keystream AES-CTR(ks, riv || BE64(seq)) over the
whole inner message), the Dl3Cmac trailer (AES-CMAC over the sealed
ciphertext with a riv-derived nonce, encrypt-then-MAC), the stream-open
arm marker that makes the dock start processing encrypted CP, and the
message builders for mode-set, cursor, and DDC/CI Set-VCP (brightness/
contrast/DPMS power). All of this is verified byte-exact against the
reference daemon's captured wire traffic.

Signed-off-by: Mike Lothian <[email protected]>
Assisted-by: Claude:claude-sonnet-5 [Claude-Code]
---
 drivers/gpu/drm/vino/cp.rs | 723 +++++++++++++++++++++++++++++++++++++
 1 file changed, 723 insertions(+)
 create mode 100644 drivers/gpu/drm/vino/cp.rs

diff --git a/drivers/gpu/drm/vino/cp.rs b/drivers/gpu/drm/vino/cp.rs
new file mode 100644
index 000000000000..4d55b96999b0
--- /dev/null
+++ b/drivers/gpu/drm/vino/cp.rs
@@ -0,0 +1,723 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Encrypted-control-plane message builders (the inner plaintext of the type=4
+//! sub=0x24 AES-CTR frames) plus the AES-CTR `seal` that encrypts and frames 
them.
+//! Layouts are from the reverse-engineered protocol; offsets cite the guide 
and
+//! should be re-checked against a capture before they drive real hardware.
+#![allow(dead_code)] // some seal/handler paths run only after the dock 
engages CP (open blocker)
+
+use super::*;
+
+/// Common CP inner header: `[id u16][sub u16][counter u16][00 00]` (sec 
6.1/sec 8.6.4).
+fn header(out: &mut KVec<u8>, id: u16, sub: u16, counter: u16) -> Result {
+    out.extend_from_slice(&id.to_le_bytes(), GFP_KERNEL)?;
+    out.extend_from_slice(&sub.to_le_bytes(), GFP_KERNEL)?;
+    out.extend_from_slice(&counter.to_le_bytes(), GFP_KERNEL)?;
+    out.extend_from_slice(&[0, 0], GFP_KERNEL)?;
+    Ok(())
+}
+
+fn pad_to(out: &mut KVec<u8>, len: usize) -> Result {
+    while out.len() < len {
+        out.push(0, GFP_KERNEL)?;
+    }
+    Ok(())
+}
+
+/// OUT heartbeat (sec 6.1): `id=0x16 sub=0x75`, two AES blocks (`10 27` at 
block1+6).
+pub(super) fn heartbeat(counter: u16) -> Result<KVec<u8>> {
+    let mut b = KVec::with_capacity(32, GFP_KERNEL)?;
+    header(&mut b, 0x16, 0x75, counter)?;
+    pad_to(&mut b, 22)?; // block0 tail + block1[0..6]
+    b.extend_from_slice(&[0x10, 0x27], GFP_KERNEL)?; // block1[6..8]
+    pad_to(&mut b, 32)?;
+    Ok(b)
+}
+
+/// OUT get-EDID request (CP-HANDSHAKE.md sec 4f): `id=0x15 sub=0x21`, the 
message that asks
+/// the dock to return the downstream monitor's EDID in an `id=0x194 sub=0x21` 
reply (parsed
+/// by [`parse_edid_from_reply`]). The request carries no payload beyond the 
inner header, so
+/// it is a single 16-byte AES block; [`seal_livemac`] appends the 16-byte 
Dl3Cmac. The dock
+/// echoes the `counter`, so any monotonic value works. The exact request body 
was never
+/// captured (only the reply), so this is the minimal well-formed form -- 
re-check against a
+/// capture if the dock ever NAKs it once CP engages.
+pub(super) fn get_edid_req(counter: u16) -> Result<KVec<u8>> {
+    let mut b = KVec::with_capacity(16, GFP_KERNEL)?;
+    header(&mut b, 0x15, 0x21, counter)?;
+    pad_to(&mut b, 16)?;
+    Ok(b)
+}
+
+/// A video timing in DisplayID-Type-I terms (sec 8.6.4), as carried by the
+/// `0x48/0x22` set-mode message. Field meanings and offsets are verified
+/// byte-exact against the golden 3840x2160@60 capture (see [`set_mode`]).
+#[derive(Clone, Copy)]
+pub(super) struct Timing {
+    pub hactive: u16,
+    pub hblank: u16,
+    pub hsync_front: u16,
+    pub hsync_width: u16,
+    pub vactive: u16,
+    pub vblank: u16,
+    pub vsync_front: u16,
+    pub vsync_width: u16,
+    pub refresh_hz: u16,
+    /// Pixel clock in 10 kHz units (e.g. 0xd040 = 533.12 MHz for 4K@60).
+    pub pixel_clock_10khz: u16,
+    /// DisplayID field at off42 -- partly decoded (0x0604 for 4K, 0x0600 for 
the
+    /// 2560x1440 sample in sec 8.6.4); high byte 0x06 constant, low byte 
mode-varying.
+    pub field42: u16,
+}
+
+impl Timing {
+    /// 3840x2160@60 (CVT-RB) -- the mode the non-HDCP dongle advertises, kept 
as a
+    /// known-good reference whose `set_mode` output is byte-exact vs the 
golden capture.
+    pub(super) const UHD_60: Timing = Timing {
+        hactive: 3840, hblank: 160, hsync_front: 48, hsync_width: 32,
+        vactive: 2160, vblank: 62, vsync_front: 3, vsync_width: 5,
+        refresh_hz: 60, pixel_clock_10khz: 0xd040, field42: 0x0604,
+    };
+}
+
+/// set-mode (sec 8.6.4): `id=0x48 sub=0x22`, a 96-byte inner message carrying 
a
+/// DisplayID-Type-I u16 timing record. **Verified byte-exact** against the 
golden
+/// `[59]` 3840x2160@60 capture for every byte except the trailing 22-byte 
session
+/// MAC (off74..95), which [`seal`]'s caller / the HDCP session layer appends.
+///
+/// Layout (inner offsets): off20 BE u32 generation=2; off26 begins the LE u16
+/// record `hactive,hblank,hsync_front,hsync_width,vactive,vblank,vsync_front,
+/// vsync_width,field42,refresh,flags(0x4000)`; off48/off58/off60/off66 carry
+/// constants observed in the 4K capture; off70 the pixel clock (10 kHz units).
+pub(super) fn set_mode(counter: u16, t: &Timing) -> Result<KVec<u8>> {
+    let mut b = KVec::with_capacity(96, GFP_KERNEL)?;
+    header(&mut b, 0x48, 0x22, counter)?;
+    pad_to(&mut b, 20)?;
+    b.extend_from_slice(&2u32.to_be_bytes(), GFP_KERNEL)?; // off20: BE 
generation=2
+    pad_to(&mut b, 26)?; // off24..25 zero; timing begins at off26
+    for v in [
+        t.hactive, t.hblank, t.hsync_front, t.hsync_width,
+        t.vactive, t.vblank, t.vsync_front, t.vsync_width,
+        t.field42, t.refresh_hz, 0x4000, /* off46 flags */ 0x6000, /* off48 */
+    ] {
+        b.extend_from_slice(&v.to_le_bytes(), GFP_KERNEL)?;
+    }
+    pad_to(&mut b, 58)?;
+    b.extend_from_slice(&0x0080u16.to_le_bytes(), GFP_KERNEL)?; // off58 
(observed const)
+    b.extend_from_slice(&0x00ffu16.to_le_bytes(), GFP_KERNEL)?; // off60 
(observed const)
+    pad_to(&mut b, 66)?;
+    b.extend_from_slice(&0x0800u16.to_le_bytes(), GFP_KERNEL)?; // off66 
(observed const)
+    pad_to(&mut b, 70)?;
+    b.extend_from_slice(&t.pixel_clock_10khz.to_le_bytes(), GFP_KERNEL)?; // 
off70
+    pad_to(&mut b, 96)?;
+    Ok(b)
+}
+
+/// Standard VESA MCCS (Monitor Control Command Set 2.2) VCP feature codes, 
driven over
+/// DDC/CI. The macOS DisplayLink agent exposes these as per-display 
brightness/contrast
+/// ("Popover did show -- starting DDC/CI communication", 
`setBrightness`/`setContrast`); the
+/// dock bridges the DDC/CI transaction to the downstream monitor's I2C slave 
0x37 -- the same
+/// monitor-I2C path the EDID read ([`get_edid_req`]) uses for the 0x50 EDID 
slave.
+pub(super) const VCP_BRIGHTNESS: u8 = 0x10;
+pub(super) const VCP_CONTRAST: u8 = 0x12;
+/// VCP 0xD6 "Power mode": value 0x01 = on, 0x04 = off (DPMS-off / hard 
standby). Lets DPMS
+/// blank the panel backlight instead of freezing the last frame (see 
[`crtc_atomic_disable`]).
+pub(super) const VCP_POWER_MODE: u8 = 0xd6;
+pub(super) const POWER_ON: u16 = 0x01;
+pub(super) const POWER_OFF: u16 = 0x04;
+
+/// Build a DDC/CI "Set VCP Feature" request: the 7 bytes a DDC/CI host writes 
to the
+/// monitor's I2C slave 0x37, after the 0x6e (= 0x37<<1) write address (VESA 
DDC/CI 1.1
+/// sec 4.4). Layout: source 0x51, length `0x80 | 4`, opcode 0x03 (Set VCP), 
VCP code,
+/// value-hi, value-lo, then an XOR checksum seeded with the destination 
address 0x6e. Pure
+/// and fully standard, so it is unit-tested byte-exact against the spec
+/// ([`super::tests::ddc_ci_set_vcp_checksum`]).
+pub(super) fn ddc_ci_set_vcp(vcp: u8, value: u16) -> [u8; 7] {
+    let body = [0x51u8, 0x84, 0x03, vcp, (value >> 8) as u8, value as u8];
+    let mut chk = 0x6eu8; // checksum seed = destination slave-write address 
(0x37 << 1)
+    for &x in &body {
+        chk ^= x;
+    }
+    [body[0], body[1], body[2], body[3], body[4], body[5], chk]
+}
+
+/// CP message that tunnels a DDC/CI Set-VCP write to the downstream monitor 
-- the brightness,
+/// contrast and DPMS-power controls the macOS/Windows agents drive over 
"DDC/CI communication".
+/// The dock's monitor-I2C bridge is the same one the EDID read uses, so this 
is modelled as the
+/// WRITE companion to the `0x15/0x21` EDID read: `id=0x15 sub=0x22`, carrying 
the I2C slave
+/// (0x37) + payload length at off20 and the 7-byte DDC/CI Set-VCP payload at 
off22.
+///
+/// The `id`/`sub` and payload offset are **inferred** from the EDID-read 
pairing -- the write
+/// transaction was never captured (it only fires once a monitor is actively 
driven, i.e. past
+/// the CP wall), so re-check against a capture once CP engages. The DDC/CI 
bytes themselves
+/// ([`ddc_ci_set_vcp`]) are standard and verified.
+pub(super) fn ddc_set_vcp(counter: u16, vcp: u8, value: u16) -> 
Result<KVec<u8>> {
+    let payload = ddc_ci_set_vcp(vcp, value);
+    let mut b = KVec::with_capacity(32, GFP_KERNEL)?;
+    header(&mut b, 0x15, 0x22, counter)?;
+    pad_to(&mut b, 20)?;
+    // off20: monitor DDC/CI I2C slave (0x37) + DDC/CI payload length.
+    b.extend_from_slice(&[0x37, payload.len() as u8], GFP_KERNEL)?;
+    // off22: the DDC/CI Set-VCP bytes (same off22 convention as the EDID 
payload).
+    b.extend_from_slice(&payload, GFP_KERNEL)?;
+    pad_to(&mut b, 32)?;
+    Ok(b)
+}
+
+/// EDID base-block sanity check: length, the `00 FF..FF 00` magic, and the 
1-byte
+/// checksum (all 128 base bytes sum to 0 mod 256). A corrupt blob must never 
drive a
+/// mode-set, so [`timing_from_edid`] rejects anything that fails this.
+fn edid_valid(edid: &[u8]) -> bool {
+    const MAGIC: [u8; 8] = [0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00];
+    edid.len() >= 128
+        && edid[..8] == MAGIC
+        && edid[..128].iter().fold(0u8, |a, &b| a.wrapping_add(b)) == 0
+}
+
+/// Parse one 18-byte EDID detailed timing descriptor into a [`Timing`], or 
`None` if it
+/// is too short or not a timing (pixel clock 0 marks a monitor descriptor). 
`field42`
+/// is left at the sec 8.6.4 default (`0x0600`) -- its low byte is 
mode-varying and not fully
+/// decoded, so the live mode-set substitution leaves the captured value in 
place.
+fn parse_dtd(d: &[u8]) -> Option<Timing> {
+    if d.len() < 18 {
+        return None;
+    }
+    let pclk = u16::from_le_bytes([d[0], d[1]]);
+    if pclk == 0 {
+        return None; // monitor descriptor, not a detailed timing
+    }
+    let hi = |v: u8, lo: u8| -> u16 { ((v as u16) << 8) | lo as u16 };
+    let hactive = hi((d[4] >> 4) & 0xf, d[2]);
+    let hblank = hi(d[4] & 0xf, d[3]);
+    let vactive = hi((d[7] >> 4) & 0xf, d[5]);
+    let vblank = hi(d[7] & 0xf, d[6]);
+    let hsync_front = (((d[11] >> 6) & 0x3) as u16) << 8 | d[8] as u16;
+    let hsync_width = (((d[11] >> 4) & 0x3) as u16) << 8 | d[9] as u16;
+    let vsync_front = (((d[11] >> 2) & 0x3) as u16) << 4 | ((d[10] >> 4) & 
0xf) as u16;
+    let vsync_width = ((d[11] & 0x3) as u16) << 4 | (d[10] & 0xf) as u16;
+    let htotal = hactive.wrapping_add(hblank) as u32;
+    let vtotal = vactive.wrapping_add(vblank) as u32;
+    let refresh_hz = if htotal != 0 && vtotal != 0 {
+        ((pclk as u32 * 10_000 + (htotal * vtotal) / 2) / (htotal * vtotal)) 
as u16
+    } else {
+        0
+    };
+    Some(Timing {
+        hactive,
+        hblank,
+        hsync_front,
+        hsync_width,
+        vactive,
+        vblank,
+        vsync_front,
+        vsync_width,
+        refresh_hz,
+        pixel_clock_10khz: pclk,
+        field42: 0x0600,
+    })
+}
+
+/// Extract the monitor's **preferred** detailed timing from an EDID for the 
live mode-set
+/// (CP-HANDSHAKE.md sec 4e). The first DTD in the base block is the preferred 
timing per the
+/// EDID spec; scan all four base descriptor slots (off 54/72/90/108) so a 
leading monitor
+/// descriptor (name/range/serial) doesn't hide it, and if the base block 
carries no DTD at
+/// all, fall back to the first DTD in the CTA-861 extension block. The blob 
is validated
+/// first; an invalid or timing-less EDID returns `None` so the caller keeps 
its known-good
+/// fallback timing rather than driving the dock with garbage.
+pub(super) fn timing_from_edid(edid: &[u8]) -> Option<Timing> {
+    if !edid_valid(edid) {
+        return None;
+    }
+    // Base-block descriptors: the first valid DTD is the preferred timing.
+    for off in [54usize, 72, 90, 108] {
+        if off + 18 <= edid.len() {
+            if let Some(t) = parse_dtd(&edid[off..off + 18]) {
+                return Some(t);
+            }
+        }
+    }
+    // No DTD in the base block: try the first CTA-861 extension's DTD area. 
CTA-861 blocks
+    // have tag 0x02 at byte 0 and a DTD-area byte offset at byte 2 (>= 4 when 
DTDs follow);
+    // descriptors run in 18-byte records up to the extension's checksum byte 
(127).
+    if edid[126] as usize >= 1 && edid.len() >= 256 {
+        let ext = &edid[128..256];
+        if ext[0] == 0x02 {
+            let start = ext[2] as usize;
+            if start >= 4 {
+                let mut off = start;
+                while off + 18 <= 127 {
+                    if let Some(t) = parse_dtd(&ext[off..off + 18]) {
+                        return Some(t);
+                    }
+                    off += 18;
+                }
+            }
+        }
+    }
+    None
+}
+
+/// Overwrite the geometry + clock fields of an in-place set-mode inner message
+/// (`id=0x48 sub=0x22`) with `t` (CP-HANDSHAKE.md sec 4e). Offsets mirror 
[`set_mode`]:
+/// the LE u16 timing record at off26 and the pixel clock at off70. `field42` 
(off42),
+/// the off66 token and the encrypted trailer are intentionally **left as 
captured**;
+/// only the EDID-derived values change, so the wire length (hence `wire_seq`) 
is
+/// unchanged. No-op if `plain` is too short.
+pub(super) fn apply_edid_timing(plain: &mut [u8], t: &Timing) {
+    if plain.len() < 72 {
+        return;
+    }
+    let put = |b: &mut [u8], off: usize, v: u16| {
+        b[off] = v as u8;
+        b[off + 1] = (v >> 8) as u8;
+    };
+    put(plain, 26, t.hactive);
+    put(plain, 28, t.hblank);
+    put(plain, 30, t.hsync_front);
+    put(plain, 32, t.hsync_width);
+    put(plain, 34, t.vactive);
+    put(plain, 36, t.vblank);
+    put(plain, 38, t.vsync_front);
+    put(plain, 40, t.vsync_width);
+    put(plain, 44, t.refresh_hz);
+    put(plain, 70, t.pixel_clock_10khz);
+}
+
+/// Convert a DRM display mode (the timing the *compositor* selected from the 
connector's
+/// EDID-derived mode list) into a set-mode [`Timing`]. This is what makes the 
dock
+/// multi-mode: `drm_edid_connector_add_modes` already advertises every 
base+extension mode
+/// from the dock's EDID, and when userspace sets any one of them the resulting
+/// `drm_display_mode` lands here verbatim -- no re-parsing of EDID offsets. 
The blanking
+/// fields map straight across (CVT/DMT/DisplayID all use the same 
front-porch/sync model),
+/// and the refresh rate comes from DRM's own `drm_mode_vrefresh` helper 
rather than a
+/// hand-rolled divide. `field42` keeps the sec 8.6.4 default (its low byte is 
mode-varying and
+/// not fully decoded); the dock tolerates the high byte `0x06`.
+///
+/// SAFETY: `mode` must point to a valid `drm_display_mode` for the duration 
of the call.
+pub(super) fn timing_from_drm_mode(mode: 
&kernel::drm::kms::modes::DisplayMode) -> Timing {
+    let refresh = mode.vrefresh() as u16;
+    let sub = |a: u16, b: u16| a.saturating_sub(b);
+    Timing {
+        hactive: mode.hdisplay(),
+        hblank: sub(mode.htotal(), mode.hdisplay()),
+        hsync_front: sub(mode.hsync_start(), mode.hdisplay()),
+        hsync_width: sub(mode.hsync_end(), mode.hsync_start()),
+        vactive: mode.vdisplay(),
+        vblank: sub(mode.vtotal(), mode.vdisplay()),
+        vsync_front: sub(mode.vsync_start(), mode.vdisplay()),
+        vsync_width: sub(mode.vsync_end(), mode.vsync_start()),
+        refresh_hz: refresh,
+        // `clock` is in kHz; the set-mode field is in 10 kHz units.
+        pixel_clock_10khz: (mode.clock() / 10).clamp(0, u16::MAX as i32) as 
u16,
+        field42: 0x0600,
+    }
+}
+
+/// Decode the inner header of a dock->host CP frame: returns `(id, sub, 
ictr)` from
+/// the first decrypted block (CP-HANDSHAKE.md sec 3), or `None` if `wire` is 
not a
+/// decryptable CP frame. Used by the live loop to log what the dock is 
replying.
+pub(super) fn reply_info(
+    ks: &[u8; 16],
+    out_riv: &[u8; 8],
+    wire: &[u8],
+) -> Option<(u16, u16, u16)> {
+    if wire.len() <= 16 {
+        return None;
+    }
+    let seq = u32::from_le_bytes([wire[12], wire[13], wire[14], wire[15]]);
+    let head = &wire[16..wire.len().min(32)];
+    let inner = open_in(ks, &in_riv(out_riv), seq, head).ok()?;
+    if inner.len() < 6 {
+        return None;
+    }
+    Some((
+        u16::from_le_bytes([inner[0], inner[1]]),
+        u16::from_le_bytes([inner[2], inner[3]]),
+        u16::from_le_bytes([inner[4], inner[5]]),
+    ))
+}
+
+/// CP `sub` ids seen on the wire (CP-HANDSHAKE.md). Used to score a candidate
+/// decrypt: a plaintext whose `sub` is one of these (and whose post-counter 
pad is
+/// zero) is almost certainly the correct key/riv.
+fn is_known_sub(sub: u16) -> bool {
+    matches!(
+        sub,
+        0x00 | 0x04 | 0x0c | 0x10 | 0x20 | 0x21 | 0x22 | 0x24 | 0x25 | 0x30 | 
0x41
+            | 0x42 | 0x43 | 0x45 | 0x75 | 0x84
+    )
+}
+
+/// Diagnostic decode: try a dock->host frame under every plausible riv 
variant and
+/// return the best-scoring inner `(riv_tag, id, sub, ictr)`. The interactive
+/// `wsub=0x45` replies decrypt under `in_riv` (byte7^1), but the **cap-phase**
+/// `wsub=0x25` frames decrypt under the session ks with **byte7 unchanged** 
(the OUT
+/// value) -- see the cold-ref transcript. `byte0^0x80` selects the head. This 
mirrors
+/// `decode-handshake.py`'s scoring so a live trace shows what the dock is 
actually
+/// asking for during the capability exchange we currently skip.
+pub(super) fn decode_any(
+    ks: &[u8; 16],
+    out_riv: &[u8; 8],
+    wire: &[u8],
+) -> Option<(&'static str, u16, u16, u16, [u8; 24])> {
+    if wire.len() <= 16 {
+        return None;
+    }
+    let seq = u32::from_le_bytes([wire[12], wire[13], wire[14], wire[15]]);
+    let head = &wire[16..wire.len().min(48)];
+    let out0 = *out_riv;
+    let in0 = in_riv(out_riv);
+    let mut out1 = out0;
+    out1[0] ^= 0x80;
+    let mut in1 = in0;
+    in1[0] ^= 0x80;
+    let variants: [(&'static str, [u8; 8]); 4] =
+        [("out/h0", out0), ("in/h0", in0), ("out/h1", out1), ("in/h1", in1)];
+    let mut best: Option<(i32, &'static str, u16, u16, u16, [u8; 24])> = None;
+    for (tag, riv) in variants.iter() {
+        let Ok(pt) = open_in(ks, riv, seq, head) else { continue };
+        if pt.len() < 8 {
+            continue;
+        }
+        let id = u16::from_le_bytes([pt[0], pt[1]]);
+        let sub = u16::from_le_bytes([pt[2], pt[3]]);
+        let ctr = u16::from_le_bytes([pt[4], pt[5]]);
+        let pad = u16::from_le_bytes([pt[6], pt[7]]);
+        let mut sc = 0i32;
+        if is_known_sub(sub) {
+            sc += 50;
+        }
+        if pad == 0 {
+            sc += 10;
+        }
+        if ctr < 0x400 {
+            sc += 5;
+        }
+        if best.map_or(true, |b| sc > b.0) {
+            // Keep the first 24 plaintext bytes so the live trace shows the 
decoded
+            // structure (e.g. the `..4c..de..` cap-descriptor template that, 
in the
+            // capture, is session-independent -- its absence flags a ks/riv 
mismatch).
+            let mut sample = [0u8; 24];
+            let n = pt.len().min(24);
+            sample[..n].copy_from_slice(&pt[..n]);
+            best = Some((sc, tag, id, sub, ctr, sample));
+        }
+    }
+    best.map(|(_, tag, id, sub, ctr, sample)| (tag, id, sub, ctr, sample))
+}
+
+// All three cursor messages share one 32-byte inner layout, recovered 
byte-exact from the
+// cold-ref session by `scripts/verify-cp-seal.py` (a 64x64 cursor, t~=41.3s):
+//   off0..7   id/sub/counter header
+//   off8..21  zero
+//   off22     0x02              constant marker
+//   off23     head_id           (0 / 1 across the cold-ref's two monitors)
+//   off24..25 field1 LE u16     (create: width  / move: X / image: 0)
+//   off26..27 field2 LE u16     (create: height / move: Y / image: 0)
+//   off28..31 trailing 4 bytes  (DLM leaks UNINITIALISED struct padding here 
-- the bytes vary
+//                                non-monotonically every message and match no 
checksum, so the
+//                                dock cannot validate them; we send zero, 
which is correct)
+// The image then appends its w*h*4 BGRA bitmap starting at off32, and its 
inner id carries a
+// 0x40 high-byte flag (0x401c, vs the 0x1c the aux/send_cp path uses). Prior 
layouts put the
+// marker/fields two bytes early and omitted the head byte (cursor_move) -- a 
latent bug, since
+// cursor traffic is post-CP-engagement. See 
captures/cp-seal-differential-20260622.md.
+fn cursor_header(b: &mut KVec<u8>, id: u16, sub: u16, counter: u16, head: u8) 
-> Result {
+    header(b, id, sub, counter)?;
+    pad_to(b, 22)?;
+    b.push(0x02, GFP_KERNEL)?; // off22 marker
+    b.push(head, GFP_KERNEL)?; // off23 head id
+    Ok(())
+}
+
+/// cursor create (sec 8.6.1): `id=0x1b sub=0x42`, advertises `w x h` for 
`head`.
+pub(super) fn cursor_create(counter: u16, head: u8, w: u16, h: u16) -> 
Result<KVec<u8>> {
+    let mut b = KVec::with_capacity(32, GFP_KERNEL)?;
+    cursor_header(&mut b, 0x1b, 0x42, counter, head)?;
+    b.extend_from_slice(&w.to_le_bytes(), GFP_KERNEL)?; // off24..25
+    b.extend_from_slice(&h.to_le_bytes(), GFP_KERNEL)?; // off26..27
+    pad_to(&mut b, 32)?; // off28..31 (DLM uninit; we zero)
+    Ok(b)
+}
+
+/// cursor move (sec 8.6.1): `id=0x1a sub=0x43`, head id @23, X @24, Y @26 
(LE).
+pub(super) fn cursor_move(counter: u16, head: u8, x: u16, y: u16) -> 
Result<KVec<u8>> {
+    let mut b = KVec::with_capacity(32, GFP_KERNEL)?;
+    cursor_header(&mut b, 0x1a, 0x43, counter, head)?;
+    b.extend_from_slice(&x.to_le_bytes(), GFP_KERNEL)?; // off24..25
+    b.extend_from_slice(&y.to_le_bytes(), GFP_KERNEL)?; // off26..27
+    pad_to(&mut b, 32)?; // off28..31 (DLM uninit; we zero)
+    Ok(b)
+}
+
+/// cursor image (sec 8.6.1): inner `id=0x401c sub=0x41` (the 0x40 high-byte 
flag marks the
+/// bitmap-bearing message). A 32-byte header (shared marker + head, no w/h -- 
those come from
+/// [`cursor_create`]) followed by the `w*h*4` BGRA bitmap at off32. `bgra` 
must be `w*h*4` bytes
+/// -- DRM hands the driver a 64x64 ARGB8888 cursor buffer and the caller 
swaps it to BGRA.
+pub(super) fn cursor_image(
+    counter: u16,
+    head: u8,
+    w: u16,
+    h: u16,
+    bgra: &[u8],
+) -> Result<KVec<u8>> {
+    // `w*h*4` can wrap a 32-bit `usize` (max ~1.7e10 > u32::MAX), which would 
let an
+    // undersized `bgra` pass the check; compute it with checked arithmetic so 
an
+    // overflow is rejected as a mismatch rather than silently bypassing 
validation.
+    let expected = (w as usize)
+        .checked_mul(h as usize)
+        .and_then(|n| n.checked_mul(4));
+    if expected != Some(bgra.len()) {
+        return Err(EINVAL);
+    }
+    let mut b = KVec::with_capacity(32 + bgra.len(), GFP_KERNEL)?;
+    cursor_header(&mut b, 0x401c, 0x41, counter, head)?;
+    pad_to(&mut b, 32)?; // off24..31 zero (no w/h here)
+    b.extend_from_slice(bgra, GFP_KERNEL)?; // bitmap @ off32
+    Ok(b)
+}
+
+/// DisplayLink "Dl3Cmac" CP-message integrity tag (16 bytes) -- **FULLY 
SOLVED + CROSS-SESSION
+/// VERIFIED 2026-06-11** (`captures/DL3CMAC-FULLY-SOLVED-20260611.md`):
+/// `tag = AES-CMAC(ks, mac_nonce(8) || BE64(wire_seq) || ciphertext)` where
+/// - `mac_nonce` = the CTR stream `riv` **with `byte0 ^= 0x80`** (this byte0 
flip is the bit
+///   prior writeups missed -- they tried `riv` / `riv^1@byte7` and OUT never 
verified),
+/// - `wire_seq` = the AES-CTR block counter (frame header off-12), 
zero-extended to BE64,
+/// - `ciphertext` = the AES-CTR ciphertext content (encrypt-then-MAC), tag 
appended IN CLEAR.
+/// `K_dl3 = ks`. Proven: 110/115 OUT + 128/135 IN corpus frames AND cold-ref 
msg0 (a different
+/// session) reproduce byte-exact. Pass the CTR `riv` directly; the byte0 flip 
is applied here.
+pub(super) fn dl3cmac_tag(
+    ks: &[u8; 16],
+    riv: &[u8; 8],
+    wire_seq: u64,
+    ciphertext: &[u8],
+) -> Result<[u8; 16]> {
+    let mut mac_nonce = *riv;
+    mac_nonce[0] ^= 0x80;
+    let mut buf = KVec::with_capacity(16 + ciphertext.len(), GFP_KERNEL)?;
+    buf.extend_from_slice(&mac_nonce, GFP_KERNEL)?;
+    buf.extend_from_slice(&wire_seq.to_be_bytes(), GFP_KERNEL)?;
+    buf.extend_from_slice(ciphertext, GFP_KERNEL)?;
+    Ok(crypto::aes_cmac(ks, &buf))
+}
+
+/// Seal a CP message with a **freshly computed live Dl3Cmac**, reusing DLM's 
captured wire
+/// `header` (so `seq`/`aux` are byte-identical) but recomputing the tail tag 
for THIS session.
+/// `content_pt` is the real inner plaintext WITHOUT the 16-byte tag region. 
Wire body =
+/// `AES-CTR(ks, riv, content_pt)` || `dl3cmac_tag(...)`. This is the 
live-generation path. See
+/// `captures/DL3CMAC-FULLY-SOLVED-20260611.md`.
+pub(super) fn seal_livemac(
+    ks: &[u8; 16],
+    riv: &[u8; 8],
+    header: &[u8],
+    content_pt: &[u8],
+) -> Result<KVec<u8>> {
+    let seq = u32::from_le_bytes([header[12], header[13], header[14], 
header[15]]);
+    let cipher = crypto::Aes128::new(ks)?;
+    let mut ct = KVec::with_capacity(content_pt.len(), GFP_KERNEL)?;
+    for (i, chunk) in content_pt.chunks(16).enumerate() {
+        let mut iv = [0u8; 16];
+        iv[..8].copy_from_slice(riv);
+        iv[12..].copy_from_slice(&seq.wrapping_add(i as u32).to_be_bytes());
+        let ksb = cipher.encrypt_block(&iv);
+        for (j, &p) in chunk.iter().enumerate() {
+            ct.push(p ^ ksb[j], GFP_KERNEL)?;
+        }
+    }
+    let tag = dl3cmac_tag(ks, riv, seq as u64, &ct)?;
+    let mut frame = KVec::with_capacity(16 + ct.len() + 16, GFP_KERNEL)?;
+    frame.extend_from_slice(&header[..16], GFP_KERNEL)?;
+    frame.extend_from_slice(&ct, GFP_KERNEL)?;
+    frame.extend_from_slice(&tag, GFP_KERNEL)?;
+    Ok(frame)
+}
+
+/// Seal an inner CP message into a wire frame (type=4 sub=0x24, `seq`). 
DisplayLink
+/// CP is **encrypt-then-MAC**: the message content is AES-CTR-encrypted, then 
a
+/// 16-byte Dl3Cmac tag (`AES-CMAC(ks, riv || BE64(seq) || ciphertext)`) is 
appended.
+/// The keystream is `AES_ECB(ks, riv(8) || u32(0) || u32_be(seq + block))` 
(sec 6.1).
+///
+/// `inner` is the captured golden plaintext `[content || 
stale-tag-region(16)]`; we
+/// encrypt only `content = inner[..len-16]` and append a **fresh** tag keyed 
by our
+/// live session, so the dock's Dl3Cmac verification passes (the stale 
replayed tag is
+/// why the dock previously dropped our CP). VERIFIED construction (sec 8.6.7).
+pub(super) fn seal(
+    ks: &[u8; 16],
+    riv: &[u8; 8],
+    seq: u32,
+    inner: &[u8],
+) -> Result<KVec<u8>> {
+    // The interactive CP stream: session ks, wire sub `0x24`.
+    seal_stream(ks, riv, 0x24, seq, inner)
+}
+
+/// Build a fully sealed interactive CP frame (`type=4 sub=0x24`) at 
`wire_seq` over `content`
+/// (the inner plaintext, WITHOUT any trailing 16-byte tag placeholder): the 
16-byte wire
+/// header -- size, `type=4`, `sub=0x24`, the per-`id` [`aux_for_id`] field, 
and `wire_seq` --
+/// followed by [`seal_livemac`] (AES-CTR ciphertext + appended live Dl3Cmac). 
Shared by the
+/// bring-up live loop ([`VinoDriver::send_live_cp`]) and the runtime KMS 
senders
+/// ([`drm_sink::VinoDrmData::send_cp`]) so both produce a byte-identical wire 
frame.
+pub(super) fn seal_interactive(
+    ks: &[u8; 16],
+    riv: &[u8; 8],
+    id: u16,
+    wire_seq: u32,
+    content: &[u8],
+) -> Result<KVec<u8>> {
+    let body_len = content.len() + 16; // AES-CTR ciphertext + 16-byte Dl3Cmac
+    let size = ((16 + body_len) - 4) as u16;
+    let aux = aux_for_id(id, body_len);
+    let mut hdr = [0u8; 16];
+    hdr[2..4].copy_from_slice(&size.to_le_bytes());
+    hdr[4..8].copy_from_slice(&4u32.to_le_bytes()); // type=4
+    hdr[8..10].copy_from_slice(&0x24u16.to_le_bytes()); // sub=0x24 
(interactive CP)
+    hdr[10..12].copy_from_slice(&aux.to_le_bytes());
+    hdr[12..16].copy_from_slice(&wire_seq.to_le_bytes());
+    seal_livemac(ks, riv, &hdr, content)
+}
+
+/// The CP wire-header `aux`@10 (`sub_len_dw`) field is a **strict 
per-inner-message-id
+/// constant** in DLM's CP stream -- verified byte-exact across all 94 
captured 1080p CP
+/// frames (`cp-hdrwire-1080p.bin`) -- **not** `body.len()/4`, which is what 
`push_frame`
+/// derives. Reproducing it makes a generated CP frame's header byte-identical 
to DLM, the
+/// leading hypothesis for the dock engaging its CP cipher (the dock acks our 
plaintext cap
+/// but emits 0 encrypted replies with the wrong `aux`). See docs/BLOCKER.md 
and memory
+/// `project_cp_aux_field_per_id_constant`. Unknown ids fall back to the dword 
count so an
+/// unrecognised message is still well-formed.
+///
+/// The cursor ids (0x1a/0x1b/0x1c) are absent from the 1080p set (cursor 
messages fire only
+/// once a cursor is set, post-engagement) and were recovered from the 
cold-ref session by the
+/// `scripts/verify-cp-seal.py` differential: every other OUT CP frame 
regenerated byte-exact,
+/// but the three cursor messages diverged in `aux` alone (DLM 0x04/0x03/0x02 
vs the body/4
+/// fallback) -- a latent bug that would have made the dock misparse every 
cursor update. This
+/// makes the generated `seal`/`seal_stream` path match DLM without a 
captured-header blob -- the
+/// basis for **live** CP generation.
+pub(super) fn aux_for_id(id: u16, body_len: usize) -> u16 {
+    match id {
+        0x14 => 0x0a,
+        0x15 => 0x09,
+        0x16 => 0x08,
+        0x19 => 0x05,
+        0x1a => 0x04, // cursor move
+        0x1b => 0x03, // cursor create
+        0x1c => 0x02, // cursor image
+        0x1f => 0x0f,
+        0x22 => 0x0c,
+        0x26 => 0x08,
+        0x2a => 0x04,
+        0x32 => 0x0c,
+        0x48 => 0x06,
+        0x9a => 0x04,
+        _ => (body_len / 4) as u16,
+    }
+}
+
+/// General AES-CTR seal under an arbitrary stream `key`/`riv` and wire sub. 
`seal`
+/// is the session-CP case (`wsub=0x24`); the **cap phase** (CP-HANDSHAKE.md 
sec 4b)
+/// needs `wsub=0x04` sealed under the dock's `id=0x32`-delivered per-head 
stream key,
+/// not the session ks -- which `seal` cannot express. Body construction is 
identical:
+/// AES-CTR(key, riv || 0x00000000 || BE32(seq+block)) over the **whole** 
inner message
+/// (no appended MAC; the inner carries its own encrypted trailer -- verified 
byte-exact
+/// vs DLM, 30/30 wire frames).
+pub(super) fn seal_stream(
+    key: &[u8; 16],
+    riv: &[u8; 8],
+    wsub: u16,
+    seq: u32,
+    inner: &[u8],
+) -> Result<KVec<u8>> {
+    let cipher = crypto::Aes128::new(key)?;
+    let mut ct = KVec::with_capacity(inner.len(), GFP_KERNEL)?;
+    for (i, chunk) in inner.chunks(16).enumerate() {
+        let mut iv = [0u8; 16];
+        iv[..8].copy_from_slice(riv);
+        iv[12..].copy_from_slice(&seq.wrapping_add(i as u32).to_be_bytes());
+        let ksb = cipher.encrypt_block(&iv);
+        for (j, &p) in chunk.iter().enumerate() {
+            ct.push(p ^ ksb[j], GFP_KERNEL)?;
+        }
+    }
+    let mut frame = KVec::with_capacity(16 + ct.len(), GFP_KERNEL)?;
+    // DLM-exact `aux`@10: a per-inner-id constant (see `aux_for_id`), not 
`body/4`. The
+    // id is read from the *plaintext* inner (off 0); `push_frame` would 
derive the wrong
+    // value and is the suspected reason the dock won't engage its CP cipher.
+    let id = if inner.len() >= 2 { u16::from_le_bytes([inner[0], inner[1]]) } 
else { 0 };
+    super::proto::push_frame_with(&mut frame, 0x04, wsub, aux_for_id(id, 
ct.len()), seq, &ct)?;
+    Ok(frame)
+}
+
+/// Derive the dock->host (IN) CP riv from the host->dock (OUT) `riv`. **It is 
the
+/// SAME riv -- no transform.** Proven 2026-06-12 by decrypting a frida-keyed 
DLM cold
+/// session's engaged `sub=0x45` replies 
(`captures/dlm-coldkeys-20260611-135237`, logged
+/// `ks`/`out_riv`): the dock's replies decrypt cleanly ONLY under the raw 
`out_riv`
+/// (`id=0x4c sub=0 ctr=8` to msg0, `id=0x14 sub=0x10` ACKs, `id=0x213` cert, 
...); the old
+/// `byte7 ^= 1` gives garbage. The earlier "byte7^1 for IN" note was never 
validated against
+/// a real engaged reply (vino never engaged) and was wrong -- it would have 
made vino
+/// misdecode
+/// every dock reply (and partly explains old "dock replies garbage under our 
ks" findings).
+pub(super) fn in_riv(out_riv: &[u8; 8]) -> [u8; 8] {
+    *out_riv
+}
+
+/// Decrypt a dock->host CP frame body (AES-CTR, the same keystream as 
[`seal`] but
+/// keyed with the IN `riv`). `ct` is the ciphertext (wire bytes after the 
16-byte
+/// cleartext header); `seq` is the wire counter at wire offset 12.
+pub(super) fn open_in(
+    ks: &[u8; 16],
+    in_riv: &[u8; 8],
+    seq: u32,
+    ct: &[u8],
+) -> Result<KVec<u8>> {
+    let cipher = crypto::Aes128::new(ks)?;
+    let mut pt = KVec::with_capacity(ct.len(), GFP_KERNEL)?;
+    for (i, chunk) in ct.chunks(16).enumerate() {
+        let mut iv = [0u8; 16];
+        iv[..8].copy_from_slice(in_riv);
+        iv[12..].copy_from_slice(&seq.wrapping_add(i as u32).to_be_bytes());
+        let ksb = cipher.encrypt_block(&iv);
+        for (j, &c) in chunk.iter().enumerate() {
+            pt.push(c ^ ksb[j], GFP_KERNEL)?;
+        }
+    }
+    Ok(pt)
+}
+
+/// If `wire` is an EDID reply (dock->host EP84, `type=4 sub=0x45`, inner
+/// `id=0x194 sub=0x21`), decrypt it with the IN riv and return the embedded 
EDID
+/// blob (base block + extensions). The EDID begins at inner offset 22; its 
total
+/// length is `128 * (1 + extension_count)`, where the extension count is 
base-block
+/// byte 126. Returns `None` for any other frame. See docs/CONTROL-PLANE.md.
+pub(super) fn parse_edid_from_reply(
+    ks: &[u8; 16],
+    out_riv: &[u8; 8],
+    wire: &[u8],
+) -> Result<Option<KVec<u8>>> {
+    // Wire header: [.. type@4 u32 .. sub@8 u16 .. seq@12 u32]; body at off16.
+    if wire.len() <= 16 || u16::from_le_bytes([wire[8], wire[9]]) != 0x45 {
+        return Ok(None);
+    }
+    let seq = u32::from_le_bytes([wire[12], wire[13], wire[14], wire[15]]);
+    let inner = open_in(ks, &in_riv(out_riv), seq, &wire[16..])?;
+    // Inner header: [id u16][sub u16][counter u16][00 00]; EDID payload at 
off22.
+    const EDID_OFF: usize = 22;
+    if inner.len() < EDID_OFF + 128 {
+        return Ok(None);
+    }
+    let id = u16::from_le_bytes([inner[0], inner[1]]);
+    let sub = u16::from_le_bytes([inner[2], inner[3]]);
+    // The get-EDID reply id is `0x194` on the wire (CP-HANDSHAKE.md sec 4f, 
ground-truthed
+    // against the cold-ref capture); older notes wrote the low byte `0x94` 
alone. Accept
+    // both so a real `0x194` reply is not silently dropped (the EDID would 
never reach the
+    // connector even after CP engages).
+    if (id != 0x94 && id != 0x194) || sub != 0x21 {
+        return Ok(None);
+    }
+    let edid = &inner[EDID_OFF..];
+    // Validate the EDID base-block magic `00 FF FF FF FF FF FF 00`.
+    const MAGIC: [u8; 8] = [0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00];
+    if edid[..8] != MAGIC {
+        return Ok(None);
+    }
+    let total = ((1 + edid[126] as usize) * 128).min(edid.len());
+    let mut out = KVec::with_capacity(total, GFP_KERNEL)?;
+    out.extend_from_slice(&edid[..total], GFP_KERNEL)?;
+    Ok(Some(out))
+}
-- 
2.55.0

Reply via email to