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