Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 200 additions & 0 deletions lisa/microsoft/testsuites/security/fscrypt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

from typing import List

from lisa import (
Logger,
Node,
TestCaseMetadata,
TestSuite,
TestSuiteMetadata,
schema,
search_space,
simple_requirement,
)
from lisa.features import Disk
from lisa.operating_system import BSD, Windows
from lisa.tools import Mount
from lisa.util import SkippedException

# A fixed 16-byte salt (32 hex chars) and passphrase. Because the fscrypt key
# descriptor is derived deterministically from salt + passphrase, re-adding the
# same key later yields the same descriptor and transparently unlocks the data.
_FSCRYPT_SALT = "0x00112233445566778899aabbccddeeff"
_FSCRYPT_PASSPHRASE = "lisa-fscrypt-pass"
_PLAINTEXT_TOKEN = "lisa-fscrypt-topsecret"


@TestSuiteMetadata(
area="security",
category="functional",
description="""
Validates native (kernel) filesystem encryption — fscrypt — end to end.

Each variation formats a data disk with encryption support, applies an
encryption policy to a directory, writes a known plaintext, then proves:
1. Data is readable while the key is present in the keyring.
2. On a fresh mount without the key, filenames are ciphertext and the
plaintext is inaccessible (directory is "locked").
3. Re-adding the identical key transparently unlocks the data.
""",
)
class FscryptSuite(TestSuite):
@TestCaseMetadata(
description="""
fscrypt lifecycle on ext4 using e4crypt (from e2fsprogs).
""",
priority=2,
requirement=simple_requirement(
disk=schema.DiskOptionSettings(
data_disk_count=search_space.IntRange(min=1),
),
unsupported_os=[Windows, BSD],
),
)
def verify_fscrypt_ext4(self, log: Logger, node: Node) -> None:
self._run_fscrypt_lifecycle(
log,
node,
fs_label="ext4",
mkfs_command="mkfs.ext4 -F -O encrypt",
crypt_tool="e4crypt",
packages=["e2fsprogs", "keyutils"],
)

@TestCaseMetadata(
description="""
fscrypt lifecycle on f2fs using f2fscrypt (from f2fs-tools).
""",
priority=3,
requirement=simple_requirement(
disk=schema.DiskOptionSettings(
data_disk_count=search_space.IntRange(min=1),
),
unsupported_os=[Windows, BSD],
),
)
def verify_fscrypt_f2fs(self, log: Logger, node: Node) -> None:
self._run_fscrypt_lifecycle(
log,
node,
fs_label="f2fs",
mkfs_command="mkfs.f2fs -f -O extra_attr,encrypt",
crypt_tool="f2fscrypt",
packages=["f2fs-tools", "keyutils"],
)

def _run_fscrypt_lifecycle(
self,
log: Logger,
node: Node,
fs_label: str,
mkfs_command: str,
crypt_tool: str,
packages: List[str],
) -> None:
mount_point = "/mnt/fscrypt"
secret_dir = f"{mount_point}/secret"
Comment on lines +97 to +98

# --- Preconditions ------------------------------------------------
for package in packages:
node.os.install_packages(package)

if node.execute(f"command -v {crypt_tool}", shell=True).exit_code != 0:
raise SkippedException(f"{crypt_tool} is not available on this image")

if (
node.execute(
"grep -q '^CONFIG_FS_ENCRYPTION=y' /boot/config-$(uname -r)",
sudo=True,
shell=True,
).exit_code
!= 0
):
raise SkippedException("kernel is not built with CONFIG_FS_ENCRYPTION=y")
Comment on lines +108 to +116
Comment on lines +108 to +116

data_disk = node.features[Disk].get_raw_data_disks()[0]
log.info(f"using data disk {data_disk} for {fs_label} fscrypt test")

mount = node.tools[Mount]
# Ensure a clean slate in case a previous run left it mounted.
mount.umount(data_disk, mount_point, erase=False)

# --- Format + mount + apply policy + write (single keyring session) ---
# add_key stores the key in the *calling process'* session keyring, so
# the key-add, policy-set and write must run in one shell invocation.
node.execute(
f"{mkfs_command} {data_disk}",
sudo=True,
shell=True,
expected_exit_code=0,
expected_exit_code_failure_message="failed to format disk",
)
mount.mount(data_disk, mount_point)

add_and_write = (
f"mkdir -p {secret_dir} "
f"&& DESC=$(printf '{_FSCRYPT_PASSPHRASE}\\n' "
f"| {crypt_tool} add_key -S {_FSCRYPT_SALT} "
f"| grep -oE '[0-9a-f]{{16}}' | head -1) "
f'&& test -n "$DESC" '
f"&& {crypt_tool} set_policy $DESC {secret_dir} "
f"&& echo {_PLAINTEXT_TOKEN} > {secret_dir}/plain.txt "
f"&& grep -q {_PLAINTEXT_TOKEN} {secret_dir}/plain.txt"
)
node.execute(
add_and_write,
sudo=True,
shell=True,
expected_exit_code=0,
expected_exit_code_failure_message=(
"failed to add key, set policy, or read back plaintext with key present"
),
)
log.info("plaintext readable while encryption key is present")

