blob: 886de26221e52670539fdbe353e7ac3187f149e0 [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 Baoba557702018-03-10 20:41:16 -080038import subprocess
Tao Baoc63626b2018-03-07 21:40:24 -080039import zipfile
Tao Baoafaa0a62017-02-27 15:08:36 -080040
Tao Baobb20e8c2018-02-01 12:00:19 -080041import common
Tao Baoafaa0a62017-02-27 15:08:36 -080042
43
Tao Baob418c302017-08-30 15:54:59 -070044def _ReadFile(file_name, unpacked_name, round_up=False):
45 """Constructs and returns a File object. Rounds up its size if needed."""
Tao Baoafaa0a62017-02-27 15:08:36 -080046
Tianjie Xu9c384d22017-06-20 17:00:55 -070047 assert os.path.exists(unpacked_name)
48 with open(unpacked_name, 'r') as f:
49 file_data = f.read()
50 file_size = len(file_data)
51 if round_up:
Tao Baoc765cca2018-01-31 17:32:40 -080052 file_size_rounded_up = common.RoundUpTo4K(file_size)
Tianjie Xu9c384d22017-06-20 17:00:55 -070053 file_data += '\0' * (file_size_rounded_up - file_size)
Tao Baob418c302017-08-30 15:54:59 -070054 return common.File(file_name, file_data)
Tianjie Xu9c384d22017-06-20 17:00:55 -070055
56
57def ValidateFileAgainstSha1(input_tmp, file_name, file_path, expected_sha1):
58 """Check if the file has the expected SHA-1."""
59
Tao Baobb20e8c2018-02-01 12:00:19 -080060 logging.info('Validating the SHA-1 of %s', file_name)
Tianjie Xu9c384d22017-06-20 17:00:55 -070061 unpacked_name = os.path.join(input_tmp, file_path)
62 assert os.path.exists(unpacked_name)
Tao Baob418c302017-08-30 15:54:59 -070063 actual_sha1 = _ReadFile(file_name, unpacked_name, False).sha1
Tianjie Xu9c384d22017-06-20 17:00:55 -070064 assert actual_sha1 == expected_sha1, \
65 'SHA-1 mismatches for {}. actual {}, expected {}'.format(
Tao Baobb20e8c2018-02-01 12:00:19 -080066 file_name, actual_sha1, expected_sha1)
Tianjie Xu9c384d22017-06-20 17:00:55 -070067
68
Tao Bao63e2f492018-05-11 23:38:46 -070069def ValidateFileConsistency(input_zip, input_tmp, info_dict):
Tianjie Xu9c384d22017-06-20 17:00:55 -070070 """Compare the files from image files and unpacked folders."""
71
Tao Baoafaa0a62017-02-27 15:08:36 -080072 def CheckAllFiles(which):
73 logging.info('Checking %s image.', which)
Tao Baoc63626b2018-03-07 21:40:24 -080074 # Allow having shared blocks when loading the sparse image, because allowing
75 # that doesn't affect the checks below (we will have all the blocks on file,
76 # unless it's skipped due to the holes).
77 image = common.GetSparseImage(which, input_tmp, input_zip, True)
Tao Baoafaa0a62017-02-27 15:08:36 -080078 prefix = '/' + which
79 for entry in image.file_map:
Tao Baoc765cca2018-01-31 17:32:40 -080080 # Skip entries like '__NONZERO-0'.
Tao Baoafaa0a62017-02-27 15:08:36 -080081 if not entry.startswith(prefix):
82 continue
83
84 # Read the blocks that the file resides. Note that it will contain the
85 # bytes past the file length, which is expected to be padded with '\0's.
86 ranges = image.file_map[entry]
Tao Baoc765cca2018-01-31 17:32:40 -080087
88 incomplete = ranges.extra.get('incomplete', False)
89 if incomplete:
90 logging.warning('Skipping %s that has incomplete block list', entry)
91 continue
92
Tao Baod32936d2018-05-17 19:42:41 -070093 # TODO(b/79951650): Handle files with non-monotonic ranges.
94 if not ranges.monotonic:
95 logging.warning(
96 'Skipping %s that has non-monotonic ranges: %s', entry, ranges)
97 continue
98
Tao Baoafaa0a62017-02-27 15:08:36 -080099 blocks_sha1 = image.RangeSha1(ranges)
100
101 # The filename under unpacked directory, such as SYSTEM/bin/sh.
102 unpacked_name = os.path.join(
103 input_tmp, which.upper(), entry[(len(prefix) + 1):])
Tao Baob418c302017-08-30 15:54:59 -0700104 unpacked_file = _ReadFile(entry, unpacked_name, True)
Tao Baob418c302017-08-30 15:54:59 -0700105 file_sha1 = unpacked_file.sha1
Tao Baoafaa0a62017-02-27 15:08:36 -0800106 assert blocks_sha1 == file_sha1, \
107 'file: %s, range: %s, blocks_sha1: %s, file_sha1: %s' % (
108 entry, ranges, blocks_sha1, file_sha1)
109
110 logging.info('Validating file consistency.')
111
Tao Bao63e2f492018-05-11 23:38:46 -0700112 # TODO(b/79617342): Validate non-sparse images.
113 if info_dict.get('extfs_sparse_flag') != '-s':
114 logging.warning('Skipped due to target using non-sparse images')
115 return
116
Tao Baoafaa0a62017-02-27 15:08:36 -0800117 # Verify IMAGES/system.img.
118 CheckAllFiles('system')
119
120 # Verify IMAGES/vendor.img if applicable.
121 if 'VENDOR/' in input_zip.namelist():
122 CheckAllFiles('vendor')
123
124 # Not checking IMAGES/system_other.img since it doesn't have the map file.
125
126
Tianjie Xu9c384d22017-06-20 17:00:55 -0700127def ValidateInstallRecoveryScript(input_tmp, info_dict):
128 """Validate the SHA-1 embedded in install-recovery.sh.
129
130 install-recovery.sh is written in common.py and has the following format:
131
132 1. full recovery:
133 ...
134 if ! applypatch -c type:device:size:SHA-1; then
135 applypatch /system/etc/recovery.img type:device sha1 size && ...
136 ...
137
138 2. recovery from boot:
139 ...
140 applypatch [-b bonus_args] boot_info recovery_info recovery_sha1 \
141 recovery_size patch_info && ...
142 ...
143
144 For full recovery, we want to calculate the SHA-1 of /system/etc/recovery.img
145 and compare it against the one embedded in the script. While for recovery
146 from boot, we want to check the SHA-1 for both recovery.img and boot.img
147 under IMAGES/.
148 """
149
150 script_path = 'SYSTEM/bin/install-recovery.sh'
151 if not os.path.exists(os.path.join(input_tmp, script_path)):
Tao Baobb20e8c2018-02-01 12:00:19 -0800152 logging.info('%s does not exist in input_tmp', script_path)
Tianjie Xu9c384d22017-06-20 17:00:55 -0700153 return
154
Tao Baobb20e8c2018-02-01 12:00:19 -0800155 logging.info('Checking %s', script_path)
Tianjie Xu9c384d22017-06-20 17:00:55 -0700156 with open(os.path.join(input_tmp, script_path), 'r') as script:
157 lines = script.read().strip().split('\n')
158 assert len(lines) >= 6
159 check_cmd = re.search(r'if ! applypatch -c \w+:.+:\w+:(\w+);',
160 lines[1].strip())
161 expected_recovery_check_sha1 = check_cmd.group(1)
162 patch_cmd = re.search(r'(applypatch.+)&&', lines[2].strip())
163 applypatch_argv = patch_cmd.group(1).strip().split()
164
165 full_recovery_image = info_dict.get("full_recovery_image") == "true"
166 if full_recovery_image:
167 assert len(applypatch_argv) == 5
168 # Check we have the same expected SHA-1 of recovery.img in both check mode
169 # and patch mode.
170 expected_recovery_sha1 = applypatch_argv[3].strip()
171 assert expected_recovery_check_sha1 == expected_recovery_sha1
172 ValidateFileAgainstSha1(input_tmp, 'recovery.img',
Tao Baobb20e8c2018-02-01 12:00:19 -0800173 'SYSTEM/etc/recovery.img', expected_recovery_sha1)
Tianjie Xu9c384d22017-06-20 17:00:55 -0700174 else:
175 # We're patching boot.img to get recovery.img where bonus_args is optional
176 if applypatch_argv[1] == "-b":
177 assert len(applypatch_argv) == 8
178 boot_info_index = 3
179 else:
180 assert len(applypatch_argv) == 6
181 boot_info_index = 1
182
183 # boot_info: boot_type:boot_device:boot_size:boot_sha1
184 boot_info = applypatch_argv[boot_info_index].strip().split(':')
185 assert len(boot_info) == 4
186 ValidateFileAgainstSha1(input_tmp, file_name='boot.img',
Tao Baobb20e8c2018-02-01 12:00:19 -0800187 file_path='IMAGES/boot.img',
188 expected_sha1=boot_info[3])
Tianjie Xu9c384d22017-06-20 17:00:55 -0700189
190 recovery_sha1_index = boot_info_index + 2
191 expected_recovery_sha1 = applypatch_argv[recovery_sha1_index]
192 assert expected_recovery_check_sha1 == expected_recovery_sha1
193 ValidateFileAgainstSha1(input_tmp, file_name='recovery.img',
Tao Baobb20e8c2018-02-01 12:00:19 -0800194 file_path='IMAGES/recovery.img',
195 expected_sha1=expected_recovery_sha1)
Tianjie Xu9c384d22017-06-20 17:00:55 -0700196
Tao Baobb20e8c2018-02-01 12:00:19 -0800197 logging.info('Done checking %s', script_path)
Tianjie Xu9c384d22017-06-20 17:00:55 -0700198
199
Tao Baoba557702018-03-10 20:41:16 -0800200def ValidateVerifiedBootImages(input_tmp, info_dict, options):
201 """Validates the Verified Boot related images.
Tao Baoafaa0a62017-02-27 15:08:36 -0800202
Tao Baoba557702018-03-10 20:41:16 -0800203 For Verified Boot 1.0, it verifies the signatures of the bootable images
204 (boot/recovery etc), as well as the dm-verity metadata in system images
205 (system/vendor/product). For Verified Boot 2.0, it calls avbtool to verify
206 vbmeta.img, which in turn verifies all the descriptors listed in vbmeta.
Tao Baoafaa0a62017-02-27 15:08:36 -0800207
Tao Baoba557702018-03-10 20:41:16 -0800208 Args:
209 input_tmp: The top-level directory of unpacked target-files.zip.
210 info_dict: The loaded info dict.
211 options: A dict that contains the user-supplied public keys to be used for
212 image verification. In particular, 'verity_key' is used to verify the
213 bootable images in VB 1.0, and the vbmeta image in VB 2.0, where
214 applicable. 'verity_key_mincrypt' will be used to verify the system
215 images in VB 1.0.
216
217 Raises:
218 AssertionError: On any verification failure.
219 """
220 # Verified boot 1.0 (images signed with boot_signer and verity_signer).
221 if info_dict.get('boot_signer') == 'true':
222 logging.info('Verifying Verified Boot images...')
223
224 # Verify the boot/recovery images (signed with boot_signer), against the
225 # given X.509 encoded pubkey (or falling back to the one in the info_dict if
226 # none given).
227 verity_key = options['verity_key']
228 if verity_key is None:
229 verity_key = info_dict['verity_key'] + '.x509.pem'
230 for image in ('boot.img', 'recovery.img', 'recovery-two-step.img'):
231 image_path = os.path.join(input_tmp, 'IMAGES', image)
232 if not os.path.exists(image_path):
233 continue
234
235 cmd = ['boot_signer', '-verify', image_path, '-certificate', verity_key]
236 proc = common.Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
237 stdoutdata, _ = proc.communicate()
238 assert proc.returncode == 0, \
239 'Failed to verify {} with boot_signer:\n{}'.format(image, stdoutdata)
240 logging.info(
241 'Verified %s with boot_signer (key: %s):\n%s', image, verity_key,
242 stdoutdata.rstrip())
243
244 # Verify verity signed system images in Verified Boot 1.0. Note that not using
245 # 'elif' here, since 'boot_signer' and 'verity' are not bundled in VB 1.0.
246 if info_dict.get('verity') == 'true':
247 # First verify that the verity key that's built into the root image (as
248 # /verity_key) matches the one given via command line, if any.
249 if info_dict.get("system_root_image") == "true":
250 verity_key_mincrypt = os.path.join(input_tmp, 'ROOT', 'verity_key')
251 else:
252 verity_key_mincrypt = os.path.join(
253 input_tmp, 'BOOT', 'RAMDISK', 'verity_key')
254 assert os.path.exists(verity_key_mincrypt), 'Missing verity_key'
255
256 if options['verity_key_mincrypt'] is None:
257 logging.warn(
258 'Skipped checking the content of /verity_key, as the key file not '
259 'provided. Use --verity_key_mincrypt to specify.')
260 else:
261 expected_key = options['verity_key_mincrypt']
262 assert filecmp.cmp(expected_key, verity_key_mincrypt, shallow=False), \
263 "Mismatching mincrypt verity key files"
264 logging.info('Verified the content of /verity_key')
265
266 # Then verify the verity signed system/vendor/product images, against the
267 # verity pubkey in mincrypt format.
268 for image in ('system.img', 'vendor.img', 'product.img'):
269 image_path = os.path.join(input_tmp, 'IMAGES', image)
270
271 # We are not checking if the image is actually enabled via info_dict (e.g.
272 # 'system_verity_block_device=...'). Because it's most likely a bug that
273 # skips signing some of the images in signed target-files.zip, while
274 # having the top-level verity flag enabled.
275 if not os.path.exists(image_path):
276 continue
277
278 cmd = ['verity_verifier', image_path, '-mincrypt', verity_key_mincrypt]
279 proc = common.Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
280 stdoutdata, _ = proc.communicate()
281 assert proc.returncode == 0, \
282 'Failed to verify {} with verity_verifier (key: {}):\n{}'.format(
283 image, verity_key_mincrypt, stdoutdata)
284 logging.info(
285 'Verified %s with verity_verifier (key: %s):\n%s', image,
286 verity_key_mincrypt, stdoutdata.rstrip())
287
288 # Handle the case of Verified Boot 2.0 (AVB).
289 if info_dict.get("avb_enable") == "true":
290 logging.info('Verifying Verified Boot 2.0 (AVB) images...')
291
292 key = options['verity_key']
293 if key is None:
294 key = info_dict['avb_vbmeta_key_path']
295 # avbtool verifies all the images that have descriptors listed in vbmeta.
296 image = os.path.join(input_tmp, 'IMAGES', 'vbmeta.img')
297 cmd = ['avbtool', 'verify_image', '--image', image, '--key', key]
298 proc = common.Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
299 stdoutdata, _ = proc.communicate()
300 assert proc.returncode == 0, \
301 'Failed to verify {} with verity_verifier (key: {}):\n{}'.format(
302 image, key, stdoutdata)
303
304 logging.info(
305 'Verified %s with avbtool (key: %s):\n%s', image, key,
306 stdoutdata.rstrip())
307
308
309def main():
310 parser = argparse.ArgumentParser(
311 description=__doc__,
312 formatter_class=argparse.RawDescriptionHelpFormatter)
313 parser.add_argument(
314 'target_files',
315 help='the input target_files.zip to be validated')
316 parser.add_argument(
317 '--verity_key',
318 help='the verity public key to verify the bootable images (Verified '
319 'Boot 1.0), or the vbmeta image (Verified Boot 2.0), where '
320 'applicable')
321 parser.add_argument(
322 '--verity_key_mincrypt',
323 help='the verity public key in mincrypt format to verify the system '
324 'images, if target using Verified Boot 1.0')
325 args = parser.parse_args()
326
327 # Unprovided args will have 'None' as the value.
328 options = vars(args)
Tao Baoafaa0a62017-02-27 15:08:36 -0800329
330 logging_format = '%(asctime)s - %(filename)s - %(levelname)-8s: %(message)s'
331 date_format = '%Y/%m/%d %H:%M:%S'
332 logging.basicConfig(level=logging.INFO, format=logging_format,
333 datefmt=date_format)
334
Tao Baoba557702018-03-10 20:41:16 -0800335 logging.info("Unzipping the input target_files.zip: %s", args.target_files)
336 input_tmp = common.UnzipTemp(args.target_files)
Tao Baoafaa0a62017-02-27 15:08:36 -0800337
Tianjie Xu9c384d22017-06-20 17:00:55 -0700338 info_dict = common.LoadInfoDict(input_tmp)
Tao Bao63e2f492018-05-11 23:38:46 -0700339 with zipfile.ZipFile(args.target_files, 'r') as input_zip:
340 ValidateFileConsistency(input_zip, input_tmp, info_dict)
341
Tianjie Xu9c384d22017-06-20 17:00:55 -0700342 ValidateInstallRecoveryScript(input_tmp, info_dict)
343
Tao Baoba557702018-03-10 20:41:16 -0800344 ValidateVerifiedBootImages(input_tmp, info_dict, options)
345
Tao Baoafaa0a62017-02-27 15:08:36 -0800346 # TODO: Check if the OTA keys have been properly updated (the ones on /system,
347 # in recovery image).
348
Tao Baoafaa0a62017-02-27 15:08:36 -0800349 logging.info("Done.")
350
351
352if __name__ == '__main__':
353 try:
Tao Baoba557702018-03-10 20:41:16 -0800354 main()
Tao Baoafaa0a62017-02-27 15:08:36 -0800355 finally:
356 common.Cleanup()