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