[Concept,5/7] test: bootctl: Add passphrase UI and TKey unlock tests

Message ID 20251208124001.775057-6-sjg@u-boot.org
State New
Headers
Series luks: Complete TKey implementation |

Commit Message

Simon Glass Dec. 8, 2025, 12:39 p.m. UTC
  From: Simon Glass <simon.glass@canonical.com>

Add tests for the bootctl passphrase entry UI and TKey-based LUKS
unlock flow:

- check_passphrase(): Tests the passphrase textline widget, verifying
  character input, backspace handling, and passphrase retrieval

- prepare_tkey_test(): Sets up the TKey emulator with a test pubkey
  and configures app mode to test replugging scenarios

- try_tkey_unlock(): Tests the complete TKey unlock flow including
  passphrase entry and LUKS partition decryption

- bootctl_logic_tkey: Full integration test for TKey-based encrypted
  boot with mouse click interactions

Co-developed-by: Claude <noreply@anthropic.com>
Signed-off-by: Simon Glass <simon.glass@canonical.com>
---

 test/boot/bootctl/bootctl.c | 433 +++++++++++++++++++++++++++++++++++-
 1 file changed, 431 insertions(+), 2 deletions(-)
  

Patch

diff --git a/test/boot/bootctl/bootctl.c b/test/boot/bootctl/bootctl.c
index 6c12cf708e0..f251cd18507 100644
--- a/test/boot/bootctl/bootctl.c
+++ b/test/boot/bootctl/bootctl.c
@@ -16,18 +16,26 @@ 
 #include <bootstd.h>
 #include <dm.h>
 #include <expo.h>
+#include <menu.h>
+#include <mouse.h>
 #include <os.h>
+#include <tkey.h>
 #include "bootctl_common.h"
 #include <bootctl/logic.h>
 #include <bootctl/measure.h>
 #include <bootctl/oslist.h>
 #include <bootctl/state.h>
 #include <bootctl/ui.h>
+#include <dm/device-internal.h>
 #include <dm/lists.h>
 #include <test/ut.h>
 #include <test/video.h>
 #include "../bootstd_common.h"
-
+#include "../../../boot/bootflow_internal.h"
+#include "../../../boot/scene_internal.h"
+#include "../bootstd_common.h"
+#include "../expo_common.h"
+//
 /* test that expected devices are available and can be probed */
 static int bootctl_base(struct unit_test_state *uts)
 {
@@ -333,6 +341,97 @@  static int bootctl_simple_measure(struct unit_test_state *uts)
 }
 BOOTCTL_TEST(bootctl_simple_measure, UTF_DM | UTF_SCAN_FDT | UTF_CONSOLE);
 
