test: fs_helper: Support LUKS keyfile and master key

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>
This commit is contained in:
Simon Glass
2025-12-07 13:12:43 -07:00
parent a23e4407d5
commit daa53f61a9
3 changed files with 70 additions and 21 deletions

View File

@@ -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')

View File

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

View File

@@ -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