# --- Lock: fresh mount without the key ---------------------------
mount.umount(data_disk, mount_point, erase=False)
mount.mount(data_disk, mount_point)

listing = node.execute(f"ls {secret_dir}", sudo=True, shell=True).stdout
if "plain.txt" in listing:
raise AssertionError(
"filename 'plain.txt' is visible in cleartext without the key — "
"encryption policy is not in effect"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run against image canonical 0001-com-ubuntu-server-jammy 22_04-lts-gen2 22.04.202607110 failed with below exception


2026-07-15 03:41:30.081[4376][DEBUG] lisa.env[generated_0].node[0].cmd[9849].stdout /dev/sdc
2026-07-15 03:41:30.098[3520][DEBUG] lisa.env[generated_0].node[0].cmd[9849] execution time: 0.144 sec, exit code: 0
2026-07-15 03:41:30.098[3520][INFO] lisa.case[verify_fscrypt_ext4][lisa_0_0] using data disk /dev/sdc for ext4 fscrypt test
2026-07-15 03:41:30.099[3520][DEBUG] lisa.env[generated_0].node[0].cmd[2480] cmd: ['sudo', 'sh', '-c', 'umount  /mnt/fscrypt'], cwd: None, shell: True, sudo: True, nohup: False, posix: True, remote: True, encoding: utf-8
2026-07-15 03:41:30.219[7896][DEBUG] lisa.env[generated_0].node[0].cmd[2480].stdout umount: /mnt/fscrypt: no mount point specified.
2026-07-15 03:41:30.227[3520][DEBUG] lisa.env[generated_0].node[0].cmd[2480] execution time: 0.128 sec, exit code: 32
2026-07-15 03:41:30.228[3520][DEBUG] lisa.env[generated_0].node[0].cmd[1118] cmd: ['sudo', 'sh', '-c', 'mkfs.ext4 -F -O encrypt /dev/sdc'], cwd: None, shell: True, sudo: True, nohup: False, posix: True, remote: True, encoding: utf-8
2026-07-15 03:41:30.349[7808][DEBUG] lisa.env[generated_0].node[0].cmd[1118].stdout mke2fs 1.46.5 (30-Dec-2021)
2026-07-15 03:41:30.492[7808][DEBUG] lisa.env[generated_0].node[0].cmd[1118].stdout Discarding device blocks:       0/8388608���������������               ���������������done                            
2026-07-15 03:41:30.494[7808][DEBUG] lisa.env[generated_0].node[0].cmd[1118].stdout Creating filesystem with 8388608 4k blocks and 2097152 inodes
2026-07-15 03:41:30.494[7808][DEBUG] lisa.env[generated_0].node[0].cmd[1118].stdout Filesystem UUID: 9f73aa62-6b5f-4996-a0c7-5fe16d2342f9
2026-07-15 03:41:30.495[7808][DEBUG] lisa.env[generated_0].node[0].cmd[1118].stdout Superblock backups stored on blocks: 
2026-07-15 03:41:30.495[7808][DEBUG] lisa.env[generated_0].node[0].cmd[1118].stdout 	32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208, 
2026-07-15 03:41:30.495[7808][DEBUG] lisa.env[generated_0].node[0].cmd[1118].stdout 	4096000, 7962624
2026-07-15 03:41:30.496[7808][DEBUG] lisa.env[generated_0].node[0].cmd[1118].stdout Allocating group tables:   0/256�������       �������done                            
2026-07-15 03:41:30.499[7808][DEBUG] lisa.env[generated_0].node[0].cmd[1118].stdout Writing inode tables:   0/256�������       �������done                            
2026-07-15 03:41:33.064[7808][DEBUG] lisa.env[generated_0].node[0].cmd[1118].stdout Creating journal (65536 blocks): done
2026-07-15 03:41:33.183[7808][DEBUG] lisa.env[generated_0].node[0].cmd[1118].stdout Writing superblocks and filesystem accounting information:   0/256�������       �������done
2026-07-15 03:41:33.188[3520][DEBUG] lisa.env[generated_0].node[0].cmd[1118] execution time: 2.960 sec, exit code: 0
2026-07-15 03:41:33.373[3520][DEBUG] lisa.env[generated_0].node[0].cmd[6768] cmd: ['sudo', 'sh', '-c', 'mount /dev/sdc /mnt/fscrypt'], cwd: None, shell: True, sudo: True, nohup: False, posix: True, remote: True, encoding: utf-8
2026-07-15 03:41:33.597[3520][DEBUG] lisa.env[generated_0].node[0].cmd[6768] execution time: 0.219 sec, exit code: 0
2026-07-15 03:41:33.597[3520][DEBUG] lisa.env[generated_0].node[0].cmd[8009] cmd: ['sudo', 'sh', '-c', 'mkdir -p /mnt/fscrypt/secret && DESC=$(printf \'lisa-fscrypt-pass\\n\' | e4crypt add_key -S 0x00112233445566778899aabbccddeeff | grep -oE \'[0-9a-f]{16}\' | head -1) && test -n "$DESC" && e4crypt set_policy $DESC /mnt/fscrypt/secret && echo lisa-fscrypt-topsecret > /mnt/fscrypt/secret/plain.txt && grep -q lisa-fscrypt-topsecret /mnt/fscrypt/secret/plain.txt'], cwd: None, shell: True, sudo: True, nohup: False, posix: True, remote: True, encoding: utf-8
2026-07-15 03:41:33.782[7188][DEBUG] lisa.env[generated_0].node[0].cmd[8009].stdout Key with descriptor [dbd2ba8ec6f15173] applied to /mnt/fscrypt/secret.
2026-07-15 03:41:33.807[3520][DEBUG] lisa.env[generated_0].node[0].cmd[8009] execution time: 0.209 sec, exit code: 0
2026-07-15 03:41:33.808[3520][INFO] lisa.case[verify_fscrypt_ext4][lisa_0_0] plaintext readable while encryption key is present
2026-07-15 03:41:33.808[3520][DEBUG] lisa.env[generated_0].node[0].cmd[9828] cmd: ['sudo', 'sh', '-c', 'umount  /mnt/fscrypt'], cwd: None, shell: True, sudo: True, nohup: False, posix: True, remote: True, encoding: utf-8
2026-07-15 03:41:33.960[3520][DEBUG] lisa.env[generated_0].node[0].cmd[9828] execution time: 0.151 sec, exit code: 0
2026-07-15 03:41:34.033[3520][DEBUG] lisa.env[generated_0].node[0].cmd[4069] cmd: ['sudo', 'sh', '-c', 'mount /dev/sdc /mnt/fscrypt'], cwd: None, shell: True, sudo: True, nohup: False, posix: True, remote: True, encoding: utf-8
2026-07-15 03:41:34.207[3520][DEBUG] lisa.env[generated_0].node[0].cmd[4069] execution time: 0.172 sec, exit code: 0
2026-07-15 03:41:34.208[3520][DEBUG] lisa.env[generated_0].node[0].cmd[912] cmd: ['sudo', 'sh', '-c', 'ls /mnt/fscrypt/secret'], cwd: None, shell: True, sudo: True, nohup: False, posix: True, remote: True, encoding: utf-8
2026-07-15 03:41:34.340[3060][DEBUG] lisa.env[generated_0].node[0].cmd[912].stdout plain.txt
2026-07-15 03:41:34.350[3520][DEBUG] lisa.env[generated_0].node[0].cmd[912] execution time: 0.142 sec, exit code: 0
2026-07-15 03:41:34.351[5592][ERROR] lisa.case[verify_fscrypt_ext4][lisa_0_0] case failed
Traceback (most recent call last):
  File "C:\app\lsg-lisa\lisa\lisa\testsuite.py", line 914, in __run_case
    _call_with_timeout(
  File "C:\app\lsg-lisa\lisa\lisa\testsuite.py", line 60, in _call_with_timeout
    func_timeout(
  File "C:\Python\Lib\site-packages\func_timeout\dafunc.py", line 108, in func_timeout
    raise_exception(exception)
  File "C:\Python\Lib\site-packages\func_timeout\py3_raise.py", line 7, in raise_exception
    raise exception[0] from None
  File "C:\app\lsg-lisa\lisa\lisa\testsuite.py", line 649, in wrapper
    func(*args, **parameters)
  File "C:\app\lsg-lisa\lisa\lisa\microsoft\testsuites\security\fscrypt.py", line 57, in verify_fscrypt_ext4
    self._run_fscrypt_lifecycle(
  File "C:\app\lsg-lisa\lisa\lisa\microsoft\testsuites\security\fscrypt.py", line 164, in _run_fscrypt_lifecycle
    raise AssertionError(
AssertionError: filename 'plain.txt' is visible in cleartext without the key � encryption policy is not in effect

)

# Reading the file without the key must fail (ENOKEY). Encrypted
# filenames make the original path non-existent, so a read is expected
# to return a non-zero exit code.
if (
node.execute(f"cat {secret_dir}/plain.txt", sudo=True, shell=True).exit_code
== 0
):
raise AssertionError("plaintext was readable without the key")
log.info("directory is locked (ciphertext filenames, no plaintext access)")
Comment on lines +158 to +177

# --- Unlock: re-add the identical key ----------------------------
recover = (
f"printf '{_FSCRYPT_PASSPHRASE}\\n' "
f"| {crypt_tool} add_key -S {_FSCRYPT_SALT} "
f"&& cat {secret_dir}/plain.txt"
)
result = node.execute(
recover,
sudo=True,
shell=True,
expected_exit_code=0,
expected_exit_code_failure_message=(
"re-adding the key did not unlock the encrypted data"
),
)
if _PLAINTEXT_TOKEN not in result.stdout:
raise AssertionError(
"recovered content did not match the original plaintext"
)
Comment on lines +194 to +197
log.info("data transparently unlocked after re-adding the key")

# --- Cleanup ------------------------------------------------------
mount.umount(data_disk, mount_point, erase=False)
Comment on lines +200 to +201
Comment on lines +200 to +201
Loading