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