+/**
+ * check_passphrase() - Test passphrase functionality for an encrypted item
+ *
+ * @uts: Test state
+ * @ui_dev: UI device to test
+ * @seq: Sequence number of the encrypted bootflow item
+ * Return: 0 if OK, -ve on error
+ */
+static int check_passphrase(struct unit_test_state *uts,
+			    struct udevice *ui_dev, int seq)
+{
+	struct bc_ui_priv *uc_priv = dev_get_uclass_priv(ui_dev);
+	const char *retrieved_passphrase = NULL;
+	struct scene_obj *label_obj, *edit_obj;
+	struct scene_obj_textline *tline;
+	struct scene *scn = uc_priv->scn;
+	bool selected;
+	int seq_out;
+
+	/* Show passphrase for the specified item (this also opens it) */
+	ut_assertok(bc_ui_show_pass(ui_dev, seq, true));
+	ut_assertok(bc_ui_render(ui_dev));
+
+	/* Verify passphrase textline and its child objects are now visible */
+	tline = scene_obj_find(scn, ITEM_PASS + seq, SCENEOBJT_TEXTLINE);
+	ut_assertnonnull(tline);
+	ut_asserteq(false, tline->obj.flags & SCENEOF_HIDE);
+	ut_assert(tline->obj.flags & SCENEOF_OPEN);
+
+	/* Verify the scene's highlight is set to the passphrase textline */
+	ut_asserteq(ITEM_PASS + seq, scn->highlight_id);
+
+	label_obj = scene_obj_find(scn, ITEM_PASS_LABEL + seq, SCENEOBJT_NONE);
+	ut_assertnonnull(label_obj);
+	ut_asserteq(false, label_obj->flags & SCENEOF_HIDE);
+
+	edit_obj = scene_obj_find(scn, ITEM_PASS_EDIT + seq, SCENEOBJT_NONE);
+	ut_assertnonnull(edit_obj);
+	ut_asserteq(false, edit_obj->flags & SCENEOF_HIDE);
+
+	/* Type 't', 'e', 's', 't' - each poll processes one character */
+	ut_asserteq(4, console_in_puts("test"));
+	ut_assertok(bc_ui_poll(ui_dev, &seq_out, &selected));
+	ut_asserteq_str("t", abuf_data(&tline->buf));
+	ut_assertok(bc_ui_poll(ui_dev, &seq_out, &selected));
+	ut_asserteq_str("te", abuf_data(&tline->buf));
+	ut_assertok(bc_ui_poll(ui_dev, &seq_out, &selected));
+	ut_asserteq_str("tes", abuf_data(&tline->buf));
+	ut_assertok(bc_ui_poll(ui_dev, &seq_out, &selected));
+	ut_asserteq_str("test", abuf_data(&tline->buf));
+
+	/* Send backspace to remove one character */
+	ut_asserteq(1, console_in_puts("\b"));
+	ut_assertok(bc_ui_poll(ui_dev, &seq_out, &selected));
+	ut_asserteq_str("tes", abuf_data(&tline->buf));
+
+	/* Re-add the 't' and verify */
+	ut_asserteq(1, console_in_puts("t"));
+	ut_assertok(bc_ui_poll(ui_dev, &seq_out, &selected));
+	ut_asserteq_str("test", abuf_data(&tline->buf));
+
+	/* Send return key to submit - should close textline and select */
+	ut_asserteq(1, console_in_puts("\n"));
+	ut_assertok(bc_ui_poll(ui_dev, &seq_out, &selected));
+	ut_assert(selected);
+	ut_asserteq(seq, seq_out);
+
+	/* Verify we can retrieve the passphrase */
+	ut_assertok(bc_ui_get_pass(ui_dev, seq, &retrieved_passphrase));
+	ut_assertnonnull(retrieved_passphrase);
+	ut_asserteq_str("test", retrieved_passphrase);
+
+	/*
+	 * Verify the LUKS partition unlock would be attempted. In a real
+	 * scenario, this would call luks_unlock(), but for the test we just
+	 * verify the passphrase was correctly captured and the UI state
+	 * indicates selection was made (which triggers the unlock logic)
+	 */
+
+	/* Test hiding the passphrase field */
+	ut_assertok(bc_ui_show_pass(ui_dev, seq, false));
+	ut_assertok(bc_ui_render(ui_dev));
+
+	/* Verify all three objects are now hidden */
+	ut_asserteq(true, tline->obj.flags & SCENEOF_HIDE);
+	ut_asserteq(true, label_obj->flags & SCENEOF_HIDE);
+	ut_asserteq(true, edit_obj->flags & SCENEOF_HIDE);
+
+	return 0;
+}
+
 static int check_multiboot_ui(struct unit_test_state *uts,
 			      struct bootstd_priv *std)
 {
@@ -457,6 +556,11 @@  static int check_multiboot_ui(struct unit_test_state *uts,
 		}
 	}
 
+	/*
+	 * Test passphrase functionality for mmc11 (item 0, which is encrypted)
+	 */
+	ut_assertok(check_passphrase(uts, ui_dev, 0));
+
 	membuf_dispose(&buf1);
 	membuf_dispose(&buf2);
 	membuf_dispose(&buf3);
@@ -497,4 +601,329 @@  static int bootctl_multiboot_ui(struct unit_test_state *uts)
 
 	return 0;
 }
