diff --git a/.github/workflows/checkbox-promote-beta-to-candidate/job.template b/.github/workflows/checkbox-promote-beta-to-candidate/job.template index 700c024f81..38f461a0f6 100644 --- a/.github/workflows/checkbox-promote-beta-to-candidate/job.template +++ b/.github/workflows/checkbox-promote-beta-to-candidate/job.template @@ -45,7 +45,7 @@ test_data: else echo "Installing checkbox runtime on device (from deb package)" _run sudo add-apt-repository -y ppa:checkbox-dev/$CHANNEL - _run install_packages checkbox-ng python3-checkbox-ng checkbox-provider-base checkbox-provider-resource checkbox-provider-sru fswebcam obexftp wmctrl iperf mesa-utils vim pastebinit fwts xorg-dev gir1.2-clutter-1.0 + _run install_packages checkbox-ng python3-checkbox-ng checkbox-provider-base checkbox-provider-resource checkbox-provider-sru fscrypt fswebcam obexftp wmctrl iperf mesa-utils vim pastebinit fwts xorg-dev gir1.2-clutter-1.0 # list installed checkbox-related packages to facilitate debugging _run "apt list --installed | grep checkbox" CHECKBOX_CLI_CMD="checkbox-cli" diff --git a/checkbox-core-snap/series16/snap/snapcraft.yaml b/checkbox-core-snap/series16/snap/snapcraft.yaml index cafbf03fb8..c1108a849c 100644 --- a/checkbox-core-snap/series16/snap/snapcraft.yaml +++ b/checkbox-core-snap/series16/snap/snapcraft.yaml @@ -287,6 +287,7 @@ parts: - ethtool - freeipmi-tools - freeglut3 + - fscrypt - fswebcam - gir1.2-cheese-3.0 - gir1.2-clutter-1.0 diff --git a/checkbox-core-snap/series18/snap/snapcraft.yaml b/checkbox-core-snap/series18/snap/snapcraft.yaml index 0c48a8793d..dafbcb43a1 100644 --- a/checkbox-core-snap/series18/snap/snapcraft.yaml +++ b/checkbox-core-snap/series18/snap/snapcraft.yaml @@ -267,6 +267,7 @@ parts: - ethtool - freeipmi-tools - freeglut3 + - fscrypt - fswebcam - gir1.2-cheese-3.0 - gir1.2-clutter-1.0 diff --git a/checkbox-core-snap/series20/snap/snapcraft.yaml b/checkbox-core-snap/series20/snap/snapcraft.yaml index 4bf4953acc..21cef6b69e 100644 --- a/checkbox-core-snap/series20/snap/snapcraft.yaml +++ b/checkbox-core-snap/series20/snap/snapcraft.yaml @@ -292,6 +292,7 @@ parts: - ethtool - freeipmi-tools - freeglut3 + - fscrypt - fswebcam - gir1.2-cheese-3.0 - gir1.2-clutter-1.0 diff --git a/checkbox-core-snap/series22/snap/snapcraft.yaml b/checkbox-core-snap/series22/snap/snapcraft.yaml index 58f6e7df13..f16bdd0c2f 100644 --- a/checkbox-core-snap/series22/snap/snapcraft.yaml +++ b/checkbox-core-snap/series22/snap/snapcraft.yaml @@ -298,6 +298,7 @@ parts: - efibootmgr - ethtool - freeipmi-tools + - fscrypt - fswebcam - gir1.2-cheese-3.0 - gir1.2-clutter-1.0 diff --git a/checkbox-core-snap/series24/snap/snapcraft.yaml b/checkbox-core-snap/series24/snap/snapcraft.yaml index 1b8ab2a2fb..ea75733974 100644 --- a/checkbox-core-snap/series24/snap/snapcraft.yaml +++ b/checkbox-core-snap/series24/snap/snapcraft.yaml @@ -297,6 +297,7 @@ parts: - efibootmgr - ethtool - freeipmi-tools + - fscrypt - fswebcam - gir1.2-cheese-3.0 - gir1.2-clutter-1.0 diff --git a/providers/base/README.rst b/providers/base/README.rst index 68b2ee8105..76e5e0940f 100644 --- a/providers/base/README.rst +++ b/providers/base/README.rst @@ -12,28 +12,28 @@ under the units folder. Base Provider Units ################### -+------------+-------------+-------------+------------------+-------------+----------------+ -| 6lowpan | eeprom | i2c | miscellanea | serial | ubuntucore | -+------------+-------------+-------------+------------------+-------------+----------------+ -| acpi | esata | image | mobilebroadband | smoke | usb | -+------------+-------------+-------------+------------------+-------------+----------------+ -| audio | ethernet | info | monitor | snapd | virtualization | -+------------+-------------+-------------+------------------+-------------+----------------+ -| benchmarks | expresscard | input | networking | socketcan | watchdog | -+------------+-------------+-------------+------------------+-------------+----------------+ -| bluetooth | fingerprint | install | nvdimm | stress | wireless | -+------------+-------------+-------------+------------------+-------------+----------------+ -| camera_ | firewire | kernel-snap | oob-management | submission | wwan | -+------------+-------------+-------------+------------------+-------------+----------------+ -| canary | firmware | keys | optical | suspend | | -+------------+-------------+-------------+------------------+-------------+----------------+ -| codecs | gadget | led | power-management | thunderbolt | | -+------------+-------------+-------------+------------------+-------------+----------------+ -| cpu | gpio | location | rtc | touchpad | | -+------------+-------------+-------------+------------------+-------------+----------------+ -| disk | graphics | mediacard | security | touchscreen | | -+------------+-------------+-------------+------------------+-------------+----------------+ -| dock | hibernate | memory | self | tpm | | -+------------+-------------+-------------+------------------+-------------+----------------+ ++-----------+-------------+-------------+-------------------+-------------+----------------+ +| 6lowpan | eeprom | hibernate | memory | self | tmp | ++-----------+-------------+-------------+-------------------+-------------+----------------+ +| acpi | esata | i2c | miscellanea | serial | ubuntucore | ++-----------+-------------+-------------+-------------------+-------------+----------------+ +| audio | ethernet | image | mobilebroadband | smoke | usb | ++-----------+-------------+-------------+-------------------+-------------+----------------+ +| benchmarks| expresscard | info | monitor | snapd | virtualization | ++-----------+-------------+-------------+-------------------+-------------+----------------+ +| bluetooth | fingerprint | input | networking | socketcan | watchdog | ++-----------+-------------+-------------+-------------------+-------------+----------------+ +| camera_ | firewire | install | nvdimm | submission | wireless | ++-----------+-------------+-------------+-------------------+-------------+----------------+ +| canary | firmware | kernel-snap | oob-management | suspend | wwan | ++-----------+-------------+-------------+-------------------+-------------+----------------+ +| codecs | fscrypt | keys | optical | tpm | | ++-----------+-------------+-------------+-------------------+-------------+----------------+ +| cpu | gadget | led | power-management | thunderbolt | | ++-----------+-------------+-------------+-------------------+-------------+----------------+ +| disk | gpio | location | rtc | touchpad | | ++-----------+-------------+-------------+-------------------+-------------+----------------+ +| dock | graphics | mediacard | security | touchscreen | | ++-----------+-------------+-------------+-------------------+-------------+----------------+ .. _camera: units/camera/README.rst diff --git a/providers/base/bin/fscrypt_test.py b/providers/base/bin/fscrypt_test.py new file mode 100755 index 0000000000..953ed400f5 --- /dev/null +++ b/providers/base/bin/fscrypt_test.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +""" +script to test fscrypt support + +Copyright (C) 2025 Canonical Ltd. + +Authors + Alexis Cellier + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License version 3, +as published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +The purpose of this script is to make a small fscrypt test on a generated +ext4 disk image to validate the filesystem encryption support. +""" + +import os +import subprocess +import tempfile +import re +from pathlib import Path + + +def main(): + """ + Create an ext4 disk image, mount it, setup fscrypt and use it with a simple + file + """ + with tempfile.TemporaryDirectory() as tmp_path: + tmp = Path(tmp_path) + mnt = tmp / "mnt" + img = tmp / "fs.img" + key_file = tmp / "key" + test_dir = mnt / "test" + test_file = test_dir / "test.txt" + test_content = "test" + fscrypt_config = Path("/etc/fscrypt.conf") + fscrypt_setup = fscrypt_config.exists() + + # Create a 50MB file + subprocess.check_call(["truncate", "-s", "50M", str(img)]) + + mnt.mkdir(parents=True, exist_ok=True) + + # Make ext4 image with encryption support + subprocess.check_call( + ["mkfs.ext4", "-F", "-O", "encrypt,stable_inodes", str(img)] + ) + + # Mount it + subprocess.check_call(["mount", str(img), str(mnt)]) + + try: + # Setup fscrypt + print("Setup fscrupt") + if not fscrypt_setup: + subprocess.run( + ["fscrypt", "setup", "--force"], + input="n\n", + text=True, + check=True, + ) + subprocess.run( + ["fscrypt", "setup", "--force", str(mnt)], + input="n\n", + text=True, + check=True, + ) + + # Confirm fscrypt is enabled + output = subprocess.check_output( + ["fscrypt", "status"], + text=True, + ) + found = False + pattern = re.compile( + "^{}.*supported\\s*Yes$".format(re.escape(str(mnt))) + ) + for line in output.splitlines(): + if pattern.match(line): + found = True + break + if not found: + raise SystemExit("Failed to setup fscrypt") + + # Write random key + with key_file.open("wb") as f: + f.write(os.urandom(32)) + + # Make test directory + test_dir.mkdir(parents=True, exist_ok=True) + + # Encrypt directory + subprocess.check_call( + [ + "fscrypt", + "encrypt", + "--quiet", + "--source=raw_key", + "--name=test_key", + "--key={}".format(str(key_file)), + str(test_dir), + ] + ) + + # Write a file inside + with test_file.open("w") as f: + f.write(test_content) + + # Lock the directory + subprocess.check_call(["fscrypt", "lock", str(test_dir)]) + + # Should not be able to list the file + if test_file.exists(): + raise SystemExit("File should not be accessible when locked") + print("File correctly inaccessible when locked") + + # Unlock the directory + subprocess.check_call( + [ + "fscrypt", + "unlock", + "--key={}".format(str(key_file)), + str(test_dir), + ] + ) + + with test_file.open("r") as f: + content = f.read().strip() + if content != test_content: + print("Expected: {} / Got: {}".format(test_content, content)) + raise SystemExit("File contents not correct after unlock") + print("File is accessible and content is correct after unlock") + finally: + subprocess.check_call(["umount", str(mnt)]) + if not fscrypt_setup and fscrypt_config.exists(): + fscrypt_config.unlink() + + +if __name__ == "__main__": + main() diff --git a/providers/base/debian/control b/providers/base/debian/control index aaf48d6f25..bb2a8bb1d2 100644 --- a/providers/base/debian/control +++ b/providers/base/debian/control @@ -42,7 +42,8 @@ Recommends: bonnie++, smartmontools, sysstat, ${plainbox:Recommends} -Suggests: fswebcam, +Suggests: fscrypt, + fswebcam, fwts, glmark2, glmark2-es2, diff --git a/providers/base/tests/test_fscrypt_test.py b/providers/base/tests/test_fscrypt_test.py new file mode 100644 index 0000000000..578b67e9ca --- /dev/null +++ b/providers/base/tests/test_fscrypt_test.py @@ -0,0 +1,88 @@ +from unittest.mock import patch +import fscrypt_test +import unittest + + +def _prepare_path_and_subprocess(mock_path, mock_subprocess): + mock_path.exists.side_effect = [True, False, True] + mock_path.mkdir.return_value = True + mock_path.__truediv__.return_value = mock_path + mock_path.__str__.return_value = "path" + mock_path.return_value = mock_path + mock_path.open = mock_path + mock_path.__enter__ = mock_path + mock_path.read.return_value = "test\n" + mock_subprocess.run.side_effect = [0, Exception("Should be called once")] + mock_subprocess.check_call.return_value = 0 + mock_subprocess.check_output.return_value = ( + "path this-is-ignore this-also supported Yes" + ) + + +class FscryptTestCase(unittest.TestCase): + @patch("fscrypt_test.Path") + @patch("fscrypt_test.subprocess") + def test_main_success(self, mock_subprocess, mock_path): + _prepare_path_and_subprocess( + mock_path, + mock_subprocess, + ) + fscrypt_test.main() + + @patch("fscrypt_test.Path") + @patch("fscrypt_test.subprocess") + def test_main_failure_setup(self, mock_subprocess, mock_path): + _prepare_path_and_subprocess( + mock_path, + mock_subprocess, + ) + # Fake fscrypt setup not working. + mock_subprocess.check_output.return_value = ( + "path this-is-ignore this-also supported No" + ) + with self.assertRaises(SystemExit) as context: + fscrypt_test.main() + self.assertEqual(str(context.exception), "Failed to setup fscrypt") + + @patch("fscrypt_test.Path") + @patch("fscrypt_test.subprocess") + def test_main_failure_lock(self, mock_subprocess, mock_path): + _prepare_path_and_subprocess( + mock_path, + mock_subprocess, + ) + # Fake fscrypt lock not working. + mock_path.exists.side_effect = [True, True, True] + with self.assertRaises(SystemExit) as context: + fscrypt_test.main() + self.assertEqual( + str(context.exception), "File should not be accessible when locked" + ) + + @patch("fscrypt_test.Path") + @patch("fscrypt_test.subprocess") + def test_main_failure_unlock(self, mock_subprocess, mock_path): + _prepare_path_and_subprocess( + mock_path, + mock_subprocess, + ) + # Fake bad unlink file content. + mock_path.read.return_value = "invalid\n" + with self.assertRaises(SystemExit) as context: + fscrypt_test.main() + self.assertEqual( + str(context.exception), "File contents not correct after unlock" + ) + + @patch("fscrypt_test.Path") + @patch("fscrypt_test.subprocess") + def test_main_failure_not_already_setup(self, mock_subprocess, mock_path): + _prepare_path_and_subprocess( + mock_path, + mock_subprocess, + ) + # Fake fscrypt.conf not already existing, so it has to be deleted. + mock_path.exists.side_effect = [False, False, True] + mock_subprocess.run.side_effect = [0, 0] + fscrypt_test.main() + self.assertTrue(mock_path.unlink.called) diff --git a/providers/base/units/fscrypt/category.pxu b/providers/base/units/fscrypt/category.pxu new file mode 100644 index 0000000000..825aa1c77a --- /dev/null +++ b/providers/base/units/fscrypt/category.pxu @@ -0,0 +1,3 @@ +unit: category +id: fscrypt +_name: Ubuntu filesystem encryption feature tests diff --git a/providers/base/units/fscrypt/jobs.pxu b/providers/base/units/fscrypt/jobs.pxu new file mode 100644 index 0000000000..49ff40b45d --- /dev/null +++ b/providers/base/units/fscrypt/jobs.pxu @@ -0,0 +1,18 @@ +id: fscrypt/check-kernel-config +plugin: shell +category_id: fscrypt +command: kernel_config.py --config-flag CONFIG_FS_ENCRYPTION +estimated_duration: 0.005 +_purpose: Checks the value of the CONFIG_FS_ENCRYPTION flag in the kernel configuration +_summary: Check if the kernel is compiled with filesystem encryption support + +id: fscrypt/check-support +plugin: shell +category_id: fscrypt +estimated_duration: 5s +user: root +depends: fscrypt/check-kernel-config +requires: executable.name == "fscrypt" +_summary: Check fscrypt support +_purpose: Create an ext4 image and test the filesystem encryption feature +command: fscrypt_test.py diff --git a/providers/base/units/fscrypt/packaging.pxu b/providers/base/units/fscrypt/packaging.pxu new file mode 100644 index 0000000000..b1c073a172 --- /dev/null +++ b/providers/base/units/fscrypt/packaging.pxu @@ -0,0 +1,3 @@ +unit: packaging meta-data +os-id: ubuntu +Depends: fscrypt diff --git a/providers/base/units/fscrypt/test-plan.pxu b/providers/base/units/fscrypt/test-plan.pxu new file mode 100644 index 0000000000..14cc8aee46 --- /dev/null +++ b/providers/base/units/fscrypt/test-plan.pxu @@ -0,0 +1,15 @@ +id: fscrypt-full +unit: test plan +_name: fscrypt tests +_description: fscrypt tests +include: +nested_part: + fscrypt-automated + +id: fscrypt-automated +unit: test plan +_name: automated fscrypt tests +_description: Automated fscrypt tests +include: + fscrypt/check-kernel-config + fscrypt/check-support