Merge tag 'xilinx-for-v2021.04-rc3' of https://gitlab.denx.de/u-boot/custodians/u...
[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 shutil
28 import struct
29 import pytest
30 import u_boot_utils as util
31 import vboot_forge
32 import vboot_evil
33
34 # Only run the full suite on a few combinations, since it doesn't add any more
35 # test coverage.
36 TESTDATA = [
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)
57 def 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()