• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 common
27import logging
28import os.path
29import re
30import sparse_img
31import sys
32
33
34def _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
47def _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
64def 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
76def 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
125def 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
197def 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
229if __name__ == '__main__':
230  try:
231    main(sys.argv[1:])
232  finally:
233    common.Cleanup()
234