[Concept,5/8] tkey: Add emulator and test

Message ID 20251019072313.3235339-6-sjg@u-boot.org
State New
Headers
Series tkey: Provide basic support for Tillitis TKey |

Commit Message

Simon Glass Oct. 19, 2025, 7:23 a.m. UTC
  From: Simon Glass <sjg@chromium.org>

Provide a simple emulator which can handle the TKey operations. Add a
test which uses it.

Co-developed-by: Claude <noreply@anthropic.com>
Signed-off-by: Simon Glass <sjg@chromium.org>
---

 arch/sandbox/dts/test.dts |   4 +
 drivers/misc/Makefile     |   3 +
 drivers/misc/tkey_emul.c  | 284 +++++++++++++++++++++++++++++++++++++
 test/dm/Makefile          |   1 +
 test/dm/tkey.c            | 290 ++++++++++++++++++++++++++++++++++++++
 5 files changed, 582 insertions(+)
 create mode 100644 drivers/misc/tkey_emul.c
 create mode 100644 test/dm/tkey.c
  

Patch

diff --git a/arch/sandbox/dts/test.dts b/arch/sandbox/dts/test.dts
index 86c01545462..9b0a5736cf8 100644
--- a/arch/sandbox/dts/test.dts
+++ b/arch/sandbox/dts/test.dts
@@ -1123,6 +1123,10 @@ 
 		};
 	};
 
