From patchwork Mon Dec 8 12:39:50 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Simon Glass X-Patchwork-Id: 853 Return-Path: X-Original-To: u-boot-concept@u-boot.org Delivered-To: u-boot-concept@u-boot.org DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197619; bh=qrfJgEb4RUCeeg+YZxDjWK9uFKx8Bhg1SxW/uE048qk=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=Xn2QbAS9LEleCvs8VXYEvnTILhJ/7ATQuuUg0Cb+YtvehvWx60XKI+dGOm4qPOggg L4z9sAp4AUf/ejmCfH6GevYnxquID7u5R/m0Q/jLPjYvP1KqhOb9BeTzJQzDtAsOLR ycRwOK67m+59MFb7m+vkFtNowggfLy0t0kGYJvFvUAQiVKUeVgU6VttpP2HAlhQj4x j2/GsQ9xJYafCu3nCmoTGsj8qH/2meePcyhzhhU2YVx+/QLo5Mp2h7CoawR0Vw7Blh DguddHO2xpPIRO8ykE8jksvYIOHurNmc16ZTeYbQmMiQWsrbxrFlQOmzrhRdAPZXdK oMYpNLJ73Omaw== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id BC35168A00 for ; Mon, 8 Dec 2025 05:40:19 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10024) with ESMTP id L7R-jroGWbWi for ; Mon, 8 Dec 2025 05:40:19 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197619; bh=qrfJgEb4RUCeeg+YZxDjWK9uFKx8Bhg1SxW/uE048qk=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=Xn2QbAS9LEleCvs8VXYEvnTILhJ/7ATQuuUg0Cb+YtvehvWx60XKI+dGOm4qPOggg L4z9sAp4AUf/ejmCfH6GevYnxquID7u5R/m0Q/jLPjYvP1KqhOb9BeTzJQzDtAsOLR ycRwOK67m+59MFb7m+vkFtNowggfLy0t0kGYJvFvUAQiVKUeVgU6VttpP2HAlhQj4x j2/GsQ9xJYafCu3nCmoTGsj8qH/2meePcyhzhhU2YVx+/QLo5Mp2h7CoawR0Vw7Blh DguddHO2xpPIRO8ykE8jksvYIOHurNmc16ZTeYbQmMiQWsrbxrFlQOmzrhRdAPZXdK oMYpNLJ73Omaw== Received: from mail.u-boot.org (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 988A7689FB for ; Mon, 8 Dec 2025 05:40:19 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197616; bh=Uzm5KVNA7TRvmjgl6TVWBBfp9o3TnGIQu7eDOGW/t/w=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=a3aN0l0QVkgK4uIOwFvcK1Cz5C3fjbfAG5jVupY+vRhKxcREJJWvT7OL0UgK8y6E+ rjIsDZIZcNLzjfb0vnWcooob9WjV21m5ButGHeihtAZ7Cpx73wTjw1csItM6Lzgihs JTKgU83OYMpR/03NGVNBkNh94daQolVQ1DjJ4fxZo7BdVb/hOh6lNf4yi0VuM2ehA0 trky2+K1lCrpWKJ/HTwRT52F7WNThH818vRp8w1LZQcgS4kMF8ELzIOVwyOqoQTWEE C0dETCJyYhjcibgzr79o9RRO4aiMTuHQovXexmwBkMHSH3NaS3TQwKS9volWUv4mkG KxmvzOXsYcKFg== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 21E0868988; Mon, 8 Dec 2025 05:40:16 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10026) with ESMTP id g-dujtxP3mXS; Mon, 8 Dec 2025 05:40:16 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197615; bh=LhuTvjuGn4ahoIZzbtvQkq77Dq5xTjIg3goaKrxG1k0=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=hclX7Bl+oWyBez8ogGr+mWy1KFUke17dhrnqQ6QaBG+PV9qk+tAPaL9Qfdy2GjB6I dzsCnt0jrPwcKCPLuTgDs7fKa28FVhVKXjHEErF00rF6UxnvScVe27F5RDsD7bn/JW PUbiaXGscm0quPWpaIWqY5nzD+DsryaDg3QXg60Rnb+I9dRkKuewBr7XkQ2wxfgdh5 q065xn6oL3CPVVK4aUpAVxt/dVOa0QKaQl876YM+ahKuZSu+ynUVO0mZU6h6F+62yQ 91C+xSFoSRPIcb2NcVGfZIW2mwJX1BqIlLtJqO8DmHis4H8O3o3T38h4Mlt/IdXTZw C/ShAYB5yM8hA== Received: from u-boot.org (unknown [73.34.74.121]) by mail.u-boot.org (Postfix) with ESMTPSA id 69F946895C; Mon, 8 Dec 2025 05:40:15 -0700 (MST) From: Simon Glass To: U-Boot Concept Date: Mon, 8 Dec 2025 05:39:50 -0700 Message-ID: <20251208124001.775057-2-sjg@u-boot.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20251208124001.775057-1-sjg@u-boot.org> References: <20251208124001.775057-1-sjg@u-boot.org> MIME-Version: 1.0 Message-ID-Hash: 3ZDETXIAJRYP7GYL3S7UAT6GMFSRDCFR X-Message-ID-Hash: 3ZDETXIAJRYP7GYL3S7UAT6GMFSRDCFR X-MailFrom: sjg@u-boot.org X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Simon Glass X-Mailman-Version: 3.3.10 Precedence: list Subject: [Concept] [PATCH 1/7] luks: Correct condition for calling handle_encrypted() List-Id: Discussion and patches related to U-Boot Concept Archived-At: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: From: Simon Glass Use CONFIG_BLK_LUKS instead of CONFIG_LUKS, which does not exist. This allows the test to pass. Signed-off-by: Simon Glass --- boot/bootctl/logic.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boot/bootctl/logic.c b/boot/bootctl/logic.c index 2ed106628e7..471e38e238c 100644 --- a/boot/bootctl/logic.c +++ b/boot/bootctl/logic.c @@ -903,7 +903,7 @@ static int logic_poll(struct udevice *dev) return log_msg_ret("gos", -ENOENT); /* If encrypted, handle pass entry and unlock */ - if (IS_ENABLED(CONFIG_LUKS) && + if (IS_ENABLED(CONFIG_BLK_LUKS) && (os->bflow.flags & BOOTFLOWF_ENCRYPTED)) { ret = handle_encrypted(dev, os, seq); if (ret) From patchwork Mon Dec 8 12:39:51 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Simon Glass X-Patchwork-Id: 854 Return-Path: X-Original-To: u-boot-concept@u-boot.org Delivered-To: u-boot-concept@u-boot.org DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197625; bh=l6Pt9JI59dgRnbEHbDDP1NSsepI54ZjC01gToIqgPio=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=BblVT0L0bYXva+c87RtMYOb6bB2d25BHucjLQTeuLnxb5Qzdgg8LL4hwsueCLjgFf /yabzdQEtGH+73+6cJMx6ee9Zep+8CohSuRhOt8G9cuiBbsFo7eMAzw0NtVQDSnxrG +YJKJN0/TraAL1HjH96d93KN2CNmcRhfnUs5Q2ZZTsyC4LXRyYnGx+c1RE7xrkjb/c AhjkauiOB2AsHzAvU400niRGMKnCANdZ9DaggfzGH5YeN5qkMV/EJUZf2+/ga3KsRx Y+9bkwhrQ3RUzwr3zSG3c3N65l+emy7N8xjGUoWXY5ONy8wYfXQNIsQBlxsDN2qDzD yvSqXxAX7NhtA== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id B2F30689FC for ; Mon, 8 Dec 2025 05:40:25 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10024) with ESMTP id HMyAqwPmj1LY for ; Mon, 8 Dec 2025 05:40:25 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197625; bh=l6Pt9JI59dgRnbEHbDDP1NSsepI54ZjC01gToIqgPio=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=BblVT0L0bYXva+c87RtMYOb6bB2d25BHucjLQTeuLnxb5Qzdgg8LL4hwsueCLjgFf /yabzdQEtGH+73+6cJMx6ee9Zep+8CohSuRhOt8G9cuiBbsFo7eMAzw0NtVQDSnxrG +YJKJN0/TraAL1HjH96d93KN2CNmcRhfnUs5Q2ZZTsyC4LXRyYnGx+c1RE7xrkjb/c AhjkauiOB2AsHzAvU400niRGMKnCANdZ9DaggfzGH5YeN5qkMV/EJUZf2+/ga3KsRx Y+9bkwhrQ3RUzwr3zSG3c3N65l+emy7N8xjGUoWXY5ONy8wYfXQNIsQBlxsDN2qDzD yvSqXxAX7NhtA== Received: from mail.u-boot.org (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 965C668958 for ; Mon, 8 Dec 2025 05:40:25 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197622; bh=Kjd47pPgB49LovUVI6k1j4Pen+kH2GTSZnjl96RVJP0=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=HetVykrIHjtZc0zofw4Dzi4IR8W2mtnQ/9syOihsLF4Dr+K2hgdk713T4T3SVlBma 4nnbx9bc9tOmbScg040LiSLuPQumIp01H3R6CP8WulMI4AXIbozyUH9le2BNfrplto eVFXE03bvF3FlZpJ1DNYvt/DuiaY9w4UTlepH0svlZrY7/MxichS56GnvqJmov6YZ7 c3d41iYdwTaDBWgkTTVspZ2M0/JRwygUK8kuir3VnlaRFKMVaH7PA39KbZ7XHwI+Bf GLQRxviRipI40DebZGhPYp7bTzHYtDWzIZCRzZWkAkpWX+UNKzprsM2Q0tDL1sJmzt SWYC94Ip4DVCw== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id DF852689FC; Mon, 8 Dec 2025 05:40:22 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10026) with ESMTP id E43ff_2Ol7nk; Mon, 8 Dec 2025 05:40:22 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197616; bh=fEwceDnLNaPlSXBVBxBeQrm9JjMl9Nfp90NEgxvmuWA=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=Ctkbw9CocWJjnJxwMOBFacP0z9rtYa1T1S7RuG5YvyYIv+sU8EfXz7m7CvIkGfBL5 Pq1O/pza3Q8Vd67ORhJzj7NC2ehhybqMn66ywA2X/6tLkPfdGWdQ/juNyNeSIbil9/ X+g+yFamb/UX8UOqLk0AwF1RmcexDR43VJSyLPJos078yd9/6mBAtdr/Qf3LSgoS4R Ryyf3X5vb+qLSsLXME09jfuNNE8aK9L6V393CMyyVqeMD+9+J63iya/+ngGJOTyxzm QWLwelE1hNmF48zj9+QpIka/ojZfYTxfEhS33dOSgDOVIyiupXBRqckxMwKjNDhf1D hmopMTT5NMybw== Received: from u-boot.org (unknown [73.34.74.121]) by mail.u-boot.org (Postfix) with ESMTPSA id 67856689E0; Mon, 8 Dec 2025 05:40:16 -0700 (MST) From: Simon Glass To: U-Boot Concept Date: Mon, 8 Dec 2025 05:39:51 -0700 Message-ID: <20251208124001.775057-3-sjg@u-boot.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20251208124001.775057-1-sjg@u-boot.org> References: <20251208124001.775057-1-sjg@u-boot.org> MIME-Version: 1.0 Message-ID-Hash: G3L7VWT5GMY24RPAHUVPLBTTNTRAQIUV X-Message-ID-Hash: G3L7VWT5GMY24RPAHUVPLBTTNTRAQIUV X-MailFrom: sjg@u-boot.org X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Simon Glass , Claude X-Mailman-Version: 3.3.10 Precedence: list Subject: [Concept] [PATCH 2/7] test: fs_helper: Support LUKS keyfile and master key List-Id: Discussion and patches related to U-Boot Concept Archived-At: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: From: Simon Glass Add encrypt_keyfile and master_keyfile parameters to FsHelper and the image setup functions. This allows creating encrypted test images using: - A key file instead of a passphrase (encrypt_keyfile) - A specific master key for pre-derived unlock testing (master_keyfile) The keyfile takes precedence over passphrase when both are provided. Also reduce Argon2 memory parameters to values suitable for U-Boot testing. These new features will allow use of a real TKey for trying out this feature locally, as well as the emulated TKey for automated testing. Co-developed-by: Claude Signed-off-by: Simon Glass --- test/py/img/common.py | 13 ++++++-- test/py/img/ubuntu.py | 12 +++++-- test/py/tests/fs_helper.py | 66 +++++++++++++++++++++++++++++--------- 3 files changed, 70 insertions(+), 21 deletions(-) diff --git a/test/py/img/common.py b/test/py/img/common.py index 74ea04771c7..547066b24a5 100644 --- a/test/py/img/common.py +++ b/test/py/img/common.py @@ -33,7 +33,8 @@ def copy_partition(ubman, fsfile, outname): def setup_extlinux_image(config, log, devnum, basename, vmlinux, initrd, dtbdir, - script, part2_size=1, use_fde=0, luks_kdf='pbkdf2'): + script, part2_size=1, use_fde=0, luks_kdf='pbkdf2', + encrypt_keyfile=None, master_keyfile=None): """Create a 20MB disk image with a single FAT partition Args: @@ -49,6 +50,10 @@ def setup_extlinux_image(config, log, devnum, basename, vmlinux, initrd, dtbdir, use_fde (int): LUKS version for full-disk encryption (0=none, 1=LUKS1, 2=LUKS2) luks_kdf (str): Key derivation function for LUKS2: 'pbkdf2' or 'argon2id'. Defaults to 'pbkdf2'. Ignored for LUKS1. + encrypt_keyfile (str, optional): Path to key file for LUKS encryption. + If provided, takes precedence over passphrase. + master_keyfile (str, optional): Path to file containing the raw master + key. If provided, this exact key is used as the LUKS master key. """ fsh = FsHelper(config, 'vfat', 18, prefix=basename) fsh.setup() @@ -84,9 +89,11 @@ def setup_extlinux_image(config, log, devnum, basename, vmlinux, initrd, dtbdir, ext4 = FsHelper(config, 'ext4', max(1, part2_size - 30), prefix=basename, part_mb=part2_size, - passphrase='test' if use_fde else None, + passphrase='test' if (use_fde and not encrypt_keyfile) else None, + encrypt_keyfile=encrypt_keyfile, luks_version=use_fde if use_fde else 2, - luks_kdf=luks_kdf) + luks_kdf=luks_kdf, + master_keyfile=master_keyfile) ext4.setup() bindir = os.path.join(ext4.srcdir, 'bin') diff --git a/test/py/img/ubuntu.py b/test/py/img/ubuntu.py index 243fa38d021..1f3016b79a6 100644 --- a/test/py/img/ubuntu.py +++ b/test/py/img/ubuntu.py @@ -7,7 +7,8 @@ from img.common import setup_extlinux_image def setup_ubuntu_image(config, log, devnum, basename, version='24.04.1 LTS', - use_fde=0, luks_kdf='pbkdf2'): + use_fde=0, luks_kdf='pbkdf2', encrypt_keyfile=None, + master_keyfile=None): """Create a Ubuntu disk image with a FAT partition and ext4 partition This creates a FAT partition containing extlinux files, kernel, etc. and a @@ -21,6 +22,11 @@ def setup_ubuntu_image(config, log, devnum, basename, version='24.04.1 LTS', use_fde (int): LUKS version for full-disk encryption (0=none, 1=LUKS1, 2=LUKS2) luks_kdf (str): Key derivation function for LUKS2: 'pbkdf2' or 'argon2id'. Defaults to 'pbkdf2'. Ignored for LUKS1. + encrypt_keyfile (str, optional): Path to key file for LUKS encryption. + If provided, takes precedence over passphrase. + master_keyfile (str, optional): Path to file containing the raw master + key. If provided, this exact key is used as the LUKS master key, + enabling pre_derived unlock mode. """ vmlinux = 'vmlinuz-6.8.0-53-generic' initrd = 'initrd.img-6.8.0-53-generic' @@ -52,4 +58,6 @@ label l0r ''' % ((version, vmlinux, initrd) * 2) setup_extlinux_image(config, log, devnum, basename, vmlinux, initrd, dtbdir, script, part2_size=60 if use_fde else 1, - use_fde=use_fde, luks_kdf=luks_kdf) + use_fde=use_fde, luks_kdf=luks_kdf, + encrypt_keyfile=encrypt_keyfile, + master_keyfile=master_keyfile) diff --git a/test/py/tests/fs_helper.py b/test/py/tests/fs_helper.py index d88cc270b95..3c4c6e2df6f 100644 --- a/test/py/tests/fs_helper.py +++ b/test/py/tests/fs_helper.py @@ -62,12 +62,21 @@ class FsHelper: fsh.mk_fs() # Creates and encrypts the FS with LUKS2+Argon2 ... + To create an encrypted LUKS2 partition with a key file: + + with FsHelper(ubman.config, 'ext4', 10, 'mmc1', + encrypt_keyfile='/path/to/keyfile') as fsh: + # create files in the fsh.srcdir directory + fsh.mk_fs() # Creates and encrypts the filesystem with key file + ... + Properties: fs_img (str): Filename for the filesystem image; this is set to a default value but can be overwritten """ def __init__(self, config, fs_type, size_mb, prefix, part_mb=None, - passphrase=None, luks_version=2, luks_kdf='pbkdf2'): + passphrase=None, encrypt_keyfile=None, luks_version=2, + luks_kdf='pbkdf2', master_keyfile=None): """Set up a new object Args: @@ -81,9 +90,14 @@ class FsHelper: the filesystem, to create space for disk-encryption metadata passphrase (str, optional): If provided, encrypt the filesystem with LUKS using this passphrase + encrypt_keyfile (str, optional): Path to key file for LUKS + encryption. If provided, takes precedence over passphrase. luks_version (int): LUKS version to use (1 or 2). Defaults to 2. luks_kdf (str): Key derivation function for LUKS2: 'pbkdf2' or 'argon2id'. Defaults to 'pbkdf2'. Ignored for LUKS1. + master_keyfile (str, optional): Path to file containing the raw + master key. If provided, this exact key is used as the LUKS + master key (via --master-key-file), enabling pre_derived unlock. """ if ('fat' not in fs_type and 'ext' not in fs_type and fs_type not in ['exfat', 'fs_generic']): @@ -96,8 +110,10 @@ class FsHelper: self.prefix = prefix self.quiet = True self.passphrase = passphrase + self.encrypt_keyfile = encrypt_keyfile self.luks_version = luks_version self.luks_kdf = luks_kdf + self.master_keyfile = master_keyfile # Use a default filename; the caller can adjust it leaf = f'{prefix}.{fs_type}.img' @@ -170,8 +186,8 @@ class FsHelper: shell=True) # Encrypt the filesystem if requested - if self.passphrase: - self.encrypt_luks(self.passphrase) + if self.passphrase or self.encrypt_keyfile: + self.encrypt_luks() def setup(self): """Set up the srcdir ready to receive files""" @@ -186,22 +202,29 @@ class FsHelper: self.tmpdir = tempfile.TemporaryDirectory('fs_helper') self.srcdir = self.tmpdir.name - def encrypt_luks(self, passphrase): + def encrypt_luks(self): """Encrypt the filesystem image with LUKS This replaces the filesystem image with a LUKS-encrypted version. The LUKS version is determined by self.luks_version. - - Args: - passphrase (str): Passphrase for the LUKS container + Uses either passphrase or keyfile for encryption. Returns: str: Path to the encrypted image Raises: CalledProcessError: If cryptsetup is not available or fails - ValueError: If an unsupported LUKS version is specified + ValueError: If an unsupported LUKS version is specified or if neither + passphrase nor keyfile is provided + Exception: If anything else goes wrong """ + # Validate that we have either passphrase or keyfile + if not self.passphrase and not self.encrypt_keyfile: + raise ValueError('Either encrypt_passphrase or encrypt_keyfile must be provided') + + # If both are provided, keyfile takes precedence + use_keyfile = self.encrypt_keyfile is not None + use_master_key = self.master_keyfile is not None # LUKS encryption parameters if self.luks_version == 1: # LUKS1 parameters @@ -216,7 +239,7 @@ class FsHelper: hash_alg = 'sha256' luks_type = 'luks2' else: - raise ValueError(f"Unsupported LUKS version: {self.luks_version}") + raise ValueError(f'Unsupported LUKS version: {self.luks_version}') key_size_str = str(key_size) @@ -236,7 +259,7 @@ class FsHelper: result = run(['sudo', 'modprobe', 'dm_mod'], stdout=DEVNULL, stderr=DEVNULL, check=False) if result.returncode != 0: - raise RuntimeError( + raise ValueError( 'Device-mapper is not available. Please ensure the dm_mod ' 'kernel module is loaded and you have permission to use ' 'device-mapper. This is required for LUKS encryption tests.') @@ -262,22 +285,33 @@ class FsHelper: # For Argon2, use low memory/time settings suitable for testing if self.luks_kdf == 'argon2id': cmd.extend([ - '--pbkdf-memory', '65536', # 64MB - '--pbkdf-parallel', '4', + '--pbkdf-memory', '8192', # 8MB (reduced for U-Boot) + '--pbkdf-parallel', '1', # Single thread for simplicity ]) + # Add master key file option if provided + if use_master_key: + cmd.extend(['--master-key-file', self.master_keyfile]) + + # Add key file or passphrase option + if use_keyfile: + cmd.extend(['--key-file', self.encrypt_keyfile]) + cmd.append(luks_img) + # When using passphrase, provide it via stdin; otherwise set input=None run(cmd, - input=f'{passphrase}\n'.encode(), + input=f'{self.passphrase}\n'.encode() if not use_keyfile else None, stdout=DEVNULL if self.quiet else None, stderr=DEVNULL if self.quiet else None, check=True) # Open the LUKS device (requires sudo) - # Use --key-file=- to read passphrase from stdin - result = run(['sudo', 'cryptsetup', 'open', '--key-file=-', - luks_img, device_name], input=passphrase.encode(), + # Use --key-file with file path or '-' for stdin + result = run(['sudo', 'cryptsetup', 'open', + '--key-file', self.encrypt_keyfile if use_keyfile else '-', + luks_img, device_name], + input=self.passphrase.encode() if not use_keyfile else None, stdout=DEVNULL if self.quiet else None, stderr=None, check=True) # Copy the filesystem data into the LUKS container From patchwork Mon Dec 8 12:39:52 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Simon Glass X-Patchwork-Id: 855 Return-Path: X-Original-To: u-boot-concept@u-boot.org Delivered-To: u-boot-concept@u-boot.org DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197629; bh=Jkn9owtvfPNW+Wmu0VCeRYkkBZH/JiWMbe5OSGhR9bY=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=pEVZdSpv5C0sm63Yj1mAhDBSGmX9nCG9G15Dpf15q66ntbgu+C4MYUpPsjj6Jb9lT kao2xHL+pWsooz9L2ibku+M/yMicY5yuXGHrCC4oJ3kbd0V9nWNYG9fsJ1rTMW2z/R Jvw0YAv7CwcN9f0h2iqZjJiBwrvFtcNF/A4LyW6XGcqvYkayjC0gFHer65RL5c7Sdm 0b1Ackx1DS8zBlJNGC3nrtRvoN+n9rFy80PgEEiZ+QwcqbkVc4d/A0HWEFx05gMqIM UMVwEjRf8AtZmQaaaFQholP9lbOaSR+2pnm4vMETNcjBhS/r9K0BykltrSUrzztVQ9 8EPPrmg2AdGBw== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 2AFD7689FA for ; Mon, 8 Dec 2025 05:40:29 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10024) with ESMTP id k5r37YiPgxxh for ; Mon, 8 Dec 2025 05:40:29 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197629; bh=Jkn9owtvfPNW+Wmu0VCeRYkkBZH/JiWMbe5OSGhR9bY=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=pEVZdSpv5C0sm63Yj1mAhDBSGmX9nCG9G15Dpf15q66ntbgu+C4MYUpPsjj6Jb9lT kao2xHL+pWsooz9L2ibku+M/yMicY5yuXGHrCC4oJ3kbd0V9nWNYG9fsJ1rTMW2z/R Jvw0YAv7CwcN9f0h2iqZjJiBwrvFtcNF/A4LyW6XGcqvYkayjC0gFHer65RL5c7Sdm 0b1Ackx1DS8zBlJNGC3nrtRvoN+n9rFy80PgEEiZ+QwcqbkVc4d/A0HWEFx05gMqIM UMVwEjRf8AtZmQaaaFQholP9lbOaSR+2pnm4vMETNcjBhS/r9K0BykltrSUrzztVQ9 8EPPrmg2AdGBw== Received: from mail.u-boot.org (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 19AB06895C for ; Mon, 8 Dec 2025 05:40:29 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197627; bh=OeEIRsysF8LXx7OaY6pGemP5pU6RCx24+8Y2FVwQg5Q=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=qQq2E9igLnXNsfRbRnTVNuhU/Z/q87lR1d2DPGL7BwJMgJn3GzPlwP4CIqLEsIW5F xd/RxV48HHq3qQh+01x+d+l79bw1PvMdFMUxAczPUH2pC4QHGd6kW/NM863Wh9I6Da oJ9s9mZ2j+H3cjUNRR3Sxlx1p3atmytkM21hY2FAcJbWJGQNwpRy+04Fyi37k+YHtZ ba63PPev4RSp4VVxWWoju79a+28N6ycK4bO6EewE2gGelVWH+ytP392yZ2C65ItPoS MM8hO5LorqYEdzWsyj4hNaAHBXEgDuoHzib+JfToYK6MMRcG5ybF/JSVJe2wl8BEx6 dJC+ExWqswDZg== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 1D05F6895C; Mon, 8 Dec 2025 05:40:27 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10026) with ESMTP id fZnEyC_Qs0Bc; Mon, 8 Dec 2025 05:40:27 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197622; bh=pLiD9ls7yypvSpYNruofkmfQcaui/XmYLUW8KBufnVw=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=D1mNrWF4paFYKgvrN5fEmZfTNENNcJnSLS+APE96dlmc014WI/Qg5Uds7fiGB0Ata KX2iOfxqrsPMMPnJKhaRZX/Ki6Jf0iKC/cCpj/2+p2hLSBZvpWWlRAbiYRqy26kkk2 IwVCrQD6ra9oMaPAR2AwJItCBrScyS36K4FuI9O/3FYAujewQdT8/YSeVs8TdKTj1/ alimfcqVPfCOHwsg0vYeSrl9AzMljriSoOmOH8Jn9gij6AZj5xp4eWl2YP0CGhaGAY 2ZF9u997VHFhCL7C9WjgAGARLQzjyXj6u7aQzseR8tM5Pa/bt6/NS1wGUAfS0ceLme McVx7dI3Uq6gw== Received: from u-boot.org (unknown [73.34.74.121]) by mail.u-boot.org (Postfix) with ESMTPSA id 45108689FF; Mon, 8 Dec 2025 05:40:22 -0700 (MST) From: Simon Glass To: U-Boot Concept Date: Mon, 8 Dec 2025 05:39:52 -0700 Message-ID: <20251208124001.775057-4-sjg@u-boot.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20251208124001.775057-1-sjg@u-boot.org> References: <20251208124001.775057-1-sjg@u-boot.org> MIME-Version: 1.0 Message-ID-Hash: T7LTX55V56776MK4TUTEUFWFF5LZ23TK X-Message-ID-Hash: T7LTX55V56776MK4TUTEUFWFF5LZ23TK X-MailFrom: sjg@u-boot.org X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Simon Glass , Claude X-Mailman-Version: 3.3.10 Precedence: list Subject: [Concept] [PATCH 3/7] test: Add mmc13 and mmc14 devices for TKey and pre-derived List-Id: Discussion and patches related to U-Boot Concept Archived-At: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: From: Simon Glass Add two new MMC test devices: - mmc13: LUKS2 encrypted with TKey-derived key, for testing TKey-based disk encryption unlock - mmc14: LUKS2 encrypted with a known master key, for testing the pre-derived master key unlock path The test setup generates keys matching the TKey emulator's deterministic output. An override.bin file can be used to test with a physical TKey. Co-developed-by: Claude Signed-off-by: Simon Glass --- arch/sandbox/dts/test.dts | 16 ++++++++++++++++ test/py/tests/test_ut.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/arch/sandbox/dts/test.dts b/arch/sandbox/dts/test.dts index 7fef23a9285..7a67565e4a9 100644 --- a/arch/sandbox/dts/test.dts +++ b/arch/sandbox/dts/test.dts @@ -50,6 +50,8 @@ mmc10 = "/mmc10"; mmc11 = "/mmc11"; mmc12 = "/mmc12"; + mmc13 = "/mmc13"; + mmc14 = "/mmc14"; pci0 = &pci0; pci1 = &pci1; pci2 = &pci2; @@ -1220,6 +1222,20 @@ filename = "mmc12.img"; }; + /* This is used for LUKS version 2 tests with TKey */ + mmc13 { + status = "disabled"; + compatible = "sandbox,mmc"; + filename = "mmc13.img"; + }; + + /* This is used for LUKS version 2 tests with pre-derived master key */ + mmc14 { + status = "disabled"; + compatible = "sandbox,mmc"; + filename = "mmc14.img"; + }; + pch { compatible = "sandbox,pch"; }; diff --git a/test/py/tests/test_ut.py b/test/py/tests/test_ut.py index e2b4d49a2e0..b9ba240c848 100644 --- a/test/py/tests/test_ut.py +++ b/test/py/tests/test_ut.py @@ -9,6 +9,7 @@ test one at a time, as well setting up some files needed by the tests. """ import collections import gzip +import hashlib import os import os.path import pytest @@ -83,9 +84,40 @@ def test_ut_dm_init_bootstd(u_boot_config, u_boot_log): setup_ubuntu_image(u_boot_config, u_boot_log, 3, 'flash', '25.04') setup_localboot_image(u_boot_config, u_boot_log) setup_vbe_image(u_boot_config, u_boot_log) + + # Generate TKey emulator disk key for LUKS encryption + # The emulator generates pubkey as 0x50 + (i & 0xf) for i in range(32) + # Disk key = SHA256(hex_string_of_pubkey), matching tkey_derive_disk_key() + # Allow override via external key file for testing with real keys + override_keyfile = os.path.join(u_boot_config.source_dir, 'override.bin') + if os.path.exists(override_keyfile): + keyfile = override_keyfile + u_boot_log.action(f'Using override TKey key: {keyfile}') + else: + pubkey = bytes([0x50 + (i & 0xf) for i in range(32)]) + disk_key = hashlib.sha256(pubkey.hex().encode()).digest() + keyfile = os.path.join(u_boot_config.persistent_data_dir, 'tkey_emul.key') + with open(keyfile, 'wb') as f: + f.write(disk_key) + u_boot_log.action(f'Generated TKey emulator disk key: {keyfile}') + setup_ubuntu_image(u_boot_config, u_boot_log, 11, 'mmc', use_fde=1) setup_ubuntu_image(u_boot_config, u_boot_log, 12, 'mmc', use_fde=2, luks_kdf='argon2id') + setup_ubuntu_image(u_boot_config, u_boot_log, 13, 'mmc', use_fde=2, + luks_kdf='argon2id', encrypt_keyfile=keyfile) + + # Create mmc14 with a known master key for pre_derived unlock testing + # For LUKS2 with aes-xts-plain64, we need a 64-byte (512-bit) master key + master_key = bytes([0x20 + (i & 0x3f) for i in range(64)]) + master_keyfile = os.path.join(u_boot_config.persistent_data_dir, + 'luks_master.key') + with open(master_keyfile, 'wb') as f: + f.write(master_key) + u_boot_log.action(f'Generated LUKS master key: {master_keyfile}') + setup_ubuntu_image(u_boot_config, u_boot_log, 14, 'mmc', use_fde=2, + luks_kdf='argon2id', encrypt_keyfile=keyfile, + master_keyfile=master_keyfile) def test_ut(ubman, ut_subtest): """Execute a "ut" subtest. From patchwork Mon Dec 8 12:39:53 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Simon Glass X-Patchwork-Id: 856 Return-Path: X-Original-To: u-boot-concept@u-boot.org Delivered-To: u-boot-concept@u-boot.org DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197631; bh=C/GGooPfVo+C+H8lT6NCIjBZ9HZ689UuYHUSyzADwPs=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=AuYsu+grLNjuTva8rs6YsLXcmkziABp98p1zvZR72pbA3gL9frwJaLN2Ahoq/ZePj eQ8VqbBDmEDIFo65d9FUg7vT7DImhu40h1uQczAfXpb21mZEKeAdFMgygsUskOw1Ue OeNDbIF7CsfVDEodtfceykH+6xg92Jbu5FIixSSZbTLEjtS5Rm//6m2TqNhSS9oKP2 pyZZHrkGuPqiP6WtHYUn5DbkzNHrGZthK59GcKVc0Jf/MZby9blj/5FYRIuvakjrkj Kxqy0Dwalz6KyCuCXHHRePu2CKvsvdTu7y7cD/bWqPm4rajon+5IUMnONQRwlKupQW RBPyz4Wo9mjXg== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 89E2968958 for ; Mon, 8 Dec 2025 05:40:31 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10024) with ESMTP id L8PqC0GgRrH0 for ; Mon, 8 Dec 2025 05:40:31 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197629; bh=C/GGooPfVo+C+H8lT6NCIjBZ9HZ689UuYHUSyzADwPs=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=k6RHf7CjdTF6naIiyScmTGhhcxlPSH5+Xag3un/Wu8D4BAyLC8YD/zhjtNM7Y3Ixc 0UVA1gjEEHlb1WXCVKLeWfQ30hTTDGF05vVE/9P4zXdd26+Yb/Orsu4EuozKXxoFzL nytSF+VMG2/8Q/GlZRXVstD69/DU8xSGqpjFDb0/9vuZB3Eqanl2waRkUhCmGDQR6p yL3k92TuOYPTY4h4l5h6yrchgXFj4UsIRB+TGJ3letPgQDpZZrYyShytHAkT1CjPfY mgZTJsrciFYlszugQvcbEkgA2KR/HEnlNL1/jHEoaHd3XO/H8vmS9DufF9+xWMqh4m GXcYH5sy5hJkQ== Received: from mail.u-boot.org (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 83B8B6895C for ; Mon, 8 Dec 2025 05:40:29 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197627; bh=ykVakMCpgn0xphFhItbZqFiCy+KRRqjwfhyETlaZEco=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=Hs7CR/Xk21gogF4LPYlHYFqAhOlsU1ne2cgHoM6c/zCcwLCYKTjqdpwzK2EQkGaTe aERL4Vd8/YNTT5DMyrPuyy3SdunRzfd0u4HddY0mOy6cMfraFM3rgv508D5G4Xm7u6 hdxU3Zit5kLlKyzHBXwr8UVt8RXIMebyx+w2YuyeeEQx9GZ7K+im2fm37MfV7E6Sb8 r5ssMemrjmcJvh0UXH826xt3kAXEW866ur1y8ybXeTubfvpfV2/FBmS97a49SB7k8Q z1oKnXgmrX045dB4kcdMwJwRpC0AG6nXgQweCnjsxkawsOKNGoSXJw/la33oDAPJQF HY2YKqlGbOrrg== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 6CAE0689E0; Mon, 8 Dec 2025 05:40:27 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10026) with ESMTP id RuPlU7Dv1VNB; Mon, 8 Dec 2025 05:40:27 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197627; bh=I5r8mqZEBRV2eQm/HUF1+xqi6El5gKPtK4U5GwmQpyM=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=QhOYPbyPcqoa7+Qemmj4rv47XtbcqZdH15i6Ka/fCdiA8AK7vXjp1fVin3vjTzG6W ShrjXrqDRpMh2WLETmcDfud2uag6y+mhA2BLCMN1M7iLTE4DNYhIV4Lcq1iJl4W2yJ eQghMIYSLcYcOe2bNfEFHyd3NcVsn0ccoduCVEwTV0t4jE//ACI8M2gr/nVt0f2Ejd cMhr5xTDM5g5em3aHermYJeapgXgUtcdsAiUQFiFwavGn77lSLONp9bKZvoonxvmFq 2wUwFVehKtqMOny90BEGlc2ZTlPNQOMyYCASYpX3wCgXKIkSwJX0pQ5MkVJt+8QxMW i2vBvbfIb0OUA== Received: from u-boot.org (unknown [73.34.74.121]) by mail.u-boot.org (Postfix) with ESMTPSA id DC37E688C1; Mon, 8 Dec 2025 05:40:26 -0700 (MST) From: Simon Glass To: U-Boot Concept Date: Mon, 8 Dec 2025 05:39:53 -0700 Message-ID: <20251208124001.775057-5-sjg@u-boot.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20251208124001.775057-1-sjg@u-boot.org> References: <20251208124001.775057-1-sjg@u-boot.org> MIME-Version: 1.0 Message-ID-Hash: X5HE4EQ33JOGT5AX53GAZFFNVS3QAZCV X-Message-ID-Hash: X5HE4EQ33JOGT5AX53GAZFFNVS3QAZCV X-MailFrom: sjg@u-boot.org X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Simon Glass , Claude X-Mailman-Version: 3.3.10 Precedence: list Subject: [Concept] [PATCH 4/7] test: luks: Add test for pre-derived master key unlock List-Id: Discussion and patches related to U-Boot Concept Archived-At: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: From: Simon Glass Add a test for the LUKS pre-derived master key unlock path using mmc14. The test verifies that: - A LUKS partition can be unlocked with the correct pre-derived key - Files can be read from the decrypted filesystem - Unlock fails with an incorrect pre-derived key This exercises the -p flag path in the luks unlock command. Co-developed-by: Claude Signed-off-by: Simon Glass --- test/boot/luks.c | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/test/boot/luks.c b/test/boot/luks.c index dfd6f7b411c..339c7d7fc94 100644 --- a/test/boot/luks.c +++ b/test/boot/luks.c @@ -299,3 +299,55 @@ static int bootstd_test_luks2_unlock(struct unit_test_state *uts) return 0; } BOOTSTD_TEST(bootstd_test_luks2_unlock, UTF_DM | UTF_SCAN_FDT | UTF_CONSOLE); + +/* Setup mmc14 device */ +static int setup_mmc14(struct unit_test_state *uts, struct udevice **mmcp) +{ + ut_assertok(setup_mmc_device(uts, "mmc14", mmcp)); + + return 0; +} + +/* Test LUKS2 unlock with pre-derived master key on mmc14 */ +static int bootstd_test_luks2_unlock_prederived(struct unit_test_state *uts) +{ + struct blk_desc *desc; + struct udevice *mmc; + loff_t file_size; + + /* + * mmc14 is encrypted with a known master key: + * bytes([0x20 + (i & 0x3f) for i in range(64)]) + * This tests the pre_derived=true path in luks_unlock() + */ + ut_assertok(setup_mmc14(uts, &mmc)); + + /* Test unlocking partition 2 with pre-derived master key (-p flag) */ + ut_assertok(run_command("luks unlock -p mmc e:2 " + "202122232425262728292a2b2c2d2e2f" + "303132333435363738393a3b3c3d3e3f" + "404142434445464748494a4b4c4d4e4f" + "505152535455565758595a5b5c5d5e5f", 0)); + ut_assert_nextline("Unlocking LUKS2 partition..."); + ut_assert_nextline("Unlocked LUKS partition as blkmap device 'luks-mmc-e:2'"); + ut_assert_console_end(); + + /* Verify that a file can be read from the decrypted filesystem */ + desc = blk_get_devnum_by_uclass_idname("blkmap", 0); + ut_assertnonnull(desc); + + ut_assertok(fs_set_blk_dev_with_part(desc, 0)); + ut_assertok(fs_size("/bin/bash", &file_size)); + ut_asserteq(5, file_size); + + /* Test unlocking with wrong pre-derived key */ + ut_asserteq(1, run_command("luks unlock -p mmc e:2 " + "0000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000", 0)); + ut_assert_nextline("Unlocking LUKS2 partition..."); + ut_assert_skip_to_line("Failed to unlock LUKS partition (err -13: Permission denied)"); + + return 0; +} +BOOTSTD_TEST(bootstd_test_luks2_unlock_prederived, + UTF_DM | UTF_SCAN_FDT | UTF_CONSOLE); From patchwork Mon Dec 8 12:39:54 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Simon Glass X-Patchwork-Id: 857 Return-Path: X-Original-To: u-boot-concept@u-boot.org Delivered-To: u-boot-concept@u-boot.org DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197636; bh=F+BR4UXUKtdT9OjbczqI+tE0qRfJYzqmyayWlWXl6WM=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=IsbbCC0dQIicSlJjgW038OWgpYYYsoQ4Xmoe2afyL7loIYIXypTqgh5ykAZJ3F/mw o5PcyG9nfBFsNA81MPmTbIZHt8zkxPpYfKd1oap3XpfUefo8qHqwSnBQhDRxeHNo2k eam9TERTscZSqRtu07r+D/9l9zD+nhUQWQh2CMzKeX8FNoG1pK1cvxq8sYXL8n39Vs uqdSGlm3Lv/YduaW+wvBxSTZjJ/LBy21VHVflXFalWTcT7zc/ahUT478hLRQ4cbuwI IOO3PzgzLc7Vbm9N/AMkeGqWtEVLAx230BDamhHswRXWKaLFiPCV/LzQXyjTSpGDM0 pW4dyWTP+495w== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id CE46B689E0 for ; Mon, 8 Dec 2025 05:40:36 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10024) with ESMTP id U2CuOHJpHjmL for ; Mon, 8 Dec 2025 05:40:36 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197636; bh=F+BR4UXUKtdT9OjbczqI+tE0qRfJYzqmyayWlWXl6WM=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=IsbbCC0dQIicSlJjgW038OWgpYYYsoQ4Xmoe2afyL7loIYIXypTqgh5ykAZJ3F/mw o5PcyG9nfBFsNA81MPmTbIZHt8zkxPpYfKd1oap3XpfUefo8qHqwSnBQhDRxeHNo2k eam9TERTscZSqRtu07r+D/9l9zD+nhUQWQh2CMzKeX8FNoG1pK1cvxq8sYXL8n39Vs uqdSGlm3Lv/YduaW+wvBxSTZjJ/LBy21VHVflXFalWTcT7zc/ahUT478hLRQ4cbuwI IOO3PzgzLc7Vbm9N/AMkeGqWtEVLAx230BDamhHswRXWKaLFiPCV/LzQXyjTSpGDM0 pW4dyWTP+495w== Received: from mail.u-boot.org (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id BD0BE6895C for ; Mon, 8 Dec 2025 05:40:36 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197633; bh=ba1fk0YfbSwl5qt4ltf7m4iZGvChS8rnfCDJrT5dUB4=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=kyao0hPEAO3Y0cfc6HAvk/ZiFR+r3wzeYS/qOD/7VF+ph9F6aGSevtpPg1af19Udu h55KQ+n95QBihrjMDrNB2jzLj5pDxh7BzcO108CGvPbTahBtNyPuhCxxquFcemPVSd MwYRxRzmjoYi8sJnJ+DghnSFx4yk8Xw7JET/Uusrx3CvB0n52EfGRabjJqorHfD8TA I2KlEIfaMA+tpjyw+P7rdDMDtpzLnrPIdy/iwdx8JCdqrY7ShlezQGAcDwZYEE+6pc ncGMsNNJ/84PHWBrEa5zlowu+wvZTmTvaygr5a6j23WHP+7NCbw2IgbSnl2l2T7H1G sFRMGGoUgRjTw== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id D570F6895C; Mon, 8 Dec 2025 05:40:33 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10026) with ESMTP id emq_xRDsOYgE; Mon, 8 Dec 2025 05:40:33 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197628; bh=Y7MfPLpMnQU4AULKbRwZbeAv5wg254BkkoaalsDQ730=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=FCqIVuLtS/IHAt2Zwo3S49q1AsUcXVj+//NRk+0x7Kvnf4lqiufVNB3g/y6eAVBEu wxhbSnAvOEpUbivG4bvl4iuOCNQsu1jPx3ZmrB04PI5Bxm17e9k/VawKZcHwmOsVv7 ByEak1JhPPo8Xfo9QuVSq3d+DTduZWeBXSBh0BqAHRY6fYGb2uUUb1NPbb5l8vVnJV 411Jo5H5TENORjisc0Qt8pdCqpHlJtcD/FwtOzHe7ucMSbvZObSnND+lvyWI7Z5TyR z6ijeVN/j0SbObWg8a7x93WaMW7BqntzMC7kvHOtL4BmWiGgApj6+vZwBSmjeaIiK/ pSyBLfpZBqyeg== Received: from u-boot.org (unknown [73.34.74.121]) by mail.u-boot.org (Postfix) with ESMTPSA id B30F3688C1; Mon, 8 Dec 2025 05:40:27 -0700 (MST) From: Simon Glass To: U-Boot Concept Date: Mon, 8 Dec 2025 05:39:54 -0700 Message-ID: <20251208124001.775057-6-sjg@u-boot.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20251208124001.775057-1-sjg@u-boot.org> References: <20251208124001.775057-1-sjg@u-boot.org> MIME-Version: 1.0 Message-ID-Hash: GG3NI2HQE2X2VW5XEOV4VGANJYSPU4KV X-Message-ID-Hash: GG3NI2HQE2X2VW5XEOV4VGANJYSPU4KV X-MailFrom: sjg@u-boot.org X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Simon Glass , Claude X-Mailman-Version: 3.3.10 Precedence: list Subject: [Concept] [PATCH 5/7] test: bootctl: Add passphrase UI and TKey unlock tests List-Id: Discussion and patches related to U-Boot Concept Archived-At: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: From: Simon Glass 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 Signed-off-by: Simon Glass --- test/boot/bootctl/bootctl.c | 433 +++++++++++++++++++++++++++++++++++- 1 file changed, 431 insertions(+), 2 deletions(-) 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 #include #include +#include +#include #include +#include #include "bootctl_common.h" #include #include #include #include #include +#include #include #include #include #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); From patchwork Mon Dec 8 12:39:55 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Simon Glass X-Patchwork-Id: 969 Return-Path: X-Original-To: u-boot-concept@u-boot.org Delivered-To: u-boot-concept@u-boot.org DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765999473; bh=MSUuJi/J6vfnuDoAqdkT3ML3SYbiV8FG3tX718XtsiU=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=hXeYrV5Xwfm9Yp1sjB2Q/I2BIHzrHlBrxZBg836I3GAnDv4RcrkChjWBvCiLyPXV2 1hXFwmoppescF8CLGcLe99Epdyz/ONa4qs6rfRxAvoTf4P6RFBnHdkMr/6vBY3EZ30 jNv71MGeVt0wlGjFgTc6Rh/wMgZN1GUqzBUxdDsVuna/mcTuQM+kdJSr4O38o34zPo bPbs4CnFVurvPVV3Yum10q887EtHhR0pJwAxW7Dr4lprlSqQHJxv3PFF9yRaItnYFw Dkmed3kVk0fXVMk35rCsxIroybuEevw/lXFBMjJWKx/NT9+FYkFdY6uIuQGE3s2YUm 5SQXfuxxkC8qg== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 1251B68C08 for ; Wed, 17 Dec 2025 12:24:33 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10024) with ESMTP id Maiq2XnBUzvl for ; Wed, 17 Dec 2025 12:24:33 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765999472; bh=MSUuJi/J6vfnuDoAqdkT3ML3SYbiV8FG3tX718XtsiU=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=tqxuccYa1oosY9Kcg3K7x28mJDoyjOfH4jP/pC3PMT0X05+fafNpsBNP6MOaCuHRq 5B0smGUWGDzIf1Ij6z1y8qUx1Aht4bPfiO0tCcPWkRsMyH1Z7oMDQ0L0xGNBeWM+fu 3vFBi68Qi5hVYvcLpeZgDxYE0dWplkW8z//9ayfAPsGnqycPi6310+MxhfVTH0OGnt BvgAfAjVBjiSTIFr/99ibd/K7PL6IYt51D6uoXJI8JmD4NdB8CrXldE+56/7kOUEPi VDmganAPztsnD/mQXnAY8oqHk695UaWCPahhtEEg4MXGH7/RESdRcwhQJp2vdyNzPa G+PxwpdE/wFvA== Received: from mail.u-boot.org (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id F11926745C for ; Wed, 17 Dec 2025 12:24:32 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197639; bh=VGbrl/xN3hvRsjFwcNHrVlggLwDiBUssIQvXx4nctVc=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=ChnqNk80wcEgaAjTbzYJ10v4VPtr+xMttNkWCwmBtw+B3rHupxIOU41ScTk8/pD9O 3FfzrfGQwRNFkGprr5RJGy/5fnBXLPcc5bBDgzRzaZHP1/Vw+IvnG5l60mkScfFmMI EipHz6muyky/1foGzYmIziuyj0J4zqcAGwIKKUC8tLnmlV32P/313JSPFcQhMy59GT kSFxKr5z9nIbBR5L5Z2FqBt4TYyM2TLJo3Q0cU8/bZeJsE6FgQ7f5boJlokWJyMwPT fjk870ApcpMPpjJ7RdkC9CWG4xxTNGgqyeSDTEvCFFWRrGj2odxHb2gNl3wUU3OLO7 wcT1SKfYeOFug== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 4E198689F3; Mon, 8 Dec 2025 05:40:39 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10026) with ESMTP id 46XZnWzGbn-j; Mon, 8 Dec 2025 05:40:39 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197632; bh=MFajqNnhyXJ1av8xLJmt8TX5hKDitWrxgI/HoY7nQtk=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=ZeQHjuJP+sBNoL/wOjuR1IUcvliOEP3PV56MO9B9t6fiACAW7xRDBf816KvP9t4QP fi4MS91XuRF4GhGE5hSZQD10UXkSK9m6ViuDn/rNZ9A8GnE0+/6PkjDOIPlFhI1iFe rMmRDOfg6kz+xFrnhVbd3WvGWDfY8gVe9xVlZjsCNHwSWuXI08Ba5YlIzaoqSySgZ3 wP6tdc7iwRYalOMRWnyHW/5LKwY9ZvbCJ1wkUndRJ9rG42LaKBFMEuvVksPeDfq/ZP zXRQP9UBIRPIEqPX+5oNk9S8t56W2AGpih+oGd1HGETgPM1Ynzo2xSKrQjjBPYUiQf 38G4KFnJ6RXuA== Received: from u-boot.org (unknown [73.34.74.121]) by mail.u-boot.org (Postfix) with ESMTPSA id 64B3A68958; Mon, 8 Dec 2025 05:40:32 -0700 (MST) From: Simon Glass To: U-Boot Concept Date: Mon, 8 Dec 2025 05:39:55 -0700 Message-ID: <20251208124001.775057-7-sjg@u-boot.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20251208124001.775057-1-sjg@u-boot.org> References: <20251208124001.775057-1-sjg@u-boot.org> MIME-Version: 1.0 X-MailFrom: sjg@u-boot.org X-Mailman-Rule-Hits: max-size X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; news-moderation; no-subject; digests; suspicious-header Message-ID-Hash: M3RYKMTWOVIZH2XEP7OK25LALDUNIKNU X-Message-ID-Hash: M3RYKMTWOVIZH2XEP7OK25LALDUNIKNU X-Mailman-Approved-At: Wed, 17 Dec 2025 19:24:30 -0700 CC: Simon Glass , Claude X-Mailman-Version: 3.3.10 Precedence: list Subject: [Concept] [PATCH 6/7] scripts: Add tkey_fde_key.py for TKey disk encryption List-Id: Discussion and patches related to U-Boot Concept Archived-At: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: From: Simon Glass Add a Python script for TKey-based full disk encryption key generation and disk encryption operations: - Generate hardware-backed encryption keys using TKey's Ed25519 signature and SHA-256 hashing - Encrypt disk images with LUKS using the derived keys - Open LUKS encrypted disks using the derived keys - Support for both interactive password input and file/stdin input - Automatic TKey device detection via USB enumeration The script derives deterministic encryption keys from a password and the TKey's unique device identifier, suitable for unlocking encrypted root filesystems at boot time. Co-developed-by: Claude Signed-off-by: Simon Glass --- scripts/tkey_fde_key.py | 2003 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 2003 insertions(+) create mode 100755 scripts/tkey_fde_key.py diff --git a/scripts/tkey_fde_key.py b/scripts/tkey_fde_key.py new file mode 100755 index 00000000000..4607e07476c --- /dev/null +++ b/scripts/tkey_fde_key.py @@ -0,0 +1,2003 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0+ +# Copyright (C) 2025 Canonical Ltd + +"""TKey Full Disk Encryption Key Generator + +This script uses tkey-sign to generate encryption keys for full-disk encryption. +It prompts the user for a passphrase and uses the TKey's hardware-based key +derivation to create a consistent encryption key. + +USAGE OVERVIEW: +============== + +This tool provides three main functions: +1. Generate hardware-backed encryption keys using TKey +2. Encrypt disk images with LUKS using the derived keys +3. Open LUKS encrypted disks using the derived keys + +BASIC USAGE: +----------- + +# Generate a key interactively (prompts for password) +tkey-fde-key.py + +# Generate key and save to file +tkey-fde-key.py -o /tmp/my-key.bin + +# Read password from file instead of interactive prompt +tkey-fde-key.py -p /path/to/passfile + +# Read password from stdin +echo 'mypassword' | tkey-fde-key.py -p - + +DISK ENCRYPTION: +--------------- + +# Encrypt a disk image with LUKS (automatically resizes image for LUKS header) +tkey-fde-key.py -e /path/to/disk.img -p /path/to/passfile + +# Encrypt disk with password from stdin +echo 'mypassword' | tkey-fde-key.py -e /path/to/disk.img -p - + +# Encrypt disk and save backup key +tkey-fde-key.py -e /path/to/disk.img -p /path/to/passfile -o /tmp/backup.key + +# Interactive encryption (will prompt for password) +tkey-fde-key.py -e /path/to/disk.img + +DISK OPENING: +------------ + +# Open an encrypted disk (creates /dev/mapper/tkey-disk) +tkey-fde-key.py -O /path/to/encrypted.img -p /path/to/passfile + +# Open with password from stdin +echo 'mypassword' | tkey-fde-key.py -O /path/to/encrypted.img -p - + +# Open with custom mapper name +tkey-fde-key.py -O /path/to/encrypted.img -m my-disk -p /path/to/passfile + +# Interactive opening (will prompt for password) +tkey-fde-key.py -O /path/to/encrypted.img + +# After opening, mount the filesystem: +sudo mount /dev/mapper/tkey-disk /mnt + +# When done, unmount and close: +sudo umount /mnt +sudo cryptsetup close tkey-disk + +IMPORTANT NOTES: +=============== + +- The same password must be used to derive the same key +- TKey must be in firmware mode or will prompt for reinsertion +- Disk operations may require root privileges +- LUKS encryption uses AES-XTS-256 with SHA256 +- Device mapper names must be unique system-wide +- Always backup important data before encryption + +SECURITY: +======== + +- Keys are derived using TKey's hardware security module +- Temporary keyfiles are created with restrictive permissions (600) +- All temporary files are automatically cleaned up +- Password confirmation required for interactive input +- Empty passwords are not allowed + +DEPENDENCIES: +============ + +- tkey-sign (TKey development tools) +- cryptsetup (for LUKS operations) +- truncate (for disk resizing) +- dmsetup (for device mapper operations) + +For more examples, see the --help output. +""" + +import argparse +import base64 +import getpass +import glob +import hashlib +import os +import subprocess +import sys +import tempfile +import time +from types import SimpleNamespace + +import serial + +# Add the tools directory to the path for u_boot_pylib +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'tools')) + +# pylint: disable=wrong-import-position,import-error +from u_boot_pylib import tools +from u_boot_pylib import tout + +# TKey frame constants (from U-Boot tkey-uclass.c) +TKEY_FRAME_ID_CMD_V1 = 0 +TKEY_ENDPOINT_FIRMWARE = 2 +TKEY_STATUS_OK = 0 +TKEY_LENGTH_1_BYTE = 0 +TKEY_FW_CMD_NAME_VERSION = 0x01 + + +def parse_args(): + """Parse command line arguments + + Returns: + argparse.Namespace: Parsed command line arguments + """ + parser = argparse.ArgumentParser( + description='Generate full-disk encryption keys using TKey hardware', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Examples: + # Generate key interactively + tkey-fde-key.py + + # Generate key and save to file + tkey-fde-key.py --output /tmp/disk.key + + # Read password from file + tkey-fde-key.py --password-file /path/to/passfile + + # Read password from stdin + echo 'mypassword' | tkey-fde-key.py --password-file - + + # Output binary format instead of hex + tkey-fde-key.py --binary + + # Encrypt a disk image with LUKS + tkey-fde-key.py -e /path/to/disk.img + + # Encrypt specific partition (will prompt for selection) + tkey-fde-key.py -e /path/to/disk.img -P 2 + + # Open an encrypted disk image + tkey-fde-key.py -O /path/to/encrypted.img + + # Open disk with custom mapper name + tkey-fde-key.py -O /path/to/encrypted.img -m my-disk +''' + ) + + parser.add_argument( + '--device', + help='TKey serial device (auto-detected if not specified)' + ) + parser.add_argument( + '--output', '-o', + help='Output file for the key (prints to stdout if not specified)' + ) + parser.add_argument( + '--binary', + action='store_true', + help='Output key in binary format (default is hex)' + ) + parser.add_argument( + '--force', '-f', + action='store_true', + help='Overwrite existing output file' + ) + parser.add_argument( + '--verbose', '-v', + action='store_true', + help='Enable verbose output' + ) + parser.add_argument( + '--debug', '-d', + action='store_true', + help='Enable debug mode (passes --verbose to tkey-sign)' + ) + parser.add_argument( + '--password-file', '-p', + help="Read password from file (use '-' for stdin)" + ) + parser.add_argument( + '--encrypt-disk', '-e', + help='Disk image file to encrypt with LUKS using the derived key' + ) + parser.add_argument( + '--open-disk', '-O', + help='LUKS encrypted disk image to open using the derived key' + ) + parser.add_argument( + '--mapper-name', '-m', + default='tkey-disk', + help='Device mapper name for opened disk (default: tkey-disk)' + ) + parser.add_argument( + '--partition', '-P', + type=int, + help='Partition number to encrypt (if not specified, will prompt or encrypt whole disk)' + ) + + return parser.parse_args() + +def run_sudo(cmd, inp=None, timeout=60, capture=True): + """Run a command with sudo, handling password prompts properly + + Args: + cmd (list): Command and arguments to run with sudo + inp (str, optional): Data to pass to stdin. Defaults to None. + timeout (int, optional): Command timeout in seconds. Defaults to 60. + capture (bool, optional): Capture stdout/stderr. Defaults to True. + + Returns: + subprocess.CompletedProcess: Result of the subprocess run + """ + # Check if we're already running as root + if not os.geteuid(): + # Already root, run command directly + sudo_cmd = cmd + else: + # Not root, use sudo with environment preservation + # Preserve PATH and other important environment variables + env_vars = [] + for var in ['PATH', 'HOME', 'USER']: + if var in os.environ: + env_vars.extend([f'{var}={os.environ[var]}']) + + if env_vars: + sudo_cmd = ['sudo', 'env'] + env_vars + cmd + else: + sudo_cmd = ['sudo'] + cmd + + cmd_str = ' '.join(sudo_cmd) + # Mask environment variables in verbose output for cleanliness + if 'env' in cmd_str: + cmd_str = cmd_str.replace('env PATH=', 'env PATH=...') + tout.detail(f'Running: {cmd_str}') + + if capture: + return subprocess.run( + sudo_cmd, + input=inp, + capture_output=True, + text=True, + timeout=timeout, + check=True + ) + # Allow interactive sudo (don't capture output) + return subprocess.run( + sudo_cmd, + input=inp, + text=True, + timeout=timeout, + check=False + ) + +def find_tkey_sign(): + """Find the full path to tkey-sign command + + Returns: + str or None: Full path to tkey-sign or None if not found + """ + # Check if tkey-sign is in current PATH + try: + result = subprocess.run(['which', 'tkey-sign'], capture_output=True, + text=True, timeout=5, check=False) + if not result.returncode: + return result.stdout.strip() + except (subprocess.TimeoutExpired, OSError): + pass + + # If running as root, check the original user's paths + original_user = os.environ.get('SUDO_USER') + if original_user and not os.geteuid(): + import pwd # pylint: disable=import-outside-toplevel + try: + user_info = pwd.getpwnam(original_user) + user_home = user_info.pw_dir + user_paths = [ + f'{user_home}/bin/tkey-sign', + f'{user_home}/.local/bin/tkey-sign', + ] + for path in user_paths: + if os.path.exists(path) and os.access(path, os.X_OK): + return path + except KeyError: + pass + + # Common system locations + common_paths = [ + '/usr/local/bin/tkey-sign', + '/usr/bin/tkey-sign', + os.path.expanduser('~/bin/tkey-sign'), + os.path.expanduser('~/.local/bin/tkey-sign'), + ] + + for path in common_paths: + if os.path.exists(path) and os.access(path, os.X_OK): + return path + + return None + +def run_tkey_sign(args, inp=None): + """Run tkey-sign command with given arguments + + Args: + args (list): Command line arguments for tkey-sign + inp (str, optional): Data to pass to stdin. Defaults to None. + + Returns: + subprocess.CompletedProcess: Result of the subprocess run + """ + # Find tkey-sign path + fname = find_tkey_sign() + if not fname: + tout.warning('tkey-sign not found, using PATH search') + fname = 'tkey-sign' + + cmd = [fname] + args + + # Show the command being run, but mask USS input for security + cmd_str = ' '.join(cmd) + if '--uss-file' in args: + tout.detail(f'Running: {cmd_str} (with USS file)') + elif inp and '--uss' in args: + tout.detail(f'Running: {cmd_str} (with USS input)') + else: + tout.detail(f'Running: {cmd_str}') + + result = subprocess.run( + cmd, + input=inp, + capture_output=True, + text=True, + timeout=30, + check=True + ) + + tout.detail(f'Command exit code: {result.returncode}') + if result.stdout: + tout.detail(f'Command stdout: {result.stdout}') + if result.stderr: + tout.detail(f'Command stderr: {result.stderr}') + + return result + +def get_tkey_pubkey(device=None): + """Get the public key from TKey with optional USS + + Args: + device (str, optional): TKey device path. Defaults to None + (auto-detect). + + Returns: + str or None: Public key string on success, None on failure + """ + with tempfile.NamedTemporaryFile(mode='w+', suffix='.pub', + delete=False) as f: + pubkey_file = f.name + + args = ['--getkey', '--public', pubkey_file, '--force'] + if device: + args.extend(['--port', device]) + + result = run_tkey_sign(args) + if not result or result.returncode: + err = result.stderr if result else 'Unknown error' + tout.error(f'Error getting public key: {err}') + if os.path.exists(pubkey_file): + os.unlink(pubkey_file) + return None + + pubkey = tools.read_file(pubkey_file, binary=False).strip() + + if os.path.exists(pubkey_file): + os.unlink(pubkey_file) + + return pubkey + +def check_tkey_mode(device=None): + """Check if TKey is in firmware mode or has an app loaded + + Uses the same logic as U-Boot's tkey_get_name_version() function to detect + mode. In firmware mode: returns name0='tk1 ' name1='mkdf' + In app mode: firmware commands return error status with error code 0x00 + + Args: + device (str, optional): TKey device path. Defaults to None + (auto-detect). + + Returns: + str or None: 'firmware' if in firmware mode, 'app' if app loaded, None + on error + """ + # Try to get name/version to determine mode (same as U-Boot does) + with tempfile.NamedTemporaryFile(mode='w', suffix='.temp', + delete=False) as temp_f: + temp_file = temp_f.name + + # Use a simple command that should work regardless of USS + args = ['--getkey', '--public', temp_file, '--force'] + if device: + args.extend(['--port', device]) + + result = run_tkey_sign(args) + + # Read content before cleanup + content = '' + if os.path.exists(temp_file): + content = tools.read_file(temp_file, binary=False).strip() + os.unlink(temp_file) + + if not result: + return None + + if result.returncode: + # Command failed - could indicate various issues + return None + + # In app mode with USS, tkey-sign warns about app already loaded + if result.stderr and 'App already loaded' in result.stderr: + return 'app' + + # In firmware mode, we get a public key without warnings + if content: + return 'firmware' + + return None + +def check_tkey_mode_raw(device='/dev/ttyACM0'): + """Check TKey mode by directly communicating with the device + + Sends a firmware NAME_VERSION command to determine if the TKey is in: + - firmware mode (responds with 'tk1 mkdf' + version) + - app mode (responds with error status) + + Args: + device (str, optional): Serial device path. Defaults to '/dev/ttyACM0'. + + Returns: + str or None: 'firmware', 'app' if app loaded, None on error + """ + try: + # Open serial connection with TKey baud rate + ser = serial.Serial(device, baudrate=62500, timeout=.5) + + tout.info(f'Opened {device} at 62500 baud') + + # Build frame header: cmd=0, endpoint=2(firmware), status=0, len=1byte + # Header format: [id:2][endpoint:2][status:1][len:2][reserved:1] + header = (((TKEY_FRAME_ID_CMD_V1 & 0x3) << 5) | + ((TKEY_ENDPOINT_FIRMWARE & 0x3) << 3) | + ((TKEY_STATUS_OK & 0x1) << 2) | + (TKEY_LENGTH_1_BYTE & 0x3)) + + # Frame: [header][command] + frame = bytes([header, TKEY_FW_CMD_NAME_VERSION]) + + tout.info(f'Sending NAME_VERSION: {frame.hex()}') + + # Send command + ser.write(frame) + ser.flush() + + # Read response (up to 64 bytes with timeout) + resp = ser.read(64) + ser.close() + + tout.info(f'Received ({len(resp)} bytes): {resp.hex()}') + + if not resp: + tout.info('No response received') + return None + + # Parse response header + if len(resp) >= 1: + hdr = resp[0] + status_bit = (hdr >> 2) & 0x1 + + tout.info(f'Response header: 0x{hdr:02x}, status: {status_bit}') + + if status_bit == 1: + # Error status - likely app mode responding to firmware cmd + tout.info('Error status bit set - device in app mode') + return 'app' + # Success status - check for firmware mode response pattern + if len(resp) >= 13: # Header + 4 + 4 + 4 bytes minimum + # Look for 'tk1 ' and 'mkdf' in resp + resp_str = resp[1:].decode('ascii', errors='ignore') + if 'tk1' in resp_str and 'mkdf' in resp_str: + tout.info('Found firmware identifiers') + return 'firmware' + + # Got success resp but couldn't parse - assume firmware + tout.info('Success response - assuming firmware mode') + return 'firmware' + + return None + + except OSError as e: + tout.info(f'Error during TKey communication: {e}') + return None + +def get_tkey_pubkey_uss(uss, device=None): + '''Get public key from TKey using a User Supplied Secret + + Args: + uss (str): User Supplied Secret (password/passphrase) for key derivation + device (str, optional): TKey device path. Defaults to None (auto-detect) + + Returns: + str or None: Public key string on success, None on failure + ''' + # Check if TKey already has an app loaded first + # Try direct serial communication first, fall back to tkey-sign + mode = None + if not device or device == '/dev/ttyACM0': + mode = check_tkey_mode_raw('/dev/ttyACM0') + + if mode is None: + # Fallback to tkey-sign method + mode = check_tkey_mode(device) + + tout.info(f'TKey mode: {mode}') + + if mode == 'app': + # App already loaded, need to wait for reinsertion + wait_tkey_replug() + else: + # Show 'Setting up TKey' only when we're about to do the USS operation + # (not when we need to wait for reinsertion) + tout.notice('Setting up TKey') + + with tempfile.NamedTemporaryFile(mode='w+', suffix='.pub', + delete=False) as f: + pubfile = f.name + + # Create temporary file for USS + with tempfile.NamedTemporaryFile(mode='w', suffix='.uss', + delete=False) as uss_f: + uss_file = uss_f.name + uss_f.write(uss) + + args = ['--getkey', '--public', pubfile, '--uss-file', uss_file, '--force'] + if device: + args.extend(['--port', device]) + + result = run_tkey_sign(args) + if not result or result.returncode: + err = result.stderr if result else 'Unknown error' + tout.error(f'Error getting public key with USS: {err}') + if os.path.exists(pubfile): + os.unlink(pubfile) + if os.path.exists(uss_file): + os.unlink(uss_file) + return None + + pubkey = tools.read_file(pubfile, binary=False).strip() + + if os.path.exists(pubfile): + os.unlink(pubfile) + if os.path.exists(uss_file): + os.unlink(uss_file) + + return pubkey + +def derive_fde_key(uss, device=None): + """Derive a full-disk encryption key using TKey hardware and USS. + + This uses the TKey's hardware-based key derivation where the USS (User + Supplied Secret) affects the internal key generation, producing different +keys for different USS values. + + The key derivation matches U-Boot's tkey_derive_disk_key(): + 1. Get the 32-byte Ed25519 public key from TKey + 2. Convert to lowercase hex string (64 characters) + 3. SHA256 hash the hex string to produce the disk key + + Args: + uss (str): User Supplied Secret (password/passphrase) for key derivation + device (str, optional): TKey device path. Defaults to None (auto-detect) + + Returns: + bytes or None: 32-byte encryption key material on success, None on + failure + """ + + # Get the public key using the USS + # Different USS values produce different private/public key pairs inside the + # TKey + pubkey_signify = get_tkey_pubkey_uss(uss, device) + if not pubkey_signify: + return None + + tout.info(f'Signify public key:\n{pubkey_signify}') + + # Parse signify format: skip comment line, decode base64 of second line + # Format: "untrusted comment: ...\n" + lines = pubkey_signify.strip().split('\n') + if len(lines) < 2: + tout.error('Invalid public key format') + return None + + # Decode base64 data (second line) + try: + pubkey_data = base64.b64decode(lines[1]) + except base64.binascii.Error as e: + tout.error(f'Error decoding public key: {e}') + return None + + # Signify format: 2-byte algorithm + 8-byte keynum + 32-byte pubkey + # Extract the 32-byte Ed25519 public key (last 32 bytes) + if len(pubkey_data) < 32: + tout.error(f'Public key data too short ({len(pubkey_data)} bytes)') + return None + + pubkey_bytes = pubkey_data[-32:] + + tout.info(f'Ed25519 public key (hex): {pubkey_bytes.hex()}') + + # Match U-Boot's tkey_derive_disk_key(): SHA256(hex_string_of_pubkey) + pubkey_hex = pubkey_bytes.hex() + key_material = hashlib.sha256(pubkey_hex.encode()).digest() + + tout.info(f'Derived disk key (hex): {key_material.hex()}') + + return key_material + +def find_tkey_device(): + """Check if TKey USB device is present + + Looks for TKey device (vendor ID 1207, product ID 8887) via USB enumeration. + + Returns: + bool: True if TKey device found, False otherwise + """ + usb_devices = glob.glob('/sys/bus/usb/devices/*/idVendor') + for vendor_file in usb_devices: + try: + vendor_id = tools.read_file(vendor_file, binary=False).strip() + if vendor_id == '1207': # Tillitis vendor ID + product = vendor_file.replace('idVendor', 'idProduct') + if os.path.exists(product): + product_id = tools.read_file(product, binary=False).strip() + if product_id == '8887': # TKey product ID + return True + except (IOError, OSError): + continue + return False + +def detect_tkey(device=None): + """Check if TKey is present at startup and prompt for insertion if needed + + Args: + device (str, optional): TKey device path. Defaults to None (auto-detect) + + Returns: + bool: True if TKey is available, False on error + """ + # Check if specific device path exists + if device and os.path.exists(device): + tout.info(f'TKey device found at {device}') + return True + + # Check for TKey via USB enumeration + if find_tkey_device(): + tout.info('TKey detected via USB enumeration') + return True + + # No TKey found - prompt for insertion + tout.notice('Please insert your TKey...') + + # Wait for TKey to be inserted + while not find_tkey_device(): + time.sleep(0.5) + + tout.info('TKey detected') + + # Give the device a moment to settle + time.sleep(1) + return True + +def wait_tkey_replug(): + """Wait for TKey to be removed and then re-inserted""" + tout.info('Waiting for TKey removal and reinsertion...') + + # Wait for device to be removed + if find_tkey_device(): + tout.notice('Please remove your TKey...') + while find_tkey_device(): + time.sleep(0.5) + tout.info('TKey removed') + + # Wait for device to be inserted + tout.notice('Please insert your TKey...') + while not find_tkey_device(): + time.sleep(0.5) + + tout.info('TKey detected') + + tout.notice('Setting up TKey') + # Give the device a moment to settle + time.sleep(.2) + +def format_key_for_luks(key_material): + '''Format the key material as hex for LUKS + + Args: + key_material (bytes): Raw key material bytes + + Returns: + str: Hexadecimal representation of the key + ''' + return key_material.hex() + +def save_key_to_file(key_material, outfile, force=False): + """Save the key material to a file + + Args: + key_material (bytes): Raw key material to save + outfile (str): Path to output file + force (bool, optional): Overwrite existing files. Defaults to False. + + Returns: + bool: True on success, False on failure + """ + if os.path.exists(outfile) and not force: + tout.error(f'Output file {outfile} already exists. ' + 'Use --force to overwrite.') + return False + + tools.write_file(outfile, key_material) + + # Set restrictive permissions + os.chmod(outfile, 0o600) + tout.notice(f'Key saved to {outfile}') + return True + +def get_password(args): + """Get password from user input or file + + Args: + args (argparse.Namespace): Parsed command line arguments + + Returns: + str or None: Password string on success, None on failure + """ + if args.password_file: + if args.password_file == '-': + # Read from stdin + tout.info('Reading password from stdin...') + password = sys.stdin.readline().rstrip('\n\r') + else: + # Read from file + tout.info(f'Reading password from file: {args.password_file}') + password = tools.read_file(args.password_file, binary=False).strip() + + if not password: + tout.error('Empty password not allowed') + return None + tout.info(f'Password length: {len(password)}, repr: {repr(password)}') + else: + # Interactive input + password = getpass.getpass('Enter password for key derivation: ') + if not password: + tout.error('Empty password not allowed') + return None + + # Confirm password only for interactive input + confirm = getpass.getpass('Confirm password: ') + if password != confirm: + tout.error('Passwords do not match') + return None + + return password + +def check_broken_luks(disk_path): + """Check if disk has broken LUKS metadata + + Args: + disk_path (str): Path to disk image file + + Returns: + bool: True if broken LUKS metadata detected, False otherwise + """ + try: + # Use cryptsetup luksDump to check for broken metadata + result = subprocess.run( + ['cryptsetup', 'luksDump', disk_path], + capture_output=True, + text=True, + timeout=30, + check=False + ) + + if result.returncode: + # Check if the error specifically mentions broken metadata + # But exclude common "not a LUKS device" messages + error_lower = result.stderr.lower() + + # These indicate broken LUKS (partial/corrupted headers) + broken_hints = ['broken', 'invalid', 'corrupted', 'damaged'] + + # These indicate no LUKS at all (normal for fresh files) + no_luks_hints = ['not a luks device', 'no luks header', + 'unrecognized'] + + # Check for "no LUKS" messages first (these are normal) + if any(hint in error_lower for hint in no_luks_hints): + tout.info('No LUKS header detected (normal for fresh disk)') + return False + + # Check for broken LUKS hints + if any(hint in error_lower for hint in broken_hints): + tout.info(f'Detected broken LUKS metadata: {result.stderr}') + return True + + return False + + except subprocess.TimeoutExpired: + tout.info('Timeout checking for broken LUKS metadata') + return False + except OSError as e: + tout.info(f'Error checking for broken LUKS: {e}') + return False + +def wipe_luks_header(disk_path): + """Wipe broken LUKS header from disk + + Args: + disk_path (str): Path to disk image file + + Returns: + bool: True on success, False on failure + """ + tout.info(f'Wiping LUKS header from {disk_path}') + + try: + # Use dd to zero out the first 32MB (typical LUKS header size) + result = subprocess.run( + ['dd', 'if=/dev/zero', f'of={disk_path}', 'bs=1M', 'count=32', + 'conv=notrunc'], + capture_output=True, + text=True, + timeout=60, + check=True + ) + + if result.returncode: + tout.error(f'Error wiping LUKS header: {result.stderr}') + return False + + tout.info('LUKS header wiped successfully') + if result.stderr: # dd output goes to stderr + tout.detail(f'dd output: {result.stderr}') + + return True + + except subprocess.TimeoutExpired: + tout.error('LUKS header wipe operation timed out') + return False + except FileNotFoundError: + tout.error('dd command not found') + return False + except OSError as e: + tout.error(f'Error during LUKS header wipe: {e}') + return False + +def parse_fdisk_line(line, disk_path): + """Parse a single fdisk output line into partition info + + Args: + line (str): A line from fdisk -l output + disk_path (str): Path to disk image file + + Returns: + dict or None: Partition info dict, or None if line is not a partition + """ + if disk_path not in line or not line.strip() or line.startswith('Disk'): + return None + + parts = line.split() + if len(parts) < 6 or not parts[0].startswith(disk_path): + return None + + try: + partition_num = int(parts[0].replace(disk_path, '').replace('p', '')) + + # Handle bootable flag - if '*' is present, it shifts other fields + if '*' in parts[1]: + bootable = True + start_sector = int(parts[2]) + end_sector = int(parts[3]) + sectors = int(parts[4]) + size = parts[5] + part_type = ' '.join(parts[7:]) if len(parts) > 7 else 'Unknown' + else: + bootable = False + start_sector = int(parts[1]) + end_sector = int(parts[2]) + sectors = int(parts[3]) + size = parts[4] + part_type = ' '.join(parts[6:]) if len(parts) > 6 else 'Unknown' + + return { + 'number': partition_num, + 'start': start_sector, + 'end': end_sector, + 'sectors': sectors, + 'size': size, + 'type': part_type, + 'bootable': bootable + } + except (ValueError, IndexError): + return None + +def get_disk_parts(disk_path): + """Get partition information from disk image + + Args: + disk_path (str): Path to disk image file + + Returns: + list or None: List of partition info dicts, None on error + """ + try: + result = subprocess.run( + ['fdisk', '-l', disk_path], + capture_output=True, + text=True, + timeout=30, + check=False + ) + + if result.returncode: + tout.info(f'Error running fdisk: {result.stderr}') + return None + + partitions = [] + for line in result.stdout.split('\n'): + part_info = parse_fdisk_line(line, disk_path) + if part_info: + partitions.append(part_info) + + if partitions: + tout.info(f'Found {len(partitions)} partitions:') + for p in partitions: + boot_flag = ' (bootable)' if p['bootable'] else '' + tout.info(f" Partition {p['number']}: {p['size']} " + f"{p['type']}{boot_flag}") + + return partitions + + except subprocess.TimeoutExpired: + tout.info('Timeout reading partition table') + return None + except FileNotFoundError: + tout.info('fdisk command not found') + return None + except OSError as e: + tout.info(f'Error reading partition table: {e}') + return None + +def select_partition(disk_path, partitions, args): + """Select which partition to encrypt + + Args: + disk_path (str): Path to disk image + partitions (list): List of partition info dicts + args (Namespace): Command line arguments + + Returns: + int or None: Selected partition number, None for whole disk + """ + # If partition specified on command line, use it + if args.partition: + if any(p['number'] == args.partition for p in partitions): + tout.info(f'Using partition {args.partition} from command line') + return args.partition + tout.error(f'Partition {args.partition} not found') + return False # Return False to indicate error + + # If no partitions found, encrypt whole disk + if not partitions: + tout.info('No partitions detected, will encrypt whole disk') + return None + + # Show partition table and prompt for selection + tout.notice(f'Disk {disk_path} contains partitions:') + for p in partitions: + boot_flag = ' (bootable)' if p['bootable'] else '' + tout.notice(f" {p['number']}: {p['size']} {p['type']}{boot_flag}") + + tout.notice(' 0: Encrypt whole disk') + + while True: + try: + resp = input('Select partition to encrypt (0 for whole disk): ') + choice = int(resp) + + if not choice: + return None # Whole disk + if any(p['number'] == choice for p in partitions): + return choice + nums = [p["number"] for p in partitions] + tout.notice(f'Invalid choice. Please select 0 or one of: {nums}') + except (ValueError, KeyboardInterrupt): + tout.notice('\nOperation cancelled') + return None + +def setup_loop_dev(disk_path): + """Set up loop device for disk image with partition support + + Args: + disk_path (str): Path to disk image + + Returns: + str or None: Loop device path on success, None on failure + """ + try: + # First, ensure sudo credentials are cached with an interactive command + if os.geteuid() != 0: + tout.info('Requesting sudo privileges for loop device operations...') + result = subprocess.run(['sudo', '-v'], timeout=30, check=False) + if result.returncode: + tout.error('Could not obtain sudo privileges') + return None + # Find available loop device + result = run_sudo( + ['losetup', '--find', '--show', '--partscan', disk_path], + timeout=30, + capture=True # We need the output for the loop device path + ) + + if result.returncode: + tout.error(f'Error setting up loop device: {result.stderr}') + return None + + loop_device = result.stdout.strip() + + tout.info(f'Set up loop device {loop_device} for {disk_path}') + + # Force partition scan + run_sudo(['partprobe', loop_device], timeout=10) + + return loop_device + + except subprocess.TimeoutExpired: + tout.error('Loop device setup timed out') + return None + except OSError as e: + tout.error(f'Error setting up loop device: {e}') + return None + +def cleanup_loop_dev(loop_device): + """Clean up loop device + + Args: + loop_device (str): Loop device path (e.g., /dev/loop0) + + Returns: + bool: True on success, False on failure + """ + try: + result = run_sudo( + ['losetup', '--detach', loop_device], + timeout=30 + ) + + if result.returncode: + tout.error(f'Error cleaning up loop device: {result.stderr}') + return False + + tout.info(f'Cleaned up loop device {loop_device}') + + return True + + except subprocess.TimeoutExpired: + tout.error('Loop device cleanup timed out') + return False + except OSError as e: + tout.error(f'Error cleaning up loop device: {e}') + return False + +def get_part_dev(loop_device, partition_num): + """Get the device path for a specific partition + + Args: + loop_device (str): Loop device path (e.g., /dev/loop0) + partition_num (int): Partition number + + Returns: + str: Partition device path (e.g., /dev/loop0p2) + """ + return f'{loop_device}p{partition_num}' + +def check_disk_image(disk_path): + """Check disk image file and determine if it needs space for LUKS header + + Args: + disk_path (str): Path to disk image file + + Returns: + SimpleNamespace or None: Namespace with disk info on success, None on + failure. Contains: size, needs_resize, luks_hdrsize, + already_encrypted + """ + if not os.path.exists(disk_path): + tout.error(f'Disk image {disk_path} does not exist') + return None + + if not os.path.isfile(disk_path): + tout.error(f'{disk_path} is not a regular file') + return None + + # Get current file size + stat_info = os.stat(disk_path) + current_size = stat_info.st_size + + size_mb = current_size / (1024 * 1024) + tout.info(f'Disk image size: {current_size} bytes ({size_mb:.1f} MB)') + + # LUKS header is typically 16 MB, but we'll use 32 MB for safety + luks_hdrsize = 32 * 1024 * 1024 # 32 MB + + # Check if disk is already LUKS encrypted + # Read first few bytes to check for LUKS signature + try: + with open(disk_path, 'rb') as f: + header = f.read(16) + if header.startswith(b'LUKS'): + tout.info('Disk image appears to already be LUKS encrypted') + return SimpleNamespace( + size=current_size, + needs_resize=False, + luks_hdrsize=0, + already_encrypted=True, + broken_luks=False + ) + except IOError as e: + tout.error(f'Error reading disk image: {e}') + return None + + # Check for broken LUKS metadata using cryptsetup + broken_luks = check_broken_luks(disk_path) + if broken_luks: + return SimpleNamespace( + size=current_size, + needs_resize=False, + luks_hdrsize=0, + already_encrypted=False, + broken_luks=True + ) + + # For unencrypted disks, we need to add space for LUKS header + return SimpleNamespace( + size=current_size, + needs_resize=True, + luks_hdrsize=luks_hdrsize, + already_encrypted=False, + broken_luks=False + ) + +def resize_disk(disk_path, additional_size): + """Resize disk image to add space for LUKS header + + Args: + disk_path (str): Path to disk image file + additional_size (int): Additional bytes to add to the image + + Returns: + bool: True on success, False on failure + """ + add_mb = additional_size / (1024 * 1024) + tout.info(f'Resizing disk image to add {additional_size} bytes ' + f'({add_mb:.1f} MB)') + + try: + # Use truncate to extend the file + # This is safer than dd as it doesn't actually write the data + current_size = os.path.getsize(disk_path) + new_size = current_size + additional_size + + tout.info(f'Extending from {current_size} to {new_size} bytes') + + # Use truncate command which is available on most systems + result = subprocess.run( + ['truncate', '--size', str(new_size), disk_path], + capture_output=True, + text=True, + timeout=30, + check=True + ) + + if result.returncode: + tout.error(f'Error resizing disk image: {result.stderr}') + return False + + tout.info('Disk image resized successfully') + + return True + + except subprocess.TimeoutExpired: + tout.error('Disk resize operation timed out') + return False + except OSError: + return False + +def backup_part_data(devpath, orig_mount, backup_dir): + """Mount partition read-only and backup all data + + Args: + devpath (str): Path to partition device + orig_mount (str): Mount point for original partition + backup_dir (str): Directory to backup data to + + Returns: + bool: True on success, False on failure + """ + tout.info(f'Mounting original partition at {orig_mount}') + + result = run_sudo(['mount', '-o', 'ro', devpath, orig_mount], + timeout=30) + if result.returncode: + tout.error(f'Error mounting original partition: {result.stderr}') + return False + + tout.info(f'Backing up data to {backup_dir}') + + result = run_sudo(['cp', '-a', f'{orig_mount}/.', backup_dir], timeout=300) + if result.returncode: + tout.error(f'Error backing up data: {result.stderr}') + run_sudo(['umount', orig_mount], timeout=30) + return False + + tout.info('Data backup completed successfully') + + result = run_sudo(['du', '-sb', backup_dir], timeout=30) + if not result.returncode: + backup_size = int(result.stdout.split()[0]) + size_mb = backup_size / (1024 * 1024) + tout.info(f'Backed up {backup_size} bytes ({size_mb:.1f} MB)') + + run_sudo(['umount', orig_mount], timeout=30) + return True + + +def format_luks_part(devpath, keyfile): + """Format a partition with LUKS2 + + Args: + devpath (str): Path to partition device + keyfile (str): Path to key file + + Returns: + bool: True on success, False on failure + """ + tout.info(f'Creating LUKS partition on {devpath}') + + cmd = [ + 'cryptsetup', 'luksFormat', + '--type', 'luks2', + '--cipher', 'aes-xts-plain64', + '--key-size', '512', + '--hash', 'sha256', + '--use-random', + '--key-file', keyfile, + '--batch-mode', + devpath + ] + + result = run_sudo(cmd, timeout=120) + if result.returncode: + tout.error(f'Error formatting partition with LUKS: {result.stderr}') + return False + return True + + +def restore_to_luks(dev_path, keyfile, backup_dir, enc_mount): + """Open LUKS, create filesystem, and restore backed up data + + Args: + dev_path (str): Path to LUKS partition device + keyfile (str): Path to key file + backup_dir (str): Directory containing backed up data + enc_mount (str): Mount point for encrypted partition + + Returns: + bool: True on success, False on failure + """ + mapper = f'tkey-temp-{os.getpid()}' + tout.info(f'Opening LUKS partition as {mapper}') + + result = run_sudo(['cryptsetup', 'open', '--key-file', keyfile, dev_path, + mapper], timeout=30) + if result.returncode: + tout.error(f'Error opening LUKS partition: {result.stderr}') + return False + + try: + mapper_dev = f'/dev/mapper/{mapper}' + tout.info(f'Creating ext4 filesystem on {mapper_dev}') + + result = run_sudo(['mkfs.ext4', '-F', mapper_dev], timeout=60) + if result.returncode: + tout.error(f'Error creating filesystem: {result.stderr}') + return False + + tout.info(f'Mounting encrypted partition at {enc_mount}') + result = run_sudo(['mount', mapper_dev, enc_mount], timeout=30) + if result.returncode: + tout.error(f'Error mounting encrypted partition: {result.stderr}') + return False + + try: + tout.info('Copying backed up data to encrypted partition') + result = run_sudo(['cp', '-a', f'{backup_dir}/.', enc_mount], + timeout=300) + if result.returncode: + tout.error(f'Error copying data: {result.stderr}') + return False + + run_sudo(['sync'], timeout=30) + tout.info('Data successfully copied to encrypted partition') + return True + finally: + run_sudo(['umount', enc_mount], timeout=30) + + finally: + run_sudo(['cryptsetup', 'close', mapper], timeout=30) + + +def encrypt_part_luks_copy(dev_path, key_material): + """Encrypt a partition with LUKS and copy existing data + + Args: + dev_path (str): Path to partition device (e.g., /dev/loop0p2) + key_material (bytes): Raw key material for encryption + + Returns: + bool: True on success, False on failure + """ + tout.info(f'Encrypting partition {dev_path} with data preservation') + + with tempfile.TemporaryDirectory() as temp_dir: + orig_mount = os.path.join(temp_dir, 'original') + enc_mount = os.path.join(temp_dir, 'encrypted') + backup_dir = os.path.join(temp_dir, 'backup') + os.makedirs(orig_mount) + os.makedirs(enc_mount) + os.makedirs(backup_dir) + + with tempfile.NamedTemporaryFile(mode='wb', suffix='.key', + delete=False) as key_f: + keyfile = key_f.name + key_f.write(key_material) + + try: + os.chmod(keyfile, 0o600) + + if not backup_part_data(dev_path, orig_mount, backup_dir): + return False + + if not format_luks_part(dev_path, keyfile): + return False + + if not restore_to_luks(dev_path, keyfile, backup_dir, enc_mount): + return False + + tout.info(f'LUKS partition created successfully on {dev_path}') + return True + + finally: + try: + os.unlink(keyfile) + except OSError: + pass + +def encrypt_part_luks(devpath, key_material): + """Encrypt a partition with LUKS, destroying existing filesystem + Args: + devpath (str): Path to partition device (e.g., /dev/loop0p2) + key_material (bytes): Raw key material for encryption + Returns: + bool: True on success, False on failure + """ + tout.info(f'Formatting partition {devpath} with LUKS') + + # Create temporary keyfile + with tempfile.NamedTemporaryFile(mode='wb', suffix='.key', + delete=False) as key_f: + keyfile = key_f.name + key_f.write(key_material) + + try: + # Set restrictive permissions on keyfile + os.chmod(keyfile, 0o600) + + # Use luksFormat to create fresh LUKS partition (destroys existing data) + cmd = [ + 'cryptsetup', 'luksFormat', + '--type', 'luks2', + '--cipher', 'aes-xts-plain64', + '--key-size', '512', + '--hash', 'sha256', + '--use-random', + '--key-file', keyfile, + '--batch-mode', + devpath + ] + + result = run_sudo(cmd, timeout=120) + if result.returncode: + tout.error(f'Error formatting partition with LUKS: {result.stderr}') + return False + + tout.info(f'LUKS partition created successfully on {devpath}') + return True + + finally: + # Clean up keyfile + try: + os.unlink(keyfile) + except OSError: + pass + +def create_fs_in_luks(devpath, key_material, fstype='ext4'): + """Open LUKS partition and create filesystem inside + Args: + devpath (str): Path to LUKS partition device + key_material (bytes): Key material to open LUKS partition + fstype (str): Type of filesystem to create (ext4, ext3, etc.) + Returns: + bool: True on success, False on failure + """ + # Create temporary keyfile + with tempfile.NamedTemporaryFile(mode='wb', suffix='.key', + delete=False) as key_f: + keyfile = key_f.name + key_f.write(key_material) + + try: + # Set restrictive permissions on keyfile + os.chmod(keyfile, 0o600) + + # Generate unique mapper name + name = f'tkey-fs-{os.getpid()}' + + # Open LUKS partition + cmd = ['cryptsetup', 'open', '--key-file', keyfile, devpath, name] + + result = run_sudo(cmd, timeout=30) + if result.returncode: + tout.error(f'Error opening LUKS partition: {result.stderr}') + return False + + try: + # Create filesystem + mapper_device = f'/dev/mapper/{name}' + + if fstype == 'ext4': + fs_cmd = ['mkfs.ext4', '-F', mapper_device] + elif fstype == 'ext3': + fs_cmd = ['mkfs.ext3', '-F', mapper_device] + else: + tout.error(f'Unsupported filesystem type: {fstype}') + return False + + result = run_sudo(fs_cmd, timeout=60) + if result.returncode: + tout.error(f'Error creating {fstype} filesystem: ' + f'{result.stderr}') + return False + + tout.info(f'{fstype} filesystem created in LUKS partition') + return True # Success path for inner operation + finally: + # Close LUKS partition + close_cmd = ['cryptsetup', 'close', name] + run_sudo(close_cmd, timeout=10) # close mapper name + + except (subprocess.SubprocessError, OSError) as e: + tout.error(f'Error during filesystem creation: {e}') + return False + + finally: + # Clean up keyfile + if os.path.exists(keyfile): + try: + os.unlink(keyfile) + except OSError: # More specific exception for file operations + pass + +def encrypt_disk_luks(disk_path, key_material): + """Encrypt disk image with LUKS using cryptsetup reencrypt + + Args: + disk_path (str): Path to disk image file + key_material (bytes): Raw key material for encryption + + Returns: + bool: True on success, False on failure + """ + tout.info(f'Encrypting disk image {disk_path} with LUKS') + + # Create temporary keyfile + with tempfile.NamedTemporaryFile(mode='wb', suffix='.key', + delete=False) as key_f: + keyfile = key_f.name + key_f.write(key_material) + + try: + # Set restrictive permissions on keyfile + os.chmod(keyfile, 0o600) + + # Assume fresh disk (no existing LUKS) for partition encryption + is_luks = False + + if is_luks: + # Existing LUKS - use reencrypt + cmd = [ + 'cryptsetup', 'reencrypt', + '--encrypt', + '--type', 'luks2', + '--cipher', 'aes-xts-plain64', + '--key-size', '512', + '--hash', 'sha256', + '--use-random', + '--key-file', keyfile, + '--batch-mode', + disk_path + ] + + else: + # Fresh disk - use detached header to avoid corrupting filesystem + header_file = f'{disk_path}.luks' + cmd = [ + 'cryptsetup', 'reencrypt', + '--encrypt', + '--type', 'luks2', + '--cipher', 'aes-xts-plain64', + '--key-size', '512', + '--hash', 'sha256', + '--use-random', + '--key-file', keyfile, + '--header', header_file, + '--batch-mode', + disk_path + ] + + + result = run_sudo( + cmd, + timeout=3600 # 1 hour timeout for large disks + ) + + if result.returncode: + tout.error(f'Error encrypting disk with LUKS: {result.stderr}') + return False + + tout.info('Disk encryption completed successfully') + if result.stdout: + tout.detail(f'cryptsetup output: {result.stdout}') + + return True + + except subprocess.TimeoutExpired: + tout.error('Disk encryption operation timed out') + return False + except FileNotFoundError: + tout.error('cryptsetup command not found. Please install cryptsetup.') + return False + except OSError as e: + tout.error(f'Error during disk encryption: {e}') + return False + finally: + # Clean up keyfile + if os.path.exists(keyfile): + os.unlink(keyfile) + +def check_mapper_status(mapper_name): + """Check if a device mapper name is already in use + + Args: + mapper_name (str): Device mapper name to check + + Returns: + bool: True if mapper is active, False otherwise + """ + mapper_path = f'/dev/mapper/{mapper_name}' + + if os.path.exists(mapper_path): + tout.info(f'Device mapper {mapper_name} is already active at ' + f'{mapper_path}') + return True + + # Also check via dmsetup + try: + result = run_sudo( + ['dmsetup', 'info', mapper_name], + timeout=10 + ) + + if not result.returncode: + tout.info(f'Device mapper {mapper_name} is active (dmsetup)') + return True + + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + + return False + +def open_luks_disk(disk_path, mapper_name, key_material): + """Open LUKS encrypted disk using cryptsetup + + Args: + disk_path (str): Path to LUKS encrypted disk image + mapper_name (str): Device mapper name for the opened disk + key_material (bytes): Raw key material for decryption + + Returns: + str or None: Path to opened device (/dev/mapper/name) on success, None + on failure + """ + tout.info(f'Opening LUKS disk {disk_path} as {mapper_name}') + + # Check if already opened + if check_mapper_status(mapper_name): + mapper_path = f'/dev/mapper/{mapper_name}' + tout.notice(f'Disk is already opened at {mapper_path}') + return mapper_path + + # Check if disk is LUKS encrypted or has detached header + header_file = f'{disk_path}.luks' + has_detached_header = os.path.exists(header_file) + + if not has_detached_header: + try: + with open(disk_path, 'rb') as f: + header = f.read(16) + if not header.startswith(b'LUKS'): + tout.error(f'{disk_path} is not LUKS encrypted') + return None + except IOError as e: + tout.error(f'Error reading disk image: {e}') + return None + + # Create temporary keyfile + with tempfile.NamedTemporaryFile(mode='wb', suffix='.key', + delete=False) as key_f: + keyfile = key_f.name + key_f.write(key_material) + + try: + # Set restrictive permissions on keyfile + os.chmod(keyfile, 0o600) + + # Use cryptsetup open (luksOpen) - requires root privileges + cmd = ['cryptsetup', 'open', '--type', 'luks', '--key-file', keyfile] + + # Add detached header if it exists + if has_detached_header: + cmd.extend(['--header', header_file]) + + cmd.extend([disk_path, mapper_name]) + + result = run_sudo(cmd, timeout=60) + if result.returncode: + tout.error(f'Error opening LUKS disk: {result.stderr}') + return None + + mapper_path = f'/dev/mapper/{mapper_name}' + + # Verify the device was created + if not os.path.exists(mapper_path): + tout.error(f'Device {mapper_path} was not created') + return None + + tout.info(f'LUKS disk opened successfully at {mapper_path}') + if result.stdout: + tout.detail(f'cryptsetup output: {result.stdout}') + + return mapper_path + + except subprocess.TimeoutExpired: + tout.error('Disk opening operation timed out') + except FileNotFoundError: + tout.error('cryptsetup command not found. Please install cryptsetup.') + except OSError as e: + tout.error(f'Error during disk opening: {e}') + finally: + # Clean up keyfile + if os.path.exists(keyfile): + os.unlink(keyfile) + return None + +def output_key(key_material, args): + """Output the derived key material + + Args: + key_material (bytes): Raw key material to output + args (argparse.Namespace): Parsed command line arguments + + Returns: + bool: True on success, False on failure + """ + if args.output: + if save_key_to_file(key_material, args.output, args.force): + tout.notice(f'Encryption key derived and saved to {args.output}') + if not args.binary: + tout.notice(f'Key (hex): {format_key_for_luks(key_material)}') + return True + return False + if args.binary: + # Output raw binary to stdout + sys.stdout.buffer.write(key_material) + else: + print(format_key_for_luks(key_material)) + return True + +def validate_args(args): + """Validate command line arguments + + Args: + args (argparse.Namespace): Parsed command line arguments + + Returns: + bool: True if arguments are valid, False otherwise + """ + if args.encrypt_disk and args.binary and not args.output: + tout.error('--binary to stdout not compatible with --encrypt-disk') + tout.error('Use --output to save binary key when encrypting') + return False + + if args.encrypt_disk and args.open_disk: + tout.error('Cannot encrypt and open disk in same operation') + return False + + if args.open_disk and args.binary and not args.output: + tout.error('--binary to stdout not compatible with --open-disk') + tout.error('Use --output to save binary key when opening') + return False + + return True + +def do_encrypt_disk(args, key): + """Handle disk encryption + + Args: + args (Namespace): Command line arguments + key (bytes): Encryption key material + + Returns: + bool: True on success, False on failure + """ + loop_device = None + try: + partitions = get_disk_parts(args.encrypt_disk) + sel_part = select_partition(args.encrypt_disk, partitions, args) + if sel_part is False: + return False + + if sel_part is None: + target_device = args.encrypt_disk + disk_info = check_disk_image(args.encrypt_disk) + if not disk_info: + return False + + if disk_info.already_encrypted: + tout.warning(f'{args.encrypt_disk} is already LUKS encrypted') + if input('Continue anyway? (y/N): ').lower() != 'y': + tout.notice('Cancelled') + return False + + if hasattr(disk_info, 'broken_luks') and disk_info.broken_luks: + tout.error(f'{args.encrypt_disk} has broken LUKS metadata') + tout.notice('Previous encryption may have been interrupted.') + if input('Wipe broken LUKS header? (y/N): ').lower() != 'y': + tout.notice('Cancelled') + tout.notice('Manual fix: dd if=/dev/zero of=disk.img ' + 'bs=1M count=32 conv=notrunc') + return False + + if not wipe_luks_header(args.encrypt_disk): + tout.error('Failed to wipe LUKS header') + return False + + disk_info = check_disk_image(args.encrypt_disk) + if not disk_info: + return False + + if disk_info.needs_resize and disk_info.luks_hdrsize: + if not resize_disk(args.encrypt_disk, disk_info.luks_hdrsize): + return False + else: + tout.notice(f'Setting up loop device for partition {sel_part}...') + loop_device = setup_loop_dev(args.encrypt_disk) + if not loop_device: + return False + + target_device = get_part_dev(loop_device, sel_part) + time.sleep(1) + + if not os.path.exists(target_device): + tout.error(f'Partition {target_device} not found') + tout.info(f'Available devices: {glob.glob(f"{loop_device}*")}') + return False + + tout.info(f'Will encrypt partition {sel_part} at {target_device}') + + if sel_part: + if not encrypt_part_luks_copy(target_device, key): + return False + tout.notice(f'Partition {sel_part} of {args.encrypt_disk} ' + 'encrypted with LUKS') + else: + if not encrypt_disk_luks(target_device, key): + return False + tout.notice(f'{args.encrypt_disk} encrypted with LUKS') + + return True + finally: + if loop_device: + cleanup_loop_dev(loop_device) + +def do_open_disk(args, key): + """Handle disk opening + + Args: + args (Namespace): Command line arguments + key (bytes): Encryption key material + + Returns: + bool: True on success, False on failure + """ + loop_device = None + try: + partitions = get_disk_parts(args.open_disk) + + if partitions: + tout.notice('Checking partitions for LUKS...') + loop_device = setup_loop_dev(args.open_disk) + if not loop_device: + return False + + enc_part = None + for part in partitions: + part_dev = get_part_dev(loop_device, part['number']) + time.sleep(1) + + if os.path.exists(part_dev): + try: + with tempfile.NamedTemporaryFile() as temp_f: + cmd = ['dd', f'if={part_dev}', f'of={temp_f.name}', + 'bs=16', 'count=1'] + result = run_sudo(cmd, timeout=10) + if not result.returncode: + temp_f.seek(0) + if temp_f.read(16).startswith(b'LUKS'): + enc_part = part + target_device = part_dev + tout.info(f'Found LUKS partition ' + f'{part["number"]}') + break + except (OSError, IOError) as e: + tout.info(f'Error checking partition ' + f'{part["number"]}: {e}') + + if not enc_part: + tout.error('No LUKS partitions found') + return False + + mapper_path = open_luks_disk(target_device, args.mapper_name, key) + if not mapper_path: + return False + + tout.notice(f'LUKS partition {enc_part["number"]} opened at ' + f'{mapper_path}') + else: + mapper_path = open_luks_disk(args.open_disk, args.mapper_name, key) + if not mapper_path: + return False + tout.notice(f'LUKS disk opened at {mapper_path}') + + tout.notice(f'Mount with: sudo mount {mapper_path} /mnt') + tout.notice(f'Close with: sudo cryptsetup close {args.mapper_name}') + return True + finally: + if loop_device: + tout.info(f'Loop device {loop_device} left active for LUKS access') + +def main(): + """Main function + + Returns: + int: Exit code (0 for success, 1 for failure) + """ + args = None + try: + args = parse_args() + + if args.debug: + tout.init(tout.DEBUG) + elif args.verbose: + tout.init(tout.INFO) + else: + tout.init(tout.NOTICE) + + if not validate_args(args): + return 1 + + password = get_password(args) + if not password: + return 1 + + if not detect_tkey(args.device): + tout.error('Failed to detect TKey') + return 1 + + key = derive_fde_key(password, args.device) + if not key: + tout.error('Failed to derive encryption key') + return 1 + + password = None + + if args.encrypt_disk: + if not do_encrypt_disk(args, key): + return 1 + tout.notice('Disk encryption completed successfully') + elif args.open_disk: + if not do_open_disk(args, key): + return 1 + tout.notice('Disk opening completed successfully') + else: + if not output_key(key, args): + return 1 + tout.notice('Key derivation completed successfully') + + if args.output: + if not output_key(key, args): + return 1 + return 0 + + except KeyboardInterrupt: + tout.notice('\nCancelled') + return 1 + except subprocess.TimeoutExpired: + tout.error('Operation timed out') + return 1 + except FileNotFoundError as e: + if 'tkey-sign' in str(e): + tout.error('tkey-sign not found') + if os.environ.get('SUDO_USER') and not os.geteuid(): + tout.notice('Note: tkey-sign may not be in root PATH.') + tout.notice('Try: sudo env PATH=$PATH ./scripts/tkey-fde-key.py') + elif 'cryptsetup' in str(e): + tout.error('cryptsetup not found. Install cryptsetup.') + elif 'truncate' in str(e): + tout.error('truncate not found') + elif 'dmsetup' in str(e): + tout.error('dmsetup not found') + else: + tout.error(f'File not found - {e}') + return 1 + except PermissionError as e: + tout.error(f'Permission denied - {e}') + tout.notice('Note: Disk operations may require root privileges') + return 1 + except OSError as e: + tout.error(f'OS/IO operation failed - {e}') + return 1 + except Exception as e: # pylint: disable=broad-exception-caught + if args and args.debug: + import traceback # pylint: disable=import-outside-toplevel + tout.error('Full traceback:') + traceback.print_exc() + else: + tout.error(f'Unexpected error: {e}') + return 1 + + +if __name__ == '__main__': + sys.exit(main()) From patchwork Mon Dec 8 12:39:56 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Simon Glass X-Patchwork-Id: 858 Return-Path: X-Original-To: u-boot-concept@u-boot.org Delivered-To: u-boot-concept@u-boot.org DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197644; bh=CsEVhXpIJLl9fgE+G4TjtlRn5q/JpS4su8OSN54Gpfc=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=WWvmOVLqZmqVrsJ4MYmVlmw7f1v8JyTGjX3yv3QRknEeePwlkFAfuACQCb1slZqoI 44qiQIEBkgJXbLuVmFuMYad8tNQ8zeI8hRLg5A6PXJeLfQqd7JEtuqbw5LxU1hO70g ZZHHY1kyRC2KJRaBW41fxn92K7c2vdlzdDbyYQnYJlqgKqzgJhgDbB9s9FawDsJdQq B5kUYb8NqheKFqqKaHH6f/zzpVBTgtDVu9TslY8p5w+lLVRBku+iFfn/5JkvfxLLup Sr/oAfnOEBVo1wBmqwowFW+wLGxUU1kffqYk3+W+t2DoZhnUl3K/gUGHFyEJ9x/llK 9jHS5TJIOVH9w== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 7242B68A02 for ; Mon, 8 Dec 2025 05:40:44 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10024) with ESMTP id skLkzU2qyopC for ; Mon, 8 Dec 2025 05:40:44 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197644; bh=CsEVhXpIJLl9fgE+G4TjtlRn5q/JpS4su8OSN54Gpfc=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=WWvmOVLqZmqVrsJ4MYmVlmw7f1v8JyTGjX3yv3QRknEeePwlkFAfuACQCb1slZqoI 44qiQIEBkgJXbLuVmFuMYad8tNQ8zeI8hRLg5A6PXJeLfQqd7JEtuqbw5LxU1hO70g ZZHHY1kyRC2KJRaBW41fxn92K7c2vdlzdDbyYQnYJlqgKqzgJhgDbB9s9FawDsJdQq B5kUYb8NqheKFqqKaHH6f/zzpVBTgtDVu9TslY8p5w+lLVRBku+iFfn/5JkvfxLLup Sr/oAfnOEBVo1wBmqwowFW+wLGxUU1kffqYk3+W+t2DoZhnUl3K/gUGHFyEJ9x/llK 9jHS5TJIOVH9w== Received: from mail.u-boot.org (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 606656895C for ; Mon, 8 Dec 2025 05:40:44 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197642; bh=ByzpF1ODGKEBWJsvhZ/31FP0oW6Uyyzj6ZH82Zr6osM=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=CVIugKJCVc6FkZ41Uf1YQu5zlvlNNSoMvnxYaO0RU6zEivaW/ODG4s+65VajnO6WI S7GScF3KV6s0O8HwELKEzvodL8U/j0QM6xcUlj64/O2aqgSatH4TB9Wx9aY412KWRx seO2cnmdU58iOWB+qp1vugWsgyFqKbzIBUUTnvupxbclnkScHUlZeG2XsbenPIl8zC oVIGHoHCxlgshCkLcFYRDTcmHFAdi5/whSDGQTvjdYhb9An1H8MkadjYLQaJ27FVET YPSOqtWfIrQr9xhvyQ79xJmsV5HHp5VXR05WLrcWSYxdlPReCS6kfdcXRCM6wfziyl L4apOAMBAhQ4g== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 416FD6895C; Mon, 8 Dec 2025 05:40:42 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10026) with ESMTP id ukvZ3crEb2iZ; Mon, 8 Dec 2025 05:40:42 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765197637; bh=Tfj8flALmAmGp3DiEd/pF6ccxLicz5QKnp83mAcyQtg=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=SoJqb+rPDl0fUK+GmTMw+47M4jQd05fj9Gy2OmQKBLTWJQ8pYctfvxxXnahyNIvAo s6ygyoy1kYQJ54Hjhb4B2B/s85ropXP9ak+vgT/8dmuLG0D+lFWiIDjhkZVAk2Ux2+ cH6NY4RBZN1qITSWT/J+9oES7TwCc4P7VYI6BqKXju2B6h5oTb8uDvmeDeYwI24hD1 tTeSyIiQ8wTjvDWJUSy1509IQytA8281GS8jIx89T90oRYeLJ/9VIMGAHtdYg4RKXN wcW64luiK10qFoYlX7swna9zfXiZJ70EidK+L3o3FO9mnieMwz0OCNvTj1RCVibWRp QeaXQxYhG0VSQ== Received: from u-boot.org (unknown [73.34.74.121]) by mail.u-boot.org (Postfix) with ESMTPSA id 99F2B688C1; Mon, 8 Dec 2025 05:40:37 -0700 (MST) From: Simon Glass To: U-Boot Concept Date: Mon, 8 Dec 2025 05:39:56 -0700 Message-ID: <20251208124001.775057-8-sjg@u-boot.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20251208124001.775057-1-sjg@u-boot.org> References: <20251208124001.775057-1-sjg@u-boot.org> MIME-Version: 1.0 Message-ID-Hash: AXULJLE4OQTNHY2IRL3CTFF33VZQZ24L X-Message-ID-Hash: AXULJLE4OQTNHY2IRL3CTFF33VZQZ24L X-MailFrom: sjg@u-boot.org X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Simon Glass , Claude X-Mailman-Version: 3.3.10 Precedence: list Subject: [Concept] [PATCH 7/7] doc: Add TKey full disk encryption documentation List-Id: Discussion and patches related to U-Boot Concept Archived-At: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: From: Simon Glass Add comprehensive documentation for TKey-based full disk encryption: - doc/usage/tkey-fde.rst: Main documentation covering the TKey FDE workflow, key derivation process, U-Boot integration, the tkey-fde-key.py script usage, creating test images, troubleshooting, and security considerations - Add cross-references from related documentation pages including blkmap, bootflow, luks, and tkey command references - Update luks.rst with a section on hardware-backed key derivation Co-developed-by: Claude Signed-off-by: Simon Glass --- doc/usage/blkmap.rst | 24 ++ doc/usage/cmd/blkmap.rst | 2 + doc/usage/cmd/bootflow.rst | 25 ++ doc/usage/cmd/luks.rst | 2 + doc/usage/cmd/tkey.rst | 2 + doc/usage/index.rst | 1 + doc/usage/luks.rst | 19 +- doc/usage/tkey-fde.rst | 585 +++++++++++++++++++++++++++++++++++++ 8 files changed, 656 insertions(+), 4 deletions(-) create mode 100644 doc/usage/tkey-fde.rst diff --git a/doc/usage/blkmap.rst b/doc/usage/blkmap.rst index e9b3cfeaaa1..b6d3fae7aff 100644 --- a/doc/usage/blkmap.rst +++ b/doc/usage/blkmap.rst @@ -110,7 +110,31 @@ Now we can access the filesystem: blkmap get sq dev devnum load blkmap ${devnum} ${loadaddr} /etc/version + +Example: Accessing LUKS-encrypted partitions +--------------------------------------------- + +When LUKS-encrypted partitions are unlocked, U-Boot automatically creates +blkmap devices that provide on-the-fly decryption. This allows transparent +access to encrypted filesystems. + +For example, after unlocking a LUKS partition:: + + luks unlock mmc 0:2 mypassword + +A blkmap device is created (e.g., ``blkmap 0``) that can be used like any +other block device:: + + ls blkmap 0 / + load blkmap 0 ${kernel_addr_r} /boot/vmlinuz + +For more information on LUKS encryption, including hardware-backed key +derivation with TKey devices, see :doc:`luks` and :doc:`tkey-fde`. + + See also -------- * :doc:`/usage/cmd/blkmap` +* :doc:`luks` - LUKS encryption support +* :doc:`tkey-fde` - TKey full disk encryption diff --git a/doc/usage/cmd/blkmap.rst b/doc/usage/cmd/blkmap.rst index aaa4cd403c0..564898abd6c 100644 --- a/doc/usage/cmd/blkmap.rst +++ b/doc/usage/cmd/blkmap.rst @@ -321,3 +321,5 @@ See Also -------- * :doc:`../blkmap` - Blkmap device documentation and examples +* :doc:`../luks` - LUKS encryption support (uses blkmap for decrypted access) +* :doc:`../tkey-fde` - TKey full disk encryption diff --git a/doc/usage/cmd/bootflow.rst b/doc/usage/cmd/bootflow.rst index 938e5c79903..4082aa3c198 100644 --- a/doc/usage/cmd/bootflow.rst +++ b/doc/usage/cmd/bootflow.rst @@ -751,4 +751,29 @@ else 1. For other subcommands, the return value $? is always 0 (true). +Encrypted Partitions +-------------------- + +Bootflow scanning automatically detects and handles LUKS-encrypted partitions. +When an encrypted partition is detected during scanning, U-Boot will: + +1. Detect the LUKS encryption and display an indicator in the bootflow list +2. Prompt the user for a password when attempting to boot +3. Attempt to unlock the partition using the provided password +4. If a TKey device is present, use hardware-backed key derivation for enhanced + security + +For detailed information on LUKS encryption and TKey-based full disk encryption, +see :doc:`../luks` and :doc:`../tkey-fde`. + + +See also +-------- + +* :doc:`../luks` - LUKS encryption support +* :doc:`../tkey-fde` - TKey full disk encryption +* :doc:`bootdev` - Boot device documentation +* :doc:`/develop/bootstd/index` - Standard boot documentation + + .. BootflowStates_: diff --git a/doc/usage/cmd/luks.rst b/doc/usage/cmd/luks.rst index 1a9cba875ce..15a87ea12cd 100644 --- a/doc/usage/cmd/luks.rst +++ b/doc/usage/cmd/luks.rst @@ -307,6 +307,8 @@ See also -------- * :doc:`../luks` - Comprehensive LUKS feature documentation +* :doc:`../tkey-fde` - TKey full disk encryption with hardware-backed keys * :doc:`blkmap` - Blkmap device documentation +* :doc:`tkey` - TKey command reference * cryptsetup project: https://gitlab.com/cryptsetup/cryptsetup * LUKS on-disk format specifications: https://gitlab.com/cryptsetup/cryptsetup/-/wikis/home diff --git a/doc/usage/cmd/tkey.rst b/doc/usage/cmd/tkey.rst index b7d138f7307..56da02f4b5c 100644 --- a/doc/usage/cmd/tkey.rst +++ b/doc/usage/cmd/tkey.rst @@ -260,4 +260,6 @@ requires a TKey driver to be configured (USB or serial). See also -------- +* :doc:`../tkey-fde` - TKey full disk encryption guide +* :doc:`luks` - LUKS command reference * `Tillitis TKey documentation `_ diff --git a/doc/usage/index.rst b/doc/usage/index.rst index 8913c0a4f9b..bee7884a066 100644 --- a/doc/usage/index.rst +++ b/doc/usage/index.rst @@ -12,6 +12,7 @@ Use U-Boot fdt_overlays fit/index luks + tkey-fde netconsole partitions cmdline diff --git a/doc/usage/luks.rst b/doc/usage/luks.rst index db8558ffb56..36d66081dc1 100644 --- a/doc/usage/luks.rst +++ b/doc/usage/luks.rst @@ -422,8 +422,7 @@ See ``test/py/tests/fs_helper.py`` for the ``FsHelper`` class: # Create LUKS2 encrypted filesystem with Argon2id with FsHelper(config, 'ext4', 30, 'test', - part_mb=60, - encrypt_passphrase='mypassword', + part_mb=60, passphrase='mypassword', luks_kdf='argon2id') as fsh: # Add files to fsh.srcdir with open(os.path.join(fsh.srcdir, 'hello.txt'), 'w') as f: @@ -434,8 +433,7 @@ See ``test/py/tests/fs_helper.py`` for the ``FsHelper`` class: # Create LUKS1 encrypted filesystem with FsHelper(config, 'ext4', 30, 'test', - part_mb=60, - encrypt_passphrase='mypassword', + part_mb=60, passphrase='mypassword', luks_version=1) as fsh: # Add files to fsh.srcdir with open(os.path.join(fsh.srcdir, 'hello.txt'), 'w') as f: @@ -472,6 +470,16 @@ CONFIG_SHA512 CONFIG_ARGON2 Enable Argon2 key derivation support (optional, for modern LUKS2) +Hardware-Backed Key Derivation +------------------------------- + +For enhanced security, LUKS partitions can be unlocked using hardware security +tokens like the Tillitis TKey. Instead of relying solely on a password, the +encryption key is derived from both the password and the device's unique +hardware identifier. + +See :doc:`tkey-fde` for complete details on using TKey for full disk encryption. + Limitations ----------- @@ -551,8 +559,11 @@ See ``test/boot/luks.c`` for tests: See Also -------- +* :doc:`tkey-fde` - TKey full disk encryption documentation * :doc:`cmd/luks` - LUKS command reference * :doc:`cmd/blkmap` - Blkmap command reference +* :doc:`cmd/tkey` - TKey command reference * :doc:`blkmap` - Blkmap device documentation * ``test/py/tests/fs_helper.py`` - Filesystem helper for creating test images +* ``scripts/tkey_fde_key.py`` - TKey key derivation tool * Linux ``cryptsetup`` documentation for LUKS disk format specification diff --git a/doc/usage/tkey-fde.rst b/doc/usage/tkey-fde.rst new file mode 100644 index 00000000000..3452dc79540 --- /dev/null +++ b/doc/usage/tkey-fde.rst @@ -0,0 +1,585 @@ +.. SPDX-License-Identifier: GPL-2.0+ + +TKey Full Disk Encryption +========================== + +Overview +-------- + +U-Boot supports using `Tillitis TKey `_ hardware-security +tokens to unlock LUKS-encrypted partitions. This provides hardware-backed +full-disk encryption (FDE) where the encryption key is derived from two pieces +of information: + +* A user password/passphrase (USS - User Supplied Secret) +* The TKey's internal Unique Device Identifier (UDI) + +The same password on the same TKey always produces the same encryption key, +making it suitable for unlocking encrypted root filesystems at boot time. + +Note: Despite its name, FDE generally refers to the encryption of a single +partition on a disk, rather than an entire disk. + +**Key Features:** + +* Hardware-backed key derivation using TKey security token +* Compatible with standard LUKS1 and LUKS2 encrypted partitions +* Automatic unlock during boot flow detection +* Test infrastructure for creating encrypted disk images +* Python tools for key generation and disk encryption + +How It Works +------------ + +TKey Key Derivation Process +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The TKey derives encryption keys through this process: + +1. **Load Signer App**: The TKey firmware loads the signer application with + your password (USS) +2. **Generate Key Pair**: The signer app combines the USS with the device's UDI + to generate an Ed25519 key pair +3. **Derive Disk Key**: The public key is hashed with SHA-256 to produce a + 32-byte encryption key +4. **Decrypt Partition**: The key is used to unlock the LUKS-encrypted + partition + +This means: + +* The same password always produces the same key (deterministic) +* Different passwords produce completely different keys +* The physical TKey device is required (UDI is device-specific) +* No key material is stored on disk - only derived when needed + + +U-Boot Integration +~~~~~~~~~~~~~~~~~~ + +When U-Boot detects a LUKS-encrypted partition during bootflow booting: + +1. Prompts the user for their password +2. Detects if a TKey device is present +3. Loads the TKey signer app with the password +4. Derives the encryption key from the TKey +5. Attempts to unlock the LUKS partition +6. Creates a blkmap device for accessing decrypted data +7. Continues with normal boot process + + +Tools and Workflow +------------------ + +The ``scripts/tkey_fde_key.py`` Script +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This Python script handles TKey key derivation and disk encryption operations. + +**Basic Usage - Generate a key:** + +:: + + # Generate key interactively (prompts for password) + $ ./scripts/tkey_fde_key.py + + # Generate key from password file + $ echo "mypassword" > passfile + $ ./scripts/tkey_fde_key.py -p passfile -o diskkey.bin --binary + + # Generate key from stdin + $ echo "mypassword" | ./scripts/tkey_fde_key.py -p - --binary + +**Encrypting a Disk Image:** + +:: + + # Create a disk image + $ dd if=/dev/zero of=rootfs.img bs=1M count=1000 + + # Encrypt the entire disk with LUKS + $ ./scripts/tkey_fde_key.py -e rootfs.img -p passfile + + # Encrypt a specific partition + $ ./scripts/tkey_fde_key.py -e disk.img -P 2 -p passfile + +**Opening an Encrypted Disk:** + +:: + + # Open encrypted disk (creates /dev/mapper/tkey-disk) + $ ./scripts/tkey_fde_key.py -O rootfs.img -p passfile + + # Mount the decrypted filesystem + $ sudo mount /dev/mapper/tkey-disk /mnt + + # When done, unmount and close + $ sudo umount /mnt + $ sudo cryptsetup close tkey-disk + +**Advanced Options:** + +:: + + # Save both encrypted disk and backup key file + $ ./scripts/tkey_fde_key.py -e disk.img -p passfile -o backup.key + + # Use verbose output to see what's happening + $ ./scripts/tkey_fde_key.py -e disk.img -p passfile --verbose + + # Use debug mode for troubleshooting + $ ./scripts/tkey_fde_key.py --debug -e disk.img -p passfile + + +Creating Test Images +-------------------- + +Test Disk Images +~~~~~~~~~~~~~~~~ + +The U-Boot test infrastructure creates several LUKS-encrypted test images: + +* ``mmc11.img`` - LUKS1 encrypted Ubuntu image +* ``mmc12.img`` - LUKS2 encrypted Ubuntu image with Argon2id KDF +* ``mmc13.img`` - LUKS2 encrypted Ubuntu image for TKey testing +* ``mmc14.img`` - LUKS2 encrypted image with pre-derived master key + +By default, ``mmc13.img`` is encrypted with a key derived from the TKey +emulator's deterministic public key. This allows testing without physical +hardware. + +Using override.bin for Physical TKey Testing +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To test with a physical TKey device instead of the emulator, create an +``override.bin`` file containing the TKey-derived disk key: + +:: + + # Generate override.bin from your physical TKey with password "test" + $ echo "test" | ./scripts/tkey_fde_key.py -p - -o override.bin --binary + + # Regenerate test images with your TKey's key + $ ./test/py/test.py -B sandbox --build-dir /tmp/b/sandbox \ + --build -k test_ut_dm_init_bootstd + + # Test unlocking mmc13 with your physical TKey + $ /tmp/b/sandbox/u-boot -T -c \ + "tkey connect sandbox_tkey; sb devon mmc13; luks unlock -t mmc d:2 test" + +When ``override.bin`` exists in the source directory, the test infrastructure +uses it instead of the emulator's key to encrypt ``mmc13.img``. This allows +you to test the full TKey unlock flow with real hardware. + +To switch back to emulator testing, simply remove the override file: + +:: + + $ rm override.bin + $ ./test/py/test.py -B sandbox --build-dir /tmp/b/sandbox \ + --build -k test_ut_dm_init_bootstd + +Using the Python Test Infrastructure +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The test infrastructure in ``test/py/tests/test_ut.py`` handles TKey key +generation automatically: + +:: + + def test_ut_dm_init_bootstd(u_boot_config, u_boot_log): + """Initialize data for bootflow tests with TKey encryption""" + + # Check for override key file (for physical TKey testing) + override_keyfile = os.path.join(u_boot_config.source_dir, 'override.bin') + if os.path.exists(override_keyfile): + keyfile = override_keyfile + u_boot_log.action(f'Using override TKey key: {keyfile}') + else: + # Generate key matching TKey emulator's deterministic pubkey + pubkey = bytes([0x50 + (i & 0xf) for i in range(32)]) + disk_key = hashlib.sha256(pubkey.hex().encode()).digest() + keyfile = os.path.join(u_boot_config.persistent_data_dir, + 'tkey_emul.key') + with open(keyfile, 'wb') as f: + f.write(disk_key) + + # Create LUKS2 encrypted image for TKey testing + setup_ubuntu_image(u_boot_config, u_boot_log, 13, 'mmc', + use_fde=2, luks_kdf='argon2id', + encrypt_keyfile=keyfile) + +**Helper Class Usage:** + +See ``test/py/tests/fs_helper.py`` for the ``FsHelper`` class: + +:: + + from fs_helper import FsHelper, DiskHelper + + # Create LUKS2 encrypted filesystem with TKey key file + with FsHelper(config, 'ext4', 30, 'test', + part_mb=60, + encrypt_keyfile='/path/to/tkey-derived-key.bin') as fsh: + fsh.setup() + # Add files to fsh.srcdir + with open(os.path.join(fsh.srcdir, 'hello.txt'), 'w') as f: + f.write('Hello from TKey FDE!\n') + + # Create encrypted filesystem + fsh.mk_fs() + +Step-by-Step Workflow +---------------------- + +Complete Example: Testing TKey FDE with mmc13 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**1. Create Test Disk Images** + +Run the test infrastructure to create encrypted images: + +:: + + $ ./test/py/test.py -B sandbox --build-dir /tmp/b/sandbox \ + --build -k test_ut_dm_init_bootstd + +This creates ``mmc13.img`` (LUKS2 with Argon2id) encrypted with the TKey +emulator's key. + +**2. Test Unlocking with TKey Emulator** + +Run U-Boot sandbox and test the unlock process with the emulator: + +:: + + $ /tmp/b/sandbox/u-boot -T -c \ + "tkey connect tkey-emul; sb devon mmc13; luks unlock -t mmc d:2 test" + +**Expected Output:** + +:: + + Connected to TKey device + Device 'mmc13' enabled + Unlocking LUKS2 partition... + Using TKey for disk encryption key + Loading TKey signer app (6d78 bytes) with USS... + TKey public key: 50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f + 50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f + TKey disk key derived successfully + TKey derived disk key: e9 b0 59 92 68 ff 8b 08 3e f8 0d bd 04 be 20 7c + e9 a1 9a 60 a8 88 cc b3 fe 93 71 0a 0a 70 a3 4e + Unlocked LUKS partition as blkmap device 'luks-mmc-d:2' + +**3. Test with Physical TKey** + +To test with a real TKey device: + +:: + + # Generate override.bin from your physical TKey with password "test" + $ echo "test" | ./scripts/tkey_fde_key.py -p - -o override.bin --binary -f -v + Reading password from stdin... + Password length: 4, repr: 'test' + TKey detected via USB enumeration + ... + Ed25519 public key (hex): df4faa680d9fd79079cc572c1f84fb3fa59ab904dad652e90a22e5b672a67eb1 + Derived disk key (hex): 1546bdaf99e9ed9867d83ae69062c9da3202a617584a35ee4ae38672ec775a7f + Key saved to override.bin + + # Regenerate mmc13.img with your TKey's key + $ ./test/py/test.py -B sandbox --build-dir /tmp/b/sandbox \ + --build -k test_ut_dm_init_bootstd + + # Test unlocking with physical TKey (ensure TKey is plugged in) + $ /tmp/b/sandbox/u-boot -T -c "sb devon mmc13; luks unlock -t mmc d:2 test" -P + +**Expected Output with Physical TKey:** + +:: + + Device 'mmc13' enabled + Unlocking LUKS2 partition... + Using TKey for disk encryption key + Loading TKey signer app (6d78 bytes) with USS... + TKey public key: df 4f aa 68 0d 9f d7 90 79 cc 57 2c 1f 84 fb 3f + a5 9a b9 04 da d6 52 e9 0a 22 e5 b6 72 a6 7e b1 + TKey disk key derived successfully + TKey derived disk key: 15 46 bd af 99 e9 ed 98 67 d8 3a e6 90 62 c9 da + 32 02 a6 17 58 4a 35 ee 4a e3 86 72 ec 77 5a 7f + Unlocked LUKS partition as blkmap device 'luks-mmc-d:2' + +**4. Verify Encryption** + +You can verify the disk is encrypted by checking with cryptsetup: + +:: + + # Extract partition 2 from the disk image + $ dd if=mmc13.img bs=512 skip=38912 count=122880 of=mmc13_part2.img + + # Check LUKS header + $ cryptsetup luksDump mmc13_part2.img + LUKS header information + Version: 2 + ... + + # Test unlock with the TKey emulator key (when no override.bin exists) + # First generate the emulator's key + $ python3 -c " + import hashlib + pubkey = bytes([0x50 + (i & 0xf) for i in range(32)]) + key = hashlib.sha256(pubkey.hex().encode()).digest() + open('emul.key', 'wb').write(key) + " + $ sudo cryptsetup open mmc13_part2.img test-luks --key-file=emul.key + $ ls /dev/mapper/test-luks + /dev/mapper/test-luks + $ sudo cryptsetup close test-luks + +Troubleshooting +--------------- + +Key Mismatch Errors +~~~~~~~~~~~~~~~~~~~ + +**Problem:** U-Boot shows "Failed to unlock LUKS partition" + +:: + + LUKS1: Keyslot 0 failed with error -13 + Failed to unlock LUKS1 with binary passphrase (err=-2) + Failed to unlock LUKS partition (err=-13: Permission denied) + +**Cause:** The disk was encrypted with a different key than U-Boot is deriving. + +**Solutions:** + +1. **Verify you're using the correct password:** Make sure you enter the same + password in U-Boot that was used to generate the key file. + +2. **Regenerate the key and verify it matches:** + + :: + + # Generate key again with same password + $ ./scripts/tkey_fde_key.py -p mykey.txt -o test-key.bin --binary + + # Compare with original + $ diff mykey test-key.bin + + # If different, TKey may have been in different state + +3. **Recreate disk images with the correct key:** If U-Boot consistently + derives a different key than what was used for encryption, capture the key + U-Boot derives and use that to encrypt the disk: + + :: + + # Run U-Boot and note the "Binary pass" hex values from debug output + # Then recreate the key file with those values + $ python3 << 'EOF' + key_hex = "10f1132e6c27e6e8...d29b6b8a" # from U-Boot output + with open('mykey', 'wb') as f: + f.write(bytes.fromhex(key_hex)) + EOF + + # Recreate disk images with this key + $ rm mmc11.img mmc12.img + $ ./test/py/test.py -B sandbox --build-dir /tmp/b/sandbox \ + -k test_ut_dm_init_bootstd + +TKey Not Detected +~~~~~~~~~~~~~~~~~ + +**Problem:** U-Boot doesn't detect the TKey device + +**Solutions:** + +* Ensure TKey is plugged in before starting U-Boot +* Check USB device is accessible: ``ls /dev/ttyACM*`` +* Try replugging the TKey device +* For sandbox, ensure ``sandbox,device-path`` is set correctly in device tree + +TKey App Already Loaded +~~~~~~~~~~~~~~~~~~~~~~~~ + +**Problem:** TKey is in app mode instead of firmware mode + +**Solution:** Remove and reinsert the TKey. The device must be in firmware mode +to load the signer app. + +Configuration +------------- + +Kconfig Options +~~~~~~~~~~~~~~~ + +To enable TKey FDE support in U-Boot: + +:: + + CONFIG_CMD_TKEY=y # TKey command + CONFIG_TKEY_DRIVER=y # TKey device driver + CONFIG_CMD_LUKS=y # LUKS command + CONFIG_BLK_LUKS=y # LUKS block device support + CONFIG_BLKMAP=y # Block device mapping + CONFIG_BOOTCTL=y # Boot control with unlock support + CONFIG_BOOTCTL_LOGIC=y # Boot control unlock logic + CONFIG_ARGON2=y # For LUKS2 Argon2id support (optional) + +Device Tree Configuration +~~~~~~~~~~~~~~~~~~~~~~~~~ + +For sandbox testing, the TKey is configured in ``arch/sandbox/dts/test.dts``: + +:: + + tkey { + compatible = "sandbox,tkey"; + sandbox,device-path = "/dev/ttyACM0"; + }; + +For real hardware, configure the USB serial device path appropriately. + +Security Considerations +----------------------- + +Key Storage +~~~~~~~~~~~ + +* The TKey-derived key should **never be stored permanently** on disk +* Only temporary key files (like ``mykey``) used during testing should exist +* In production, keys should be derived fresh each boot from TKey + password +* The ``mykey`` file is for **testing only** and should be kept secure + +Password Security +~~~~~~~~~~~~~~~~~ + +* Use a strong password (at least 16 characters recommended) +* Different passwords produce completely different encryption keys +* The TKey's UDI adds additional entropy to the key derivation +* Consider using a hardware security token for additional protection + +Hardware Security +~~~~~~~~~~~~~~~~~ + +* Physical access to the TKey is required to derive keys +* The TKey's UDI is unique per device - keys cannot be derived without it +* If the TKey is lost, encrypted data cannot be recovered +* Consider keeping a backup TKey or traditional key recovery mechanism + +Memory Security +~~~~~~~~~~~~~~~ + +* Keys are held in memory while the device is unlocked +* Memory is not securely erased on warm reboot +* This is acceptable for boot-time use but not for long-term key storage + + +Comparison with Traditional LUKS +--------------------------------- + +**Traditional LUKS (Password Only):** + +* Encryption key derived only from password +* Vulnerable to offline password cracking attacks +* No hardware requirement - same password works anywhere + +**TKey-Enhanced LUKS:** + +* Encryption key derived from password + TKey UDI +* Requires physical TKey device to derive key +* Resistant to offline password cracking (attacker needs both password and + TKey) +* Same password + same TKey always produces same key +* Different TKey devices produce different keys even with same password + +Example Use Case: Secure Boot +------------------------------ + +A typical secure boot workflow with TKey FDE: + +1. **System Powers On** + + * U-Boot starts and scans for boot devices + * Finds LUKS-encrypted root partition + +2. **User Authentication** + + * U-Boot prompts user for password + * User inserts TKey device + * User enters password + +3. **Key Derivation** + + * U-Boot loads TKey signer app with password + * TKey derives encryption key from password + UDI + * Returns 32-byte encryption key to U-Boot + +4. **Partition Unlock** + + * U-Boot attempts to unlock LUKS partition + * If successful, creates blkmap device + * Encrypted data is accessible as standard block device + +5. **Boot Continues** + + * U-Boot loads kernel from unlocked partition + * System boots into encrypted root filesystem + +Testing +------- + +Unit Tests +~~~~~~~~~~ + +Run the bootctl TKey unlock tests: + +:: + + $ ./test/py/test.py -B sandbox --build-dir /tmp/b/sandbox \ + -k bootctl_logic_tkey + + +Manual Testing +~~~~~~~~~~~~~~ + +Test mmc13 with the TKey emulator: + +:: + + # 1. Build sandbox and create test images + $ crosfw sandbox -L + $ ./test/py/test.py -B sandbox --build-dir /tmp/b/sandbox \ + --build -k test_ut_dm_init_bootstd + + # 2. Test unlocking mmc13 with TKey emulator + $ /tmp/b/sandbox/u-boot -T -c \ + "tkey connect tkey-emul; sb devon mmc13; luks unlock -t mmc d:2 test" + +Test mmc13 with a physical TKey: + +:: + + # 1. Generate override.bin with your TKey and password "test" + $ echo "test" | ./scripts/tkey_fde_key.py -p - -o override.bin --binary + + # 2. Regenerate test images + $ ./test/py/test.py -B sandbox --build-dir /tmp/b/sandbox \ + --build -k test_ut_dm_init_bootstd + + # 3. Test unlocking with physical TKey + $ /tmp/b/sandbox/u-boot -T -c \ + "tkey connect sandbox_tkey; sb devon mmc13; luks unlock -t mmc d:2 test" + +See Also +-------- + +* :doc:`luks` - LUKS encryption documentation +* :doc:`cmd/luks` - LUKS command reference +* :doc:`cmd/tkey` - TKey command reference +* :doc:`cmd/blkmap` - Blkmap device mapping +* ``scripts/tkey_fde_key.py`` - TKey key derivation tool +* ``test/py/tests/fs_helper.py`` - Filesystem test helpers +* `Tillitis TKey Documentation `_