-BOOTCTL_TEST(bootctl_multiboot_ui, UTF_DM | UTF_SCAN_FDT);
+BOOTCTL_TEST(bootctl_multiboot_ui, UTF_DM | UTF_SCAN_FDT | UTF_CONSOLE);
+
+/**
+ * click_os() - Click on an OS in the bootctl UI
+ *
+ * @uts: Unit test state
+ * @lpriv: Logic private data
+ * @seq: Sequence number of the OS to click
+ * Return: 0 if OK, -ve on error
+ */
+static int click_os(struct unit_test_state *uts, struct logic_priv *lpriv,
+		    int seq)
+{
+	struct bc_ui_priv *uc_priv;
+	struct scene_obj *obj;
+	struct scene *scn;
+	struct expo *exp;
+
+	uc_priv = dev_get_uclass_priv(lpriv->ui);
+	scn = uc_priv->scn;
+	exp = uc_priv->expo;
+
+	/* Get the position of ITEM_DESC + seq and queue a click there */
+	obj = scene_obj_find(scn, ITEM_DESC + seq, SCENEOBJT_NONE);
+	ut_assertnonnull(obj);
+	/* Click halfway along the object, 5 pixels from the top */
+	ut_assertok(mouse_queue_click_for_test(exp->mouse,
+					       obj->bbox.x0 + (obj->bbox.x1 -
+						       obj->bbox.x0) / 2,
+					       obj->bbox.y0 + 5));
+
+	return 0;
+}
+
+/**
+ * prepare_tkey_test() - Prepare bootctl logic for TKey unlock testing
+ *
+ * This helper sets up the complete test environment including:
+ * - Preparing the logic and finding bootflows
+ * - Configuring TKey emulator with test pubkey
+ * - Setting TKey to app mode to test replugging
+ * - Starting the logic and polling to find OSes
+ * - Verifying encrypted bootflows were found
+ *
+ * @uts: Unit test state
+ * @logic: Bootctl logic device
+ * @emul_out: Returns the TKey emulator device
+ * @test_pubkey: Public key to configure in emulator
+ * Return: 0 on success, -ve on error
+ */
+static int prepare_tkey_test(struct unit_test_state *uts,
+			     struct udevice *logic,
+			     struct udevice **emul_out,
+			     const u8 *test_pubkey)
+{
+	struct logic_priv *lpriv = dev_get_priv(logic);
+	struct udevice *emul;
+
+	/*
+	 * Prepare the logic. TKey device will be found automatically in
+	 * tkey_poll() when needed (uses first device, which is tkey-emul)
+	 */
+	ut_assertok(bc_logic_prepare(logic));
+	ut_assertnonnull(lpriv->ui);
+	ut_assertnonnull(lpriv->oslist);
+
+	/*
+	 * Configure the emulator to return a pubkey that matches the test
+	 * LUKS image. The test image was created with this specific TKey.
+	 * Get the emulator device to configure it.
+	 */
+	ut_assertok(uclass_get_device_by_name(UCLASS_TKEY, "tkey-emul",
+					      &emul));
+	ut_assertok(tkey_emul_set_pubkey_for_test(emul, test_pubkey));
+
+	/*
+	 * Put TKey into app mode. This will force the unlock logic to
+	 * request replugging the TKey.
+	 */
+	ut_assertok(tkey_emul_set_app_mode_for_test(emul, true));
+
+	/* Start the logic */
+	ut_assertok(bc_logic_start(logic));
+
+	/*
+	 * Override the TKey device to use the emulator. logic_start() finds
+	 * the first device, but we want to use tkey-emul for testing.
+	 */
+	lpriv->tkey = emul;
+
+	/* Poll twice to find both OSes (no delays, so completes quickly) */
+	ut_assertok(bc_logic_poll(logic));
+	ut_assertok(bc_logic_poll(logic));
+
+	/* Verify both OSes were found */
+	ut_asserteq(2, lpriv->osinfo.count);
+
+	/* First OS should be mmc13 and should be marked as encrypted */
+	ut_asserteq_str("mmc13.bootdev.part_1",
+			alist_getw(&lpriv->osinfo, 0,
+				   struct osinfo)->bflow.name);
+	ut_assert(alist_getw(&lpriv->osinfo, 0, struct osinfo)->bflow.flags &
+		  BOOTFLOWF_ENCRYPTED);
+
+	/* Verify TKey is enabled (device will be found later in tkey_poll) */
+	ut_assert(lpriv->opt_tkey);
+
+	*emul_out = emul;
+	return 0;
+}
+
+/**
+ * try_tkey_unlock() - Try to unlock with TKey using a passphrase
+ *
+ * @uts: Unit test state
+ * @logic: Logic device
+ * @emul: TKey emulator device
+ * @test_pubkey: Expected public key (or NULL to keep wrong key for failure
+ * test)
+ * @passphrase: Passphrase to enter
+ * @load_iterations_out: Pointer to store load iteration count
+ * Return: 0 if OK, -ve on error
+ */
+static int try_tkey_unlock(struct unit_test_state *uts, struct udevice *logic,
+			   struct udevice *emul, const u8 *test_pubkey,
+			   const char *passphrase, int *load_iterations_out)
+{
+	struct logic_priv *lpriv = dev_get_priv(logic);
+	int load_iterations;
+	int i;
+
+	/* Verify passphrase is being requested */
+	ut_asserteq(UNS_WAITING_PASS, lpriv->ustate);
+	ut_asserteq(0, lpriv->selected_seq);
+
+	/* Type the passphrase - each poll processes one character */
+	ut_asserteq(strlen(passphrase), console_in_puts(passphrase));
+	for (i = 0; i < strlen(passphrase); i++)
+		ut_assertok(bc_logic_poll(logic));
+
+	/* Press return to submit the passphrase */
+	ut_asserteq(1, console_in_puts("\n"));
+
+	/* Poll to process return - should transition to UNS_TKEY_START */
+	ut_assertok(bc_logic_poll(logic));
+	ut_asserteq(UNS_TKEY_START, lpriv->ustate);
+
+	/* Poll - should transition to UNS_TKEY_WAIT_INSERT */
+	ut_assertok(bc_logic_poll(logic));
+	ut_asserteq(UNS_TKEY_WAIT_INSERT, lpriv->ustate);
+
+	/* Poll - TKey should be detected, transition to UNS_TKEY_INSERTED */
+	ut_assertok(bc_logic_poll(logic));
+	ut_asserteq(UNS_TKEY_INSERTED, lpriv->ustate);
+
+	/*
+	 * Poll - TKey is in app mode, should request removal
+	 * Transition to UNS_TKEY_WAIT_REMOVE
+	 */
+	ut_assertok(bc_logic_poll(logic));
+	ut_asserteq(UNS_TKEY_WAIT_REMOVE, lpriv->ustate);
+
+	/* Simulate TKey removal by disconnecting the emulator */
+	ut_assertok(tkey_emul_set_connected_for_test(emul, false));
+
+	/* Poll - should detect removal, transition to UNS_TKEY_WAIT_INSERT */
+	ut_assertok(bc_logic_poll(logic));
+	ut_asserteq(UNS_TKEY_WAIT_INSERT, lpriv->ustate);
+
+	/* Simulate TKey reinsertion (reconnect the device) */
+	ut_assertok(tkey_emul_set_connected_for_test(emul, true));
+
+	/*
+	 * Poll - TKey should be detected again, transition to
+	 * UNS_TKEY_INSERTED
+	 */
+	ut_assertok(bc_logic_poll(logic));
+	ut_asserteq(UNS_TKEY_INSERTED, lpriv->ustate);
+
+	/*
+	 * After reprobe, the emulator gets new priv data.
+	 * Set the pubkey if provided (for success), or skip it (for failure)
+	 */
+	if (test_pubkey)
+		ut_assertok(tkey_emul_set_pubkey_for_test(emul, test_pubkey));
+
+	/* Poll - should start loading, transition to UNS_TKEY_LOADING */
+	ut_assertok(bc_logic_poll(logic));
+	ut_asserteq(UNS_TKEY_LOADING, lpriv->ustate);
+
+	/* Poll while TKey app is loading */
+	load_iterations = 0;
+	while (lpriv->ustate == UNS_TKEY_LOADING) {
+		ut_assertok(bc_logic_poll(logic));
+		load_iterations++;
+		/* Exact count: 28KB / 127 bytes */
+		ut_assert(load_iterations <= 221);
+	}
+
+	/* Verify loading completed - should be in UNS_TKEY_READY */
+	ut_asserteq(UNS_TKEY_READY, lpriv->ustate);
+	ut_asserteq(221, load_iterations);
+
+	if (load_iterations_out)
+		*load_iterations_out = load_iterations;
+
+	/* Poll - should derive key and transition to UNS_TKEY_UNLOCK */
+	ut_assertok(bc_logic_poll(logic));
+	ut_asserteq(UNS_TKEY_UNLOCK, lpriv->ustate);
+
+	/* Poll - should perform unlock and transition to UNS_UNLOCK_RESULT */
+	ut_assertok(bc_logic_poll(logic));
+	ut_asserteq(UNS_UNLOCK_RESULT, lpriv->ustate);
+
+	/* Poll - should process result */
+	ut_assertok(bc_logic_poll(logic));
+
+	return 0;
+}
+
+/* test TKey unlock with logic device - wrong then correct passphrase */
+static int bootctl_logic_tkey(struct unit_test_state *uts)
+{
+	/* Correct pubkey matching emulator default - produces valid disk key */
+	const u8 test_pubkey[32] = {
+		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
+	};
+	/*
+	 * Wrong pubkey - produces an invalid disk key for testing unlock
+	 * failure
+	 */
+	const u8 wrong_pubkey[32] = {
+		0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+		0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+		0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
+		0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f
+	};
+	struct udevice *emul, *logic, *dev;
+	struct logic_priv *lpriv;
+	ofnode root, node;
+
+	test_set_skip_delays(true);
+	bootstd_reset_usb();
+
+	/* Enable mmc13 device which has the TKey-encrypted partition */
+	root = oftree_root(oftree_default());
+	node = ofnode_find_subnode(root, "mmc13");
+	ut_assert(ofnode_valid(node));
+	ut_assertok(lists_bind_fdt(gd->dm_root, node, &dev, NULL, false));
+
+	/* Get the logic device */
+	ut_assertok(bootctl_get_dev(UCLASS_BOOTCTL, &logic));
+	lpriv = dev_get_priv(logic);
+
+	/* Enable TKey support and disable autoboot */
+	lpriv->opt_tkey = true;
+	lpriv->opt_autoboot = false;
+
+	/* Set boot order to include mmc13 before prepare */
+	lpriv->opt_labels = "mmc13 usb3";
+
+	/* Prepare the test environment and verify encrypted bootflows found */
+	ut_assertok(prepare_tkey_test(uts, logic, &emul, test_pubkey));
+
+	/* Queue a click on the first OS (seq 0) to select it */
+	ut_assertok(click_os(uts, lpriv, 0));
+
+	/* Poll the logic - should process the click and ask for passphrase */
+	ut_assertok(bc_logic_poll(logic));
+
+	/*
+	 * First, test wrong passphrase to verify UNS_BAD_PASS state.
+	 * Use wrong_pubkey to simulate a TKey producing an invalid disk key.
+	 */
+	ut_assertok(try_tkey_unlock(uts, logic, emul, wrong_pubkey, "wrongpw",
+				    NULL));
+
+	/* Unlock should fail, transition to UNS_BAD_PASS */
+	ut_asserteq(UNS_BAD_PASS, lpriv->ustate);
+
+	/*
+	 * Poll while in error display state - should remain in UNS_BAD_PASS
+	 * Error timeout is checked but we skip delays in tests
+	 */
+	ut_assertok(bc_logic_poll(logic));
+	ut_asserteq(UNS_BAD_PASS, lpriv->ustate);
+
+	/*
+	 * Advance time past the error timeout (5 seconds) to trigger
+	 * transition back to UNS_IDLE
+	 */
+	timer_test_add_offset(6000);  /* 6 seconds */
+
+	/* Poll - error timeout should expire, transition to UNS_IDLE */
+	ut_assertok(bc_logic_poll(logic));
+	ut_asserteq(UNS_IDLE, lpriv->ustate);
+
+	/* Click on the OS again to re-select it */
+	ut_assertok(click_os(uts, lpriv, 0));
+
+	/* Poll - should process click and ask for passphrase again */
+	ut_assertok(bc_logic_poll(logic));
+
+	/*
+	 * Now type the correct passphrase. The test image was created with
+	 * USS "test" which produces the pubkey configured in the emulator
+	 * above.
+	 */
+	ut_assertok(try_tkey_unlock(uts, logic, emul, test_pubkey, "test",
+				    NULL));
+
+	/* Unlock should succeed, transition to UNS_OK */
+	ut_asserteq(UNS_OK, lpriv->ustate);
+
+	/* Verify TKey device was found and used */
+	ut_assertnonnull(lpriv->tkey);
+	ut_assert(lpriv->tkey_present);
+
+	test_set_skip_delays(false);
+
+	return 0;
+}
+BOOTCTL_TEST(bootctl_logic_tkey, UTF_DM | UTF_SCAN_FDT | UTF_CONSOLE);