1 #!/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 """ 18 Validate a given (signed) target_files.zip. 19 20 It 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 26 import common 27 import logging 28 import os.path 29 import re 30 import sparse_img 31 import sys 32 33 34 def _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 47 def _ReadFile(file_name, unpacked_name, round_up=False): 48 """Constructs and returns a File object. Rounds up its size if needed.""" 49 50 def RoundUpTo4K(value): 51 rounded_up = value + 4095 52 return rounded_up - (rounded_up % 4096) 53 54 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) 62 63 64 def 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 = _ReadFile(file_name, unpacked_name, False).sha1 71 assert actual_sha1 == expected_sha1, \ 72 'SHA-1 mismatches for {}. actual {}, expected {}'.format( 73 file_name, actual_sha1, expected_sha1) 74 75 76 def ValidateFileConsistency(input_zip, input_tmp): 77 """Compare the files from image files and unpacked folders.""" 78 79 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):]) 95 unpacked_file = _ReadFile(entry, unpacked_name, True) 96 file_size = unpacked_file.size 97 98 # block.map may contain less blocks, because mke2fs may skip allocating 99 # blocks if they contain all zeros. We can't reconstruct such a file from 100 # its block list. (Bug: 65213616) 101 if file_size > ranges.size() * 4096: 102 logging.warning( 103 'Skipping %s that has less blocks: file size %d-byte,' 104 ' ranges %s (%d-byte)', entry, file_size, ranges, 105 ranges.size() * 4096) 106 continue 107 108 file_sha1 = unpacked_file.sha1 109 assert blocks_sha1 == file_sha1, \ 110 'file: %s, range: %s, blocks_sha1: %s, file_sha1: %s' % ( 111 entry, ranges, blocks_sha1, file_sha1) 112 113 logging.info('Validating file consistency.') 114 115 # Verify IMAGES/system.img. 116 CheckAllFiles('system') 117 118 # Verify IMAGES/vendor.img if applicable. 119 if 'VENDOR/' in input_zip.namelist(): 120 CheckAllFiles('vendor') 121 122 # Not checking IMAGES/system_other.img since it doesn't have the map file. 123 124 125 def ValidateInstallRecoveryScript(input_tmp, info_dict): 126 """Validate the SHA-1 embedded in install-recovery.sh. 127 128 install-recovery.sh is written in common.py and has the following format: 129 130 1. full recovery: 131 ... 132 if ! applypatch -c type:device:size:SHA-1; then 133 applypatch /system/etc/recovery.img type:device sha1 size && ... 134 ... 135 136 2. recovery from boot: 137 ... 138 applypatch [-b bonus_args] boot_info recovery_info recovery_sha1 \ 139 recovery_size patch_info && ... 140 ... 141 142 For full recovery, we want to calculate the SHA-1 of /system/etc/recovery.img 143 and compare it against the one embedded in the script. While for recovery 144 from boot, we want to check the SHA-1 for both recovery.img and boot.img 145 under IMAGES/. 146 """ 147 148 script_path = 'SYSTEM/bin/install-recovery.sh' 149 if not os.path.exists(os.path.join(input_tmp, script_path)): 150 logging.info('{} does not exist in input_tmp'.format(script_path)) 151 return 152 153 logging.info('Checking {}'.format(script_path)) 154 with open(os.path.join(input_tmp, script_path), 'r') as script: 155 lines = script.read().strip().split('\n') 156 assert len(lines) >= 6 157 check_cmd = re.search(r'if ! applypatch -c \w+:.+:\w+:(\w+);', 158 lines[1].strip()) 159 expected_recovery_check_sha1 = check_cmd.group(1) 160 patch_cmd = re.search(r'(applypatch.+)&&', lines[2].strip()) 161 applypatch_argv = patch_cmd.group(1).strip().split() 162 163 full_recovery_image = info_dict.get("full_recovery_image") == "true" 164 if full_recovery_image: 165 assert len(applypatch_argv) == 5 166 # Check we have the same expected SHA-1 of recovery.img in both check mode 167 # and patch mode. 168 expected_recovery_sha1 = applypatch_argv[3].strip() 169 assert expected_recovery_check_sha1 == expected_recovery_sha1 170 ValidateFileAgainstSha1(input_tmp, 'recovery.img', 171 'SYSTEM/etc/recovery.img', expected_recovery_sha1) 172 else: 173 # We're patching boot.img to get recovery.img where bonus_args is optional 174 if applypatch_argv[1] == "-b": 175 assert len(applypatch_argv) == 8 176 boot_info_index = 3 177 else: 178 assert len(applypatch_argv) == 6 179 boot_info_index = 1 180 181 # boot_info: boot_type:boot_device:boot_size:boot_sha1 182 boot_info = applypatch_argv[boot_info_index].strip().split(':') 183 assert len(boot_info) == 4 184 ValidateFileAgainstSha1(input_tmp, file_name='boot.img', 185 file_path='IMAGES/boot.img', expected_sha1=boot_info[3]) 186 187 recovery_sha1_index = boot_info_index + 2 188 expected_recovery_sha1 = applypatch_argv[recovery_sha1_index] 189 assert expected_recovery_check_sha1 == expected_recovery_sha1 190 ValidateFileAgainstSha1(input_tmp, file_name='recovery.img', 191 file_path='IMAGES/recovery.img', 192 expected_sha1=expected_recovery_sha1) 193 194 logging.info('Done checking {}'.format(script_path)) 195 196 197 def main(argv): 198 def option_handler(): 199 return True 200 201 args = common.ParseOptions( 202 argv, __doc__, extra_opts="", 203 extra_long_opts=[], 204 extra_option_handler=option_handler) 205 206 if len(args) != 1: 207 common.Usage(__doc__) 208 sys.exit(1) 209 210 logging_format = '%(asctime)s - %(filename)s - %(levelname)-8s: %(message)s' 211 date_format = '%Y/%m/%d %H:%M:%S' 212 logging.basicConfig(level=logging.INFO, format=logging_format, 213 datefmt=date_format) 214 215 logging.info("Unzipping the input target_files.zip: %s", args[0]) 216 input_tmp, input_zip = common.UnzipTemp(args[0]) 217 218 ValidateFileConsistency(input_zip, input_tmp) 219 220 info_dict = common.LoadInfoDict(input_tmp) 221 ValidateInstallRecoveryScript(input_tmp, info_dict) 222 223 # TODO: Check if the OTA keys have been properly updated (the ones on /system, 224 # in recovery image). 225 226 logging.info("Done.") 227 228 229 if __name__ == '__main__': 230 try: 231 main(sys.argv[1:]) 232 finally: 233 common.Cleanup() 234