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

AKASHI Takahiro takahiro.akashi at linaro.org
Wed Sep 18 01:26:42 UTC 2019


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



More information about the U-Boot mailing list