test: vboot: Tidy up the code a little
[oweals/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 pytest
28 import sys
29 import struct
30 import u_boot_utils as util
31 import vboot_forge
32
33 TESTDATA = [
34     ['sha1', '', False],
35     ['sha1', '-pss', False],
36     ['sha256', '', False],
37     ['sha256', '-pss', False],
38     ['sha256', '-pss', True],
39 ]
40
41 @pytest.mark.boardspec('sandbox')
42 @pytest.mark.buildconfigspec('fit_signature')
43 @pytest.mark.requiredtool('dtc')
44 @pytest.mark.requiredtool('fdtget')
45 @pytest.mark.requiredtool('fdtput')
46 @pytest.mark.requiredtool('openssl')
47 @pytest.mark.parametrize("sha_algo,padding,required", TESTDATA)
48 def test_vboot(u_boot_console, sha_algo, padding, required):
49     """Test verified boot signing with mkimage and verification with 'bootm'.
50
51     This works using sandbox only as it needs to update the device tree used
52     by U-Boot to hold public keys from the signing process.
53
54     The SHA1 and SHA256 tests are combined into a single test since the
55     key-generation process is quite slow and we want to avoid doing it twice.
56     """
57     def dtc(dts):
58         """Run the device tree compiler to compile a .dts file
59
60         The output file will be the same as the input file but with a .dtb
61         extension.
62
63         Args:
64             dts: Device tree file to compile.
65         """
66         dtb = dts.replace('.dts', '.dtb')
67         util.run_and_log(cons, 'dtc %s %s%s -O dtb '
68                          '-o %s%s' % (dtc_args, datadir, dts, tmpdir, dtb))
69
70     def run_bootm(sha_algo, test_type, expect_string, boots):
71         """Run a 'bootm' command U-Boot.
72
73         This always starts a fresh U-Boot instance since the device tree may
74         contain a new public key.
75
76         Args:
77             test_type: A string identifying the test type.
78             expect_string: A string which is expected in the output.
79             sha_algo: Either 'sha1' or 'sha256', to select the algorithm to
80                     use.
81             boots: A boolean that is True if Linux should boot and False if
82                     we are expected to not boot
83         """
84         cons.restart_uboot()
85         with cons.log.section('Verified boot %s %s' % (sha_algo, test_type)):
86             output = cons.run_command_list(
87                 ['host load hostfs - 100 %stest.fit' % tmpdir,
88                 'fdt addr 100',
89                 'bootm 100'])
90         assert(expect_string in ''.join(output))
91         if boots:
92             assert('sandbox: continuing, as we cannot run' in ''.join(output))
93         else:
94             assert('sandbox: continuing, as we cannot run'
95                    not in ''.join(output))
96
97     def make_fit(its):
98         """Make a new FIT from the .its source file.
99
100         This runs 'mkimage -f' to create a new FIT.
101
102         Args:
103             its: Filename containing .its source.
104         """
105         util.run_and_log(cons, [mkimage, '-D', dtc_args, '-f',
106                                 '%s%s' % (datadir, its), fit])
107
108     def sign_fit(sha_algo):
109         """Sign the FIT
110
111         Signs the FIT and writes the signature into it. It also writes the
112         public key into the dtb.
113
114         Args:
115             sha_algo: Either 'sha1' or 'sha256', to select the algorithm to
116                     use.
117         """
118         cons.log.action('%s: Sign images' % sha_algo)
119         util.run_and_log(cons, [mkimage, '-F', '-k', tmpdir, '-K', dtb,
120                                 '-r', fit])
121
122     def sign_fit_norequire(sha_algo):
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         """
132         cons.log.action('%s: Sign images' % sha_algo)
133         util.run_and_log(cons, [mkimage, '-F', '-k', tmpdir, '-K', dtb,
134                                 fit])
135
136     def replace_fit_totalsize(size):
137         """Replace FIT header's totalsize with something greater.
138
139         The totalsize must be less than or equal to FIT_SIGNATURE_MAX_SIZE.
140         If the size is greater, the signature verification should return false.
141
142         Args:
143             size: The new totalsize of the header
144
145         Returns:
146             prev_size: The previous totalsize read from the header
147         """
148         total_size = 0
149         with open(fit, 'r+b') as handle:
150             handle.seek(4)
151             total_size = handle.read(4)
152             handle.seek(4)
153             handle.write(struct.pack(">I", size))
154         return struct.unpack(">I", total_size)[0]
155
156     def test_with_algo(sha_algo, padding):
157         """Test verified boot with the given hash algorithm.
158
159         This is the main part of the test code. The same procedure is followed
160         for both hashing algorithms.
161
162         Args:
163             sha_algo: Either 'sha1' or 'sha256', to select the algorithm to
164                     use.
165         """
166         # Compile our device tree files for kernel and U-Boot. These are
167         # regenerated here since mkimage will modify them (by adding a
168         # public key) below.
169         dtc('sandbox-kernel.dts')
170         dtc('sandbox-u-boot.dts')
171
172         # Build the FIT, but don't sign anything yet
173         cons.log.action('%s: Test FIT with signed images' % sha_algo)
174         make_fit('sign-images-%s%s.its' % (sha_algo , padding))
175         run_bootm(sha_algo, 'unsigned images', 'dev-', True)
176
177         # Sign images with our dev keys
178         sign_fit(sha_algo)
179         run_bootm(sha_algo, 'signed images', 'dev+', True)
180
181         # Create a fresh .dtb without the public keys
182         dtc('sandbox-u-boot.dts')
183
184         cons.log.action('%s: Test FIT with signed configuration' % sha_algo)
185         make_fit('sign-configs-%s%s.its' % (sha_algo , padding))
186         run_bootm(sha_algo, 'unsigned config', '%s+ OK' % sha_algo, True)
187
188         # Sign images with our dev keys
189         sign_fit(sha_algo)
190         run_bootm(sha_algo, 'signed config', 'dev+', True)
191
192         cons.log.action('%s: Check signed config on the host' % sha_algo)
193
194         util.run_and_log(cons, [fit_check_sign, '-f', fit, '-k', dtb])
195
196         # Make sure that U-Boot checks that the config is in the list of hashed
197         # nodes. If it isn't, a security bypass is possible.
198         with open(fit, 'rb') as fp:
199             root, strblock = vboot_forge.read_fdt(fp)
200         root, strblock = vboot_forge.manipulate(root, strblock)
201         with open(fit, 'w+b') as fp:
202             vboot_forge.write_fdt(root, strblock, fp)
203         util.run_and_log_expect_exception(cons,
204                 [fit_check_sign, '-f', fit, '-k', dtb],
205                 1, 'Failed to verify required signature')
206
207         run_bootm(sha_algo, 'forged config', 'Bad Data Hash', False)
208
209         # Create a new properly signed fit and replace header bytes
210         make_fit('sign-configs-%s%s.its' % (sha_algo, padding))
211         sign_fit(sha_algo)
212         bcfg = u_boot_console.config.buildconfig
213         max_size = int(bcfg.get('config_fit_signature_max_size', 0x10000000), 0)
214         existing_size = replace_fit_totalsize(max_size + 1)
215         run_bootm(sha_algo, 'Signed config with bad hash', 'Bad Data Hash',
216                   False)
217         cons.log.action('%s: Check overflowed FIT header totalsize' % sha_algo)
218
219         # Replace with existing header bytes
220         replace_fit_totalsize(existing_size)
221         run_bootm(sha_algo, 'signed config', 'dev+', True)
222         cons.log.action('%s: Check default FIT header totalsize' % sha_algo)
223
224         # Increment the first byte of the signature, which should cause failure
225         sig = util.run_and_log(cons, 'fdtget -t bx %s %s value' %
226                                (fit, sig_node))
227         byte_list = sig.split()
228         byte = int(byte_list[0], 16)
229         byte_list[0] = '%x' % (byte + 1)
230         sig = ' '.join(byte_list)
231         util.run_and_log(cons, 'fdtput -t bx %s %s value %s' %
232                          (fit, sig_node, sig))
233
234         run_bootm(sha_algo, 'Signed config with bad hash', 'Bad Data Hash',
235                   False)
236
237         cons.log.action('%s: Check bad config on the host' % sha_algo)
238         util.run_and_log_expect_exception(cons, [fit_check_sign, '-f', fit,
239                 '-k', dtb], 1, 'Failed to verify required signature')
240
241     def test_required_key(sha_algo, padding):
242         """Test verified boot with the given hash algorithm.
243
244         This function tests if U-Boot rejects an image when a required key isn't
245         used to sign a FIT.
246
247         Args:
248             sha_algo: Either 'sha1' or 'sha256', to select the algorithm to use
249         """
250         # Compile our device tree files for kernel and U-Boot. These are
251         # regenerated here since mkimage will modify them (by adding a
252         # public key) below.
253         dtc('sandbox-kernel.dts')
254         dtc('sandbox-u-boot.dts')
255
256         cons.log.action('%s: Test FIT with configs images' % sha_algo)
257
258         # Build the FIT with prod key (keys required) and sign it. This puts the
259         # signature into sandbox-u-boot.dtb, marked 'required'
260         make_fit('sign-configs-%s%s-prod.its' % (sha_algo , padding))
261         sign_fit(sha_algo)
262
263         # Build the FIT with dev key (keys NOT required). This adds the
264         # signature into sandbox-u-boot.dtb, NOT marked 'required'.
265         make_fit('sign-configs-%s%s.its' % (sha_algo , padding))
266         sign_fit(sha_algo)
267
268         # So now sandbox-u-boot.dtb two signatures, for the prod and dev keys.
269         # Only the prod key is set as 'required'. But FIT we just built has
270         # a dev signature only (sign_fit() overwrites the FIT).
271         # Try to boot the FIT with dev key. This FIT should not be accepted by
272         # U-Boot because the prod key is required.
273         run_bootm(sha_algo, 'required key', '', False)
274
275     cons = u_boot_console
276     tmpdir = cons.config.result_dir + '/'
277     tmp = tmpdir + 'vboot.tmp'
278     datadir = cons.config.source_dir + '/test/py/tests/vboot/'
279     fit = '%stest.fit' % tmpdir
280     mkimage = cons.config.build_dir + '/tools/mkimage'
281     fit_check_sign = cons.config.build_dir + '/tools/fit_check_sign'
282     dtc_args = '-I dts -O dtb -i %s' % tmpdir
283     dtb = '%ssandbox-u-boot.dtb' % tmpdir
284     sig_node = '/configurations/conf-1/signature'
285
286     # Create an RSA key pair
287     public_exponent = 65537
288     util.run_and_log(cons, 'openssl genpkey -algorithm RSA -out %sdev.key '
289                      '-pkeyopt rsa_keygen_bits:2048 '
290                      '-pkeyopt rsa_keygen_pubexp:%d' %
291                      (tmpdir, public_exponent))
292
293     # Create a certificate containing the public key
294     util.run_and_log(cons, 'openssl req -batch -new -x509 -key %sdev.key -out '
295                      '%sdev.crt' % (tmpdir, tmpdir))
296
297     # Create an RSA key pair (prod)
298     public_exponent = 65537
299     util.run_and_log(cons, 'openssl genpkey -algorithm RSA -out %sprod.key '
300                      '-pkeyopt rsa_keygen_bits:2048 '
301                      '-pkeyopt rsa_keygen_pubexp:%d' %
302                      (tmpdir, public_exponent))
303
304     # Create a certificate containing the public key (prod)
305     util.run_and_log(cons, 'openssl req -batch -new -x509 -key %sprod.key -out '
306                      '%sprod.crt' % (tmpdir, tmpdir))
307
308     # Create a number kernel image with zeroes
309     with open('%stest-kernel.bin' % tmpdir, 'w') as fd:
310         fd.write(5000 * chr(0))
311
312     try:
313         # We need to use our own device tree file. Remember to restore it
314         # afterwards.
315         old_dtb = cons.config.dtb
316         cons.config.dtb = dtb
317         if required:
318             test_required_key(sha_algo, padding)
319         else:
320             test_with_algo(sha_algo, padding)
321     finally:
322         # Go back to the original U-Boot with the correct dtb.
323         cons.config.dtb = old_dtb
324         cons.restart_uboot()