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