sign_target_files_apks.py revision 73d5abbd3f26d086ab4082f66b322f28a32bf7e5
1#!/usr/bin/env python 2# 3# Copyright (C) 2008 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""" 18Signs all the APK files in a target-files zipfile, producing a new 19target-files zip. 20 21Usage: sign_target_files_apks [flags] input_target_files output_target_files 22 23 -e (--extra_apks) <name,name,...=key> 24 Add extra APK name/key pairs as though they appeared in 25 apkcerts.txt (so mappings specified by -k and -d are applied). 26 Keys specified in -e override any value for that app contained 27 in the apkcerts.txt file. Option may be repeated to give 28 multiple extra packages. 29 30 -k (--key_mapping) <src_key=dest_key> 31 Add a mapping from the key name as specified in apkcerts.txt (the 32 src_key) to the real key you wish to sign the package with 33 (dest_key). Option may be repeated to give multiple key 34 mappings. 35 36 -d (--default_key_mappings) <dir> 37 Set up the following key mappings: 38 39 $devkey/devkey ==> $dir/releasekey 40 $devkey/testkey ==> $dir/releasekey 41 $devkey/media ==> $dir/media 42 $devkey/shared ==> $dir/shared 43 $devkey/platform ==> $dir/platform 44 45 where $devkey is the directory part of the value of 46 default_system_dev_certificate from the input target-files's 47 META/misc_info.txt. (Defaulting to "build/target/product/security" 48 if the value is not present in misc_info. 49 50 -d and -k options are added to the set of mappings in the order 51 in which they appear on the command line. 52 53 -o (--replace_ota_keys) 54 Replace the certificate (public key) used by OTA package 55 verification with the one specified in the input target_files 56 zip (in the META/otakeys.txt file). Key remapping (-k and -d) 57 is performed on this key. 58 59 -t (--tag_changes) <+tag>,<-tag>,... 60 Comma-separated list of changes to make to the set of tags (in 61 the last component of the build fingerprint). Prefix each with 62 '+' or '-' to indicate whether that tag should be added or 63 removed. Changes are processed in the order they appear. 64 Default value is "-test-keys,-dev-keys,+release-keys". 65 66""" 67 68import sys 69 70if sys.hexversion < 0x02040000: 71 print >> sys.stderr, "Python 2.4 or newer is required." 72 sys.exit(1) 73 74import base64 75import cStringIO 76import copy 77import errno 78import os 79import re 80import subprocess 81import tempfile 82import zipfile 83 84import common 85 86OPTIONS = common.OPTIONS 87 88OPTIONS.extra_apks = {} 89OPTIONS.key_map = {} 90OPTIONS.replace_ota_keys = False 91OPTIONS.tag_changes = ("-test-keys", "-dev-keys", "+release-keys") 92 93def GetApkCerts(tf_zip): 94 certmap = common.ReadApkCerts(tf_zip) 95 96 # apply the key remapping to the contents of the file 97 for apk, cert in certmap.iteritems(): 98 certmap[apk] = OPTIONS.key_map.get(cert, cert) 99 100 # apply all the -e options, overriding anything in the file 101 for apk, cert in OPTIONS.extra_apks.iteritems(): 102 if not cert: 103 cert = "PRESIGNED" 104 certmap[apk] = OPTIONS.key_map.get(cert, cert) 105 106 return certmap 107 108 109def CheckAllApksSigned(input_tf_zip, apk_key_map): 110 """Check that all the APKs we want to sign have keys specified, and 111 error out if they don't.""" 112 unknown_apks = [] 113 for info in input_tf_zip.infolist(): 114 if info.filename.endswith(".apk"): 115 name = os.path.basename(info.filename) 116 if name not in apk_key_map: 117 unknown_apks.append(name) 118 if unknown_apks: 119 print "ERROR: no key specified for:\n\n ", 120 print "\n ".join(unknown_apks) 121 print "\nUse '-e <apkname>=' to specify a key (which may be an" 122 print "empty string to not sign this apk)." 123 sys.exit(1) 124 125 126def SignApk(data, keyname, pw): 127 unsigned = tempfile.NamedTemporaryFile() 128 unsigned.write(data) 129 unsigned.flush() 130 131 signed = tempfile.NamedTemporaryFile() 132 133 common.SignFile(unsigned.name, signed.name, keyname, pw, align=4) 134 135 data = signed.read() 136 unsigned.close() 137 signed.close() 138 139 return data 140 141 142def SignApks(input_tf_zip, output_tf_zip, apk_key_map, key_passwords): 143 maxsize = max([len(os.path.basename(i.filename)) 144 for i in input_tf_zip.infolist() 145 if i.filename.endswith('.apk')]) 146 147 for info in input_tf_zip.infolist(): 148 data = input_tf_zip.read(info.filename) 149 out_info = copy.copy(info) 150 if info.filename.endswith(".apk"): 151 name = os.path.basename(info.filename) 152 key = apk_key_map[name] 153 if key not in common.SPECIAL_CERT_STRINGS: 154 print " signing: %-*s (%s)" % (maxsize, name, key) 155 signed_data = SignApk(data, key, key_passwords[key]) 156 output_tf_zip.writestr(out_info, signed_data) 157 else: 158 # an APK we're not supposed to sign. 159 print "NOT signing: %s" % (name,) 160 output_tf_zip.writestr(out_info, data) 161 elif info.filename in ("SYSTEM/build.prop", 162 "RECOVERY/RAMDISK/default.prop"): 163 print "rewriting %s:" % (info.filename,) 164 new_data = RewriteProps(data) 165 output_tf_zip.writestr(out_info, new_data) 166 elif info.filename.endswith("mac_permissions.xml"): 167 print "rewriting %s with new keys." % (info.filename,) 168 new_data = ReplaceCerts(data) 169 output_tf_zip.writestr(out_info, new_data) 170 else: 171 # a non-APK file; copy it verbatim 172 output_tf_zip.writestr(out_info, data) 173 174 175def ReplaceCerts(data): 176 """Given a string of data, replace all occurences of a set 177 of X509 certs with a newer set of X509 certs and return 178 the updated data string.""" 179 for old, new in OPTIONS.key_map.iteritems(): 180 try: 181 if OPTIONS.verbose: 182 print " Replacing %s.x509.pem with %s.x509.pem" % (old, new) 183 f = open(old + ".x509.pem") 184 old_cert16 = base64.b16encode(common.ParseCertificate(f.read())).lower() 185 f.close() 186 f = open(new + ".x509.pem") 187 new_cert16 = base64.b16encode(common.ParseCertificate(f.read())).lower() 188 f.close() 189 # Only match entire certs. 190 pattern = "\\b"+old_cert16+"\\b" 191 (data, num) = re.subn(pattern, new_cert16, data, flags=re.IGNORECASE) 192 if OPTIONS.verbose: 193 print " Replaced %d occurence(s) of %s.x509.pem with " \ 194 "%s.x509.pem" % (num, old, new) 195 except IOError, e: 196 if (e.errno == errno.ENOENT and not OPTIONS.verbose): 197 continue 198 199 print " Error accessing %s. %s. Skip replacing %s.x509.pem " \ 200 "with %s.x509.pem." % (e.filename, e.strerror, old, new) 201 202 return data 203 204 205def EditTags(tags): 206 """Given a string containing comma-separated tags, apply the edits 207 specified in OPTIONS.tag_changes and return the updated string.""" 208 tags = set(tags.split(",")) 209 for ch in OPTIONS.tag_changes: 210 if ch[0] == "-": 211 tags.discard(ch[1:]) 212 elif ch[0] == "+": 213 tags.add(ch[1:]) 214 return ",".join(sorted(tags)) 215 216 217def RewriteProps(data): 218 output = [] 219 for line in data.split("\n"): 220 line = line.strip() 221 original_line = line 222 if line and line[0] != '#': 223 key, value = line.split("=", 1) 224 if key == "ro.build.fingerprint": 225 pieces = value.split("/") 226 pieces[-1] = EditTags(pieces[-1]) 227 value = "/".join(pieces) 228 elif key == "ro.build.description": 229 pieces = value.split(" ") 230 assert len(pieces) == 5 231 pieces[-1] = EditTags(pieces[-1]) 232 value = " ".join(pieces) 233 elif key == "ro.build.tags": 234 value = EditTags(value) 235 elif key == "ro.build.display.id": 236 # change, eg, "JWR66N dev-keys" to "JWR66N" 237 value = value.split() 238 if len(value) > 1 and value[-1].endswith("-keys"): 239 value.pop() 240 value = " ".join(value) 241 line = key + "=" + value 242 if line != original_line: 243 print " replace: ", original_line 244 print " with: ", line 245 output.append(line) 246 return "\n".join(output) + "\n" 247 248 249def ReplaceOtaKeys(input_tf_zip, output_tf_zip, misc_info): 250 try: 251 keylist = input_tf_zip.read("META/otakeys.txt").split() 252 except KeyError: 253 raise common.ExternalError("can't read META/otakeys.txt from input") 254 255 extra_recovery_keys = misc_info.get("extra_recovery_keys", None) 256 if extra_recovery_keys: 257 extra_recovery_keys = [OPTIONS.key_map.get(k, k) + ".x509.pem" 258 for k in extra_recovery_keys.split()] 259 if extra_recovery_keys: 260 print "extra recovery-only key(s): " + ", ".join(extra_recovery_keys) 261 else: 262 extra_recovery_keys = [] 263 264 mapped_keys = [] 265 for k in keylist: 266 m = re.match(r"^(.*)\.x509\.pem$", k) 267 if not m: 268 raise common.ExternalError("can't parse \"%s\" from META/otakeys.txt" % (k,)) 269 k = m.group(1) 270 mapped_keys.append(OPTIONS.key_map.get(k, k) + ".x509.pem") 271 272 if mapped_keys: 273 print "using:\n ", "\n ".join(mapped_keys) 274 print "for OTA package verification" 275 else: 276 devkey = misc_info.get("default_system_dev_certificate", 277 "build/target/product/security/testkey") 278 mapped_keys.append( 279 OPTIONS.key_map.get(devkey, devkey) + ".x509.pem") 280 print "META/otakeys.txt has no keys; using", mapped_keys[0] 281 282 # recovery uses a version of the key that has been slightly 283 # predigested (by DumpPublicKey.java) and put in res/keys. 284 # extra_recovery_keys are used only in recovery. 285 286 p = common.Run(["java", "-jar", 287 os.path.join(OPTIONS.search_path, "framework", "dumpkey.jar")] 288 + mapped_keys + extra_recovery_keys, 289 stdout=subprocess.PIPE) 290 data, _ = p.communicate() 291 if p.returncode != 0: 292 raise common.ExternalError("failed to run dumpkeys") 293 common.ZipWriteStr(output_tf_zip, "RECOVERY/RAMDISK/res/keys", data) 294 295 # SystemUpdateActivity uses the x509.pem version of the keys, but 296 # put into a zipfile system/etc/security/otacerts.zip. 297 # We DO NOT include the extra_recovery_keys (if any) here. 298 299 tempfile = cStringIO.StringIO() 300 certs_zip = zipfile.ZipFile(tempfile, "w") 301 for k in mapped_keys: 302 certs_zip.write(k) 303 certs_zip.close() 304 common.ZipWriteStr(output_tf_zip, "SYSTEM/etc/security/otacerts.zip", 305 tempfile.getvalue()) 306 307 308def BuildKeyMap(misc_info, key_mapping_options): 309 for s, d in key_mapping_options: 310 if s is None: # -d option 311 devkey = misc_info.get("default_system_dev_certificate", 312 "build/target/product/security/testkey") 313 devkeydir = os.path.dirname(devkey) 314 315 OPTIONS.key_map.update({ 316 devkeydir + "/testkey": d + "/releasekey", 317 devkeydir + "/devkey": d + "/releasekey", 318 devkeydir + "/media": d + "/media", 319 devkeydir + "/shared": d + "/shared", 320 devkeydir + "/platform": d + "/platform", 321 }) 322 else: 323 OPTIONS.key_map[s] = d 324 325 326def main(argv): 327 328 key_mapping_options = [] 329 330 def option_handler(o, a): 331 if o in ("-e", "--extra_apks"): 332 names, key = a.split("=") 333 names = names.split(",") 334 for n in names: 335 OPTIONS.extra_apks[n] = key 336 elif o in ("-d", "--default_key_mappings"): 337 key_mapping_options.append((None, a)) 338 elif o in ("-k", "--key_mapping"): 339 key_mapping_options.append(a.split("=", 1)) 340 elif o in ("-o", "--replace_ota_keys"): 341 OPTIONS.replace_ota_keys = True 342 elif o in ("-t", "--tag_changes"): 343 new = [] 344 for i in a.split(","): 345 i = i.strip() 346 if not i or i[0] not in "-+": 347 raise ValueError("Bad tag change '%s'" % (i,)) 348 new.append(i[0] + i[1:].strip()) 349 OPTIONS.tag_changes = tuple(new) 350 else: 351 return False 352 return True 353 354 args = common.ParseOptions(argv, __doc__, 355 extra_opts="e:d:k:ot:", 356 extra_long_opts=["extra_apks=", 357 "default_key_mappings=", 358 "key_mapping=", 359 "replace_ota_keys", 360 "tag_changes="], 361 extra_option_handler=option_handler) 362 363 if len(args) != 2: 364 common.Usage(__doc__) 365 sys.exit(1) 366 367 input_zip = zipfile.ZipFile(args[0], "r") 368 output_zip = zipfile.ZipFile(args[1], "w") 369 370 misc_info = common.LoadInfoDict(input_zip) 371 372 BuildKeyMap(misc_info, key_mapping_options) 373 374 apk_key_map = GetApkCerts(input_zip) 375 CheckAllApksSigned(input_zip, apk_key_map) 376 377 key_passwords = common.GetKeyPasswords(set(apk_key_map.values())) 378 SignApks(input_zip, output_zip, apk_key_map, key_passwords) 379 380 if OPTIONS.replace_ota_keys: 381 ReplaceOtaKeys(input_zip, output_zip, misc_info) 382 383 input_zip.close() 384 output_zip.close() 385 386 print "done." 387 388 389if __name__ == '__main__': 390 try: 391 main(sys.argv[1:]) 392 except common.ExternalError, e: 393 print 394 print " ERROR: %s" % (e,) 395 print 396 sys.exit(1) 397