• 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 the following checks to assert the integrity of the input zip.
21
22 - It verifies the file consistency between the ones in IMAGES/system.img (read
23   via IMAGES/system.map) and the ones under unpacked folder of SYSTEM/. The
24   same check also applies to the vendor image if present.
25
26 - It verifies the install-recovery script consistency, by comparing the
27   checksums in the script against the ones of IMAGES/{boot,recovery}.img.
28
29 - It verifies the signed Verified Boot related images, for both of Verified
30   Boot 1.0 and 2.0 (aka AVB).
31"""
32
33import argparse
34import filecmp
35import logging
36import os.path
37import re
38import zipfile
39
40import common
41
42
43def _ReadFile(file_name, unpacked_name, round_up=False):
44  """Constructs and returns a File object. Rounds up its size if needed."""
45
46  assert os.path.exists(unpacked_name)
47  with open(unpacked_name, 'r') as f:
48    file_data = f.read()
49  file_size = len(file_data)
50  if round_up:
51    file_size_rounded_up = common.RoundUpTo4K(file_size)
52    file_data += '\0' * (file_size_rounded_up - file_size)
53  return common.File(file_name, file_data)
54
55
56def ValidateFileAgainstSha1(input_tmp, file_name, file_path, expected_sha1):
57  """Check if the file has the expected SHA-1."""
58
59  logging.info('Validating the SHA-1 of %s', file_name)
60  unpacked_name = os.path.join(input_tmp, file_path)
61  assert os.path.exists(unpacked_name)
62  actual_sha1 = _ReadFile(file_name, unpacked_name, False).sha1
63  assert actual_sha1 == expected_sha1, \
64      'SHA-1 mismatches for {}. actual {}, expected {}'.format(
65          file_name, actual_sha1, expected_sha1)
66
67
68def ValidateFileConsistency(input_zip, input_tmp, info_dict):
69  """Compare the files from image files and unpacked folders."""
70
71  def CheckAllFiles(which):
72    logging.info('Checking %s image.', which)
73    # Allow having shared blocks when loading the sparse image, because allowing
74    # that doesn't affect the checks below (we will have all the blocks on file,
75    # unless it's skipped due to the holes).
76    image = common.GetSparseImage(which, input_tmp, input_zip, True)
77    prefix = '/' + which
78    for entry in image.file_map:
79      # Skip entries like '__NONZERO-0'.
80      if not entry.startswith(prefix):
81        continue
82
83      # Read the blocks that the file resides. Note that it will contain the
84      # bytes past the file length, which is expected to be padded with '\0's.
85      ranges = image.file_map[entry]
86
87      # Use the original RangeSet if applicable, which includes the shared
88      # blocks. And this needs to happen before checking the monotonicity flag.
89      if ranges.extra.get('uses_shared_blocks'):
90        file_ranges = ranges.extra['uses_shared_blocks']
91      else:
92        file_ranges = ranges
93
94      incomplete = file_ranges.extra.get('incomplete', False)
95      if incomplete:
96        logging.warning('Skipping %s that has incomplete block list', entry)
97        continue
98
99      # TODO(b/79951650): Handle files with non-monotonic ranges.
100      if not file_ranges.monotonic:
101        logging.warning(
102            'Skipping %s that has non-monotonic ranges: %s', entry, file_ranges)
103        continue
104
105      blocks_sha1 = image.RangeSha1(file_ranges)
106
107      # The filename under unpacked directory, such as SYSTEM/bin/sh.
108      unpacked_name = os.path.join(
109          input_tmp, which.upper(), entry[(len(prefix) + 1):])
110      unpacked_file = _ReadFile(entry, unpacked_name, True)
111      file_sha1 = unpacked_file.sha1
112      assert blocks_sha1 == file_sha1, \
113          'file: %s, range: %s, blocks_sha1: %s, file_sha1: %s' % (
114              entry, file_ranges, blocks_sha1, file_sha1)
115
116  logging.info('Validating file consistency.')
117
118  # TODO(b/79617342): Validate non-sparse images.
119  if info_dict.get('extfs_sparse_flag') != '-s':
120    logging.warning('Skipped due to target using non-sparse images')
121    return
122
123  # Verify IMAGES/system.img.
124  CheckAllFiles('system')
125
126  # Verify IMAGES/vendor.img if applicable.
127  if 'VENDOR/' in input_zip.namelist():
128    CheckAllFiles('vendor')
129
130  # Not checking IMAGES/system_other.img since it doesn't have the map file.
131
132
133def ValidateInstallRecoveryScript(input_tmp, info_dict):
134  """Validate the SHA-1 embedded in install-recovery.sh.
135
136  install-recovery.sh is written in common.py and has the following format:
137
138  1. full recovery:
139  ...
140  if ! applypatch --check type:device:size:sha1; then
141    applypatch --flash /system/etc/recovery.img \\
142        type:device:size:sha1 && \\
143  ...
144
145  2. recovery from boot:
146  ...
147  if ! applypatch --check type:recovery_device:recovery_size:recovery_sha1; then
148    applypatch [--bonus bonus_args] \\
149        --patch /system/recovery-from-boot.p \\
150        --source type:boot_device:boot_size:boot_sha1 \\
151        --target type:recovery_device:recovery_size:recovery_sha1 && \\
152  ...
153
154  For full recovery, we want to calculate the SHA-1 of /system/etc/recovery.img
155  and compare it against the one embedded in the script. While for recovery
156  from boot, we want to check the SHA-1 for both recovery.img and boot.img
157  under IMAGES/.
158  """
159
160  script_path = 'SYSTEM/bin/install-recovery.sh'
161  if not os.path.exists(os.path.join(input_tmp, script_path)):
162    logging.info('%s does not exist in input_tmp', script_path)
163    return
164
165  logging.info('Checking %s', script_path)
166  with open(os.path.join(input_tmp, script_path), 'r') as script:
167    lines = script.read().strip().split('\n')
168  assert len(lines) >= 10
169  check_cmd = re.search(r'if ! applypatch --check (\w+:.+:\w+:\w+);',
170                        lines[1].strip())
171  check_partition = check_cmd.group(1)
172  assert len(check_partition.split(':')) == 4
173
174  full_recovery_image = info_dict.get("full_recovery_image") == "true"
175  if full_recovery_image:
176    assert len(lines) == 10, "Invalid line count: {}".format(lines)
177
178    # Expect something like "EMMC:/dev/block/recovery:28:5f9c..62e3".
179    target = re.search(r'--target (.+) &&', lines[4].strip())
180    assert target is not None, \
181        "Failed to parse target line \"{}\"".format(lines[4])
182    flash_partition = target.group(1)
183
184    # Check we have the same recovery target in the check and flash commands.
185    assert check_partition == flash_partition, \
186        "Mismatching targets: {} vs {}".format(check_partition, flash_partition)
187
188    # Validate the SHA-1 of the recovery image.
189    recovery_sha1 = flash_partition.split(':')[3]
190    ValidateFileAgainstSha1(
191        input_tmp, 'recovery.img', 'SYSTEM/etc/recovery.img', recovery_sha1)
192  else:
193    assert len(lines) == 11, "Invalid line count: {}".format(lines)
194
195    # --source boot_type:boot_device:boot_size:boot_sha1
196    source = re.search(r'--source (\w+:.+:\w+:\w+) \\', lines[4].strip())
197    assert source is not None, \
198        "Failed to parse source line \"{}\"".format(lines[4])
199
200    source_partition = source.group(1)
201    source_info = source_partition.split(':')
202    assert len(source_info) == 4, \
203        "Invalid source partition: {}".format(source_partition)
204    ValidateFileAgainstSha1(input_tmp, file_name='boot.img',
205                            file_path='IMAGES/boot.img',
206                            expected_sha1=source_info[3])
207
208    # --target recovery_type:recovery_device:recovery_size:recovery_sha1
209    target = re.search(r'--target (\w+:.+:\w+:\w+) && \\', lines[5].strip())
210    assert target is not None, \
211        "Failed to parse target line \"{}\"".format(lines[5])
212    target_partition = target.group(1)
213
214    # Check we have the same recovery target in the check and patch commands.
215    assert check_partition == target_partition, \
216        "Mismatching targets: {} vs {}".format(
217            check_partition, target_partition)
218
219    recovery_info = target_partition.split(':')
220    assert len(recovery_info) == 4, \
221        "Invalid target partition: {}".format(target_partition)
222    ValidateFileAgainstSha1(input_tmp, file_name='recovery.img',
223                            file_path='IMAGES/recovery.img',
224                            expected_sha1=recovery_info[3])
225
226  logging.info('Done checking %s', script_path)
227
228
229def ValidateVerifiedBootImages(input_tmp, info_dict, options):
230  """Validates the Verified Boot related images.
231
232  For Verified Boot 1.0, it verifies the signatures of the bootable images
233  (boot/recovery etc), as well as the dm-verity metadata in system images
234  (system/vendor/product). For Verified Boot 2.0, it calls avbtool to verify
235  vbmeta.img, which in turn verifies all the descriptors listed in vbmeta.
236
237  Args:
238    input_tmp: The top-level directory of unpacked target-files.zip.
239    info_dict: The loaded info dict.
240    options: A dict that contains the user-supplied public keys to be used for
241        image verification. In particular, 'verity_key' is used to verify the
242        bootable images in VB 1.0, and the vbmeta image in VB 2.0, where
243        applicable. 'verity_key_mincrypt' will be used to verify the system
244        images in VB 1.0.
245
246  Raises:
247    AssertionError: On any verification failure.
248  """
249  # Verified boot 1.0 (images signed with boot_signer and verity_signer).
250  if info_dict.get('boot_signer') == 'true':
251    logging.info('Verifying Verified Boot images...')
252
253    # Verify the boot/recovery images (signed with boot_signer), against the
254    # given X.509 encoded pubkey (or falling back to the one in the info_dict if
255    # none given).
256    verity_key = options['verity_key']
257    if verity_key is None:
258      verity_key = info_dict['verity_key'] + '.x509.pem'
259    for image in ('boot.img', 'recovery.img', 'recovery-two-step.img'):
260      image_path = os.path.join(input_tmp, 'IMAGES', image)
261      if not os.path.exists(image_path):
262        continue
263
264      cmd = ['boot_signer', '-verify', image_path, '-certificate', verity_key]
265      proc = common.Run(cmd)
266      stdoutdata, _ = proc.communicate()
267      assert proc.returncode == 0, \
268          'Failed to verify {} with boot_signer:\n{}'.format(image, stdoutdata)
269      logging.info(
270          'Verified %s with boot_signer (key: %s):\n%s', image, verity_key,
271          stdoutdata.rstrip())
272
273  # Verify verity signed system images in Verified Boot 1.0. Note that not using
274  # 'elif' here, since 'boot_signer' and 'verity' are not bundled in VB 1.0.
275  if info_dict.get('verity') == 'true':
276    # First verify that the verity key that's built into the root image (as
277    # /verity_key) matches the one given via command line, if any.
278    if info_dict.get("system_root_image") == "true":
279      verity_key_mincrypt = os.path.join(input_tmp, 'ROOT', 'verity_key')
280    else:
281      verity_key_mincrypt = os.path.join(
282          input_tmp, 'BOOT', 'RAMDISK', 'verity_key')
283    assert os.path.exists(verity_key_mincrypt), 'Missing verity_key'
284
285    if options['verity_key_mincrypt'] is None:
286      logging.warn(
287          'Skipped checking the content of /verity_key, as the key file not '
288          'provided. Use --verity_key_mincrypt to specify.')
289    else:
290      expected_key = options['verity_key_mincrypt']
291      assert filecmp.cmp(expected_key, verity_key_mincrypt, shallow=False), \
292          "Mismatching mincrypt verity key files"
293      logging.info('Verified the content of /verity_key')
294
295    # Then verify the verity signed system/vendor/product images, against the
296    # verity pubkey in mincrypt format.
297    for image in ('system.img', 'vendor.img', 'product.img'):
298      image_path = os.path.join(input_tmp, 'IMAGES', image)
299
300      # We are not checking if the image is actually enabled via info_dict (e.g.
301      # 'system_verity_block_device=...'). Because it's most likely a bug that
302      # skips signing some of the images in signed target-files.zip, while
303      # having the top-level verity flag enabled.
304      if not os.path.exists(image_path):
305        continue
306
307      cmd = ['verity_verifier', image_path, '-mincrypt', verity_key_mincrypt]
308      proc = common.Run(cmd)
309      stdoutdata, _ = proc.communicate()
310      assert proc.returncode == 0, \
311          'Failed to verify {} with verity_verifier (key: {}):\n{}'.format(
312              image, verity_key_mincrypt, stdoutdata)
313      logging.info(
314          'Verified %s with verity_verifier (key: %s):\n%s', image,
315          verity_key_mincrypt, stdoutdata.rstrip())
316
317  # Handle the case of Verified Boot 2.0 (AVB).
318  if info_dict.get("avb_enable") == "true":
319    logging.info('Verifying Verified Boot 2.0 (AVB) images...')
320
321    key = options['verity_key']
322    if key is None:
323      key = info_dict['avb_vbmeta_key_path']
324
325    # avbtool verifies all the images that have descriptors listed in vbmeta.
326    image = os.path.join(input_tmp, 'IMAGES', 'vbmeta.img')
327    cmd = ['avbtool', 'verify_image', '--image', image, '--key', key]
328
329    # Append the args for chained partitions if any.
330    for partition in common.AVB_PARTITIONS:
331      key_name = 'avb_' + partition + '_key_path'
332      if info_dict.get(key_name) is not None:
333        chained_partition_arg = common.GetAvbChainedPartitionArg(
334            partition, info_dict, options[key_name])
335        cmd.extend(["--expected_chain_partition", chained_partition_arg])
336
337    proc = common.Run(cmd)
338    stdoutdata, _ = proc.communicate()
339    assert proc.returncode == 0, \
340        'Failed to verify {} with avbtool (key: {}):\n{}'.format(
341            image, key, stdoutdata)
342
343    logging.info(
344        'Verified %s with avbtool (key: %s):\n%s', image, key,
345        stdoutdata.rstrip())
346
347
348def main():
349  parser = argparse.ArgumentParser(
350      description=__doc__,
351      formatter_class=argparse.RawDescriptionHelpFormatter)
352  parser.add_argument(
353      'target_files',
354      help='the input target_files.zip to be validated')
355  parser.add_argument(
356      '--verity_key',
357      help='the verity public key to verify the bootable images (Verified '
358           'Boot 1.0), or the vbmeta image (Verified Boot 2.0, aka AVB), where '
359           'applicable')
360  for partition in common.AVB_PARTITIONS:
361    parser.add_argument(
362        '--avb_' + partition + '_key_path',
363        help='the public or private key in PEM format to verify AVB chained '
364             'partition of {}'.format(partition))
365  parser.add_argument(
366      '--verity_key_mincrypt',
367      help='the verity public key in mincrypt format to verify the system '
368           'images, if target using Verified Boot 1.0')
369  args = parser.parse_args()
370
371  # Unprovided args will have 'None' as the value.
372  options = vars(args)
373
374  logging_format = '%(asctime)s - %(filename)s - %(levelname)-8s: %(message)s'
375  date_format = '%Y/%m/%d %H:%M:%S'
376  logging.basicConfig(level=logging.INFO, format=logging_format,
377                      datefmt=date_format)
378
379  logging.info("Unzipping the input target_files.zip: %s", args.target_files)
380  input_tmp = common.UnzipTemp(args.target_files)
381
382  info_dict = common.LoadInfoDict(input_tmp)
383  with zipfile.ZipFile(args.target_files, 'r') as input_zip:
384    ValidateFileConsistency(input_zip, input_tmp, info_dict)
385
386  ValidateInstallRecoveryScript(input_tmp, info_dict)
387
388  ValidateVerifiedBootImages(input_tmp, info_dict, options)
389
390  # TODO: Check if the OTA keys have been properly updated (the ones on /system,
391  # in recovery image).
392
393  logging.info("Done.")
394
395
396if __name__ == '__main__':
397  try:
398    main()
399  finally:
400    common.Cleanup()
401