blob: eeb802bc4062982c2278a36a223c9059d5b06a6a [file] [log] [blame]
Tao Baoafaa0a62017-02-27 15:08:36 -08001#!/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
Tao Baoba557702018-03-10 20:41:16 -080020It performs the following checks to assert the integrity of the input zip.
21
Tao Baoafaa0a62017-02-27 15:08:36 -080022 - 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.
Tao Baoba557702018-03-10 20:41:16 -080025
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).
Tao Baoafaa0a62017-02-27 15:08:36 -080031"""
32
Tao Baoba557702018-03-10 20:41:16 -080033import argparse
34import filecmp
Tao Baoafaa0a62017-02-27 15:08:36 -080035import logging
36import os.path
Tianjie Xu9c384d22017-06-20 17:00:55 -070037import re
Tao Baoc63626b2018-03-07 21:40:24 -080038import zipfile
Tao Baoafaa0a62017-02-27 15:08:36 -080039
Tao Baobb20e8c2018-02-01 12:00:19 -080040import common
Tao Baoafaa0a62017-02-27 15:08:36 -080041
42
Tao Baob418c302017-08-30 15:54:59 -070043def _ReadFile(file_name, unpacked_name, round_up=False):
44 """Constructs and returns a File object. Rounds up its size if needed."""
Tao Baoafaa0a62017-02-27 15:08:36 -080045
Tianjie Xu9c384d22017-06-20 17:00:55 -070046 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:
Tao Baoc765cca2018-01-31 17:32:40 -080051 file_size_rounded_up = common.RoundUpTo4K(file_size)
Tianjie Xu9c384d22017-06-20 17:00:55 -070052 file_data += '\0' * (file_size_rounded_up - file_size)
Tao Baob418c302017-08-30 15:54:59 -070053 return common.File(file_name, file_data)
Tianjie Xu9c384d22017-06-20 17:00:55 -070054
55
56def ValidateFileAgainstSha1(input_tmp, file_name, file_path, expected_sha1):
57 """Check if the file has the expected SHA-1."""
58
Tao Baobb20e8c2018-02-01 12:00:19 -080059 logging.info('Validating the SHA-1 of %s', file_name)
Tianjie Xu9c384d22017-06-20 17:00:55 -070060 unpacked_name = os.path.join(input_tmp, file_path)
61 assert os.path.exists(unpacked_name)
Tao Baob418c302017-08-30 15:54:59 -070062 actual_sha1 = _ReadFile(file_name, unpacked_name, False).sha1
Tianjie Xu9c384d22017-06-20 17:00:55 -070063 assert actual_sha1 == expected_sha1, \
64 'SHA-1 mismatches for {}. actual {}, expected {}'.format(
Tao Baobb20e8c2018-02-01 12:00:19 -080065 file_name, actual_sha1, expected_sha1)
Tianjie Xu9c384d22017-06-20 17:00:55 -070066
67
Tao Bao63e2f492018-05-11 23:38:46 -070068def ValidateFileConsistency(input_zip, input_tmp, info_dict):
Tianjie Xu9c384d22017-06-20 17:00:55 -070069 """Compare the files from image files and unpacked folders."""
70
Tao Baoafaa0a62017-02-27 15:08:36 -080071 def CheckAllFiles(which):
72 logging.info('Checking %s image.', which)
Tao Baoc63626b2018-03-07 21:40:24 -080073 # 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)
Tao Baoafaa0a62017-02-27 15:08:36 -080077 prefix = '/' + which
78 for entry in image.file_map:
Tao Baoc765cca2018-01-31 17:32:40 -080079 # Skip entries like '__NONZERO-0'.
Tao Baoafaa0a62017-02-27 15:08:36 -080080 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]
Tao Baoc765cca2018-01-31 17:32:40 -080086
87 incomplete = ranges.extra.get('incomplete', False)
88 if incomplete:
89 logging.warning('Skipping %s that has incomplete block list', entry)
90 continue
91
Tao Bao2a20f342018-12-03 15:08:23 -080092 # Use the original RangeSet if applicable, which includes the shared
93 # blocks. And this needs to happen before checking the monotonicity flag.
94 if ranges.extra.get('uses_shared_blocks'):
95 file_ranges = ranges.extra['uses_shared_blocks']
96 else:
97 file_ranges = ranges
98
Tao Baod32936d2018-05-17 19:42:41 -070099 # TODO(b/79951650): Handle files with non-monotonic ranges.
Tao Bao2a20f342018-12-03 15:08:23 -0800100 if not file_ranges.monotonic:
Tao Baod32936d2018-05-17 19:42:41 -0700101 logging.warning(
Tao Bao2a20f342018-12-03 15:08:23 -0800102 'Skipping %s that has non-monotonic ranges: %s', entry, file_ranges)
Tao Baod32936d2018-05-17 19:42:41 -0700103 continue
104
Tao Bao2a20f342018-12-03 15:08:23 -0800105 blocks_sha1 = image.RangeSha1(file_ranges)
Tao Baoafaa0a62017-02-27 15:08:36 -0800106
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):])
Tao Baob418c302017-08-30 15:54:59 -0700110 unpacked_file = _ReadFile(entry, unpacked_name, True)
Tao Baob418c302017-08-30 15:54:59 -0700111 file_sha1 = unpacked_file.sha1
Tao Baoafaa0a62017-02-27 15:08:36 -0800112 assert blocks_sha1 == file_sha1, \
113 'file: %s, range: %s, blocks_sha1: %s, file_sha1: %s' % (
Tao Bao2a20f342018-12-03 15:08:23 -0800114 entry, file_ranges, blocks_sha1, file_sha1)
Tao Baoafaa0a62017-02-27 15:08:36 -0800115
116 logging.info('Validating file consistency.')
117
Tao Bao63e2f492018-05-11 23:38:46 -0700118 # 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
Tao Baoafaa0a62017-02-27 15:08:36 -0800123 # 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
Tianjie Xu9c384d22017-06-20 17:00:55 -0700133def 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 ...
Tao Bao4948aed2018-07-13 16:11:16 -0700140 if ! applypatch --check type:device:size:sha1; then
141 applypatch --flash /system/etc/recovery.img \\
142 type:device:size:sha1 && \\
Tianjie Xu9c384d22017-06-20 17:00:55 -0700143 ...
144
145 2. recovery from boot:
146 ...
Tao Bao4948aed2018-07-13 16:11:16 -0700147 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 && \\
Tianjie Xu9c384d22017-06-20 17:00:55 -0700152 ...
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)):
Tao Baobb20e8c2018-02-01 12:00:19 -0800162 logging.info('%s does not exist in input_tmp', script_path)
Tianjie Xu9c384d22017-06-20 17:00:55 -0700163 return
164
Tao Baobb20e8c2018-02-01 12:00:19 -0800165 logging.info('Checking %s', script_path)
Tianjie Xu9c384d22017-06-20 17:00:55 -0700166 with open(os.path.join(input_tmp, script_path), 'r') as script:
167 lines = script.read().strip().split('\n')
Tao Bao4948aed2018-07-13 16:11:16 -0700168 assert len(lines) >= 10
169 check_cmd = re.search(r'if ! applypatch --check (\w+:.+:\w+:\w+);',
Tianjie Xu9c384d22017-06-20 17:00:55 -0700170 lines[1].strip())
Tao Bao4948aed2018-07-13 16:11:16 -0700171 check_partition = check_cmd.group(1)
172 assert len(check_partition.split(':')) == 4
Tianjie Xu9c384d22017-06-20 17:00:55 -0700173
174 full_recovery_image = info_dict.get("full_recovery_image") == "true"
175 if full_recovery_image:
Tao Bao4948aed2018-07-13 16:11:16 -0700176 assert len(lines) == 10, "Invalid line count: {}".format(lines)
Tianjie Xu9c384d22017-06-20 17:00:55 -0700177
Tao Bao4948aed2018-07-13 16:11:16 -0700178 # 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)
Tianjie Xu9c384d22017-06-20 17:00:55 -0700204 ValidateFileAgainstSha1(input_tmp, file_name='boot.img',
Tao Baobb20e8c2018-02-01 12:00:19 -0800205 file_path='IMAGES/boot.img',
Tao Bao4948aed2018-07-13 16:11:16 -0700206 expected_sha1=source_info[3])
Tianjie Xu9c384d22017-06-20 17:00:55 -0700207
Tao Bao4948aed2018-07-13 16:11:16 -0700208 # --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)
Tianjie Xu9c384d22017-06-20 17:00:55 -0700222 ValidateFileAgainstSha1(input_tmp, file_name='recovery.img',
Tao Baobb20e8c2018-02-01 12:00:19 -0800223 file_path='IMAGES/recovery.img',
Tao Bao4948aed2018-07-13 16:11:16 -0700224 expected_sha1=recovery_info[3])
Tianjie Xu9c384d22017-06-20 17:00:55 -0700225
Tao Baobb20e8c2018-02-01 12:00:19 -0800226 logging.info('Done checking %s', script_path)
Tianjie Xu9c384d22017-06-20 17:00:55 -0700227
228
Tao Baoba557702018-03-10 20:41:16 -0800229def ValidateVerifiedBootImages(input_tmp, info_dict, options):
230 """Validates the Verified Boot related images.
Tao Baoafaa0a62017-02-27 15:08:36 -0800231
Tao Baoba557702018-03-10 20:41:16 -0800232 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.
Tao Baoafaa0a62017-02-27 15:08:36 -0800236
Tao Baoba557702018-03-10 20:41:16 -0800237 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]
Tao Bao73dd4f42018-10-04 16:25:33 -0700265 proc = common.Run(cmd)
Tao Baoba557702018-03-10 20:41:16 -0800266 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]
Tao Bao73dd4f42018-10-04 16:25:33 -0700308 proc = common.Run(cmd)
Tao Baoba557702018-03-10 20:41:16 -0800309 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
Tao Bao2a20f342018-12-03 15:08:23 -0800321 # TODO(b/120517892): Temporarily disable the verification for AVB-signed
322 # images. Needing supporting changes in caller to pass in the desired keys.
323 logging.info('Temporarily disabled due to b/120517892')
Tao Baoba557702018-03-10 20:41:16 -0800324
325
326def main():
327 parser = argparse.ArgumentParser(
328 description=__doc__,
329 formatter_class=argparse.RawDescriptionHelpFormatter)
330 parser.add_argument(
331 'target_files',
332 help='the input target_files.zip to be validated')
333 parser.add_argument(
334 '--verity_key',
335 help='the verity public key to verify the bootable images (Verified '
Tao Bao02a08592018-07-22 12:40:45 -0700336 'Boot 1.0), or the vbmeta image (Verified Boot 2.0, aka AVB), where '
Tao Baoba557702018-03-10 20:41:16 -0800337 'applicable')
Tao Bao02a08592018-07-22 12:40:45 -0700338 for partition in common.AVB_PARTITIONS:
339 parser.add_argument(
340 '--avb_' + partition + '_key_path',
341 help='the public or private key in PEM format to verify AVB chained '
342 'partition of {}'.format(partition))
Tao Baoba557702018-03-10 20:41:16 -0800343 parser.add_argument(
344 '--verity_key_mincrypt',
345 help='the verity public key in mincrypt format to verify the system '
346 'images, if target using Verified Boot 1.0')
347 args = parser.parse_args()
348
349 # Unprovided args will have 'None' as the value.
350 options = vars(args)
Tao Baoafaa0a62017-02-27 15:08:36 -0800351
352 logging_format = '%(asctime)s - %(filename)s - %(levelname)-8s: %(message)s'
353 date_format = '%Y/%m/%d %H:%M:%S'
354 logging.basicConfig(level=logging.INFO, format=logging_format,
355 datefmt=date_format)
356
Tao Baoba557702018-03-10 20:41:16 -0800357 logging.info("Unzipping the input target_files.zip: %s", args.target_files)
358 input_tmp = common.UnzipTemp(args.target_files)
Tao Baoafaa0a62017-02-27 15:08:36 -0800359
Tianjie Xu9c384d22017-06-20 17:00:55 -0700360 info_dict = common.LoadInfoDict(input_tmp)
Tao Bao63e2f492018-05-11 23:38:46 -0700361 with zipfile.ZipFile(args.target_files, 'r') as input_zip:
362 ValidateFileConsistency(input_zip, input_tmp, info_dict)
363
Tianjie Xu9c384d22017-06-20 17:00:55 -0700364 ValidateInstallRecoveryScript(input_tmp, info_dict)
365
Tao Baoba557702018-03-10 20:41:16 -0800366 ValidateVerifiedBootImages(input_tmp, info_dict, options)
367
Tao Baoafaa0a62017-02-27 15:08:36 -0800368 # TODO: Check if the OTA keys have been properly updated (the ones on /system,
369 # in recovery image).
370
Tao Baoafaa0a62017-02-27 15:08:36 -0800371 logging.info("Done.")
372
373
374if __name__ == '__main__':
375 try:
Tao Baoba557702018-03-10 20:41:16 -0800376 main()
Tao Baoafaa0a62017-02-27 15:08:36 -0800377 finally:
378 common.Cleanup()