validate_target_files.py revision 242a9e5fb56566db7236088e1b2bcce0ab87688c
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 30import zipfile 31 32import common 33 34 35def _ReadFile(file_name, unpacked_name, round_up=False): 36 """Constructs and returns a File object. Rounds up its size if needed.""" 37 38 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: 43 file_size_rounded_up = common.RoundUpTo4K(file_size) 44 file_data += '\0' * (file_size_rounded_up - file_size) 45 return common.File(file_name, file_data) 46 47 48def ValidateFileAgainstSha1(input_tmp, file_name, file_path, expected_sha1): 49 """Check if the file has the expected SHA-1.""" 50 51 logging.info('Validating the SHA-1 of %s', file_name) 52 unpacked_name = os.path.join(input_tmp, file_path) 53 assert os.path.exists(unpacked_name) 54 actual_sha1 = _ReadFile(file_name, unpacked_name, False).sha1 55 assert actual_sha1 == expected_sha1, \ 56 'SHA-1 mismatches for {}. actual {}, expected {}'.format( 57 file_name, actual_sha1, expected_sha1) 58 59 60def ValidateFileConsistency(input_zip, input_tmp, info_dict): 61 """Compare the files from image files and unpacked folders.""" 62 63 def CheckAllFiles(which): 64 logging.info('Checking %s image.', which) 65 # 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) 69 prefix = '/' + which 70 for entry in image.file_map: 71 # Skip entries like '__NONZERO-0'. 72 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] 78 79 incomplete = ranges.extra.get('incomplete', False) 80 if incomplete: 81 logging.warning('Skipping %s that has incomplete block list', entry) 82 continue 83 84 blocks_sha1 = image.RangeSha1(ranges) 85 86 # The filename under unpacked directory, such as SYSTEM/bin/sh. 87 unpacked_name = os.path.join( 88 input_tmp, which.upper(), entry[(len(prefix) + 1):]) 89 unpacked_file = _ReadFile(entry, unpacked_name, True) 90 file_sha1 = unpacked_file.sha1 91 assert blocks_sha1 == file_sha1, \ 92 'file: %s, range: %s, blocks_sha1: %s, file_sha1: %s' % ( 93 entry, ranges, blocks_sha1, file_sha1) 94 95 logging.info('Validating file consistency.') 96 97 # TODO(b/79617342): Validate non-sparse images. 98 if info_dict.get('extfs_sparse_flag') != '-s': 99 logging.warning('Skipped due to target using non-sparse images') 100 return 101 102 # Verify IMAGES/system.img. 103 CheckAllFiles('system') 104 105 # Verify IMAGES/vendor.img if applicable. 106 if 'VENDOR/' in input_zip.namelist(): 107 CheckAllFiles('vendor') 108 109 # Not checking IMAGES/system_other.img since it doesn't have the map file. 110 111 112def ValidateInstallRecoveryScript(input_tmp, info_dict): 113 """Validate the SHA-1 embedded in install-recovery.sh. 114 115 install-recovery.sh is written in common.py and has the following format: 116 117 1. full recovery: 118 ... 119 if ! applypatch -c type:device:size:SHA-1; then 120 applypatch /system/etc/recovery.img type:device sha1 size && ... 121 ... 122 123 2. recovery from boot: 124 ... 125 applypatch [-b bonus_args] boot_info recovery_info recovery_sha1 \ 126 recovery_size patch_info && ... 127 ... 128 129 For full recovery, we want to calculate the SHA-1 of /system/etc/recovery.img 130 and compare it against the one embedded in the script. While for recovery 131 from boot, we want to check the SHA-1 for both recovery.img and boot.img 132 under IMAGES/. 133 """ 134 135 script_path = 'SYSTEM/bin/install-recovery.sh' 136 if not os.path.exists(os.path.join(input_tmp, script_path)): 137 logging.info('%s does not exist in input_tmp', script_path) 138 return 139 140 logging.info('Checking %s', script_path) 141 with open(os.path.join(input_tmp, script_path), 'r') as script: 142 lines = script.read().strip().split('\n') 143 assert len(lines) >= 6 144 check_cmd = re.search(r'if ! applypatch -c \w+:.+:\w+:(\w+);', 145 lines[1].strip()) 146 expected_recovery_check_sha1 = check_cmd.group(1) 147 patch_cmd = re.search(r'(applypatch.+)&&', lines[2].strip()) 148 applypatch_argv = patch_cmd.group(1).strip().split() 149 150 full_recovery_image = info_dict.get("full_recovery_image") == "true" 151 if full_recovery_image: 152 assert len(applypatch_argv) == 5 153 # Check we have the same expected SHA-1 of recovery.img in both check mode 154 # and patch mode. 155 expected_recovery_sha1 = applypatch_argv[3].strip() 156 assert expected_recovery_check_sha1 == expected_recovery_sha1 157 ValidateFileAgainstSha1(input_tmp, 'recovery.img', 158 'SYSTEM/etc/recovery.img', expected_recovery_sha1) 159 else: 160 # We're patching boot.img to get recovery.img where bonus_args is optional 161 if applypatch_argv[1] == "-b": 162 assert len(applypatch_argv) == 8 163 boot_info_index = 3 164 else: 165 assert len(applypatch_argv) == 6 166 boot_info_index = 1 167 168 # boot_info: boot_type:boot_device:boot_size:boot_sha1 169 boot_info = applypatch_argv[boot_info_index].strip().split(':') 170 assert len(boot_info) == 4 171 ValidateFileAgainstSha1(input_tmp, file_name='boot.img', 172 file_path='IMAGES/boot.img', 173 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 %s', script_path) 183 184 185def 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 = common.UnzipTemp(args[0]) 205 206 info_dict = common.LoadInfoDict(input_tmp) 207 with zipfile.ZipFile(args[0], 'r') as input_zip: 208 ValidateFileConsistency(input_zip, input_tmp, info_dict) 209 210 ValidateInstallRecoveryScript(input_tmp, info_dict) 211 212 # TODO: Check if the OTA keys have been properly updated (the ones on /system, 213 # in recovery image). 214 215 logging.info("Done.") 216 217 218if __name__ == '__main__': 219 try: 220 main(sys.argv[1:]) 221 finally: 222 common.Cleanup() 223