+	tkey-emul {
+		compatible = "tkey,emul";
+	};
+
 	mmc2 {
 		compatible = "sandbox,mmc";
 		non-removable;
diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile
index 8db02bb9614..298bc1c0a69 100644
--- a/drivers/misc/Makefile
+++ b/drivers/misc/Makefile
@@ -18,6 +18,9 @@  obj-$(CONFIG_CROS_EC_I2C) += cros_ec_i2c.o
 obj-$(CONFIG_CROS_EC_SPI) += cros_ec_spi.o
 obj-$(CONFIG_SANDBOX) += p2sb_sandbox.o p2sb_emul.o
 obj-$(CONFIG_SANDBOX) += swap_case.o
+ifdef CONFIG_SANDBOX
+obj-$(CONFIG_TKEY) += tkey_emul.o
+endif
 endif
 
 ifdef CONFIG_$(PHASE_)DM_I2C
diff --git a/drivers/misc/tkey_emul.c b/drivers/misc/tkey_emul.c
new file mode 100644
index 00000000000..d8d81280486
--- /dev/null
+++ b/drivers/misc/tkey_emul.c
@@ -0,0 +1,284 @@ 
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C) 2025 Canonical Ltd
+ *
+ * TKey emulator for testing TKey functionality in sandbox
+ */
+
+#define LOG_CATEGORY	UCLASS_TKEY
+
+#include <dm.h>
+#include <errno.h>
+#include <log.h>
+#include <malloc.h>
+#include <tkey.h>
+#include <linux/delay.h>
+#include <linux/string.h>
+#include <asm/unaligned.h>
+
+/* TKey protocol frame structure */
+#define FRAME_SIZE		128
+#define FRAME_HEADER_SIZE	1
+#define FRAME_DATA_SIZE		(FRAME_SIZE - FRAME_HEADER_SIZE)
+
+/* Frame header bit masks and values */
+#define FRAME_ENDPOINT_MASK	0x18
+#define FRAME_ENDPOINT_SHIFT	3
+#define ENDPOINT_FIRMWARE	2
+#define ENDPOINT_APP		3
+
+/* Firmware Commands */
+#define FW_CMD_GET_NAME_VERSION	0x01
+#define FW_CMD_GET_UDI		0x08
+#define FW_CMD_LOAD_APP		0x03
+#define FW_CMD_LOAD_APP_DATA	0x05
+
+/* App Commands */
+#define APP_CMD_GET_PUBKEY	0x01
+
+/* USB Response format markers */
+#define USB_FRAME_MARKER	0x52
+#define USB_RSP_NAME_VERSION	0x02
+#define USB_RSP_GET_UDI		0x09
+
+/* Status codes */
+#define STATUS_OK		0x00
+#define STATUS_ERROR		0x01
+
+/*
+ * struct tkey_emul_priv - TKey emulator state
+ *
+ * @app_loaded: Whether an app is loaded (app mode vs firmware mode)
+ * @udi: Unique Device Identifier (8 bytes)
+ * @app_size: Size of loaded app
+ * @pubkey: Simulated public key (32 bytes)
+ * @resp: Buffer for storing response to be read
+ * @resp_len: Length of data in response buffer
+ * @total_loaded: Track total app data loaded
+ */
+struct tkey_emul_priv {
+	bool app_loaded;
+	u8 udi[8];
+	u32 app_size;
+	u8 pubkey[32];
+	u8 resp[FRAME_SIZE];
+	int resp_len;
+	u32 total_loaded;
+};
+
+static int tkey_emul_read(struct udevice *dev, void *buf, int len,
+			  int timeout_ms)
+{
+	/*
+	 * Read operations are immediate with no actual I/O. The data is
+	 * prepared by write operations in the emulated response buffer
+	 */
+	log_debug("read: %d bytes requested\n", len);
+
+	return -ENOSYS;
+}
+
+static int handle_fw_get_name_version(struct tkey_emul_priv *priv)
+{
+	/* USB format: 0x52 0x02 [tk1 ] [name1] [version] */
+	priv->resp[0] = USB_FRAME_MARKER;
+	priv->resp[1] = USB_RSP_NAME_VERSION;
+	memcpy(priv->resp + 2, "tk1 ", 4);
+
+	/* name1 changes based on firmware vs app mode */
+	if (priv->app_loaded)
+		memcpy(priv->resp + 6, "sign", 4);
+	else
+		memcpy(priv->resp + 6, "mkdf", 4);
+	put_unaligned_le32(4, priv->resp + 10);
+	priv->resp_len = 14;
+	log_debug("GET_NAME_VERSION (mode=%s)\n",
+		  priv->app_loaded ? "app" : "firmware");
+
+	return 0;
+}
+
+static int handle_fw_get_udi(struct tkey_emul_priv *priv)
+{
+	/* UDI is only available in firmware mode */
+	if (priv->app_loaded) {
+		priv->resp_len = 0;
+		log_debug("GET_UDI rejected (app mode)\n");
+	} else {
+		priv->resp[0] = USB_FRAME_MARKER;
+		priv->resp[1] = USB_RSP_GET_UDI;
+		priv->resp[2] = STATUS_OK;
+		memcpy(priv->resp + 3, priv->udi, 8);
+		priv->resp_len = 11;
+		log_debug("GET_UDI OK\n");
+	}
+
+	return 0;
+}
+
+static int handle_fw_load_app(struct tkey_emul_priv *priv, const u8 *data)
+{
+	/* App size is in bytes 2-5 (big endian) */
+	priv->app_size = get_unaligned_be32(data + 2);
+
+	/* Simple ACK - just return status */
+	priv->resp[0] = STATUS_OK;
+	priv->resp_len = 1;
+	log_debug("LOAD_APP (size=%u)\n", priv->app_size);
+
+	return 0;
+}
+
+static int handle_fw_load_app_data(struct tkey_emul_priv *priv, const u8 *data)
+{
+	int chunk_size = get_unaligned_be32(data + 2);
+
+	priv->total_loaded += chunk_size;
+
+	/* Simple ACK */
+	priv->resp[0] = STATUS_OK;
+	priv->resp_len = 1;
+
+	if (priv->total_loaded >= priv->app_size) {
+		/* App fully loaded - enter app mode */
+		priv->app_loaded = true;
+		priv->total_loaded = 0;
+		log_debug("App loaded, entering app mode\n");
+	} else {
+		log_debug("LOAD_APP_DATA (%u/%u)\n",
+			  priv->total_loaded, priv->app_size);
+	}
+
+	return 0;
+}
+
+static int handle_firmware_cmd(struct udevice *dev, u8 cmd, const u8 *data)
+{
+	struct tkey_emul_priv *priv = dev_get_priv(dev);
+
+	switch (cmd) {
+	case FW_CMD_GET_NAME_VERSION:
+		return handle_fw_get_name_version(priv);
+	case FW_CMD_GET_UDI:
+		return handle_fw_get_udi(priv);
+	case FW_CMD_LOAD_APP:
+		return handle_fw_load_app(priv, data);
+	case FW_CMD_LOAD_APP_DATA:
+		return handle_fw_load_app_data(priv, data);
+	default:
+		log_err("Unknown firmware command %02x\n", cmd);
+		return -EINVAL;
+	}
+}
+
+static int handle_app_get_pubkey(struct tkey_emul_priv *priv)
+{
+	memcpy(priv->resp, priv->pubkey, 32);
+	priv->resp_len = 32;
+	log_debug("GET_PUBKEY\n");
+
+	return 0;
+}
+
+static int handle_app_cmd(struct udevice *dev, u8 cmd)
+{
+	struct tkey_emul_priv *priv = dev_get_priv(dev);
+
+	if (!priv->app_loaded) {
+		log_err("App command sent but not in app mode\n");
+		return -EINVAL;
+	}
+
+	switch (cmd) {
+	case APP_CMD_GET_PUBKEY:
+		return handle_app_get_pubkey(priv);
+	default:
+		log_err("Unknown app command %02x\n", cmd);
+		return -EINVAL;
+	}
+}
+
+static int tkey_emul_write(struct udevice *dev, const void *buf, int len)
+{
+	const u8 *data = buf;
+	u8 header, endpoint, cmd;
+	int ret;
+
+	if (len < 2)
+		return -EINVAL;
+
+	header = data[0];
+	endpoint = (header & FRAME_ENDPOINT_MASK) >> FRAME_ENDPOINT_SHIFT;
+	cmd = data[1];
+
+	log_debug("header %02x endpoint %u cmd %02x\n", header, endpoint, cmd);
+
+	/* Route to appropriate endpoint handler */
+	if (endpoint == ENDPOINT_FIRMWARE) {
+		ret = handle_firmware_cmd(dev, cmd, data);
+	} else if (endpoint == ENDPOINT_APP) {
+		ret = handle_app_cmd(dev, cmd);
+	} else {
+		log_err("Unknown endpoint %u\n", endpoint);
+		return -EINVAL;
+	}
+
+	return ret ? ret : len;
+}
+
+static int tkey_emul_read_all(struct udevice *dev, void *buf, int maxlen,
+			      int timeout_ms)
+{
+	struct tkey_emul_priv *priv = dev_get_priv(dev);
+	int len = min(priv->resp_len, maxlen);
+
+	log_debug("read_all: %d bytes max, returning %d bytes\n", maxlen, len);
+
+	/* Copy the raw USB response data including the 0x52 marker */
+	if (len > 0)
+		memcpy(buf, priv->resp, len);
+
+	return len;
+}
+
+static int tkey_emul_probe(struct udevice *dev)
+{
+	struct tkey_emul_priv *priv = dev_get_priv(dev);
+	int i;
+
+	/* Generate a deterministic UDI based on device name */
+	for (i = 0; i < 8; i++)
+		priv->udi[i] = 0xa0 + i;
+
+	/* Generate a deterministic public key */
+	for (i = 0; i < 32; i++)
+		priv->pubkey[i] = 0x50 + (i & 0xf);
+
+	log_debug("init with UDI: ");
+	for (i = 0; i < 8; i++)
+		log_debug("%02x", priv->udi[i]);
+	log_debug("\n");
+
+	return 0;
+}
+
+/* TKey uclass operations */
+static const struct tkey_ops tkey_emul_ops = {
+	.read = tkey_emul_read,
+	.write = tkey_emul_write,
+	.read_all = tkey_emul_read_all,
+};
+
+static const struct udevice_id tkey_emul_ids[] = {
+	{ .compatible = "tkey,emul" },
+	{ }
+};
+
+U_BOOT_DRIVER(tkey_emul) = {
+	.name		= "tkey_emul",
+	.id		= UCLASS_TKEY,
+	.of_match	= tkey_emul_ids,
+	.probe		= tkey_emul_probe,
+	.ops		= &tkey_emul_ops,
+	.priv_auto	= sizeof(struct tkey_emul_priv),
+};
diff --git a/test/dm/Makefile b/test/dm/Makefile
index b38de3e12d0..45aee79c8f2 100644
--- a/test/dm/Makefile
+++ b/test/dm/Makefile
@@ -121,6 +121,7 @@  obj-$(CONFIG_SYSINFO) += sysinfo.o
 obj-$(CONFIG_SYSINFO_GPIO) += sysinfo-gpio.o
 obj-$(CONFIG_UT_DM) += tag.o
 obj-$(CONFIG_TEE) += tee.o
