Home | History | Annotate | Download | only in releasetools
      1 #!/usr/bin/env python
      2 
      3 # Copyright (C) 2017 The Android Open Source Project
      4 #
      5 # Licensed under the Apache License, Version 2.0 (the "License");
      6 # you may not use this file except in compliance with the License.
      7 # You may obtain a copy of the License at
      8 #
      9 #      http://www.apache.org/licenses/LICENSE-2.0
     10 #
     11 # Unless required by applicable law or agreed to in writing, software
     12 # distributed under the License is distributed on an "AS IS" BASIS,
     13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14 # See the License for the specific language governing permissions and
     15 # limitations under the License.
     16 
     17 """
     18 Validate a given (signed) target_files.zip.
     19 
     20 It performs the following checks to assert the integrity of the input zip.
     21 
     22  - It verifies the file consistency between the ones in IMAGES/system.img (read
     23    via IMAGES/system.map) and the ones under unpacked folder of SYSTEM/. The
     24    same check also applies to the vendor image if present.
     25 
     26  - It verifies the install-recovery script consistency, by comparing the
     27    checksums in the script against the ones of IMAGES/{boot,recovery}.img.
     28 
     29  - It verifies the signed Verified Boot related images, for both of Verified
     30    Boot 1.0 and 2.0 (aka AVB).
     31 """
     32 
     33 import argparse
     34 import filecmp
     35 import logging
     36 import os.path
     37 import re
     38 import zipfile
     39 
     40 import common
     41 
     42 
     43 def _ReadFile(file_name, unpacked_name, round_up=False):
     44   """Constructs and returns a File object. Rounds up its size if needed."""
     45 
     46   assert os.path.exists(unpacked_name)
     47   with open(unpacked_name, 'r') as f:
     48     file_data = f.read()
     49   file_size = len(file_data)
     50   if round_up:
     51     file_size_rounded_up = common.RoundUpTo4K(file_size)
     52     file_data += '\0' * (file_size_rounded_up - file_size)
     53   return common.File(file_name, file_data)
     54 
     55 
     56 def ValidateFileAgainstSha1(input_tmp, file_name, file_path, expected_sha1):
     57   """Check if the file has the expected SHA-1."""
     58 
     59   logging.info('Validating the SHA-1 of %s', file_name)
     60   unpacked_name = os.path.join(input_tmp, file_path)
     61   assert os.path.exists(unpacked_name)
     62   actual_sha1 = _ReadFile(file_name, unpacked_name, False).sha1
     63   assert actual_sha1 == expected_sha1, \
     64       'SHA-1 mismatches for {}. actual {}, expected {}'.format(
     65           file_name, actual_sha1, expected_sha1)
     66 
     67 
     68 def ValidateFileConsistency(input_zip, input_tmp, info_dict):
     69   """Compare the files from image files and unpacked folders."""
     70 
     71   def CheckAllFiles(which):
     72     logging.info('Checking %s image.', which)
     73     # Allow having shared blocks when loading the sparse image, because allowing
     74     # that doesn't affect the checks below (we will have all the blocks on file,
     75     # unless it's skipped due to the holes).
     76     image = common.GetSparseImage(which, input_tmp, input_zip, True)
     77     prefix = '/' + which
     78     for entry in image.file_map:
     79       # Skip entries like '__NONZERO-0'.
     80       if not entry.startswith(prefix):
     81         continue
     82 
     83       # Read the blocks that the file resides. Note that it will contain the
     84       # bytes past the file length, which is expected to be padded with '\0's.
     85       ranges = image.file_map[entry]
     86 
     87       # Use the original RangeSet if applicable, which includes the shared
     88       # blocks. And this needs to happen before checking the monotonicity flag.
     89       if ranges.extra.get('uses_shared_blocks'):
     90         file_ranges = ranges.extra['uses_shared_blocks']
     91       else:
     92         file_ranges = ranges
     93 
     94       incomplete = file_ranges.extra.get('incomplete', False)
     95       if incomplete:
     96         logging.warning('Skipping %s that has incomplete block list', entry)
     97         continue
     98 
     99       # TODO(b/79951650): Handle files with non-monotonic ranges.
    100       if not file_ranges.monotonic:
    101         logging.warning(
    102             'Skipping %s that has non-monotonic ranges: %s', entry, file_ranges)
    103         continue
    104 
    105       blocks_sha1 = image.RangeSha1(file_ranges)
    106 
    107       # The filename under unpacked directory, such as SYSTEM/bin/sh.
    108       unpacked_name = os.path.join(
    109           input_tmp, which.upper(), entry[(len(prefix) + 1):])
    110       unpacked_file = _ReadFile(entry, unpacked_name, True)
    111       file_sha1 = unpacked_file.sha1
    112       assert blocks_sha1 == file_sha1, \
    113           'file: %s, range: %s, blocks_sha1: %s, file_sha1: %s' % (
    114               entry, file_ranges, blocks_sha1, file_sha1)
    115 
    116   logging.info('Validating file consistency.')
    117 
    118   # TODO(b/79617342): Validate non-sparse images.
    119   if info_dict.get('extfs_sparse_flag') != '-s':
    120     logging.warning('Skipped due to target using non-sparse images')
    121     return
    122 
    123   # Verify IMAGES/system.img.
    124   CheckAllFiles('system')
    125 
    126   # Verify IMAGES/vendor.img if applicable.
    127   if 'VENDOR/' in input_zip.namelist():
    128     CheckAllFiles('vendor')
    129 
    130   # Not checking IMAGES/system_other.img since it doesn't have the map file.
    131 
    132 
    133 def ValidateInstallRecoveryScript(input_tmp, info_dict):
    134   """Validate the SHA-1 embedded in install-recovery.sh.
    135 
    136   install-recovery.sh is written in common.py and has the following format:
    137 
    138   1. full recovery:
    139   ...
    140   if ! applypatch --check type:device:size:sha1; then
    141     applypatch --flash /system/etc/recovery.img \\
    142         type:device:size:sha1 && \\
    143   ...
    144 
    145   2. recovery from boot:
    146   ...
    147   if ! applypatch --check type:recovery_device:recovery_size:recovery_sha1; then
    148     applypatch [--bonus bonus_args] \\
    149         --patch /system/recovery-from-boot.p \\
    150         --source type:boot_device:boot_size:boot_sha1 \\
    151         --target type:recovery_device:recovery_size:recovery_sha1 && \\
    152   ...
    153 
    154   For full recovery, we want to calculate the SHA-1 of /system/etc/recovery.img
    155   and compare it against the one embedded in the script. While for recovery
    156   from boot, we want to check the SHA-1 for both recovery.img and boot.img
    157   under IMAGES/.
    158   """
    159 
    160   script_path = 'SYSTEM/bin/install-recovery.sh'
    161   if not os.path.exists(os.path.join(input_tmp, script_path)):
    162     logging.info('%s does not exist in input_tmp', script_path)
    163     return
    164 
    165   logging.info('Checking %s', script_path)
    166   with open(os.path.join(input_tmp, script_path), 'r') as script:
    167     lines = script.read().strip().split('\n')
    168   assert len(lines) >= 10
    169   check_cmd = re.search(r'if ! applypatch --check (\w+:.+:\w+:\w+);',
    170                         lines[1].strip())
    171   check_partition = check_cmd.group(1)
    172   assert len(check_partition.split(':')) == 4
    173 
    174   full_recovery_image = info_dict.get("full_recovery_image") == "true"
    175   if full_recovery_image:
    176     assert len(lines) == 10, "Invalid line count: {}".format(lines)
    177 
    178     # Expect something like "EMMC:/dev/block/recovery:28:5f9c..62e3".
    179     target = re.search(r'--target (.+) &&', lines[4].strip())
    180     assert target is not None, \
    181         "Failed to parse target line \"{}\"".format(lines[4])
    182     flash_partition = target.group(1)
    183 
    184     # Check we have the same recovery target in the check and flash commands.
    185     assert check_partition == flash_partition, \
    186         "Mismatching targets: {} vs {}".format(check_partition, flash_partition)
    187 
    188     # Validate the SHA-1 of the recovery image.
    189     recovery_sha1 = flash_partition.split(':')[3]
    190     ValidateFileAgainstSha1(
    191         input_tmp, 'recovery.img', 'SYSTEM/etc/recovery.img', recovery_sha1)
    192   else:
    193     assert len(lines) == 11, "Invalid line count: {}".format(lines)
    194 
    195     # --source boot_type:boot_device:boot_size:boot_sha1
    196     source = re.search(r'--source (\w+:.+:\w+:\w+) \\', lines[4].strip())
    197     assert source is not None, \
    198         "Failed to parse source line \"{}\"".format(lines[4])
    199 
    200     source_partition = source.group(1)
    201     source_info = source_partition.split(':')
    202     assert len(source_info) == 4, \
    203         "Invalid source partition: {}".format(source_partition)
    204     ValidateFileAgainstSha1(input_tmp, file_name='boot.img',
    205                             file_path='IMAGES/boot.img',
    206                             expected_sha1=source_info[3])
    207 
    208     # --target recovery_type:recovery_device:recovery_size:recovery_sha1
    209     target = re.search(r'--target (\w+:.+:\w+:\w+) && \\', lines[5].strip())
    210     assert target is not None, \
    211         "Failed to parse target line \"{}\"".format(lines[5])
    212     target_partition = target.group(1)
    213 
    214     # Check we have the same recovery target in the check and patch commands.
    215     assert check_partition == target_partition, \
    216         "Mismatching targets: {} vs {}".format(
    217             check_partition, target_partition)
    218 
    219     recovery_info = target_partition.split(':')
    220     assert len(recovery_info) == 4, \
    221         "Invalid target partition: {}".format(target_partition)
    222     ValidateFileAgainstSha1(input_tmp, file_name='recovery.img',
    223                             file_path='IMAGES/recovery.img',
    224                             expected_sha1=recovery_info[3])
    225 
    226   logging.info('Done checking %s', script_path)
    227 
    228 
    229 def ValidateVerifiedBootImages(input_tmp, info_dict, options):
    230   """Validates the Verified Boot related images.
    231 
    232   For Verified Boot 1.0, it verifies the signatures of the bootable images
    233   (boot/recovery etc), as well as the dm-verity metadata in system images
    234   (system/vendor/product). For Verified Boot 2.0, it calls avbtool to verify
    235   vbmeta.img, which in turn verifies all the descriptors listed in vbmeta.
    236 
    237   Args:
    238     input_tmp: The top-level directory of unpacked target-files.zip.
    239     info_dict: The loaded info dict.
    240     options: A dict that contains the user-supplied public keys to be used for
    241         image verification. In particular, 'verity_key' is used to verify the
    242         bootable images in VB 1.0, and the vbmeta image in VB 2.0, where
    243         applicable. 'verity_key_mincrypt' will be used to verify the system
    244         images in VB 1.0.
    245 
    246   Raises:
    247     AssertionError: On any verification failure.
    248   """
    249   # Verified boot 1.0 (images signed with boot_signer and verity_signer).
    250   if info_dict.get('boot_signer') == 'true':
    251     logging.info('Verifying Verified Boot images...')
    252 
    253     # Verify the boot/recovery images (signed with boot_signer), against the
    254     # given X.509 encoded pubkey (or falling back to the one in the info_dict if
    255     # none given).
    256     verity_key = options['verity_key']
    257     if verity_key is None:
    258       verity_key = info_dict['verity_key'] + '.x509.pem'
    259     for image in ('boot.img', 'recovery.img', 'recovery-two-step.img'):
    260       image_path = os.path.join(input_tmp, 'IMAGES', image)
    261       if not os.path.exists(image_path):
    262         continue
    263 
    264       cmd = ['boot_signer', '-verify', image_path, '-certificate', verity_key]
    265       proc = common.Run(cmd)
    266       stdoutdata, _ = proc.communicate()
    267       assert proc.returncode == 0, \
    268           'Failed to verify {} with boot_signer:\n{}'.format(image, stdoutdata)
    269       logging.info(
    270           'Verified %s with boot_signer (key: %s):\n%s', image, verity_key,
    271           stdoutdata.rstrip())
    272 
    273   # Verify verity signed system images in Verified Boot 1.0. Note that not using
    274   # 'elif' here, since 'boot_signer' and 'verity' are not bundled in VB 1.0.
    275   if info_dict.get('verity') == 'true':
    276     # First verify that the verity key that's built into the root image (as
    277     # /verity_key) matches the one given via command line, if any.
    278     if info_dict.get("system_root_image") == "true":
    279       verity_key_mincrypt = os.path.join(input_tmp, 'ROOT', 'verity_key')
    280     else:
    281       verity_key_mincrypt = os.path.join(
    282           input_tmp, 'BOOT', 'RAMDISK', 'verity_key')
    283     assert os.path.exists(verity_key_mincrypt), 'Missing verity_key'
    284 
    285     if options['verity_key_mincrypt'] is None:
    286       logging.warn(
    287           'Skipped checking the content of /verity_key, as the key file not '
    288           'provided. Use --verity_key_mincrypt to specify.')
    289     else:
    290       expected_key = options['verity_key_mincrypt']
    291       assert filecmp.cmp(expected_key, verity_key_mincrypt, shallow=False), \
    292           "Mismatching mincrypt verity key files"
    293       logging.info('Verified the content of /verity_key')
    294 
    295     # Then verify the verity signed system/vendor/product images, against the
    296     # verity pubkey in mincrypt format.
    297     for image in ('system.img', 'vendor.img', 'product.img'):
    298       image_path = os.path.join(input_tmp, 'IMAGES', image)
    299 
    300       # We are not checking if the image is actually enabled via info_dict (e.g.
    301       # 'system_verity_block_device=...'). Because it's most likely a bug that
    302       # skips signing some of the images in signed target-files.zip, while
    303       # having the top-level verity flag enabled.
    304       if not os.path.exists(image_path):
    305         continue
    306 
    307       cmd = ['verity_verifier', image_path, '-mincrypt', verity_key_mincrypt]
    308       proc = common.Run(cmd)
    309       stdoutdata, _ = proc.communicate()
    310       assert proc.returncode == 0, \
    311           'Failed to verify {} with verity_verifier (key: {}):\n{}'.format(
    312               image, verity_key_mincrypt, stdoutdata)
    313       logging.info(
    314           'Verified %s with verity_verifier (key: %s):\n%s', image,
    315           verity_key_mincrypt, stdoutdata.rstrip())
    316 
    317   # Handle the case of Verified Boot 2.0 (AVB).
    318   if info_dict.get("avb_enable") == "true":
    319     logging.info('Verifying Verified Boot 2.0 (AVB) images...')
    320 
    321     key = options['verity_key']
    322     if key is None:
    323       key = info_dict['avb_vbmeta_key_path']
    324 
    325     # avbtool verifies all the images that have descriptors listed in vbmeta.
    326     image = os.path.join(input_tmp, 'IMAGES', 'vbmeta.img')
    327     cmd = ['avbtool', 'verify_image', '--image', image, '--key', key]
    328 
    329     # Append the args for chained partitions if any.
    330     for partition in common.AVB_PARTITIONS:
    331       key_name = 'avb_' + partition + '_key_path'
    332       if info_dict.get(key_name) is not None:
    333         chained_partition_arg = common.GetAvbChainedPartitionArg(
    334             partition, info_dict, options[key_name])
    335         cmd.extend(["--expected_chain_partition", chained_partition_arg])
    336 
    337     proc = common.Run(cmd)
    338     stdoutdata, _ = proc.communicate()
    339     assert proc.returncode == 0, \
    340         'Failed to verify {} with avbtool (key: {}):\n{}'.format(
    341             image, key, stdoutdata)
    342 
    343     logging.info(
    344         'Verified %s with avbtool (key: %s):\n%s', image, key,
    345         stdoutdata.rstrip())
    346 
    347 
    348 def main():
    349   parser = argparse.ArgumentParser(
    350       description=__doc__,
    351       formatter_class=argparse.RawDescriptionHelpFormatter)
    352   parser.add_argument(
    353       'target_files',
    354       help='the input target_files.zip to be validated')
    355   parser.add_argument(
    356       '--verity_key',
    357       help='the verity public key to verify the bootable images (Verified '
    358            'Boot 1.0), or the vbmeta image (Verified Boot 2.0, aka AVB), where '
    359            'applicable')
    360   for partition in common.AVB_PARTITIONS:
    361     parser.add_argument(
    362         '--avb_' + partition + '_key_path',
    363         help='the public or private key in PEM format to verify AVB chained '
    364              'partition of {}'.format(partition))
    365   parser.add_argument(
    366       '--verity_key_mincrypt',
    367       help='the verity public key in mincrypt format to verify the system '
    368            'images, if target using Verified Boot 1.0')
    369   args = parser.parse_args()
    370 
    371   # Unprovided args will have 'None' as the value.
    372   options = vars(args)
    373 
    374   logging_format = '%(asctime)s - %(filename)s - %(levelname)-8s: %(message)s'
    375   date_format = '%Y/%m/%d %H:%M:%S'
    376   logging.basicConfig(level=logging.INFO, format=logging_format,
    377                       datefmt=date_format)
    378 
    379   logging.info("Unzipping the input target_files.zip: %s", args.target_files)
    380   input_tmp = common.UnzipTemp(args.target_files)
    381 
    382   info_dict = common.LoadInfoDict(input_tmp)
    383   with zipfile.ZipFile(args.target_files, 'r') as input_zip:
    384     ValidateFileConsistency(input_zip, input_tmp, info_dict)
    385 
    386   ValidateInstallRecoveryScript(input_tmp, info_dict)
    387 
    388   ValidateVerifiedBootImages(input_tmp, info_dict, options)
    389 
    390   # TODO: Check if the OTA keys have been properly updated (the ones on /system,
    391   # in recovery image).
    392 
    393   logging.info("Done.")
    394 
    395 
    396 if __name__ == '__main__':
    397   try:
    398     main()
    399   finally:
    400     common.Cleanup()
    401