github.com/westcoastroms/westcoastroms-build@v0.0.0-20190928114312-2350e5a73030/build/make/tools/releasetools/validate_target_files.py (about)

     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()