validate_target_files.py revision 242a9e5fb56566db7236088e1b2bcce0ab87688c
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
30import zipfile
31
32import common
33
34
35def _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
48def 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
60def 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      blocks_sha1 = image.RangeSha1(ranges)
85
86      # The filename under unpacked directory, such as SYSTEM/bin/sh.
87      unpacked_name = os.path.join(
88          input_tmp, which.upper(), entry[(len(prefix) + 1):])
89      unpacked_file = _ReadFile(entry, unpacked_name, True)
90      file_sha1 = unpacked_file.sha1
91      assert blocks_sha1 == file_sha1, \
92          'file: %s, range: %s, blocks_sha1: %s, file_sha1: %s' % (
93              entry, ranges, blocks_sha1, file_sha1)
94
95  logging.info('Validating file consistency.')
96
97  # TODO(b/79617342): Validate non-sparse images.
98  if info_dict.get('extfs_sparse_flag') != '-s':
99    logging.warning('Skipped due to target using non-sparse images')
100    return
101
102  # Verify IMAGES/system.img.
103  CheckAllFiles('system')
104
105  # Verify IMAGES/vendor.img if applicable.
106  if 'VENDOR/' in input_zip.namelist():
107    CheckAllFiles('vendor')
108
109  # Not checking IMAGES/system_other.img since it doesn't have the map file.
110
111
112def ValidateInstallRecoveryScript(input_tmp, info_dict):
113  """Validate the SHA-1 embedded in install-recovery.sh.
114
115  install-recovery.sh is written in common.py and has the following format:
116
117  1. full recovery:
118  ...
119  if ! applypatch -c type:device:size:SHA-1; then
120  applypatch /system/etc/recovery.img type:device sha1 size && ...
121  ...
122
123  2. recovery from boot:
124  ...
125  applypatch [-b bonus_args] boot_info recovery_info recovery_sha1 \
126  recovery_size patch_info && ...
127  ...
128
129  For full recovery, we want to calculate the SHA-1 of /system/etc/recovery.img
130  and compare it against the one embedded in the script. While for recovery
131  from boot, we want to check the SHA-1 for both recovery.img and boot.img
132  under IMAGES/.
133  """
134
135  script_path = 'SYSTEM/bin/install-recovery.sh'
136  if not os.path.exists(os.path.join(input_tmp, script_path)):
137    logging.info('%s does not exist in input_tmp', script_path)
138    return
139
140  logging.info('Checking %s', script_path)
141  with open(os.path.join(input_tmp, script_path), 'r') as script:
142    lines = script.read().strip().split('\n')
143  assert len(lines) >= 6
144  check_cmd = re.search(r'if ! applypatch -c \w+:.+:\w+:(\w+);',
145                        lines[1].strip())
146  expected_recovery_check_sha1 = check_cmd.group(1)
147  patch_cmd = re.search(r'(applypatch.+)&&', lines[2].strip())
148  applypatch_argv = patch_cmd.group(1).strip().split()
149
150  full_recovery_image = info_dict.get("full_recovery_image") == "true"
151  if full_recovery_image:
152    assert len(applypatch_argv) == 5
153    # Check we have the same expected SHA-1 of recovery.img in both check mode
154    # and patch mode.
155    expected_recovery_sha1 = applypatch_argv[3].strip()
156    assert expected_recovery_check_sha1 == expected_recovery_sha1
157    ValidateFileAgainstSha1(input_tmp, 'recovery.img',
158                            'SYSTEM/etc/recovery.img', expected_recovery_sha1)
159  else:
160    # We're patching boot.img to get recovery.img where bonus_args is optional
161    if applypatch_argv[1] == "-b":
162      assert len(applypatch_argv) == 8
163      boot_info_index = 3
164    else:
165      assert len(applypatch_argv) == 6
166      boot_info_index = 1
167
168    # boot_info: boot_type:boot_device:boot_size:boot_sha1
169    boot_info = applypatch_argv[boot_info_index].strip().split(':')
170    assert len(boot_info) == 4
171    ValidateFileAgainstSha1(input_tmp, file_name='boot.img',
172                            file_path='IMAGES/boot.img',
173                            expected_sha1=boot_info[3])
174
175    recovery_sha1_index = boot_info_index + 2
176    expected_recovery_sha1 = applypatch_argv[recovery_sha1_index]
177    assert expected_recovery_check_sha1 == expected_recovery_sha1
178    ValidateFileAgainstSha1(input_tmp, file_name='recovery.img',
179                            file_path='IMAGES/recovery.img',
180                            expected_sha1=expected_recovery_sha1)
181
182  logging.info('Done checking %s', script_path)
183
184
185def main(argv):
186  def option_handler():
187    return True
188
189  args = common.ParseOptions(
190      argv, __doc__, extra_opts="",
191      extra_long_opts=[],
192      extra_option_handler=option_handler)
193
194  if len(args) != 1:
195    common.Usage(__doc__)
196    sys.exit(1)
197
198  logging_format = '%(asctime)s - %(filename)s - %(levelname)-8s: %(message)s'
199  date_format = '%Y/%m/%d %H:%M:%S'
200  logging.basicConfig(level=logging.INFO, format=logging_format,
201                      datefmt=date_format)
202
203  logging.info("Unzipping the input target_files.zip: %s", args[0])
204  input_tmp = common.UnzipTemp(args[0])
205
206  info_dict = common.LoadInfoDict(input_tmp)
207  with zipfile.ZipFile(args[0], 'r') as input_zip:
208    ValidateFileConsistency(input_zip, input_tmp, info_dict)
209
210  ValidateInstallRecoveryScript(input_tmp, info_dict)
211
212  # TODO: Check if the OTA keys have been properly updated (the ones on /system,
213  # in recovery image).
214
215  logging.info("Done.")
216
217
218if __name__ == '__main__':
219  try:
220    main(sys.argv[1:])
221  finally:
222    common.Cleanup()
223