[Concept,2/7] test: fs_helper: Support LUKS keyfile and master key

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

Commit Message

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

Add 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 <noreply@anthropic.com>
Signed-off-by: Simon Glass <simon.glass@canonical.com>
---

 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(-)
  

Patch

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