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