blob: 8c9e07c08b60ec0a9cdd2319c913375218349fb8 [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
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
Tao Baoafaa0a62017-02-27 15:08:36 -080026import logging
27import os.path
Tianjie Xu9c384d22017-06-20 17:00:55 -070028import re
Tao Baoafaa0a62017-02-27 15:08:36 -080029import sys
Tao Baof6f13ac2018-03-07 21:40:24 -080030import zipfile
Tao Baoafaa0a62017-02-27 15:08:36 -080031
Tao Baobb20e8c2018-02-01 12:00:19 -080032import common
Tao Baoafaa0a62017-02-27 15:08:36 -080033
34
Tao Baob418c302017-08-30 15:54:59 -070035def _ReadFile(file_name, unpacked_name, round_up=False):
36 """Constructs and returns a File object. Rounds up its size if needed."""
Tao Baoafaa0a62017-02-27 15:08:36 -080037
Tianjie Xu9c384d22017-06-20 17:00:55 -070038 assert os.path.exists(unpacked_name)
39 with open(unpacked_name, 'r') as f:
40 file_data = f.read()
41 file_size = len(file_data)
42 if round_up:
Tao Baoc765cca2018-01-31 17:32:40 -080043 file_size_rounded_up = common.RoundUpTo4K(file_size)
Tianjie Xu9c384d22017-06-20 17:00:55 -070044 file_data += '\0' * (file_size_rounded_up - file_size)
Tao Baob418c302017-08-30 15:54:59 -070045 return common.File(file_name, file_data)
Tianjie Xu9c384d22017-06-20 17:00:55 -070046
47
48def ValidateFileAgainstSha1(input_tmp, file_name, file_path, expected_sha1):
49 """Check if the file has the expected SHA-1."""
50
Tao Baobb20e8c2018-02-01 12:00:19 -080051 logging.info('Validating the SHA-1 of %s', file_name)
Tianjie Xu9c384d22017-06-20 17:00:55 -070052 unpacked_name = os.path.join(input_tmp, file_path)
53 assert os.path.exists(unpacked_name)
Tao Baob418c302017-08-30 15:54:59 -070054 actual_sha1 = _ReadFile(file_name, unpacked_name, False).sha1
Tianjie Xu9c384d22017-06-20 17:00:55 -070055 assert actual_sha1 == expected_sha1, \
56 'SHA-1 mismatches for {}. actual {}, expected {}'.format(
Tao Baobb20e8c2018-02-01 12:00:19 -080057 file_name, actual_sha1, expected_sha1)
Tianjie Xu9c384d22017-06-20 17:00:55 -070058
59
Tao Bao242a9e52018-05-11 23:38:46 -070060def ValidateFileConsistency(input_zip, input_tmp, info_dict):
Tianjie Xu9c384d22017-06-20 17:00:55 -070061 """Compare the files from image files and unpacked folders."""
62
Tao Baoafaa0a62017-02-27 15:08:36 -080063 def CheckAllFiles(which):
64 logging.info('Checking %s image.', which)
Tao Baof6f13ac2018-03-07 21:40:24 -080065 # Allow having shared blocks when loading the sparse image, because allowing
66 # that doesn't affect the checks below (we will have all the blocks on file,
67 # unless it's skipped due to the holes).
68 image = common.GetSparseImage(which, input_tmp, input_zip, True)
Tao Baoafaa0a62017-02-27 15:08:36 -080069 prefix = '/' + which
70 for entry in image.file_map:
Tao Baoc765cca2018-01-31 17:32:40 -080071 # Skip entries like '__NONZERO-0'.
Tao Baoafaa0a62017-02-27 15:08:36 -080072 if not entry.startswith(prefix):
73 continue
74
75 # Read the blocks that the file resides. Note that it will contain the
76 # bytes past the file length, which is expected to be padded with '\0's.
77 ranges = image.file_map[entry]
Tao Baoc765cca2018-01-31 17:32:40 -080078
79 incomplete = ranges.extra.get('incomplete', False)
80 if incomplete:
81 logging.warning('Skipping %s that has incomplete block list', entry)
82 continue
83
Tao Baodd24c972018-05-17 19:42:41 -070084 # TODO(b/79951650): Handle files with non-monotonic ranges.
85 if not ranges.monotonic:
86 logging.warning(
87 'Skipping %s that has non-monotonic ranges: %s', entry, ranges)
88 continue
89
Tao Baoafaa0a62017-02-27 15:08:36 -080090 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):])
Tao Baob418c302017-08-30 15:54:59 -070095 unpacked_file = _ReadFile(entry, unpacked_name, True)
Tao Baob418c302017-08-30 15:54:59 -070096 file_sha1 = unpacked_file.sha1
Tao Baoafaa0a62017-02-27 15:08:36 -080097 assert blocks_sha1 == file_sha1, \
98 'file: %s, range: %s, blocks_sha1: %s, file_sha1: %s' % (
99 entry, ranges, blocks_sha1, file_sha1)
100
101 logging.info('Validating file consistency.')
102
Tao Bao242a9e52018-05-11 23:38:46 -0700103 # TODO(b/79617342): Validate non-sparse images.
104 if info_dict.get('extfs_sparse_flag') != '-s':
105 logging.warning('Skipped due to target using non-sparse images')
106 return
107
Tao Baoafaa0a62017-02-27 15:08:36 -0800108 # Verify IMAGES/system.img.
109 CheckAllFiles('system')
110
111 # Verify IMAGES/vendor.img if applicable.
112 if 'VENDOR/' in input_zip.namelist():
113 CheckAllFiles('vendor')
114
115 # Not checking IMAGES/system_other.img since it doesn't have the map file.
116
117
Tianjie Xu9c384d22017-06-20 17:00:55 -0700118def ValidateInstallRecoveryScript(input_tmp, info_dict):
119 """Validate the SHA-1 embedded in install-recovery.sh.
120
121 install-recovery.sh is written in common.py and has the following format:
122
123 1. full recovery:
124 ...
125 if ! applypatch -c type:device:size:SHA-1; then
126 applypatch /system/etc/recovery.img type:device sha1 size && ...
127 ...
128
129 2. recovery from boot:
130 ...
131 applypatch [-b bonus_args] boot_info recovery_info recovery_sha1 \
132 recovery_size patch_info && ...
133 ...
134
135 For full recovery, we want to calculate the SHA-1 of /system/etc/recovery.img
136 and compare it against the one embedded in the script. While for recovery
137 from boot, we want to check the SHA-1 for both recovery.img and boot.img
138 under IMAGES/.
139 """
140
141 script_path = 'SYSTEM/bin/install-recovery.sh'
142 if not os.path.exists(os.path.join(input_tmp, script_path)):
Tao Baobb20e8c2018-02-01 12:00:19 -0800143 logging.info('%s does not exist in input_tmp', script_path)
Tianjie Xu9c384d22017-06-20 17:00:55 -0700144 return
145
Tao Baobb20e8c2018-02-01 12:00:19 -0800146 logging.info('Checking %s', script_path)
Tianjie Xu9c384d22017-06-20 17:00:55 -0700147 with open(os.path.join(input_tmp, script_path), 'r') as script:
148 lines = script.read().strip().split('\n')
149 assert len(lines) >= 6
150 check_cmd = re.search(r'if ! applypatch -c \w+:.+:\w+:(\w+);',
151 lines[1].strip())
152 expected_recovery_check_sha1 = check_cmd.group(1)
153 patch_cmd = re.search(r'(applypatch.+)&&', lines[2].strip())
154 applypatch_argv = patch_cmd.group(1).strip().split()
155
156 full_recovery_image = info_dict.get("full_recovery_image") == "true"
157 if full_recovery_image:
158 assert len(applypatch_argv) == 5
159 # Check we have the same expected SHA-1 of recovery.img in both check mode
160 # and patch mode.
161 expected_recovery_sha1 = applypatch_argv[3].strip()
162 assert expected_recovery_check_sha1 == expected_recovery_sha1
163 ValidateFileAgainstSha1(input_tmp, 'recovery.img',
Tao Baobb20e8c2018-02-01 12:00:19 -0800164 'SYSTEM/etc/recovery.img', expected_recovery_sha1)
Tianjie Xu9c384d22017-06-20 17:00:55 -0700165 else:
166 # We're patching boot.img to get recovery.img where bonus_args is optional
167 if applypatch_argv[1] == "-b":
168 assert len(applypatch_argv) == 8
169 boot_info_index = 3
170 else:
171 assert len(applypatch_argv) == 6
172 boot_info_index = 1
173
174 # boot_info: boot_type:boot_device:boot_size:boot_sha1
175 boot_info = applypatch_argv[boot_info_index].strip().split(':')
176 assert len(boot_info) == 4
177 ValidateFileAgainstSha1(input_tmp, file_name='boot.img',
Tao Baobb20e8c2018-02-01 12:00:19 -0800178 file_path='IMAGES/boot.img',
179 expected_sha1=boot_info[3])
Tianjie Xu9c384d22017-06-20 17:00:55 -0700180
181 recovery_sha1_index = boot_info_index + 2
182 expected_recovery_sha1 = applypatch_argv[recovery_sha1_index]
183 assert expected_recovery_check_sha1 == expected_recovery_sha1
184 ValidateFileAgainstSha1(input_tmp, file_name='recovery.img',
Tao Baobb20e8c2018-02-01 12:00:19 -0800185 file_path='IMAGES/recovery.img',
186 expected_sha1=expected_recovery_sha1)
Tianjie Xu9c384d22017-06-20 17:00:55 -0700187
Tao Baobb20e8c2018-02-01 12:00:19 -0800188 logging.info('Done checking %s', script_path)
Tianjie Xu9c384d22017-06-20 17:00:55 -0700189
190
Tao Baoafaa0a62017-02-27 15:08:36 -0800191def main(argv):
192 def option_handler():
193 return True
194
195 args = common.ParseOptions(
196 argv, __doc__, extra_opts="",
197 extra_long_opts=[],
198 extra_option_handler=option_handler)
199
200 if len(args) != 1:
201 common.Usage(__doc__)
202 sys.exit(1)
203
204 logging_format = '%(asctime)s - %(filename)s - %(levelname)-8s: %(message)s'
205 date_format = '%Y/%m/%d %H:%M:%S'
206 logging.basicConfig(level=logging.INFO, format=logging_format,
207 datefmt=date_format)
208
209 logging.info("Unzipping the input target_files.zip: %s", args[0])
Tao Baodba59ee2018-01-09 13:21:02 -0800210 input_tmp = common.UnzipTemp(args[0])
Tao Baoafaa0a62017-02-27 15:08:36 -0800211
Tianjie Xu9c384d22017-06-20 17:00:55 -0700212 info_dict = common.LoadInfoDict(input_tmp)
Tao Bao242a9e52018-05-11 23:38:46 -0700213 with zipfile.ZipFile(args[0], 'r') as input_zip:
214 ValidateFileConsistency(input_zip, input_tmp, info_dict)
215
Tianjie Xu9c384d22017-06-20 17:00:55 -0700216 ValidateInstallRecoveryScript(input_tmp, info_dict)
217
Tao Baoafaa0a62017-02-27 15:08:36 -0800218 # TODO: Check if the OTA keys have been properly updated (the ones on /system,
219 # in recovery image).
220
Tao Baoafaa0a62017-02-27 15:08:36 -0800221 logging.info("Done.")
222
223
224if __name__ == '__main__':
225 try:
226 main(sys.argv[1:])
227 finally:
228 common.Cleanup()