1#!/usr/bin/env python 2# 3# Copyright (C) 2019 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 17import logging 18import os.path 19import re 20import shlex 21import sys 22import zipfile 23 24import common 25 26logger = logging.getLogger(__name__) 27 28OPTIONS = common.OPTIONS 29 30 31class ApexInfoError(Exception): 32 """An Exception raised during Apex Information command.""" 33 34 def __init__(self, message): 35 Exception.__init__(self, message) 36 37 38class ApexSigningError(Exception): 39 """An Exception raised during Apex Payload signing.""" 40 41 def __init__(self, message): 42 Exception.__init__(self, message) 43 44 45def SignApexPayload(payload_file, payload_key_path, payload_key_name, algorithm, 46 salt, signing_args=None): 47 """Signs a given payload_file with the payload key.""" 48 # Add the new footer. Old footer, if any, will be replaced by avbtool. 49 cmd = ['avbtool', 'add_hashtree_footer', 50 '--do_not_generate_fec', 51 '--algorithm', algorithm, 52 '--key', payload_key_path, 53 '--prop', 'apex.key:{}'.format(payload_key_name), 54 '--image', payload_file, 55 '--salt', salt] 56 if signing_args: 57 cmd.extend(shlex.split(signing_args)) 58 59 try: 60 common.RunAndCheckOutput(cmd) 61 except common.ExternalError as e: 62 raise ApexSigningError, \ 63 'Failed to sign APEX payload {} with {}:\n{}'.format( 64 payload_file, payload_key_path, e), sys.exc_info()[2] 65 66 # Verify the signed payload image with specified public key. 67 logger.info('Verifying %s', payload_file) 68 VerifyApexPayload(payload_file, payload_key_path) 69 70 71def VerifyApexPayload(payload_file, payload_key): 72 """Verifies the APEX payload signature with the given key.""" 73 cmd = ['avbtool', 'verify_image', '--image', payload_file, 74 '--key', payload_key] 75 try: 76 common.RunAndCheckOutput(cmd) 77 except common.ExternalError as e: 78 raise ApexSigningError, \ 79 'Failed to validate payload signing for {} with {}:\n{}'.format( 80 payload_file, payload_key, e), sys.exc_info()[2] 81 82 83def ParseApexPayloadInfo(payload_path): 84 """Parses the APEX payload info. 85 86 Args: 87 payload_path: The path to the payload image. 88 89 Raises: 90 ApexInfoError on parsing errors. 91 92 Returns: 93 A dict that contains payload property-value pairs. The dict should at least 94 contain Algorithm, Salt and apex.key. 95 """ 96 if not os.path.exists(payload_path): 97 raise ApexInfoError('Failed to find image: {}'.format(payload_path)) 98 99 cmd = ['avbtool', 'info_image', '--image', payload_path] 100 try: 101 output = common.RunAndCheckOutput(cmd) 102 except common.ExternalError as e: 103 raise ApexInfoError, \ 104 'Failed to get APEX payload info for {}:\n{}'.format( 105 payload_path, e), sys.exc_info()[2] 106 107 # Extract the Algorithm / Salt / Prop info from payload (i.e. an image signed 108 # with avbtool). For example, 109 # Algorithm: SHA256_RSA4096 110 PAYLOAD_INFO_PATTERN = ( 111 r'^\s*(?P<key>Algorithm|Salt|Prop)\:\s*(?P<value>.*?)$') 112 payload_info_matcher = re.compile(PAYLOAD_INFO_PATTERN) 113 114 payload_info = {} 115 for line in output.split('\n'): 116 line_info = payload_info_matcher.match(line) 117 if not line_info: 118 continue 119 120 key, value = line_info.group('key'), line_info.group('value') 121 122 if key == 'Prop': 123 # Further extract the property key-value pair, from a 'Prop:' line. For 124 # example, 125 # Prop: apex.key -> 'com.android.runtime' 126 # Note that avbtool writes single or double quotes around values. 127 PROPERTY_DESCRIPTOR_PATTERN = r'^\s*(?P<key>.*?)\s->\s*(?P<value>.*?)$' 128 129 prop_matcher = re.compile(PROPERTY_DESCRIPTOR_PATTERN) 130 prop = prop_matcher.match(value) 131 if not prop: 132 raise ApexInfoError( 133 'Failed to parse prop string {}'.format(value)) 134 135 prop_key, prop_value = prop.group('key'), prop.group('value') 136 if prop_key == 'apex.key': 137 # avbtool dumps the prop value with repr(), which contains single / 138 # double quotes that we don't want. 139 payload_info[prop_key] = prop_value.strip('\"\'') 140 141 else: 142 payload_info[key] = value 143 144 # Sanity check. 145 for key in ('Algorithm', 'Salt', 'apex.key'): 146 if key not in payload_info: 147 raise ApexInfoError( 148 'Failed to find {} prop in {}'.format(key, payload_path)) 149 150 return payload_info 151 152 153def SignApex(apex_data, payload_key, container_key, container_pw, 154 codename_to_api_level_map, signing_args=None): 155 """Signs the current APEX with the given payload/container keys. 156 157 Args: 158 apex_data: Raw APEX data. 159 payload_key: The path to payload signing key (w/ extension). 160 container_key: The path to container signing key (w/o extension). 161 container_pw: The matching password of the container_key, or None. 162 codename_to_api_level_map: A dict that maps from codename to API level. 163 signing_args: Additional args to be passed to the payload signer. 164 165 Returns: 166 The path to the signed APEX file. 167 """ 168 apex_file = common.MakeTempFile(prefix='apex-', suffix='.apex') 169 with open(apex_file, 'wb') as apex_fp: 170 apex_fp.write(apex_data) 171 172 APEX_PAYLOAD_IMAGE = 'apex_payload.img' 173 APEX_PUBKEY = 'apex_pubkey' 174 175 # 1a. Extract and sign the APEX_PAYLOAD_IMAGE entry with the given 176 # payload_key. 177 payload_dir = common.MakeTempDir(prefix='apex-payload-') 178 with zipfile.ZipFile(apex_file) as apex_fd: 179 payload_file = apex_fd.extract(APEX_PAYLOAD_IMAGE, payload_dir) 180 181 payload_info = ParseApexPayloadInfo(payload_file) 182 SignApexPayload( 183 payload_file, 184 payload_key, 185 payload_info['apex.key'], 186 payload_info['Algorithm'], 187 payload_info['Salt'], 188 signing_args) 189 190 # 1b. Update the embedded payload public key. 191 payload_public_key = common.ExtractAvbPublicKey(payload_key) 192 193 common.ZipDelete(apex_file, APEX_PAYLOAD_IMAGE) 194 common.ZipDelete(apex_file, APEX_PUBKEY) 195 apex_zip = zipfile.ZipFile(apex_file, 'a') 196 common.ZipWrite(apex_zip, payload_file, arcname=APEX_PAYLOAD_IMAGE) 197 common.ZipWrite(apex_zip, payload_public_key, arcname=APEX_PUBKEY) 198 common.ZipClose(apex_zip) 199 200 # 2. Align the files at page boundary (same as in apexer). 201 aligned_apex = common.MakeTempFile(prefix='apex-container-', suffix='.apex') 202 common.RunAndCheckOutput(['zipalign', '-f', '4096', apex_file, aligned_apex]) 203 204 # 3. Sign the APEX container with container_key. 205 signed_apex = common.MakeTempFile(prefix='apex-container-', suffix='.apex') 206 207 # Specify the 4K alignment when calling SignApk. 208 extra_signapk_args = OPTIONS.extra_signapk_args[:] 209 extra_signapk_args.extend(['-a', '4096']) 210 211 common.SignFile( 212 aligned_apex, 213 signed_apex, 214 container_key, 215 container_pw, 216 codename_to_api_level_map=codename_to_api_level_map, 217 extra_signapk_args=extra_signapk_args) 218 219 return signed_apex 220