[U-Boot] [RFC 14/15] efi_loader, pytest: add UEFI secure boot tests (image)

AKASHI Takahiro
2019-09-18T01:26:42Z

test_efi_secboot/test_signed.py: provide test cases of image authentication
for signed images
test_efi_secboot/test_unsigned.py: provide test cases of image
authentication for unsigned images

This test relies on efitools and user should compile it on his own.
(Currently some local change has been applied.)

Signed-off-by: AKASHI Takahiro <takahiro.akashi@linaro.org>
---
test/py/tests/test_efi_secboot/conftest.py | 168 ++++++++++++++++++
test/py/tests/test_efi_secboot/defs.py | 7 +
test/py/tests/test_efi_secboot/test_signed.py | 97 ++++++++++
.../tests/test_efi_secboot/test_unsigned.py | 126 +++++++++++++
4 files changed, 398 insertions(+)
create mode 100644 test/py/tests/test_efi_secboot/conftest.py
create mode 100644 test/py/tests/test_efi_secboot/defs.py
create mode 100644 test/py/tests/test_efi_secboot/test_signed.py
create mode 100644 test/py/tests/test_efi_secboot/test_unsigned.py

diff --git a/test/py/tests/test_efi_secboot/conftest.py b/test/py/tests/test_efi_secboot/conftest.py
new file mode 100644
index 000000000000..3806f305a138
--- /dev/null
+++ b/test/py/tests/test_efi_secboot/conftest.py
@@ -0,0 +1,168 @@
+# SPDX-License-Identifier: GPL-2.0+
+# Copyright (c) 2019, Linaro Limited
+# Author: AKASHI Takahiro <takahiro.akashi@linaro.org>
+
+import os
+import os.path
+import pytest
+import re
+from subprocess import call, check_call, check_output, CalledProcessError
+from defs import *
+
+#
+# Helper functions
+#
+def mk_fs(config, fs_type, size, id):
+ """Create a file system volume.
+
+ Args:
+ fs_type: File system type.
+ size: Size of file system in MiB.
+ id: Prefix string of volume's file name.
+
+ Return:
+ Nothing.
+ """
+ fs_img = '%s.%s.img' % (id, fs_type)
+ fs_img = config.persistent_data_dir + '/' + fs_img
+
+ if fs_type == 'fat16':
+ mkfs_opt = '-F 16'
+ elif fs_type == 'fat32':
+ mkfs_opt = '-F 32'
+ elif fs_type == 'ext4':
+ mkfs_opt = '-O ^metadata_csum'
+ else:
+ mkfs_opt = ''
+
+ if re.match('fat', fs_type):
+ fs_lnxtype = 'vfat'
+ else:
+ fs_lnxtype = fs_type
+
+ count = (size + 1048576 - 1) / 1048576
+
+ try:
+ check_call('rm -f %s' % fs_img, shell=True)
+ check_call('dd if=/dev/zero of=%s bs=1M count=%d'
+ % (fs_img, count), shell=True)
+ check_call('mkfs.%s %s %s'
+ % (fs_lnxtype, mkfs_opt, fs_img), shell=True)
+ return fs_img
+ except CalledProcessError:
+ call('rm -f %s' % fs_img, shell=True)
+ raise
+
+# from test/py/conftest.py
+def tool_is_in_path(tool):
+ """Check whether a given command is available on host.
+
+ Args:
+ tool: Command name.
+
+ Return:
+ True if available, False if not.
+ """
+ for path in os.environ['PATH'].split(os.pathsep):
+ fn = os.path.join(path, tool)
+ if os.path.isfile(fn) and os.access(fn, os.X_OK):
+ return True
+ return False
+
+fuse_mounted = False
+
+def mount_fs(fs_type, device, mount_point):
+ """Mount a volume.
+
+ Args:
+ fs_type: File system type.
+ device: Volume's file name.
+ mount_point: Mount point.
+
+ Return:
+ Nothing.
+ """
+ global fuse_mounted
+
+ fuse_mounted = False
+ try:
+ if tool_is_in_path('guestmount'):
+ fuse_mounted = True
+ check_call('guestmount -a %s -m /dev/sda %s'
+ % (device, mount_point), shell=True)
+ else:
+ mount_opt = 'loop,rw'
+ if re.match('fat', fs_type):
+ mount_opt += ',umask=0000'
+
+ check_call('sudo mount -o %s %s %s'
+ % (mount_opt, device, mount_point), shell=True)
+
+ # may not be effective for some file systems
+ check_call('sudo chmod a+rw %s' % mount_point, shell=True)
+ except CalledProcessError:
+ raise
+
+def umount_fs(mount_point):
+ """Unmount a volume.
+
+ Args:
+ mount_point: Mount point.
+
+ Return:
+ Nothing.
+ """
+ if fuse_mounted:
+ call('sync')
+ call('guestunmount %s' % mount_point, shell=True)
+ else:
+ call('sudo umount %s' % mount_point, shell=True)
+
+#
+# Fixture for TestEfiSignedImage test
+#
+# NOTE: yield_fixture was deprecated since pytest-3.0
+ at pytest.yield_fixture()
+def efi_boot_env(request, u_boot_config):
+ """Set up a file system to be used in basic fs test.
+
+ Args:
+ request: Pytest request object.
+ u_boot_config: U-boot configuration.
+
+ Return:
+ A fixture for basic fs test, i.e. a triplet of file system type,
+ volume file name and a list of MD5 hashes.
+ """
+ fs_type = 'fat32'
+ fs_img = ''
+
+ mount_dir = u_boot_config.persistent_data_dir + '/mnt'
+
+ try:
+
+ # test volume
+ fs_img = mk_fs(u_boot_config, 'fat32', TEST_VOL_SIZE, 'test')
+
+ # Mount the image so we can populate it.
+ check_output('mkdir -p %s' % mount_dir, shell=True)
+ mount_fs(fs_type, fs_img, mount_dir)
+
+ # Copy files
+ check_output('cp %s/*.efi %s' % (EFITOOLS_PATH, mount_dir), shell=True)
+ check_output('cp %s/PK* %s' % (EFITOOLS_PATH, mount_dir), shell=True)
+ check_output('cp %s/noPK* %s' % (EFITOOLS_PATH, mount_dir), shell=True)
+ check_output('cp %s/KEK* %s' % (EFITOOLS_PATH, mount_dir), shell=True)
+ check_output('cp %s/DB* %s' % (EFITOOLS_PATH, mount_dir), shell=True)
+
+ umount_fs(mount_dir)
+ except CalledProcessError as e:
+ pytest.skip('Setup failed: %s' % e.cmd)
+ return
+ else:
+ yield fs_img
+ finally:
+ umount_fs(mount_dir)
+ call('rmdir %s' % mount_dir, shell=True)
+ if fs_img:
+ call('rm -f %s' % fs_img, shell=True)
diff --git a/test/py/tests/test_efi_secboot/defs.py b/test/py/tests/test_efi_secboot/defs.py
new file mode 100644
index 000000000000..cbe99ef3a6ba
--- /dev/null
+++ b/test/py/tests/test_efi_secboot/defs.py
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: GPL-2.0+
+
+# 64MiB
+TEST_VOL_SIZE=0x4000000
+
+# efitools build directory
+EFITOOLS_PATH='/home/akashi/x86/efitools'
diff --git a/test/py/tests/test_efi_secboot/test_signed.py b/test/py/tests/test_efi_secboot/test_signed.py
new file mode 100644
index 000000000000..05b193a5266b
--- /dev/null
+++ b/test/py/tests/test_efi_secboot/test_signed.py
@@ -0,0 +1,97 @@
+# SPDX-License-Identifier: GPL-2.0+
+# Copyright (c) 2019, Linaro Limited
+# Author: AKASHI Takahiro <takahiro.akashi@linaro.org>
+#
+# U-Boot UEFI: Signed Image Authentication Test
+
+"""
+This test verifies image authentication for signed images.
+"""
+
+import pytest
+import re
+from defs import *
+
+ at pytest.mark.boardspec('sandbox')
+ at pytest.mark.buildconfigspec('efi_secure_boot')
+ at pytest.mark.slow
+class TestEfiSignedImage(object):
+ def test_efi_signed_image_auth1(self, u_boot_console, efi_boot_env):
+ """
+ Test Case 1 - authenticated by DB
+ """
+ disk_img = efi_boot_env
+ with u_boot_console.log.section('Test Case 1a'):
+ # Test Case 1a, run signed image if no DB/DBX
+ output = u_boot_console.run_command_list([
+ 'host bind 0 %s' % disk_img,
+ 'efidebug boot add 1 HELLO1 host 0:0 /HelloWorld_simple-signed.efi ""',
+ 'efidebug boot next 1',
+ 'bootefi bootmgr'])
+ assert(re.search('Hello World!', ''.join(output)))
+
+ with u_boot_console.log.section('Test Case 1b'):
+ # Test Case 1b, run unsigned image if no DB/DBX
+ output = u_boot_console.run_command_list([
+ 'efidebug boot add 2 HELLO2 host 0:0 /HelloWorld_simple.efi ""',
+ 'efidebug boot next 2',
+ 'bootefi bootmgr'])
+ assert(re.search('Hello World!', ''.join(output)))
+
+ with u_boot_console.log.section('Test Case 1c'):
+ # Test Case 1c, not authenticated by DB
+ output = u_boot_console.run_command_list([
+ 'efidebug boot add 3 UPDV host 0:0 /UpdateVars.efi ""',
+ 'setenv bootargs "cmd db DB.auth"',
+ 'efidebug boot next 3',
+ 'bootefi bootmgr',
+ 'setenv bootargs "cmd KEK KEK.auth"',
+ 'efidebug boot next 3',
+ 'bootefi bootmgr',
+ 'setenv bootargs "cmd PK PK.auth"',
+ 'efidebug boot next 3',
+ 'bootefi bootmgr',
+ 'efidebug boot next 2',
+ 'bootefi bootmgr'])
+ assert(not re.search('Hello World!', ''.join(output)))
+
+ with u_boot_console.log.section('Test Case 1d'):
+ # Test Case 1d, authenticated by DB
+ output = u_boot_console.run_command_list([
+ 'efidebug boot next 1',
+ 'bootefi bootmgr'])
+ assert(re.search('Hello World!', ''.join(output)))
+
+ def test_efi_signed_image_auth2(self, u_boot_console, efi_boot_env):
+ """
+ Test Case 2 - rejected by DBX
+ """
+ disk_img = efi_boot_env
+ with u_boot_console.log.section('Test Case 2a'):
+ # Test Case 2a, rejected by DBX
+ output = u_boot_console.run_command_list([
+ 'host bind 0 %s' % disk_img,
+ 'efidebug boot add 1 UPDV host 0:0 /UpdateVars-signed.efi ""',
+ 'setenv bootargs "cmd dbx DB.auth"',
+ 'efidebug boot next 1',
+ 'bootefi bootmgr',
+ 'setenv bootargs "cmd KEK KEK.auth"',
+ 'efidebug boot next 1',
+ 'bootefi bootmgr',
+ 'setenv bootargs "cmd PK PK.auth"',
+ 'efidebug boot next 1',
+ 'bootefi bootmgr',
+ 'efidebug boot add 2 HELLO1 host 0:0 /HelloWorld_simple-signed.efi ""',
+ 'efidebug boot next 2',
+ 'bootefi bootmgr'])
+ assert(not re.search('Hello World!', ''.join(output)))
+
+ with u_boot_console.log.section('Test Case 2b'):
+ # Test Case 2b, rejected by DBX even if DB allows
+ output = u_boot_console.run_command_list([
+ 'setenv bootargs "cmd db DB.auth"',
+ 'efidebug boot next 1',
+ 'bootefi bootmgr',
+ 'efidebug boot next 2',
+ 'bootefi bootmgr'])
+ assert(not re.search('Hello World!', ''.join(output)))
diff --git a/test/py/tests/test_efi_secboot/test_unsigned.py b/test/py/tests/test_efi_secboot/test_unsigned.py
new file mode 100644
index 000000000000..232793e431b3
--- /dev/null
+++ b/test/py/tests/test_efi_secboot/test_unsigned.py
@@ -0,0 +1,126 @@
+# SPDX-License-Identifier: GPL-2.0+
+# Copyright (c) 2019, Linaro Limited
+# Author: AKASHI Takahiro <takahiro.akashi@linaro.org>
+#
+# U-Boot UEFI: Signed Image Authentication Test
+
+"""
+This test verifies image authentication for unsigned images.
+"""
+
+import pytest
+import re
+from defs import *
+
+ at pytest.mark.boardspec('sandbox')
+ at pytest.mark.buildconfigspec('efi_secure_boot')
+ at pytest.mark.slow
+class TestEfiUnsignedImage(object):
+ def test_efi_unsigned_image_auth1(self, u_boot_console, efi_boot_env):
+ """
+ Test Case 1 - rejected when not hash in DB or DBX
+ """
+ disk_img = efi_boot_env
+ with u_boot_console.log.section('Test Case 1'):
+ # Test Case 1
+ output = u_boot_console.run_command_list([
+ 'host bind 0 %s' % disk_img,
+ 'efidebug boot add 1 UPDV host 0:0 /UpdateVars.efi ""',
+ 'setenv bootargs "cmd KEK KEK.auth"',
+ 'efidebug boot next 1',
+ 'bootefi bootmgr',
+ 'setenv bootargs "cmd PK PK.auth"',
+ 'efidebug boot next 1',
+ 'bootefi bootmgr'])
+ assert(not re.search('\'UPDV\' failed', ''.join(output)))
+
+ output = u_boot_console.run_command_list([
+ 'efidebug boot add 2 HELLO1 host 0:0 /HelloWorld_simple.efi ""',
+ 'efidebug boot next 2',
+ 'bootefi bootmgr'])
+ assert(re.search('\'HELLO1\' failed', ''.join(output)))
+
+ def test_efi_unsigned_image_auth2(self, u_boot_console, efi_boot_env):
+ """
+ Test Case 2 - authenticated by DB with hash
+ """
+ disk_img = efi_boot_env
+ with u_boot_console.log.section('Test Case 2'):
+ # Test Case 2
+ output = u_boot_console.run_command_list([
+ 'host bind 0 %s' % disk_img,
+ 'efidebug boot add 1 UPDV host 0:0 /UpdateVars.efi ""',
+ 'setenv bootargs "cmd db DB_HWs_hash.auth"',
+ 'efidebug boot next 1',
+ 'bootefi bootmgr',
+ 'setenv bootargs "cmd KEK KEK.auth"',
+ 'efidebug boot next 1',
+ 'bootefi bootmgr',
+ 'setenv bootargs "cmd PK PK.auth"',
+ 'efidebug boot next 1',
+ 'bootefi bootmgr'])
+ assert(not re.search('\'UPDV\' failed', ''.join(output)))
+
+ output = u_boot_console.run_command_list([
+ 'efidebug boot add 2 HELLO1 host 0:0 /HelloWorld_simple.efi ""',
+ 'efidebug boot next 2',
+ 'bootefi bootmgr'])
+ assert(re.search('Hello World!', ''.join(output)))
+
+ def test_efi_unsigned_image_auth3(self, u_boot_console, efi_boot_env):
+ """
+ Test Case 3 - rejected by DBX with hash
+ """
+ disk_img = efi_boot_env
+ with u_boot_console.log.section('Test Case 3a'):
+ # Test Case 3a, rejected by DBX
+ output = u_boot_console.run_command_list([
+ 'host bind 0 %s' % disk_img,
+ 'efidebug boot add 1 UPDV host 0:0 /UpdateVars-signed.efi ""',
+ 'setenv bootargs "cmd dbx DBX_HWs_hash.auth"',
+ 'efidebug boot next 1',
+ 'bootefi bootmgr',
+ 'setenv bootargs "cmd KEK KEK.auth"',
+ 'efidebug boot next 1',
+ 'bootefi bootmgr',
+ 'setenv bootargs "cmd PK PK.auth"',
+ 'efidebug boot next 1',
+ 'bootefi bootmgr'])
+ assert(not re.search('\'UPDV\' failed', ''.join(output)))
+
+ output = u_boot_console.run_command_list([
+ 'efidebug boot add 2 HELLO1 host 0:0 /HelloWorld_simple.efi ""',
+ 'efidebug boot next 2',
+ 'bootefi bootmgr'])
+ assert(re.search('\'HELLO1\' failed', ''.join(output)))
+
+ # TODO: can be merged with auth3
+ def test_efi_unsigned_image_auth4(self, u_boot_console, efi_boot_env):
+ """
+ Test Case 4 - rejected by DBX with hash
+ """
+ disk_img = efi_boot_env
+ with u_boot_console.log.section('Test Case 4a'):
+ # Test Case 4a, rejected by DBX even if DB allows
+ output = u_boot_console.run_command_list([
+ 'host bind 0 %s' % disk_img,
+ 'efidebug boot add 1 UPDV host 0:0 /UpdateVars-signed.efi ""',
+ 'setenv bootargs "cmd db DB_HWs_hash.auth"',
+ 'efidebug boot next 1',
+ 'bootefi bootmgr',
+ 'setenv bootargs "cmd dbx DBX_HWs_hash.auth"',
+ 'efidebug boot next 1',
+ 'bootefi bootmgr',
+ 'setenv bootargs "cmd KEK KEK.auth"',
+ 'efidebug boot next 1',
+ 'bootefi bootmgr',
+ 'setenv bootargs "cmd PK PK.auth"',
+ 'efidebug boot next 1',
+ 'bootefi bootmgr'])
+ assert(not re.search('\'UPDV\' failed', ''.join(output)))
+
+ output = u_boot_console.run_command_list([
+ 'efidebug boot add 2 HELLO1 host 0:0 /HelloWorld_simple.efi ""',
+ 'efidebug boot next 2',
+ 'bootefi bootmgr'])
+ assert(re.search('\'HELLO1\' failed', ''.join(output)))
--
2.21.0