1# SPDX-License-Identifier:	GPL-2.0+
2# Copyright (c) 2016, Google Inc.
3#
4# U-Boot Verified Boot Test
5
6"""
7This tests verified boot in the following ways:
8
9For image verification:
10- Create FIT (unsigned) with mkimage
11- Check that verification shows that no keys are verified
12- Sign image
13- Check that verification shows that a key is now verified
14
15For configuration verification:
16- Corrupt signature and check for failure
17- Create FIT (with unsigned configuration) with mkimage
18- Check that image verification works
19- Sign the FIT and mark the key as 'required' for verification
20- Check that image verification works
21- Corrupt the signature
22- Check that image verification no-longer works
23
24Tests run with both SHA1 and SHA256 hashing.
25"""
26
27import shutil
28import struct
29import pytest
30import u_boot_utils as util
31import vboot_forge
32import vboot_evil
33
34# Only run the full suite on a few combinations, since it doesn't add any more
35# test coverage.
36TESTDATA = [
37    ['sha1', '', None, False, True],
38    ['sha1', '', '-E -p 0x10000', False, False],
39    ['sha1', '-pss', None, False, False],
40    ['sha1', '-pss', '-E -p 0x10000', False, False],
41    ['sha256', '', None, False, False],
42    ['sha256', '', '-E -p 0x10000', False, False],
43    ['sha256', '-pss', None, False, False],
44    ['sha256', '-pss', '-E -p 0x10000', False, False],
45    ['sha256', '-pss', None, True, False],
46    ['sha256', '-pss', '-E -p 0x10000', True, True],
47]
48
49@pytest.mark.boardspec('sandbox')
50@pytest.mark.buildconfigspec('fit_signature')
51@pytest.mark.requiredtool('dtc')
52@pytest.mark.requiredtool('fdtget')
53@pytest.mark.requiredtool('fdtput')
54@pytest.mark.requiredtool('openssl')
55@pytest.mark.parametrize("sha_algo,padding,sign_options,required,full_test",
56                         TESTDATA)
57def test_vboot(u_boot_console, sha_algo, padding, sign_options, required,
58               full_test):
59    """Test verified boot signing with mkimage and verification with 'bootm'.
60
61    This works using sandbox only as it needs to update the device tree used
62    by U-Boot to hold public keys from the signing process.
63
64    The SHA1 and SHA256 tests are combined into a single test since the
65    key-generation process is quite slow and we want to avoid doing it twice.
66    """
67    def dtc(dts):
68        """Run the device tree compiler to compile a .dts file
69
70        The output file will be the same as the input file but with a .dtb
71        extension.
72
73        Args:
74            dts: Device tree file to compile.
75        """
76        dtb = dts.replace('.dts', '.dtb')
77        util.run_and_log(cons, 'dtc %s %s%s -O dtb '
78                         '-o %s%s' % (dtc_args, datadir, dts, tmpdir, dtb))
79
80    def run_bootm(sha_algo, test_type, expect_string, boots, fit=None):
81        """Run a 'bootm' command U-Boot.
82
83        This always starts a fresh U-Boot instance since the device tree may
84        contain a new public key.
85
86        Args:
87            test_type: A string identifying the test type.
88            expect_string: A string which is expected in the output.
89            sha_algo: Either 'sha1' or 'sha256', to select the algorithm to
90                    use.
91            boots: A boolean that is True if Linux should boot and False if
92                    we are expected to not boot
93            fit: FIT filename to load and verify
94        """
95        if not fit:
96            fit = '%stest.fit' % tmpdir
97        cons.restart_uboot()
98        with cons.log.section('Verified boot %s %s' % (sha_algo, test_type)):
99            output = cons.run_command_list(
100                ['host load hostfs - 100 %s' % fit,
101                 'fdt addr 100',
102                 'bootm 100'])
103        assert expect_string in ''.join(output)
104        if boots:
105            assert 'sandbox: continuing, as we cannot run' in ''.join(output)
106        else:
107            assert('sandbox: continuing, as we cannot run'
108                   not in ''.join(output))
109
110    def make_fit(its):
111        """Make a new FIT from the .its source file.
112
113        This runs 'mkimage -f' to create a new FIT.
114
115        Args:
116            its: Filename containing .its source.
117        """
118        util.run_and_log(cons, [mkimage, '-D', dtc_args, '-f',
119                                '%s%s' % (datadir, its), fit])
120
121    def sign_fit(sha_algo, options):
122        """Sign the FIT
123
124        Signs the FIT and writes the signature into it. It also writes the
125        public key into the dtb.
126
127        Args:
128            sha_algo: Either 'sha1' or 'sha256', to select the algorithm to
129                    use.
130            options: Options to provide to mkimage.
131        """
132        args = [mkimage, '-F', '-k', tmpdir, '-K', dtb, '-r', fit]
133        if options:
134            args += options.split(' ')
135        cons.log.action('%s: Sign images' % sha_algo)
136        util.run_and_log(cons, args)
137
138    def sign_fit_norequire(sha_algo, options):
139        """Sign the FIT
140
141        Signs the FIT and writes the signature into it. It also writes the
142        public key into the dtb. It does not mark key as 'required' in dtb.
143
144        Args:
145            sha_algo: Either 'sha1' or 'sha256', to select the algorithm to
146                    use.
147            options: Options to provide to mkimage.
148        """
149        args = [mkimage, '-F', '-k', tmpdir, '-K', dtb, fit]
150        if options:
151            args += options.split(' ')
152        cons.log.action('%s: Sign images' % sha_algo)
153        util.run_and_log(cons, args)
154
155    def replace_fit_totalsize(size):
156        """Replace FIT header's totalsize with something greater.
157
158        The totalsize must be less than or equal to FIT_SIGNATURE_MAX_SIZE.
159        If the size is greater, the signature verification should return false.
160
161        Args:
162            size: The new totalsize of the header
163
164        Returns:
165            prev_size: The previous totalsize read from the header
166        """
167        total_size = 0
168        with open(fit, 'r+b') as handle:
169            handle.seek(4)
170            total_size = handle.read(4)
171            handle.seek(4)
172            handle.write(struct.pack(">I", size))
173        return struct.unpack(">I", total_size)[0]
174
175    def create_rsa_pair(name):
176        """Generate a new RSA key paid and certificate
177
178        Args:
179            name: Name of of the key (e.g. 'dev')
180        """
181        public_exponent = 65537
182        util.run_and_log(cons, 'openssl genpkey -algorithm RSA -out %s%s.key '
183                     '-pkeyopt rsa_keygen_bits:2048 '
184                     '-pkeyopt rsa_keygen_pubexp:%d' %
185                     (tmpdir, name, public_exponent))
186
187        # Create a certificate containing the public key
188        util.run_and_log(cons, 'openssl req -batch -new -x509 -key %s%s.key '
189                         '-out %s%s.crt' % (tmpdir, name, tmpdir, name))
190
191    def test_with_algo(sha_algo, padding, sign_options):
192        """Test verified boot with the given hash algorithm.
193
194        This is the main part of the test code. The same procedure is followed
195        for both hashing algorithms.
196
197        Args:
198            sha_algo: Either 'sha1' or 'sha256', to select the algorithm to
199                    use.
200            padding: Either '' or '-pss', to select the padding to use for the
201                    rsa signature algorithm.
202            sign_options: Options to mkimage when signing a fit image.
203        """
204        # Compile our device tree files for kernel and U-Boot. These are
205        # regenerated here since mkimage will modify them (by adding a
206        # public key) below.
207        dtc('sandbox-kernel.dts')
208        dtc('sandbox-u-boot.dts')
209
210        # Build the FIT, but don't sign anything yet
211        cons.log.action('%s: Test FIT with signed images' % sha_algo)
212        make_fit('sign-images-%s%s.its' % (sha_algo, padding))
213        run_bootm(sha_algo, 'unsigned images', 'dev-', True)
214
215        # Sign images with our dev keys
216        sign_fit(sha_algo, sign_options)
217        run_bootm(sha_algo, 'signed images', 'dev+', True)
218
219        # Create a fresh .dtb without the public keys
220        dtc('sandbox-u-boot.dts')
221
222        cons.log.action('%s: Test FIT with signed configuration' % sha_algo)
223        make_fit('sign-configs-%s%s.its' % (sha_algo, padding))
224        run_bootm(sha_algo, 'unsigned config', '%s+ OK' % sha_algo, True)
225
226        # Sign images with our dev keys
227        sign_fit(sha_algo, sign_options)
228        run_bootm(sha_algo, 'signed config', 'dev+', True)
229
230        cons.log.action('%s: Check signed config on the host' % sha_algo)
231
232        util.run_and_log(cons, [fit_check_sign, '-f', fit, '-k', dtb])
233
234        if full_test:
235            # Make sure that U-Boot checks that the config is in the list of
236            # hashed nodes. If it isn't, a security bypass is possible.
237            ffit = '%stest.forged.fit' % tmpdir
238            shutil.copyfile(fit, ffit)
239            with open(ffit, 'rb') as fd:
240                root, strblock = vboot_forge.read_fdt(fd)
241            root, strblock = vboot_forge.manipulate(root, strblock)
242            with open(ffit, 'w+b') as fd:
243                vboot_forge.write_fdt(root, strblock, fd)
244            util.run_and_log_expect_exception(
245                cons, [fit_check_sign, '-f', ffit, '-k', dtb],
246                1, 'Failed to verify required signature')
247
248            run_bootm(sha_algo, 'forged config', 'Bad Data Hash', False, ffit)
249
250            # Try adding an evil root node. This should be detected.
251            efit = '%stest.evilf.fit' % tmpdir
252            shutil.copyfile(fit, efit)
253            vboot_evil.add_evil_node(fit, efit, evil_kernel, 'fakeroot')
254
255            util.run_and_log_expect_exception(
256                cons, [fit_check_sign, '-f', efit, '-k', dtb],
257                1, 'Failed to verify required signature')
258            run_bootm(sha_algo, 'evil fakeroot', 'Bad FIT kernel image format',
259                      False, efit)
260
261            # Try adding an @ to the kernel node name. This should be detected.
262            efit = '%stest.evilk.fit' % tmpdir
263            shutil.copyfile(fit, efit)
264            vboot_evil.add_evil_node(fit, efit, evil_kernel, 'kernel@')
265
266            msg = 'Signature checking prevents use of unit addresses (@) in nodes'
267            util.run_and_log_expect_exception(
268                cons, [fit_check_sign, '-f', efit, '-k', dtb],
269                1, msg)
270            run_bootm(sha_algo, 'evil kernel@', msg, False, efit)
271
272        # Create a new properly signed fit and replace header bytes
273        make_fit('sign-configs-%s%s.its' % (sha_algo, padding))
274        sign_fit(sha_algo, sign_options)
275        bcfg = u_boot_console.config.buildconfig
276        max_size = int(bcfg.get('config_fit_signature_max_size', 0x10000000), 0)
277        existing_size = replace_fit_totalsize(max_size + 1)
278        run_bootm(sha_algo, 'Signed config with bad hash', 'Bad Data Hash',
279                  False)
280        cons.log.action('%s: Check overflowed FIT header totalsize' % sha_algo)
281
282        # Replace with existing header bytes
283        replace_fit_totalsize(existing_size)
284        run_bootm(sha_algo, 'signed config', 'dev+', True)
285        cons.log.action('%s: Check default FIT header totalsize' % sha_algo)
286
287        # Increment the first byte of the signature, which should cause failure
288        sig = util.run_and_log(cons, 'fdtget -t bx %s %s value' %
289                               (fit, sig_node))
290        byte_list = sig.split()
291        byte = int(byte_list[0], 16)
292        byte_list[0] = '%x' % (byte + 1)
293        sig = ' '.join(byte_list)
294        util.run_and_log(cons, 'fdtput -t bx %s %s value %s' %
295                         (fit, sig_node, sig))
296
297        run_bootm(sha_algo, 'Signed config with bad hash', 'Bad Data Hash',
298                  False)
299
300        cons.log.action('%s: Check bad config on the host' % sha_algo)
301        util.run_and_log_expect_exception(
302            cons, [fit_check_sign, '-f', fit, '-k', dtb],
303            1, 'Failed to verify required signature')
304
305    def test_required_key(sha_algo, padding, sign_options):
306        """Test verified boot with the given hash algorithm.
307
308        This function tests if U-Boot rejects an image when a required key isn't
309        used to sign a FIT.
310
311        Args:
312            sha_algo: Either 'sha1' or 'sha256', to select the algorithm to use
313            padding: Either '' or '-pss', to select the padding to use for the
314                    rsa signature algorithm.
315            sign_options: Options to mkimage when signing a fit image.
316        """
317        # Compile our device tree files for kernel and U-Boot. These are
318        # regenerated here since mkimage will modify them (by adding a
319        # public key) below.
320        dtc('sandbox-kernel.dts')
321        dtc('sandbox-u-boot.dts')
322
323        cons.log.action('%s: Test FIT with configs images' % sha_algo)
324
325        # Build the FIT with prod key (keys required) and sign it. This puts the
326        # signature into sandbox-u-boot.dtb, marked 'required'
327        make_fit('sign-configs-%s%s-prod.its' % (sha_algo, padding))
328        sign_fit(sha_algo, sign_options)
329
330        # Build the FIT with dev key (keys NOT required). This adds the
331        # signature into sandbox-u-boot.dtb, NOT marked 'required'.
332        make_fit('sign-configs-%s%s.its' % (sha_algo, padding))
333        sign_fit_norequire(sha_algo, sign_options)
334
335        # So now sandbox-u-boot.dtb two signatures, for the prod and dev keys.
336        # Only the prod key is set as 'required'. But FIT we just built has
337        # a dev signature only (sign_fit_norequire() overwrites the FIT).
338        # Try to boot the FIT with dev key. This FIT should not be accepted by
339        # U-Boot because the prod key is required.
340        run_bootm(sha_algo, 'required key', '', False)
341
342        # Build the FIT with dev key (keys required) and sign it. This puts the
343        # signature into sandbox-u-boot.dtb, marked 'required'.
344        make_fit('sign-configs-%s%s.its' % (sha_algo, padding))
345        sign_fit(sha_algo, sign_options)
346
347        # Set the required-mode policy to "any".
348        # So now sandbox-u-boot.dtb two signatures, for the prod and dev keys.
349        # Both the dev and prod key are set as 'required'. But FIT we just built has
350        # a dev signature only (sign_fit() overwrites the FIT).
351        # Try to boot the FIT with dev key. This FIT should be accepted by
352        # U-Boot because the dev key is required and policy is "any" required key.
353        util.run_and_log(cons, 'fdtput -t s %s /signature required-mode any' %
354                         (dtb))
355        run_bootm(sha_algo, 'multi required key', 'dev+', True)
356
357        # Set the required-mode policy to "all".
358        # So now sandbox-u-boot.dtb two signatures, for the prod and dev keys.
359        # Both the dev and prod key are set as 'required'. But FIT we just built has
360        # a dev signature only (sign_fit() overwrites the FIT).
361        # Try to boot the FIT with dev key. This FIT should not be accepted by
362        # U-Boot because the prod key is required and policy is "all" required key
363        util.run_and_log(cons, 'fdtput -t s %s /signature required-mode all' %
364                         (dtb))
365        run_bootm(sha_algo, 'multi required key', '', False)
366
367    cons = u_boot_console
368    tmpdir = cons.config.result_dir + '/'
369    datadir = cons.config.source_dir + '/test/py/tests/vboot/'
370    fit = '%stest.fit' % tmpdir
371    mkimage = cons.config.build_dir + '/tools/mkimage'
372    fit_check_sign = cons.config.build_dir + '/tools/fit_check_sign'
373    dtc_args = '-I dts -O dtb -i %s' % tmpdir
374    dtb = '%ssandbox-u-boot.dtb' % tmpdir
375    sig_node = '/configurations/conf-1/signature'
376
377    create_rsa_pair('dev')
378    create_rsa_pair('prod')
379
380    # Create a number kernel image with zeroes
381    with open('%stest-kernel.bin' % tmpdir, 'wb') as fd:
382        fd.write(500 * b'\0')
383
384    # Create a second kernel image with ones
385    evil_kernel = '%stest-kernel1.bin' % tmpdir
386    with open(evil_kernel, 'wb') as fd:
387        fd.write(500 * b'\x01')
388
389    try:
390        # We need to use our own device tree file. Remember to restore it
391        # afterwards.
392        old_dtb = cons.config.dtb
393        cons.config.dtb = dtb
394        if required:
395            test_required_key(sha_algo, padding, sign_options)
396        else:
397            test_with_algo(sha_algo, padding, sign_options)
398    finally:
399        # Go back to the original U-Boot with the correct dtb.
400        cons.config.dtb = old_dtb
401        cons.restart_uboot()
402