From ba557707d844cbc9306a9915f1fbbf0458e40403 Mon Sep 17 00:00:00 2001 From: Tao Bao Date: Sat, 10 Mar 2018 20:41:16 -0800 Subject: [PATCH] releasetools: Support validating Verified Boot images. For a given (signed) target-files.zip, this CLs allows verifying the Verified Boot related images. It works with both of VB 1.0 and VB 2.0 images. As part of the CL, it also moves validate_target_files.py to argparse, which is more flexible than the traditional getopt module. Also add unittests for the VB 1.0 path. VB 2.0 tests will be added in follow-up CL. Example usage: - Run the script on aosp_bullhead target-files.zip. $ ./build/make/tools/releasetools/validate_target_files.py \ --verity_key build/target/product/security/verity.x509.pem \ --verity_key_mincrypt build/target/product/security/verity_key \ aosp_bullhead-target_files-4522605.zip - Run the script on aosp_walleye target-files.zip. $ ./build/make/tools/releasetools/validate_target_files.py \ --verity_key external/avb/test/data/testkey_rsa4096.pem \ aosp_walleye-target_files-4627254.zip Bug: 63706333 Bug: 65486807 Test: Run validate_target_files.py on target_files.zip files. Test: PYTHONPATH=build/make/tools/releasetools python -m unittest \ test_validate_target_files Change-Id: I170f14d5828d15f3687d8af0a89a816968069057 --- .../test_validate_target_files.py | 180 ++++++++++++++++++ tools/releasetools/testdata/testkey_mincrypt | Bin 0 -> 524 bytes tools/releasetools/validate_target_files.py | 160 ++++++++++++++-- 3 files changed, 324 insertions(+), 16 deletions(-) create mode 100644 tools/releasetools/test_validate_target_files.py create mode 100644 tools/releasetools/testdata/testkey_mincrypt diff --git a/tools/releasetools/test_validate_target_files.py b/tools/releasetools/test_validate_target_files.py new file mode 100644 index 000000000..bae648f22 --- /dev/null +++ b/tools/releasetools/test_validate_target_files.py @@ -0,0 +1,180 @@ +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Unittests for validate_target_files.py. + +Note: This file calls functions in build_image.py that hard-code the path in +relative to ANDROID_BUILD_TOP (e.g. +system/extras/verity/build_verity_metadata.py). So the test needs to be +triggered under ANDROID_BUILD_TOP or the top-level OTA tools directory (i.e. +the one after unzipping otatools.zip). + + (from ANDROID_BUILD_TOP) + $ PYTHONPATH=build/make/tools/releasetools python -m unittest \\ + test_validate_target_files + + (from OTA tools directory) + $ PYTHONPATH=releasetools python -m unittest test_validate_target_files +""" + +from __future__ import print_function + +import os +import os.path +import shutil +import subprocess +import unittest + +import build_image +import common +import test_utils +from validate_target_files import ValidateVerifiedBootImages + + +class ValidateTargetFilesTest(unittest.TestCase): + + def setUp(self): + self.testdata_dir = test_utils.get_testdata_dir() + + def tearDown(self): + common.Cleanup() + + def _generate_boot_image(self, output_file): + kernel = common.MakeTempFile(prefix='kernel-') + with open(kernel, 'wb') as kernel_fp: + kernel_fp.write(os.urandom(10)) + + cmd = ['mkbootimg', '--kernel', kernel, '-o', output_file] + proc = common.Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + stdoutdata, _ = proc.communicate() + self.assertEqual( + 0, proc.returncode, + "Failed to run mkbootimg: {}".format(stdoutdata)) + + cmd = ['boot_signer', '/boot', output_file, + os.path.join(self.testdata_dir, 'testkey.pk8'), + os.path.join(self.testdata_dir, 'testkey.x509.pem'), output_file] + proc = common.Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + stdoutdata, _ = proc.communicate() + self.assertEqual( + 0, proc.returncode, + "Failed to sign boot image with boot_signer: {}".format(stdoutdata)) + + def test_ValidateVerifiedBootImages_bootImage(self): + input_tmp = common.MakeTempDir() + os.mkdir(os.path.join(input_tmp, 'IMAGES')) + boot_image = os.path.join(input_tmp, 'IMAGES', 'boot.img') + self._generate_boot_image(boot_image) + + info_dict = { + 'boot_signer' : 'true', + } + options = { + 'verity_key' : os.path.join(self.testdata_dir, 'testkey.x509.pem'), + } + ValidateVerifiedBootImages(input_tmp, info_dict, options) + + def test_ValidateVerifiedBootImages_bootImage_wrongKey(self): + input_tmp = common.MakeTempDir() + os.mkdir(os.path.join(input_tmp, 'IMAGES')) + boot_image = os.path.join(input_tmp, 'IMAGES', 'boot.img') + self._generate_boot_image(boot_image) + + info_dict = { + 'boot_signer' : 'true', + } + options = { + 'verity_key' : os.path.join(self.testdata_dir, 'verity.x509.pem'), + } + self.assertRaises( + AssertionError, ValidateVerifiedBootImages, input_tmp, info_dict, + options) + + def test_ValidateVerifiedBootImages_bootImage_corrupted(self): + input_tmp = common.MakeTempDir() + os.mkdir(os.path.join(input_tmp, 'IMAGES')) + boot_image = os.path.join(input_tmp, 'IMAGES', 'boot.img') + self._generate_boot_image(boot_image) + + # Corrupt the late byte of the image. + with open(boot_image, 'r+b') as boot_fp: + boot_fp.seek(-1, os.SEEK_END) + last_byte = boot_fp.read(1) + last_byte = chr(255 - ord(last_byte)) + boot_fp.seek(-1, os.SEEK_END) + boot_fp.write(last_byte) + + info_dict = { + 'boot_signer' : 'true', + } + options = { + 'verity_key' : os.path.join(self.testdata_dir, 'testkey.x509.pem'), + } + self.assertRaises( + AssertionError, ValidateVerifiedBootImages, input_tmp, info_dict, + options) + + def _generate_system_image(self, output_file): + verity_fec = True + partition_size = 1024 * 1024 + adjusted_size, verity_size = build_image.AdjustPartitionSizeForVerity( + partition_size, verity_fec) + + # Use an empty root directory. + system_root = common.MakeTempDir() + cmd = ['mkuserimg_mke2fs.sh', '-s', system_root, output_file, 'ext4', + '/system', str(adjusted_size), '-j', '0'] + proc = common.Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + stdoutdata, _ = proc.communicate() + self.assertEqual( + 0, proc.returncode, + "Failed to create system image with mkuserimg_mke2fs.sh: {}".format( + stdoutdata)) + + # Append the verity metadata. + prop_dict = { + 'original_partition_size' : str(partition_size), + 'partition_size' : str(adjusted_size), + 'verity_block_device' : '/dev/block/system', + 'verity_key' : os.path.join(self.testdata_dir, 'testkey'), + 'verity_signer_cmd' : 'verity_signer', + 'verity_size' : str(verity_size), + } + self.assertTrue( + build_image.MakeVerityEnabledImage(output_file, verity_fec, prop_dict)) + + def test_ValidateVerifiedBootImages_systemImage(self): + input_tmp = common.MakeTempDir() + os.mkdir(os.path.join(input_tmp, 'IMAGES')) + system_image = os.path.join(input_tmp, 'IMAGES', 'system.img') + self._generate_system_image(system_image) + + # Pack the verity key. + verity_key_mincrypt = os.path.join( + input_tmp, 'BOOT', 'RAMDISK', 'verity_key') + os.makedirs(os.path.dirname(verity_key_mincrypt)) + shutil.copyfile( + os.path.join(self.testdata_dir, 'testkey_mincrypt'), + verity_key_mincrypt) + + info_dict = { + 'verity' : 'true', + } + options = { + 'verity_key' : os.path.join(self.testdata_dir, 'testkey.x509.pem'), + 'verity_key_mincrypt' : verity_key_mincrypt, + } + ValidateVerifiedBootImages(input_tmp, info_dict, options) diff --git a/tools/releasetools/testdata/testkey_mincrypt b/tools/releasetools/testdata/testkey_mincrypt new file mode 100644 index 0000000000000000000000000000000000000000..7f5d31b5c4ba715f8f28496cc939f595d9ae96e3 GIT binary patch literal 524 zcmV+n0`vVq0001cbU&|6wf#Nvkf{j1_JaG)*11o2iNWdsG`!yCw3f9mP}r;O1#ii)goSTGMw-y24Z7KBv8Ufn1i01lSyUva=n=O zZXhkMA}4dEp+G6GpJSHSPclayjaasS;cdUxNj_2lSpoKMzhJ_Xkk1^OWKjnV9#K&s z&sq6EZ=UVBJZJ)UzV6s{qd(b3zQX_+(=rxZu>yizoYm|AKpHl-j4#05(x8XaO6W}5=xmgy*av67bC%sMOf~09K;5i#hqr+b z9LGJei}X|;Q#xS9{aW?fF4Kj_xz&amvoi|DMVrGHG7n&q#=K&@yn477$ikok9Kf~j90HW`ph-+T zWiLBbW4QkxNt0DMJS~<2#t-qXOD z^lV@LUbgi&aeOQwgcytQRjO!UDi2s9oS0Y*CrxB7Azvqz-LZS^u)4Kd#x7t^B-V(} OYv6fzSh5!Z00987#{-T4 literal 0 HcmV?d00001 diff --git a/tools/releasetools/validate_target_files.py b/tools/releasetools/validate_target_files.py index db1ba2eb6..e8cea298a 100755 --- a/tools/releasetools/validate_target_files.py +++ b/tools/releasetools/validate_target_files.py @@ -17,16 +17,25 @@ """ Validate a given (signed) target_files.zip. -It performs checks to ensure the integrity of the input zip. +It performs the following checks to assert the integrity of the input zip. + - It verifies the file consistency between the ones in IMAGES/system.img (read via IMAGES/system.map) and the ones under unpacked folder of SYSTEM/. The same check also applies to the vendor image if present. + + - It verifies the install-recovery script consistency, by comparing the + checksums in the script against the ones of IMAGES/{boot,recovery}.img. + + - It verifies the signed Verified Boot related images, for both of Verified + Boot 1.0 and 2.0 (aka AVB). """ +import argparse +import filecmp import logging import os.path import re -import sys +import subprocess import zipfile import common @@ -177,33 +186,152 @@ def ValidateInstallRecoveryScript(input_tmp, info_dict): logging.info('Done checking %s', script_path) -def main(argv): - def option_handler(): - return True +def ValidateVerifiedBootImages(input_tmp, info_dict, options): + """Validates the Verified Boot related images. - args = common.ParseOptions( - argv, __doc__, extra_opts="", - extra_long_opts=[], - extra_option_handler=option_handler) + For Verified Boot 1.0, it verifies the signatures of the bootable images + (boot/recovery etc), as well as the dm-verity metadata in system images + (system/vendor/product). For Verified Boot 2.0, it calls avbtool to verify + vbmeta.img, which in turn verifies all the descriptors listed in vbmeta. - if len(args) != 1: - common.Usage(__doc__) - sys.exit(1) + Args: + input_tmp: The top-level directory of unpacked target-files.zip. + info_dict: The loaded info dict. + options: A dict that contains the user-supplied public keys to be used for + image verification. In particular, 'verity_key' is used to verify the + bootable images in VB 1.0, and the vbmeta image in VB 2.0, where + applicable. 'verity_key_mincrypt' will be used to verify the system + images in VB 1.0. + + Raises: + AssertionError: On any verification failure. + """ + # Verified boot 1.0 (images signed with boot_signer and verity_signer). + if info_dict.get('boot_signer') == 'true': + logging.info('Verifying Verified Boot images...') + + # Verify the boot/recovery images (signed with boot_signer), against the + # given X.509 encoded pubkey (or falling back to the one in the info_dict if + # none given). + verity_key = options['verity_key'] + if verity_key is None: + verity_key = info_dict['verity_key'] + '.x509.pem' + for image in ('boot.img', 'recovery.img', 'recovery-two-step.img'): + image_path = os.path.join(input_tmp, 'IMAGES', image) + if not os.path.exists(image_path): + continue + + cmd = ['boot_signer', '-verify', image_path, '-certificate', verity_key] + proc = common.Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + stdoutdata, _ = proc.communicate() + assert proc.returncode == 0, \ + 'Failed to verify {} with boot_signer:\n{}'.format(image, stdoutdata) + logging.info( + 'Verified %s with boot_signer (key: %s):\n%s', image, verity_key, + stdoutdata.rstrip()) + + # Verify verity signed system images in Verified Boot 1.0. Note that not using + # 'elif' here, since 'boot_signer' and 'verity' are not bundled in VB 1.0. + if info_dict.get('verity') == 'true': + # First verify that the verity key that's built into the root image (as + # /verity_key) matches the one given via command line, if any. + if info_dict.get("system_root_image") == "true": + verity_key_mincrypt = os.path.join(input_tmp, 'ROOT', 'verity_key') + else: + verity_key_mincrypt = os.path.join( + input_tmp, 'BOOT', 'RAMDISK', 'verity_key') + assert os.path.exists(verity_key_mincrypt), 'Missing verity_key' + + if options['verity_key_mincrypt'] is None: + logging.warn( + 'Skipped checking the content of /verity_key, as the key file not ' + 'provided. Use --verity_key_mincrypt to specify.') + else: + expected_key = options['verity_key_mincrypt'] + assert filecmp.cmp(expected_key, verity_key_mincrypt, shallow=False), \ + "Mismatching mincrypt verity key files" + logging.info('Verified the content of /verity_key') + + # Then verify the verity signed system/vendor/product images, against the + # verity pubkey in mincrypt format. + for image in ('system.img', 'vendor.img', 'product.img'): + image_path = os.path.join(input_tmp, 'IMAGES', image) + + # We are not checking if the image is actually enabled via info_dict (e.g. + # 'system_verity_block_device=...'). Because it's most likely a bug that + # skips signing some of the images in signed target-files.zip, while + # having the top-level verity flag enabled. + if not os.path.exists(image_path): + continue + + cmd = ['verity_verifier', image_path, '-mincrypt', verity_key_mincrypt] + proc = common.Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + stdoutdata, _ = proc.communicate() + assert proc.returncode == 0, \ + 'Failed to verify {} with verity_verifier (key: {}):\n{}'.format( + image, verity_key_mincrypt, stdoutdata) + logging.info( + 'Verified %s with verity_verifier (key: %s):\n%s', image, + verity_key_mincrypt, stdoutdata.rstrip()) + + # Handle the case of Verified Boot 2.0 (AVB). + if info_dict.get("avb_enable") == "true": + logging.info('Verifying Verified Boot 2.0 (AVB) images...') + + key = options['verity_key'] + if key is None: + key = info_dict['avb_vbmeta_key_path'] + # avbtool verifies all the images that have descriptors listed in vbmeta. + image = os.path.join(input_tmp, 'IMAGES', 'vbmeta.img') + cmd = ['avbtool', 'verify_image', '--image', image, '--key', key] + proc = common.Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + stdoutdata, _ = proc.communicate() + assert proc.returncode == 0, \ + 'Failed to verify {} with verity_verifier (key: {}):\n{}'.format( + image, key, stdoutdata) + + logging.info( + 'Verified %s with avbtool (key: %s):\n%s', image, key, + stdoutdata.rstrip()) + + +def main(): + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument( + 'target_files', + help='the input target_files.zip to be validated') + parser.add_argument( + '--verity_key', + help='the verity public key to verify the bootable images (Verified ' + 'Boot 1.0), or the vbmeta image (Verified Boot 2.0), where ' + 'applicable') + parser.add_argument( + '--verity_key_mincrypt', + help='the verity public key in mincrypt format to verify the system ' + 'images, if target using Verified Boot 1.0') + args = parser.parse_args() + + # Unprovided args will have 'None' as the value. + options = vars(args) logging_format = '%(asctime)s - %(filename)s - %(levelname)-8s: %(message)s' date_format = '%Y/%m/%d %H:%M:%S' logging.basicConfig(level=logging.INFO, format=logging_format, datefmt=date_format) - logging.info("Unzipping the input target_files.zip: %s", args[0]) - input_tmp = common.UnzipTemp(args[0]) + logging.info("Unzipping the input target_files.zip: %s", args.target_files) + input_tmp = common.UnzipTemp(args.target_files) - with zipfile.ZipFile(args[0], 'r') as input_zip: + with zipfile.ZipFile(args.target_files, 'r') as input_zip: ValidateFileConsistency(input_zip, input_tmp) info_dict = common.LoadInfoDict(input_tmp) ValidateInstallRecoveryScript(input_tmp, info_dict) + ValidateVerifiedBootImages(input_tmp, info_dict, options) + # TODO: Check if the OTA keys have been properly updated (the ones on /system, # in recovery image). @@ -212,6 +340,6 @@ def main(argv): if __name__ == '__main__': try: - main(sys.argv[1:]) + main() finally: common.Cleanup()