blob: 82b31076f7aa1d49022fa92198ad166c0e89071c [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
Kelvin Zhang26390482021-11-02 14:31:10 -070039
Tao Bao22632cc2019-10-03 23:12:55 -070040from hashlib import sha1
Kelvin Zhang26390482021-11-02 14:31:10 -070041from common import IsSparseImage
Tao Baoafaa0a62017-02-27 15:08:36 -080042
Tao Baobb20e8c2018-02-01 12:00:19 -080043import common
Tao Bao22632cc2019-10-03 23:12:55 -070044import rangelib
Tao Baoafaa0a62017-02-27 15:08:36 -080045
46
Tao Baob418c302017-08-30 15:54:59 -070047def _ReadFile(file_name, unpacked_name, round_up=False):
48 """Constructs and returns a File object. Rounds up its size if needed."""
Tianjie Xu9c384d22017-06-20 17:00:55 -070049 assert os.path.exists(unpacked_name)
Tao Baoda30cfa2017-12-01 16:19:46 -080050 with open(unpacked_name, 'rb') as f:
Tianjie Xu9c384d22017-06-20 17:00:55 -070051 file_data = f.read()
52 file_size = len(file_data)
53 if round_up:
Tao Baoc765cca2018-01-31 17:32:40 -080054 file_size_rounded_up = common.RoundUpTo4K(file_size)
Tao Bao22632cc2019-10-03 23:12:55 -070055 file_data += b'\0' * (file_size_rounded_up - file_size)
Tao Baob418c302017-08-30 15:54:59 -070056 return common.File(file_name, file_data)
Tianjie Xu9c384d22017-06-20 17:00:55 -070057
58
59def ValidateFileAgainstSha1(input_tmp, file_name, file_path, expected_sha1):
60 """Check if the file has the expected SHA-1."""
61
Tao Baobb20e8c2018-02-01 12:00:19 -080062 logging.info('Validating the SHA-1 of %s', file_name)
Tianjie Xu9c384d22017-06-20 17:00:55 -070063 unpacked_name = os.path.join(input_tmp, file_path)
64 assert os.path.exists(unpacked_name)
Tao Baob418c302017-08-30 15:54:59 -070065 actual_sha1 = _ReadFile(file_name, unpacked_name, False).sha1
Tianjie Xu9c384d22017-06-20 17:00:55 -070066 assert actual_sha1 == expected_sha1, \
67 'SHA-1 mismatches for {}. actual {}, expected {}'.format(
Tao Baobb20e8c2018-02-01 12:00:19 -080068 file_name, actual_sha1, expected_sha1)
Tianjie Xu9c384d22017-06-20 17:00:55 -070069
70
Tao Bao63e2f492018-05-11 23:38:46 -070071def ValidateFileConsistency(input_zip, input_tmp, info_dict):
Tianjie Xu9c384d22017-06-20 17:00:55 -070072 """Compare the files from image files and unpacked folders."""
73
Tao Baoafaa0a62017-02-27 15:08:36 -080074 def CheckAllFiles(which):
75 logging.info('Checking %s image.', which)
Kelvin Zhang26390482021-11-02 14:31:10 -070076 path = os.path.join(input_tmp, "IMAGES", which + ".img")
77 if not IsSparseImage(path):
78 logging.info("%s is non-sparse image", which)
79 image = common.GetNonSparseImage(which, input_tmp)
80 else:
81 logging.info("%s is sparse image", which)
82 # Allow having shared blocks when loading the sparse image, because allowing
83 # that doesn't affect the checks below (we will have all the blocks on file,
84 # unless it's skipped due to the holes).
85 image = common.GetSparseImage(which, input_tmp, input_zip, True)
Tao Baoafaa0a62017-02-27 15:08:36 -080086 prefix = '/' + which
87 for entry in image.file_map:
Tao Baoc765cca2018-01-31 17:32:40 -080088 # Skip entries like '__NONZERO-0'.
Tao Baoafaa0a62017-02-27 15:08:36 -080089 if not entry.startswith(prefix):
90 continue
91
92 # Read the blocks that the file resides. Note that it will contain the
93 # bytes past the file length, which is expected to be padded with '\0's.
94 ranges = image.file_map[entry]
Tao Baoc765cca2018-01-31 17:32:40 -080095
Tao Bao2a20f342018-12-03 15:08:23 -080096 # Use the original RangeSet if applicable, which includes the shared
97 # blocks. And this needs to happen before checking the monotonicity flag.
98 if ranges.extra.get('uses_shared_blocks'):
99 file_ranges = ranges.extra['uses_shared_blocks']
100 else:
101 file_ranges = ranges
102
xunchangc0f77ee2019-02-20 15:03:43 -0800103 incomplete = file_ranges.extra.get('incomplete', False)
104 if incomplete:
105 logging.warning('Skipping %s that has incomplete block list', entry)
106 continue
107
Tao Bao22632cc2019-10-03 23:12:55 -0700108 # If the file has non-monotonic ranges, read each range in order.
Tao Bao2a20f342018-12-03 15:08:23 -0800109 if not file_ranges.monotonic:
Tao Bao22632cc2019-10-03 23:12:55 -0700110 h = sha1()
111 for file_range in file_ranges.extra['text_str'].split(' '):
112 for data in image.ReadRangeSet(rangelib.RangeSet(file_range)):
113 h.update(data)
114 blocks_sha1 = h.hexdigest()
115 else:
116 blocks_sha1 = image.RangeSha1(file_ranges)
Tao Baoafaa0a62017-02-27 15:08:36 -0800117
118 # The filename under unpacked directory, such as SYSTEM/bin/sh.
119 unpacked_name = os.path.join(
120 input_tmp, which.upper(), entry[(len(prefix) + 1):])
Tao Baob418c302017-08-30 15:54:59 -0700121 unpacked_file = _ReadFile(entry, unpacked_name, True)
Tao Baob418c302017-08-30 15:54:59 -0700122 file_sha1 = unpacked_file.sha1
Tao Baoafaa0a62017-02-27 15:08:36 -0800123 assert blocks_sha1 == file_sha1, \
124 'file: %s, range: %s, blocks_sha1: %s, file_sha1: %s' % (
Tao Bao2a20f342018-12-03 15:08:23 -0800125 entry, file_ranges, blocks_sha1, file_sha1)
Tao Baoafaa0a62017-02-27 15:08:36 -0800126
127 logging.info('Validating file consistency.')
128
Tao Bao63e2f492018-05-11 23:38:46 -0700129 # TODO(b/79617342): Validate non-sparse images.
130 if info_dict.get('extfs_sparse_flag') != '-s':
131 logging.warning('Skipped due to target using non-sparse images')
132 return
133
Bowgo Tsai341fe022022-03-09 15:12:58 +0800134 # Verify IMAGES/system.img if applicable.
135 # Some targets, e.g., gki_arm64, gki_x86_64, etc., are system.img-less.
136 if 'IMAGES/system.img' in input_zip.namelist():
137 CheckAllFiles('system')
Tao Baoafaa0a62017-02-27 15:08:36 -0800138
139 # Verify IMAGES/vendor.img if applicable.
140 if 'VENDOR/' in input_zip.namelist():
141 CheckAllFiles('vendor')
142
143 # Not checking IMAGES/system_other.img since it doesn't have the map file.
144
145
Tianjie Xu9c384d22017-06-20 17:00:55 -0700146def ValidateInstallRecoveryScript(input_tmp, info_dict):
147 """Validate the SHA-1 embedded in install-recovery.sh.
148
149 install-recovery.sh is written in common.py and has the following format:
150
151 1. full recovery:
152 ...
Tao Bao4948aed2018-07-13 16:11:16 -0700153 if ! applypatch --check type:device:size:sha1; then
Bill Peckhame868aec2019-09-17 17:06:47 -0700154 applypatch --flash /vendor/etc/recovery.img \\
Tao Bao4948aed2018-07-13 16:11:16 -0700155 type:device:size:sha1 && \\
Tianjie Xu9c384d22017-06-20 17:00:55 -0700156 ...
157
158 2. recovery from boot:
159 ...
Tao Bao4948aed2018-07-13 16:11:16 -0700160 if ! applypatch --check type:recovery_device:recovery_size:recovery_sha1; then
161 applypatch [--bonus bonus_args] \\
Bill Peckhame868aec2019-09-17 17:06:47 -0700162 --patch /vendor/recovery-from-boot.p \\
Tao Bao4948aed2018-07-13 16:11:16 -0700163 --source type:boot_device:boot_size:boot_sha1 \\
164 --target type:recovery_device:recovery_size:recovery_sha1 && \\
Tianjie Xu9c384d22017-06-20 17:00:55 -0700165 ...
166
Bill Peckhame868aec2019-09-17 17:06:47 -0700167 For full recovery, we want to calculate the SHA-1 of /vendor/etc/recovery.img
Tianjie Xu9c384d22017-06-20 17:00:55 -0700168 and compare it against the one embedded in the script. While for recovery
169 from boot, we want to check the SHA-1 for both recovery.img and boot.img
170 under IMAGES/.
171 """
172
Bill Peckhame868aec2019-09-17 17:06:47 -0700173 board_uses_vendorimage = info_dict.get("board_uses_vendorimage") == "true"
174
175 if board_uses_vendorimage:
176 script_path = 'VENDOR/bin/install-recovery.sh'
177 recovery_img = 'VENDOR/etc/recovery.img'
178 else:
179 script_path = 'SYSTEM/vendor/bin/install-recovery.sh'
180 recovery_img = 'SYSTEM/vendor/etc/recovery.img'
181
Tianjie Xu9c384d22017-06-20 17:00:55 -0700182 if not os.path.exists(os.path.join(input_tmp, script_path)):
Tao Baobb20e8c2018-02-01 12:00:19 -0800183 logging.info('%s does not exist in input_tmp', script_path)
Tianjie Xu9c384d22017-06-20 17:00:55 -0700184 return
185
Tao Baobb20e8c2018-02-01 12:00:19 -0800186 logging.info('Checking %s', script_path)
Tianjie Xu9c384d22017-06-20 17:00:55 -0700187 with open(os.path.join(input_tmp, script_path), 'r') as script:
188 lines = script.read().strip().split('\n')
Tao Bao4948aed2018-07-13 16:11:16 -0700189 assert len(lines) >= 10
190 check_cmd = re.search(r'if ! applypatch --check (\w+:.+:\w+:\w+);',
Tianjie Xu9c384d22017-06-20 17:00:55 -0700191 lines[1].strip())
Tao Bao4948aed2018-07-13 16:11:16 -0700192 check_partition = check_cmd.group(1)
193 assert len(check_partition.split(':')) == 4
Tianjie Xu9c384d22017-06-20 17:00:55 -0700194
195 full_recovery_image = info_dict.get("full_recovery_image") == "true"
196 if full_recovery_image:
Tao Bao4948aed2018-07-13 16:11:16 -0700197 assert len(lines) == 10, "Invalid line count: {}".format(lines)
Tianjie Xu9c384d22017-06-20 17:00:55 -0700198
Tao Bao4948aed2018-07-13 16:11:16 -0700199 # Expect something like "EMMC:/dev/block/recovery:28:5f9c..62e3".
200 target = re.search(r'--target (.+) &&', lines[4].strip())
201 assert target is not None, \
202 "Failed to parse target line \"{}\"".format(lines[4])
203 flash_partition = target.group(1)
204
205 # Check we have the same recovery target in the check and flash commands.
206 assert check_partition == flash_partition, \
Kelvin Zhang4093d602021-05-25 09:17:38 -0400207 "Mismatching targets: {} vs {}".format(
208 check_partition, flash_partition)
Tao Bao4948aed2018-07-13 16:11:16 -0700209
210 # Validate the SHA-1 of the recovery image.
211 recovery_sha1 = flash_partition.split(':')[3]
212 ValidateFileAgainstSha1(
Bill Peckhame868aec2019-09-17 17:06:47 -0700213 input_tmp, 'recovery.img', recovery_img, recovery_sha1)
Tao Bao4948aed2018-07-13 16:11:16 -0700214 else:
215 assert len(lines) == 11, "Invalid line count: {}".format(lines)
216
217 # --source boot_type:boot_device:boot_size:boot_sha1
218 source = re.search(r'--source (\w+:.+:\w+:\w+) \\', lines[4].strip())
219 assert source is not None, \
220 "Failed to parse source line \"{}\"".format(lines[4])
221
222 source_partition = source.group(1)
223 source_info = source_partition.split(':')
224 assert len(source_info) == 4, \
225 "Invalid source partition: {}".format(source_partition)
Tianjie Xu9c384d22017-06-20 17:00:55 -0700226 ValidateFileAgainstSha1(input_tmp, file_name='boot.img',
Tao Baobb20e8c2018-02-01 12:00:19 -0800227 file_path='IMAGES/boot.img',
Tao Bao4948aed2018-07-13 16:11:16 -0700228 expected_sha1=source_info[3])
Tianjie Xu9c384d22017-06-20 17:00:55 -0700229
Tao Bao4948aed2018-07-13 16:11:16 -0700230 # --target recovery_type:recovery_device:recovery_size:recovery_sha1
231 target = re.search(r'--target (\w+:.+:\w+:\w+) && \\', lines[5].strip())
232 assert target is not None, \
233 "Failed to parse target line \"{}\"".format(lines[5])
234 target_partition = target.group(1)
235
236 # Check we have the same recovery target in the check and patch commands.
237 assert check_partition == target_partition, \
238 "Mismatching targets: {} vs {}".format(
239 check_partition, target_partition)
240
241 recovery_info = target_partition.split(':')
242 assert len(recovery_info) == 4, \
243 "Invalid target partition: {}".format(target_partition)
Tianjie Xu9c384d22017-06-20 17:00:55 -0700244 ValidateFileAgainstSha1(input_tmp, file_name='recovery.img',
Tao Baobb20e8c2018-02-01 12:00:19 -0800245 file_path='IMAGES/recovery.img',
Tao Bao4948aed2018-07-13 16:11:16 -0700246 expected_sha1=recovery_info[3])
Tianjie Xu9c384d22017-06-20 17:00:55 -0700247
Tao Baobb20e8c2018-02-01 12:00:19 -0800248 logging.info('Done checking %s', script_path)
Tianjie Xu9c384d22017-06-20 17:00:55 -0700249
Tianjie2e0b8352021-01-12 14:04:58 -0800250
Kelvin Zhang5d2b56b2020-06-26 11:37:28 -0400251# Symlink files in `src` to `dst`, if the files do not
252# already exists in `dst` directory.
253def symlinkIfNotExists(src, dst):
254 if not os.path.isdir(src):
255 return
256 for filename in os.listdir(src):
257 if os.path.exists(os.path.join(dst, filename)):
258 continue
259 os.symlink(os.path.join(src, filename), os.path.join(dst, filename))
Tianjie Xu9c384d22017-06-20 17:00:55 -0700260
Tianjie2e0b8352021-01-12 14:04:58 -0800261
Kelvin Zhang4093d602021-05-25 09:17:38 -0400262def ValidatePartitionFingerprints(input_tmp, info_dict):
263 build_info = common.BuildInfo(info_dict)
264 # Expected format:
265 # Prop: com.android.build.vendor.fingerprint -> 'generic/aosp_cf_x86_64_phone/vsoc_x86_64:S/AOSP.MASTER/7335886:userdebug/test-keys'
266 # Prop: com.android.build.vendor_boot.fingerprint -> 'generic/aosp_cf_x86_64_phone/vsoc_x86_64:S/AOSP.MASTER/7335886:userdebug/test-keys'
267 p = re.compile(
268 r"Prop: com.android.build.(?P<partition>\w+).fingerprint -> '(?P<fingerprint>[\w\/:\.-]+)'")
269 for vbmeta_partition in ["vbmeta", "vbmeta_system"]:
270 image = os.path.join(input_tmp, "IMAGES", vbmeta_partition + ".img")
Kelvin Zhanga19fb312021-07-26 14:05:02 -0400271 if not os.path.exists(image):
272 assert vbmeta_partition != "vbmeta",\
273 "{} is a required partition for AVB.".format(
274 vbmeta_partition)
275 logging.info("vb partition %s not present, skipping", vbmeta_partition)
276 continue
277
Kelvin Zhang4093d602021-05-25 09:17:38 -0400278 output = common.RunAndCheckOutput(
279 [info_dict["avb_avbtool"], "info_image", "--image", image])
280 matches = p.findall(output)
281 for (partition, fingerprint) in matches:
282 actual_fingerprint = build_info.GetPartitionFingerprint(
283 partition)
284 if actual_fingerprint is None:
285 logging.warning(
286 "Failed to get fingerprint for partition %s", partition)
287 continue
288 assert fingerprint == actual_fingerprint, "Fingerprint mismatch for partition {}, expected: {} actual: {}".format(
289 partition, fingerprint, actual_fingerprint)
290
291
Tao Baoba557702018-03-10 20:41:16 -0800292def ValidateVerifiedBootImages(input_tmp, info_dict, options):
293 """Validates the Verified Boot related images.
Tao Baoafaa0a62017-02-27 15:08:36 -0800294
Tao Baoba557702018-03-10 20:41:16 -0800295 For Verified Boot 1.0, it verifies the signatures of the bootable images
296 (boot/recovery etc), as well as the dm-verity metadata in system images
297 (system/vendor/product). For Verified Boot 2.0, it calls avbtool to verify
298 vbmeta.img, which in turn verifies all the descriptors listed in vbmeta.
Tao Baoafaa0a62017-02-27 15:08:36 -0800299
Tao Baoba557702018-03-10 20:41:16 -0800300 Args:
301 input_tmp: The top-level directory of unpacked target-files.zip.
302 info_dict: The loaded info dict.
303 options: A dict that contains the user-supplied public keys to be used for
304 image verification. In particular, 'verity_key' is used to verify the
305 bootable images in VB 1.0, and the vbmeta image in VB 2.0, where
306 applicable. 'verity_key_mincrypt' will be used to verify the system
307 images in VB 1.0.
308
309 Raises:
310 AssertionError: On any verification failure.
311 """
Kelvin Zhang5d2b56b2020-06-26 11:37:28 -0400312 # See bug 159299583
313 # After commit 5277d1015, some images (e.g. acpio.img and tos.img) are no
314 # longer copied from RADIO to the IMAGES folder. But avbtool assumes that
315 # images are in IMAGES folder. So we symlink them.
316 symlinkIfNotExists(os.path.join(input_tmp, "RADIO"),
Kelvin Zhang4093d602021-05-25 09:17:38 -0400317 os.path.join(input_tmp, "IMAGES"))
Tao Baoba557702018-03-10 20:41:16 -0800318 # Verified boot 1.0 (images signed with boot_signer and verity_signer).
319 if info_dict.get('boot_signer') == 'true':
320 logging.info('Verifying Verified Boot images...')
321
322 # Verify the boot/recovery images (signed with boot_signer), against the
323 # given X.509 encoded pubkey (or falling back to the one in the info_dict if
324 # none given).
325 verity_key = options['verity_key']
326 if verity_key is None:
327 verity_key = info_dict['verity_key'] + '.x509.pem'
328 for image in ('boot.img', 'recovery.img', 'recovery-two-step.img'):
Tao Bao04808502019-07-25 23:11:41 -0700329 if image == 'recovery-two-step.img':
330 image_path = os.path.join(input_tmp, 'OTA', image)
331 else:
332 image_path = os.path.join(input_tmp, 'IMAGES', image)
Tao Baoba557702018-03-10 20:41:16 -0800333 if not os.path.exists(image_path):
334 continue
335
336 cmd = ['boot_signer', '-verify', image_path, '-certificate', verity_key]
Tao Bao73dd4f42018-10-04 16:25:33 -0700337 proc = common.Run(cmd)
Tao Baoba557702018-03-10 20:41:16 -0800338 stdoutdata, _ = proc.communicate()
339 assert proc.returncode == 0, \
340 'Failed to verify {} with boot_signer:\n{}'.format(image, stdoutdata)
341 logging.info(
342 'Verified %s with boot_signer (key: %s):\n%s', image, verity_key,
343 stdoutdata.rstrip())
344
345 # Verify verity signed system images in Verified Boot 1.0. Note that not using
346 # 'elif' here, since 'boot_signer' and 'verity' are not bundled in VB 1.0.
347 if info_dict.get('verity') == 'true':
Tao Baoc9981932019-09-16 12:10:43 -0700348 # First verify that the verity key is built into the root image (regardless
349 # of system-as-root).
350 verity_key_mincrypt = os.path.join(input_tmp, 'ROOT', 'verity_key')
Tao Baoba557702018-03-10 20:41:16 -0800351 assert os.path.exists(verity_key_mincrypt), 'Missing verity_key'
352
Tao Baoc9981932019-09-16 12:10:43 -0700353 # Verify /verity_key matches the one given via command line, if any.
Tao Baoba557702018-03-10 20:41:16 -0800354 if options['verity_key_mincrypt'] is None:
355 logging.warn(
356 'Skipped checking the content of /verity_key, as the key file not '
357 'provided. Use --verity_key_mincrypt to specify.')
358 else:
359 expected_key = options['verity_key_mincrypt']
360 assert filecmp.cmp(expected_key, verity_key_mincrypt, shallow=False), \
361 "Mismatching mincrypt verity key files"
362 logging.info('Verified the content of /verity_key')
363
Tao Baoc9981932019-09-16 12:10:43 -0700364 # For devices with a separate ramdisk (i.e. non-system-as-root), there must
365 # be a copy in ramdisk.
366 if info_dict.get("system_root_image") != "true":
367 verity_key_ramdisk = os.path.join(
368 input_tmp, 'BOOT', 'RAMDISK', 'verity_key')
Kelvin Zhang4093d602021-05-25 09:17:38 -0400369 assert os.path.exists(
370 verity_key_ramdisk), 'Missing verity_key in ramdisk'
Tao Baoc9981932019-09-16 12:10:43 -0700371
372 assert filecmp.cmp(
373 verity_key_mincrypt, verity_key_ramdisk, shallow=False), \
Kelvin Zhang4093d602021-05-25 09:17:38 -0400374 'Mismatching verity_key files in root and ramdisk'
Tao Baoc9981932019-09-16 12:10:43 -0700375 logging.info('Verified the content of /verity_key in ramdisk')
376
Tao Baoba557702018-03-10 20:41:16 -0800377 # Then verify the verity signed system/vendor/product images, against the
378 # verity pubkey in mincrypt format.
379 for image in ('system.img', 'vendor.img', 'product.img'):
380 image_path = os.path.join(input_tmp, 'IMAGES', image)
381
382 # We are not checking if the image is actually enabled via info_dict (e.g.
383 # 'system_verity_block_device=...'). Because it's most likely a bug that
384 # skips signing some of the images in signed target-files.zip, while
385 # having the top-level verity flag enabled.
386 if not os.path.exists(image_path):
387 continue
388
389 cmd = ['verity_verifier', image_path, '-mincrypt', verity_key_mincrypt]
Tao Bao73dd4f42018-10-04 16:25:33 -0700390 proc = common.Run(cmd)
Tao Baoba557702018-03-10 20:41:16 -0800391 stdoutdata, _ = proc.communicate()
392 assert proc.returncode == 0, \
393 'Failed to verify {} with verity_verifier (key: {}):\n{}'.format(
394 image, verity_key_mincrypt, stdoutdata)
395 logging.info(
396 'Verified %s with verity_verifier (key: %s):\n%s', image,
397 verity_key_mincrypt, stdoutdata.rstrip())
398
399 # Handle the case of Verified Boot 2.0 (AVB).
Bowgo Tsai341fe022022-03-09 15:12:58 +0800400 if info_dict.get("avb_building_vbmeta_image") == "true":
Tao Baoba557702018-03-10 20:41:16 -0800401 logging.info('Verifying Verified Boot 2.0 (AVB) images...')
402
Tao Baoa81d4292019-03-26 12:13:04 -0700403 key = options['verity_key']
404 if key is None:
405 key = info_dict['avb_vbmeta_key_path']
406
Kelvin Zhang4093d602021-05-25 09:17:38 -0400407 ValidatePartitionFingerprints(input_tmp, info_dict)
408
Tao Baoa81d4292019-03-26 12:13:04 -0700409 # avbtool verifies all the images that have descriptors listed in vbmeta.
cfig1aeef722019-09-20 22:45:06 +0800410 # Using `--follow_chain_partitions` so it would additionally verify chained
411 # vbmeta partitions (e.g. vbmeta_system).
Tao Baoa81d4292019-03-26 12:13:04 -0700412 image = os.path.join(input_tmp, 'IMAGES', 'vbmeta.img')
Tao Bao1ac886e2019-06-26 11:58:22 -0700413 cmd = [info_dict['avb_avbtool'], 'verify_image', '--image', image,
Tianjie Xu9bd832a2020-02-06 13:12:56 -0800414 '--follow_chain_partitions']
Tao Baoa81d4292019-03-26 12:13:04 -0700415
Hongguang Chenf23364d2020-04-27 18:36:36 -0700416 # Custom images.
417 custom_partitions = info_dict.get(
418 "avb_custom_images_partition_list", "").strip().split()
419
Tao Baoa81d4292019-03-26 12:13:04 -0700420 # Append the args for chained partitions if any.
Hongguang Chenf23364d2020-04-27 18:36:36 -0700421 for partition in (common.AVB_PARTITIONS + common.AVB_VBMETA_PARTITIONS +
422 tuple(custom_partitions)):
Tao Baoa81d4292019-03-26 12:13:04 -0700423 key_name = 'avb_' + partition + '_key_path'
424 if info_dict.get(key_name) is not None:
cfig1aeef722019-09-20 22:45:06 +0800425 if info_dict.get('ab_update') != 'true' and partition == 'recovery':
426 continue
427
Tao Bao08c190f2019-06-03 23:07:58 -0700428 # Use the key file from command line if specified; otherwise fall back
429 # to the one in info dict.
430 key_file = options.get(key_name, info_dict[key_name])
Tao Baoa81d4292019-03-26 12:13:04 -0700431 chained_partition_arg = common.GetAvbChainedPartitionArg(
Tao Bao08c190f2019-06-03 23:07:58 -0700432 partition, info_dict, key_file)
Dennis Song4aae62e2023-10-02 04:31:34 +0000433 cmd.extend(['--expected_chain_partition',
434 chained_partition_arg.to_string()])
Tao Baoa81d4292019-03-26 12:13:04 -0700435
Tianjie5ec1a7a2020-06-25 22:59:54 -0700436 # Handle the boot image with a non-default name, e.g. boot-5.4.img
437 boot_images = info_dict.get("boot_images")
438 if boot_images:
439 # we used the 1st boot image to generate the vbmeta. Rename the filename
440 # to boot.img so that avbtool can find it correctly.
441 first_image_name = boot_images.split()[0]
442 first_image_path = os.path.join(input_tmp, 'IMAGES', first_image_name)
443 assert os.path.isfile(first_image_path)
444 renamed_boot_image_path = os.path.join(input_tmp, 'IMAGES', 'boot.img')
445 os.rename(first_image_path, renamed_boot_image_path)
446
Tao Baoa81d4292019-03-26 12:13:04 -0700447 proc = common.Run(cmd)
448 stdoutdata, _ = proc.communicate()
449 assert proc.returncode == 0, \
450 'Failed to verify {} with avbtool (key: {}):\n{}'.format(
451 image, key, stdoutdata)
452
453 logging.info(
454 'Verified %s with avbtool (key: %s):\n%s', image, key,
455 stdoutdata.rstrip())
Tao Baoba557702018-03-10 20:41:16 -0800456
cfig1aeef722019-09-20 22:45:06 +0800457 # avbtool verifies recovery image for non-A/B devices.
458 if (info_dict.get('ab_update') != 'true' and
Kelvin Zhang4093d602021-05-25 09:17:38 -0400459 info_dict.get('no_recovery') != 'true'):
cfig1aeef722019-09-20 22:45:06 +0800460 image = os.path.join(input_tmp, 'IMAGES', 'recovery.img')
461 key = info_dict['avb_recovery_key_path']
462 cmd = [info_dict['avb_avbtool'], 'verify_image', '--image', image,
463 '--key', key]
464 proc = common.Run(cmd)
465 stdoutdata, _ = proc.communicate()
466 assert proc.returncode == 0, \
467 'Failed to verify {} with avbtool (key: {}):\n{}'.format(
468 image, key, stdoutdata)
469 logging.info(
470 'Verified %s with avbtool (key: %s):\n%s', image, key,
471 stdoutdata.rstrip())
472
Tianjie2e0b8352021-01-12 14:04:58 -0800473
474def CheckDataInconsistency(lines):
Kelvin Zhang4093d602021-05-25 09:17:38 -0400475 build_prop = {}
476 for line in lines:
477 if line.startswith("import") or line.startswith("#"):
478 continue
479 if "=" not in line:
480 continue
Tianjie2e0b8352021-01-12 14:04:58 -0800481
Kelvin Zhang4093d602021-05-25 09:17:38 -0400482 key, value = line.rstrip().split("=", 1)
483 if key in build_prop:
484 logging.info("Duplicated key found for {}".format(key))
485 if value != build_prop[key]:
486 logging.error("Key {} is defined twice with different values {} vs {}"
487 .format(key, value, build_prop[key]))
488 return key
489 build_prop[key] = value
Kelvin Zhangf2e846f2020-06-29 16:04:51 -0400490
Tianjie2e0b8352021-01-12 14:04:58 -0800491
Kelvin Zhangf2e846f2020-06-29 16:04:51 -0400492def CheckBuildPropDuplicity(input_tmp):
493 """Check all buld.prop files inside directory input_tmp, raise error
494 if they contain duplicates"""
495
496 if not os.path.isdir(input_tmp):
497 raise ValueError("Expect {} to be a directory".format(input_tmp))
498 for name in os.listdir(input_tmp):
499 if not name.isupper():
500 continue
501 for prop_file in ['build.prop', 'etc/build.prop']:
502 path = os.path.join(input_tmp, name, prop_file)
503 if not os.path.exists(path):
504 continue
505 logging.info("Checking {}".format(path))
506 with open(path, 'r') as fp:
Tianjie2e0b8352021-01-12 14:04:58 -0800507 dupKey = CheckDataInconsistency(fp.readlines())
Kelvin Zhangf2e846f2020-06-29 16:04:51 -0400508 if dupKey:
Tianjie2e0b8352021-01-12 14:04:58 -0800509 raise ValueError("{} contains duplicate keys for {}".format(
510 path, dupKey))
511
Tao Baoba557702018-03-10 20:41:16 -0800512
513def main():
514 parser = argparse.ArgumentParser(
515 description=__doc__,
516 formatter_class=argparse.RawDescriptionHelpFormatter)
517 parser.add_argument(
518 'target_files',
519 help='the input target_files.zip to be validated')
520 parser.add_argument(
521 '--verity_key',
522 help='the verity public key to verify the bootable images (Verified '
Tao Bao02a08592018-07-22 12:40:45 -0700523 'Boot 1.0), or the vbmeta image (Verified Boot 2.0, aka AVB), where '
Tao Baoba557702018-03-10 20:41:16 -0800524 'applicable')
Tao Bao08c190f2019-06-03 23:07:58 -0700525 for partition in common.AVB_PARTITIONS + common.AVB_VBMETA_PARTITIONS:
Tao Bao02a08592018-07-22 12:40:45 -0700526 parser.add_argument(
527 '--avb_' + partition + '_key_path',
528 help='the public or private key in PEM format to verify AVB chained '
529 'partition of {}'.format(partition))
Tao Baoba557702018-03-10 20:41:16 -0800530 parser.add_argument(
531 '--verity_key_mincrypt',
532 help='the verity public key in mincrypt format to verify the system '
533 'images, if target using Verified Boot 1.0')
534 args = parser.parse_args()
535
536 # Unprovided args will have 'None' as the value.
537 options = vars(args)
Tao Baoafaa0a62017-02-27 15:08:36 -0800538
539 logging_format = '%(asctime)s - %(filename)s - %(levelname)-8s: %(message)s'
540 date_format = '%Y/%m/%d %H:%M:%S'
541 logging.basicConfig(level=logging.INFO, format=logging_format,
542 datefmt=date_format)
543
Tao Baoba557702018-03-10 20:41:16 -0800544 logging.info("Unzipping the input target_files.zip: %s", args.target_files)
545 input_tmp = common.UnzipTemp(args.target_files)
Tao Baoafaa0a62017-02-27 15:08:36 -0800546
Tianjie Xu9c384d22017-06-20 17:00:55 -0700547 info_dict = common.LoadInfoDict(input_tmp)
Kelvin Zhang928c2342020-09-22 16:15:57 -0400548 with zipfile.ZipFile(args.target_files, 'r', allowZip64=True) as input_zip:
Tao Bao63e2f492018-05-11 23:38:46 -0700549 ValidateFileConsistency(input_zip, input_tmp, info_dict)
550
Kelvin Zhangf2e846f2020-06-29 16:04:51 -0400551 CheckBuildPropDuplicity(input_tmp)
552
Tianjie Xu9c384d22017-06-20 17:00:55 -0700553 ValidateInstallRecoveryScript(input_tmp, info_dict)
554
Tao Baoba557702018-03-10 20:41:16 -0800555 ValidateVerifiedBootImages(input_tmp, info_dict, options)
556
Tao Baoafaa0a62017-02-27 15:08:36 -0800557 # TODO: Check if the OTA keys have been properly updated (the ones on /system,
558 # in recovery image).
559
Tao Baoafaa0a62017-02-27 15:08:36 -0800560 logging.info("Done.")
561
562
563if __name__ == '__main__':
564 try:
Tao Baoba557702018-03-10 20:41:16 -0800565 main()
Tao Baoafaa0a62017-02-27 15:08:36 -0800566 finally:
567 common.Cleanup()