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 checks to ensure the integrity of the input zip.
     21  - It verifies the file consistency between the ones in IMAGES/system.img (read
     22    via IMAGES/system.map) and the ones under unpacked folder of SYSTEM/. The
     23    same check also applies to the vendor image if present.
     24 """
     25 
     26 import logging
     27 import os.path
     28 import re
     29 import sys
     30 import zipfile
     31 
     32 import common
     33 
     34 
     35 def _ReadFile(file_name, unpacked_name, round_up=False):
     36   """Constructs and returns a File object. Rounds up its size if needed."""
     37 
     38   assert os.path.exists(unpacked_name)
     39   with open(unpacked_name, 'r') as f:
     40     file_data = f.read()
     41   file_size = len(file_data)
     42   if round_up:
     43     file_size_rounded_up = common.RoundUpTo4K(file_size)
     44     file_data += '\0' * (file_size_rounded_up - file_size)
     45   return common.File(file_name, file_data)
     46 
     47 
     48 def ValidateFileAgainstSha1(input_tmp, file_name, file_path, expected_sha1):
     49   """Check if the file has the expected SHA-1."""
     50 
     51   logging.info('Validating the SHA-1 of %s', file_name)
     52   unpacked_name = os.path.join(input_tmp, file_path)
     53   assert os.path.exists(unpacked_name)
     54   actual_sha1 = _ReadFile(file_name, unpacked_name, False).sha1
     55   assert actual_sha1 == expected_sha1, \
     56       'SHA-1 mismatches for {}. actual {}, expected {}'.format(
     57           file_name, actual_sha1, expected_sha1)
     58 
     59 
     60 def ValidateFileConsistency(input_zip, input_tmp, info_dict):
     61   """Compare the files from image files and unpacked folders."""
     62 
     63   def CheckAllFiles(which):
     64     logging.info('Checking %s image.', which)
     65     # Allow having shared blocks when loading the sparse image, because allowing
     66     # that doesn't affect the checks below (we will have all the blocks on file,
     67     # unless it's skipped due to the holes).
     68     image = common.GetSparseImage(which, input_tmp, input_zip, True)
     69     prefix = '/' + which
     70     for entry in image.file_map:
     71       # Skip entries like '__NONZERO-0'.
     72       if not entry.startswith(prefix):
     73         continue
     74 
     75       # Read the blocks that the file resides. Note that it will contain the
     76       # bytes past the file length, which is expected to be padded with '\0's.
     77       ranges = image.file_map[entry]
     78 
     79       incomplete = ranges.extra.get('incomplete', False)
     80       if incomplete:
     81         logging.warning('Skipping %s that has incomplete block list', entry)
     82         continue
     83 
     84       # TODO(b/79951650): Handle files with non-monotonic ranges.
     85       if not ranges.monotonic:
     86         logging.warning(
     87             'Skipping %s that has non-monotonic ranges: %s', entry, ranges)
     88         continue
     89 
     90       blocks_sha1 = image.RangeSha1(ranges)
     91 
     92       # The filename under unpacked directory, such as SYSTEM/bin/sh.
     93       unpacked_name = os.path.join(
     94           input_tmp, which.upper(), entry[(len(prefix) + 1):])
     95       unpacked_file = _ReadFile(entry, unpacked_name, True)
     96       file_sha1 = unpacked_file.sha1
     97       assert blocks_sha1 == file_sha1, \
     98           'file: %s, range: %s, blocks_sha1: %s, file_sha1: %s' % (
     99               entry, ranges, blocks_sha1, file_sha1)
    100 
    101   logging.info('Validating file consistency.')
    102 
    103   # TODO(b/79617342): Validate non-sparse images.
    104   if info_dict.get('extfs_sparse_flag') != '-s':
    105     logging.warning('Skipped due to target using non-sparse images')
    106     return
    107 
    108   # Verify IMAGES/system.img.
    109   CheckAllFiles('system')
    110 
    111   # Verify IMAGES/vendor.img if applicable.
    112   if 'VENDOR/' in input_zip.namelist():
    113     CheckAllFiles('vendor')
    114 
    115   # Not checking IMAGES/system_other.img since it doesn't have the map file.
    116 
    117 
    118 def ValidateInstallRecoveryScript(input_tmp, info_dict):
    119   """Validate the SHA-1 embedded in install-recovery.sh.
    120 
    121   install-recovery.sh is written in common.py and has the following format:
    122 
    123   1. full recovery:
    124   ...
    125   if ! applypatch -c type:device:size:SHA-1; then
    126   applypatch /system/etc/recovery.img type:device sha1 size && ...
    127   ...
    128 
    129   2. recovery from boot:
    130   ...
    131   applypatch [-b bonus_args] boot_info recovery_info recovery_sha1 \
    132   recovery_size patch_info && ...
    133   ...
    134 
    135   For full recovery, we want to calculate the SHA-1 of /system/etc/recovery.img
    136   and compare it against the one embedded in the script. While for recovery
    137   from boot, we want to check the SHA-1 for both recovery.img and boot.img
    138   under IMAGES/.
    139   """
    140 
    141   script_path = 'SYSTEM/bin/install-recovery.sh'
    142   if not os.path.exists(os.path.join(input_tmp, script_path)):
    143     logging.info('%s does not exist in input_tmp', script_path)
    144     return
    145 
    146   logging.info('Checking %s', script_path)
    147   with open(os.path.join(input_tmp, script_path), 'r') as script:
    148     lines = script.read().strip().split('\n')
    149   assert len(lines) >= 6
    150   check_cmd = re.search(r'if ! applypatch -c \w+:.+:\w+:(\w+);',
    151                         lines[1].strip())
    152   expected_recovery_check_sha1 = check_cmd.group(1)
    153   patch_cmd = re.search(r'(applypatch.+)&&', lines[2].strip())
    154   applypatch_argv = patch_cmd.group(1).strip().split()
    155 
    156   full_recovery_image = info_dict.get("full_recovery_image") == "true"
    157   if full_recovery_image:
    158     assert len(applypatch_argv) == 5
    159     # Check we have the same expected SHA-1 of recovery.img in both check mode
    160     # and patch mode.
    161     expected_recovery_sha1 = applypatch_argv[3].strip()
    162     assert expected_recovery_check_sha1 == expected_recovery_sha1
    163     ValidateFileAgainstSha1(input_tmp, 'recovery.img',
    164                             'SYSTEM/etc/recovery.img', expected_recovery_sha1)
    165   else:
    166     # We're patching boot.img to get recovery.img where bonus_args is optional
    167     if applypatch_argv[1] == "-b":
    168       assert len(applypatch_argv) == 8
    169       boot_info_index = 3
    170     else:
    171       assert len(applypatch_argv) == 6
    172       boot_info_index = 1
    173 
    174     # boot_info: boot_type:boot_device:boot_size:boot_sha1
    175     boot_info = applypatch_argv[boot_info_index].strip().split(':')
    176     assert len(boot_info) == 4
    177     ValidateFileAgainstSha1(input_tmp, file_name='boot.img',
    178                             file_path='IMAGES/boot.img',
    179                             expected_sha1=boot_info[3])
    180 
    181     recovery_sha1_index = boot_info_index + 2
    182     expected_recovery_sha1 = applypatch_argv[recovery_sha1_index]
    183     assert expected_recovery_check_sha1 == expected_recovery_sha1
    184     ValidateFileAgainstSha1(input_tmp, file_name='recovery.img',
    185                             file_path='IMAGES/recovery.img',
    186                             expected_sha1=expected_recovery_sha1)
    187 
    188   logging.info('Done checking %s', script_path)
    189 
    190 
    191 def main(argv):
    192   def option_handler():
    193     return True
    194 
    195   args = common.ParseOptions(
    196       argv, __doc__, extra_opts="",
    197       extra_long_opts=[],
    198       extra_option_handler=option_handler)
    199 
    200   if len(args) != 1:
    201     common.Usage(__doc__)
    202     sys.exit(1)
    203 
    204   logging_format = '%(asctime)s - %(filename)s - %(levelname)-8s: %(message)s'
    205   date_format = '%Y/%m/%d %H:%M:%S'
    206   logging.basicConfig(level=logging.INFO, format=logging_format,
    207                       datefmt=date_format)
    208 
    209   logging.info("Unzipping the input target_files.zip: %s", args[0])
    210   input_tmp = common.UnzipTemp(args[0])
    211 
    212   info_dict = common.LoadInfoDict(input_tmp)
    213   with zipfile.ZipFile(args[0], 'r') as input_zip:
    214     ValidateFileConsistency(input_zip, input_tmp, info_dict)
    215 
    216   ValidateInstallRecoveryScript(input_tmp, info_dict)
    217 
    218   # TODO: Check if the OTA keys have been properly updated (the ones on /system,
    219   # in recovery image).
    220 
    221   logging.info("Done.")
    222 
    223 
    224 if __name__ == '__main__':
    225   try:
    226     main(sys.argv[1:])
    227   finally:
    228     common.Cleanup()
    229