+obj-$(CONFIG_TKEY) += tkey.o
 obj-$(CONFIG_TIMER) += timer.o
 obj-$(CONFIG_TPM_V2) += tpm.o
 obj-$(CONFIG_DM_USB) += usb.o
diff --git a/test/dm/tkey.c b/test/dm/tkey.c
new file mode 100644
index 00000000000..9dffae66a5e
--- /dev/null
+++ b/test/dm/tkey.c
@@ -0,0 +1,290 @@ 
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C) 2025 Canonical Ltd
+ *
+ * Test for TKey uclass and emulator
+ */
+
+#include <dm.h>
+#include <dm/test.h>
+#include <test/test.h>
+#include <test/ut.h>
+#include <tkey.h>
+
+/* Test that we can find a TKey device */
+static int dm_test_tkey_find(struct unit_test_state *uts)
+{
+	struct udevice *dev;
+
+	ut_assertok(uclass_first_device_err(UCLASS_TKEY, &dev));
+	ut_assertnonnull(dev);
+
+	return 0;
+}
+DM_TEST(dm_test_tkey_find, UTF_SCAN_FDT);
+
+/* Test getting UDI from TKey */
+static int dm_test_tkey_get_udi(struct unit_test_state *uts)
+{
+	u8 udi[TKEY_UDI_SIZE];
+	struct udevice *dev;
+
+	ut_assertok(uclass_first_device_err(UCLASS_TKEY, &dev));
+
+	ut_assertok(tkey_get_udi(dev, udi));
+
+	/* Verify emulator returns expected UDI */
+	ut_asserteq(0xa0, udi[0]);
+	ut_asserteq(0xa1, udi[1]);
+	ut_asserteq(0xa2, udi[2]);
+	ut_asserteq(0xa3, udi[3]);
+	ut_asserteq(0xa4, udi[4]);
+	ut_asserteq(0xa5, udi[5]);
+	ut_asserteq(0xa6, udi[6]);
+	ut_asserteq(0xa7, udi[7]);
+
+	return 0;
+}
+DM_TEST(dm_test_tkey_get_udi, UTF_SCAN_FDT);
+
+/* Test getting name and version from TKey */
+static int dm_test_tkey_get_name_version(struct unit_test_state *uts)
+{
+	char name0[TKEY_NAME_SIZE], name1[TKEY_NAME_SIZE];
+	struct udevice *dev;
+	u32 version;
+
+	ut_assertok(uclass_first_device_err(UCLASS_TKEY, &dev));
+
+	/* Get name and version */
+	ut_assertok(tkey_get_name_version(dev, name0, name1, &version));
+
+	/* Verify emulator returns expected values */
+	ut_asserteq_str("tk1 ", name0);
+	ut_asserteq_str("mkdf", name1);
+	ut_asserteq(4, version);
+
+	return 0;
+}
+DM_TEST(dm_test_tkey_get_name_version, UTF_SCAN_FDT);
+
+/* Test checking firmware mode */
+static int dm_test_tkey_in_app_mode(struct unit_test_state *uts)
+{
+	struct udevice *dev;
+	int ret;
+
+	ut_assertok(uclass_first_device_err(UCLASS_TKEY, &dev));
+
+	/* Check mode - should be in firmware mode initially */
+	ret = tkey_in_app_mode(dev);
+	ut_assert(ret >= 0);
+	ut_asserteq(0, ret);  /* 0 = firmware mode */
+
+	return 0;
+}
+DM_TEST(dm_test_tkey_in_app_mode, UTF_SCAN_FDT);
+
+/* Test loading an app */
+static int dm_test_tkey_load_app(struct unit_test_state *uts)
+{
+	struct udevice *dev;
+	u8 dummy_app[128];
+	int ret;
+
+	ut_assertok(uclass_first_device_err(UCLASS_TKEY, &dev));
+
+	/* Create a dummy app */
+	memset(dummy_app, 0x42, sizeof(dummy_app));
+
+	/* Load the app */
+	ret = tkey_load_app(dev, dummy_app, sizeof(dummy_app));
+	ut_assertok(ret);
+
+	/* After loading, should be in app mode */
+	ret = tkey_in_app_mode(dev);
+	ut_assert(ret >= 0);
+	ut_asserteq(1, ret);  /* 1 = app mode */
+
+	return 0;
+}
+DM_TEST(dm_test_tkey_load_app, UTF_SCAN_FDT);
+
+/* Test getting public key from signer app */
+static int dm_test_tkey_get_pubkey(struct unit_test_state *uts)
+{
+	u8 pubkey[TKEY_PUBKEY_SIZE];
+	struct udevice *dev;
+	u8 dummy_app[128];
+	int i;
+
+	ut_assertok(uclass_first_device_err(UCLASS_TKEY, &dev));
+
+	/* Load a dummy app first */
+	memset(dummy_app, 0x42, sizeof(dummy_app));
+	ut_assertok(tkey_load_app(dev, dummy_app, sizeof(dummy_app)));
+
+	/* Get public key */
+	ut_assertok(tkey_get_pubkey(dev, pubkey));
+
+	/* Verify emulator returns expected pattern */
+	for (i = 0; i < TKEY_PUBKEY_SIZE; i++)
+		ut_asserteq(0x50 + (i & 0xf), pubkey[i]);
+
+	return 0;
+}
+DM_TEST(dm_test_tkey_get_pubkey, UTF_SCAN_FDT);
+
+/* Test deriving wrapping key from password */
+static int dm_test_tkey_derive_wrapping_key(struct unit_test_state *uts)
+{
+	u8 wrapping_key[TKEY_WRAPPING_KEY_SIZE];
+	const char *password = "test_password";
+	/* Expected BLAKE2b(UDI || password) where UDI = a0a1a2a3a4a5a6a7 */
+	const u8 expected[TKEY_WRAPPING_KEY_SIZE] = {
+		0x95, 0x22, 0x9c, 0xd3, 0x76, 0x89, 0x8f, 0x3f,
+		0xb0, 0x22, 0xa6, 0x27, 0x34, 0x9d, 0xc9, 0x85,
+		0xbc, 0x46, 0x75, 0xda, 0x58, 0x0d, 0x26, 0x96,
+		0xbd, 0xd6, 0xf7, 0x1f, 0x48, 0x8e, 0x30, 0x6c,
+	};
+	struct udevice *dev;
+
+	ut_assertok(uclass_first_device_err(UCLASS_TKEY, &dev));
+
+	/* Derive wrapping key from password */
+	ut_assertok(tkey_derive_wrapping_key(dev, password, wrapping_key));
+
+	/* Verify the exact wrapping key value */
+	ut_asserteq_mem(expected, wrapping_key, TKEY_WRAPPING_KEY_SIZE);
+
+	return 0;
+}
+DM_TEST(dm_test_tkey_derive_wrapping_key, UTF_SCAN_FDT);
+
+/* Test deriving disk key with USS */
+static int dm_test_tkey_derive_disk_key(struct unit_test_state *uts)
+{
+	const char *uss = "user_secret";
+	u8 disk_key[TKEY_DISK_KEY_SIZE];
+	u8 pubkey[TKEY_PUBKEY_SIZE];
+	u8 key_hash[TKEY_HASH_SIZE];
+	/* Expected pubkey from emulator (deterministic pattern) */
+	const u8 expected_pubkey[TKEY_PUBKEY_SIZE] = {
+		0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57,
+		0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
+		0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57,
+		0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
+	};
+	/* Expected disk key: BLAKE2b(pubkey) */
+	const u8 expected_disk_key[TKEY_DISK_KEY_SIZE] = {
+		0x22, 0x8b, 0x2f, 0x6a, 0xbf, 0x8b, 0xe0, 0x56,
+		0x49, 0xb2, 0x41, 0x75, 0x86, 0x15, 0x0b, 0xbf,
+		0x3e, 0x1b, 0x3f, 0x66, 0x9a, 0xfa, 0x1c, 0x61,
+		0x51, 0xdd, 0xc7, 0x29, 0x57, 0x93, 0x3c, 0x21,
+	};
+	/* Expected key hash: BLAKE2b(disk_key) */
+	const u8 expected_key_hash[TKEY_HASH_SIZE] = {
+		0xa7, 0x2a, 0x46, 0xb8, 0xf8, 0xc7, 0xff, 0x08,
+		0x24, 0x41, 0x6a, 0xda, 0x88, 0x6f, 0x62, 0xb6,
+		0xc2, 0x80, 0x88, 0x96, 0xd7, 0x12, 0x01, 0xa3,
+		0x28, 0x14, 0xab, 0x43, 0x2c, 0x7a, 0x81, 0xcf,
+	};
+	struct udevice *dev;
+	u8 dummy_app[128];
+
+	ut_assertok(uclass_first_device_err(UCLASS_TKEY, &dev));
+
+	/* Create a dummy signer app */
+	memset(dummy_app, 0x42, sizeof(dummy_app));
+
+	/* Derive disk key */
+	ut_assertok(tkey_derive_disk_key(dev, dummy_app, sizeof(dummy_app),
+					 uss, strlen(uss), disk_key, pubkey,
+					 key_hash));
+
+	ut_asserteq_mem(expected_pubkey, pubkey, TKEY_PUBKEY_SIZE);
+	ut_asserteq_mem(expected_disk_key, disk_key, TKEY_DISK_KEY_SIZE);
+	ut_asserteq_mem(expected_key_hash, key_hash, TKEY_HASH_SIZE);
+
+	return 0;
+}
+DM_TEST(dm_test_tkey_derive_disk_key, UTF_SCAN_FDT);
+
+/* Test UDI not available in app mode */
+static int dm_test_tkey_udi_app_mode(struct unit_test_state *uts)
+{
+	u8 udi[TKEY_UDI_SIZE];
+	struct udevice *dev;
+	u8 dummy_app[128];
+
+	ut_assertok(uclass_first_device_err(UCLASS_TKEY, &dev));
+
+	/* Load an app to enter app mode */
+	memset(dummy_app, 0x42, sizeof(dummy_app));
+	ut_assertok(tkey_load_app(dev, dummy_app, sizeof(dummy_app)));
+
+	/* Verify we're in app mode */
+	ut_asserteq(1, tkey_in_app_mode(dev));
+
+	/* Try to get UDI - emulator returns -EIO for empty response */
+	ut_asserteq(-EIO, tkey_get_udi(dev, udi));
+
+	return 0;
+}
+DM_TEST(dm_test_tkey_udi_app_mode, UTF_SCAN_FDT);
+
+/* Test loading app with USS */
+static int dm_test_tkey_load_app_with_uss(struct unit_test_state *uts)
+{
+	struct udevice *dev;
+	u8 dummy_app[128];
+	const char *uss = "my_secret";
+
+	ut_assertok(uclass_first_device_err(UCLASS_TKEY, &dev));
+
+	/* Create a dummy app */
+	memset(dummy_app, 0x55, sizeof(dummy_app));
+
+	/* Load app with USS */
+	ut_assertok(tkey_load_app_with_uss(dev, dummy_app, sizeof(dummy_app),
+					   uss, strlen(uss)));
+
+	/* Should be in app mode */
+	ut_asserteq(1, tkey_in_app_mode(dev));
+
+	return 0;
+}
+DM_TEST(dm_test_tkey_load_app_with_uss, UTF_SCAN_FDT);
+
+/* Test basic read/write operations */
+static int dm_test_tkey_read_write(struct unit_test_state *uts)
+{
+	/* Expected USB response: 0x52 0x02 [tk1 ] [mkdf] [version=4] */
+	static const u8 expected[14] = {
+		0x52, 0x02,		/* USB marker and response type */
+		't', 'k', '1', ' ',	/* name0 */
+		'm', 'k', 'd', 'f',	/* name1 */
+		0x04, 0x00, 0x00, 0x00,	/* version = 4 (little-endian) */
+	};
+	struct udevice *dev;
+	u8 write_buf[129];  /* Header + command */
+	u8 read_buf[256];
+
+	ut_assertok(uclass_first_device_err(UCLASS_TKEY, &dev));
+
+	/* Prepare a GET_NAME_VERSION command */
+	write_buf[0] = 0x10;  /* Header: CMD, FIRMWARE endpoint */
+	write_buf[1] = 0x01;  /* CMD_GET_NAME_VERSION */
+
+	/* Write the command - should return 2 bytes written */
+	ut_asserteq(2, tkey_write(dev, write_buf, 2));
+
+	/* Read the response - should get exactly 14 bytes */
+	ut_asserteq(14, tkey_read_all(dev, read_buf, sizeof(read_buf), 1000));
+
+	/* Verify full response matches expected */
+	ut_asserteq_mem(expected, read_buf, 14);
+
+	return 0;
+}
+DM_TEST(dm_test_tkey_read_write, UTF_SCAN_FDT);