blob: 8ac3322c631eb2ab3243f297ece1da37328802e3 [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
26import common
27import logging
28import os.path
Tianjie Xu9c384d22017-06-20 17:00:55 -070029import re
Tao Baoafaa0a62017-02-27 15:08:36 -080030import sparse_img
31import sys
32
33
34def _GetImage(which, tmpdir):
35 assert which in ('system', 'vendor')
36
37 path = os.path.join(tmpdir, 'IMAGES', which + '.img')
38 mappath = os.path.join(tmpdir, 'IMAGES', which + '.map')
39
40 # Map file must exist (allowed to be empty).
41 assert os.path.exists(path) and os.path.exists(mappath)
42
43 clobbered_blocks = '0'
44 return sparse_img.SparseImage(path, mappath, clobbered_blocks)
45
46
Tianjie Xu9c384d22017-06-20 17:00:55 -070047def _CalculateFileSha1(file_name, unpacked_name, round_up=False):
48 """Calculate the SHA-1 for a given file. Round up its size to 4K if needed."""
Tao Baoafaa0a62017-02-27 15:08:36 -080049
50 def RoundUpTo4K(value):
51 rounded_up = value + 4095
52 return rounded_up - (rounded_up % 4096)
53
Tianjie Xu9c384d22017-06-20 17:00:55 -070054 assert os.path.exists(unpacked_name)
55 with open(unpacked_name, 'r') as f:
56 file_data = f.read()
57 file_size = len(file_data)
58 if round_up:
59 file_size_rounded_up = RoundUpTo4K(file_size)
60 file_data += '\0' * (file_size_rounded_up - file_size)
61 return common.File(file_name, file_data).sha1
62
63
64def ValidateFileAgainstSha1(input_tmp, file_name, file_path, expected_sha1):
65 """Check if the file has the expected SHA-1."""
66
67 logging.info('Validating the SHA-1 of {}'.format(file_name))
68 unpacked_name = os.path.join(input_tmp, file_path)
69 assert os.path.exists(unpacked_name)
70 actual_sha1 = _CalculateFileSha1(file_name, unpacked_name, False)
71 assert actual_sha1 == expected_sha1, \
72 'SHA-1 mismatches for {}. actual {}, expected {}'.format(
73 file_name, actual_sha1, expected_sha1)
74
75
76def ValidateFileConsistency(input_zip, input_tmp):
77 """Compare the files from image files and unpacked folders."""
78
Tao Baoafaa0a62017-02-27 15:08:36 -080079 def CheckAllFiles(which):
80 logging.info('Checking %s image.', which)
81 image = _GetImage(which, input_tmp)
82 prefix = '/' + which
83 for entry in image.file_map:
84 if not entry.startswith(prefix):
85 continue
86
87 # Read the blocks that the file resides. Note that it will contain the
88 # bytes past the file length, which is expected to be padded with '\0's.
89 ranges = image.file_map[entry]
90 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):])
Tianjie Xu9c384d22017-06-20 17:00:55 -070095 file_sha1 = _CalculateFileSha1(entry, unpacked_name, True)
Tao Baoafaa0a62017-02-27 15:08:36 -080096
97 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
103 # Verify IMAGES/system.img.
104 CheckAllFiles('system')
105
106 # Verify IMAGES/vendor.img if applicable.
107 if 'VENDOR/' in input_zip.namelist():
108 CheckAllFiles('vendor')
109
110 # Not checking IMAGES/system_other.img since it doesn't have the map file.
111
112
Tianjie Xu9c384d22017-06-20 17:00:55 -0700113def ValidateInstallRecoveryScript(input_tmp, info_dict):
114 """Validate the SHA-1 embedded in install-recovery.sh.
115
116 install-recovery.sh is written in common.py and has the following format:
117
118 1. full recovery:
119 ...
120 if ! applypatch -c type:device:size:SHA-1; then
121 applypatch /system/etc/recovery.img type:device sha1 size && ...
122 ...
123
124 2. recovery from boot:
125 ...
126 applypatch [-b bonus_args] boot_info recovery_info recovery_sha1 \
127 recovery_size patch_info && ...
128 ...
129
130 For full recovery, we want to calculate the SHA-1 of /system/etc/recovery.img
131 and compare it against the one embedded in the script. While for recovery
132 from boot, we want to check the SHA-1 for both recovery.img and boot.img
133 under IMAGES/.
134 """
135
136 script_path = 'SYSTEM/bin/install-recovery.sh'
137 if not os.path.exists(os.path.join(input_tmp, script_path)):
138 logging.info('{} does not exist in input_tmp'.format(script_path))
139 return
140
141 logging.info('Checking {}'.format(script_path))
142 with open(os.path.join(input_tmp, script_path), 'r') as script:
143 lines = script.read().strip().split('\n')
144 assert len(lines) >= 6
145 check_cmd = re.search(r'if ! applypatch -c \w+:.+:\w+:(\w+);',
146 lines[1].strip())
147 expected_recovery_check_sha1 = check_cmd.group(1)
148 patch_cmd = re.search(r'(applypatch.+)&&', lines[2].strip())
149 applypatch_argv = patch_cmd.group(1).strip().split()
150
151 full_recovery_image = info_dict.get("full_recovery_image") == "true"
152 if full_recovery_image:
153 assert len(applypatch_argv) == 5
154 # Check we have the same expected SHA-1 of recovery.img in both check mode
155 # and patch mode.
156 expected_recovery_sha1 = applypatch_argv[3].strip()
157 assert expected_recovery_check_sha1 == expected_recovery_sha1
158 ValidateFileAgainstSha1(input_tmp, 'recovery.img',
159 'SYSTEM/etc/recovery.img', expected_recovery_sha1)
160 else:
161 # We're patching boot.img to get recovery.img where bonus_args is optional
162 if applypatch_argv[1] == "-b":
163 assert len(applypatch_argv) == 8
164 boot_info_index = 3
165 else:
166 assert len(applypatch_argv) == 6
167 boot_info_index = 1
168
169 # boot_info: boot_type:boot_device:boot_size:boot_sha1
170 boot_info = applypatch_argv[boot_info_index].strip().split(':')
171 assert len(boot_info) == 4
172 ValidateFileAgainstSha1(input_tmp, file_name='boot.img',
173 file_path='IMAGES/boot.img', expected_sha1=boot_info[3])
174
175 recovery_sha1_index = boot_info_index + 2
176 expected_recovery_sha1 = applypatch_argv[recovery_sha1_index]
177 assert expected_recovery_check_sha1 == expected_recovery_sha1
178 ValidateFileAgainstSha1(input_tmp, file_name='recovery.img',
179 file_path='IMAGES/recovery.img',
180 expected_sha1=expected_recovery_sha1)
181
182 logging.info('Done checking {}'.format(script_path))
183
184
Tao Baoafaa0a62017-02-27 15:08:36 -0800185def main(argv):
186 def option_handler():
187 return True
188
189 args = common.ParseOptions(
190 argv, __doc__, extra_opts="",
191 extra_long_opts=[],
192 extra_option_handler=option_handler)
193
194 if len(args) != 1:
195 common.Usage(__doc__)
196 sys.exit(1)
197
198 logging_format = '%(asctime)s - %(filename)s - %(levelname)-8s: %(message)s'
199 date_format = '%Y/%m/%d %H:%M:%S'
200 logging.basicConfig(level=logging.INFO, format=logging_format,
201 datefmt=date_format)
202
203 logging.info("Unzipping the input target_files.zip: %s", args[0])
204 input_tmp, input_zip = common.UnzipTemp(args[0])
205
206 ValidateFileConsistency(input_zip, input_tmp)
207
Tianjie Xu9c384d22017-06-20 17:00:55 -0700208 info_dict = common.LoadInfoDict(input_tmp)
209 ValidateInstallRecoveryScript(input_tmp, info_dict)
210
Tao Baoafaa0a62017-02-27 15:08:36 -0800211 # TODO: Check if the OTA keys have been properly updated (the ones on /system,
212 # in recovery image).
213
Tao Baoafaa0a62017-02-27 15:08:36 -0800214 logging.info("Done.")
215
216
217if __name__ == '__main__':
218 try:
219 main(sys.argv[1:])
220 finally:
221 common.Cleanup()