sign_target_files_apks.py revision 6ed1491402e5db6450388147991e66e5938744ac
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 < 0x02070000: 71 print >> sys.stderr, "Python 2.7 or newer is required." 72 sys.exit(1) 73 74import base64 75import cStringIO 76import copy 77import errno 78import os 79import re 80import shutil 81import subprocess 82import tempfile 83import zipfile 84 85import add_img_to_target_files 86import common 87 88OPTIONS = common.OPTIONS 89 90OPTIONS.extra_apks = {} 91OPTIONS.key_map = {} 92OPTIONS.replace_ota_keys = False 93OPTIONS.replace_verity_public_key = False 94OPTIONS.replace_verity_private_key = False 95OPTIONS.tag_changes = ("-test-keys", "-dev-keys", "+release-keys") 96 97def GetApkCerts(tf_zip): 98 certmap = common.ReadApkCerts(tf_zip) 99 100 # apply the key remapping to the contents of the file 101 for apk, cert in certmap.iteritems(): 102 certmap[apk] = OPTIONS.key_map.get(cert, cert) 103 104 # apply all the -e options, overriding anything in the file 105 for apk, cert in OPTIONS.extra_apks.iteritems(): 106 if not cert: 107 cert = "PRESIGNED" 108 certmap[apk] = OPTIONS.key_map.get(cert, cert) 109 110 return certmap 111 112 113def CheckAllApksSigned(input_tf_zip, apk_key_map): 114 """Check that all the APKs we want to sign have keys specified, and 115 error out if they don't.""" 116 unknown_apks = [] 117 for info in input_tf_zip.infolist(): 118 if info.filename.endswith(".apk"): 119 name = os.path.basename(info.filename) 120 if name not in apk_key_map: 121 unknown_apks.append(name) 122 if unknown_apks: 123 print "ERROR: no key specified for:\n\n ", 124 print "\n ".join(unknown_apks) 125 print "\nUse '-e <apkname>=' to specify a key (which may be an" 126 print "empty string to not sign this apk)." 127 sys.exit(1) 128 129 130def SignApk(data, keyname, pw): 131 unsigned = tempfile.NamedTemporaryFile() 132 unsigned.write(data) 133 unsigned.flush() 134 135 signed = tempfile.NamedTemporaryFile() 136 137 common.SignFile(unsigned.name, signed.name, keyname, pw, align=4) 138 139 data = signed.read() 140 unsigned.close() 141 signed.close() 142 143 return data 144 145 146def ProcessTargetFiles(input_tf_zip, output_tf_zip, misc_info, 147 apk_key_map, key_passwords): 148 149 maxsize = max([len(os.path.basename(i.filename)) 150 for i in input_tf_zip.infolist() 151 if i.filename.endswith('.apk')]) 152 rebuild_recovery = False 153 154 tmpdir = tempfile.mkdtemp() 155 def write_to_temp(fn, attr, data): 156 fn = os.path.join(tmpdir, fn) 157 if fn.endswith("/"): 158 fn = os.path.join(tmpdir, fn) 159 os.mkdir(fn) 160 else: 161 d = os.path.dirname(fn) 162 if d and not os.path.exists(d): 163 os.makedirs(d) 164 165 if attr >> 16 == 0xa1ff: 166 os.symlink(data, fn) 167 else: 168 with open(fn, "wb") as f: 169 f.write(data) 170 171 for info in input_tf_zip.infolist(): 172 if info.filename.startswith("IMAGES/"): 173 continue 174 175 data = input_tf_zip.read(info.filename) 176 out_info = copy.copy(info) 177 178 # Replace keys if requested. 179 if (info.filename == "META/misc_info.txt" and 180 OPTIONS.replace_verity_private_key): 181 ReplaceVerityPrivateKey(input_tf_zip, output_tf_zip, misc_info, 182 OPTIONS.replace_verity_private_key[1]) 183 elif (info.filename == "BOOT/RAMDISK/verity_key" and 184 OPTIONS.replace_verity_public_key): 185 new_data = ReplaceVerityPublicKey(output_tf_zip, 186 OPTIONS.replace_verity_public_key[1]) 187 write_to_temp(info.filename, info.external_attr, new_data) 188 # Copy BOOT/, RECOVERY/, META/, ROOT/ to rebuild recovery patch. 189 elif (info.filename.startswith("BOOT/") or 190 info.filename.startswith("RECOVERY/") or 191 info.filename.startswith("META/") or 192 info.filename == "SYSTEM/etc/recovery-resource.dat"): 193 write_to_temp(info.filename, info.external_attr, data) 194 195 # Sign APKs. 196 if info.filename.endswith(".apk"): 197 name = os.path.basename(info.filename) 198 key = apk_key_map[name] 199 if key not in common.SPECIAL_CERT_STRINGS: 200 print " signing: %-*s (%s)" % (maxsize, name, key) 201 signed_data = SignApk(data, key, key_passwords[key]) 202 common.ZipWriteStr(output_tf_zip, out_info, signed_data) 203 else: 204 # an APK we're not supposed to sign. 205 print "NOT signing: %s" % (name,) 206 common.ZipWriteStr(output_tf_zip, out_info, data) 207 elif info.filename in ("SYSTEM/build.prop", 208 "VENDOR/build.prop", 209 "BOOT/RAMDISK/default.prop", 210 "RECOVERY/RAMDISK/default.prop"): 211 print "rewriting %s:" % (info.filename,) 212 new_data = RewriteProps(data, misc_info) 213 common.ZipWriteStr(output_tf_zip, out_info, new_data) 214 if info.filename in ("BOOT/RAMDISK/default.prop", 215 "RECOVERY/RAMDISK/default.prop"): 216 write_to_temp(info.filename, info.external_attr, new_data) 217 elif info.filename.endswith("mac_permissions.xml"): 218 print "rewriting %s with new keys." % (info.filename,) 219 new_data = ReplaceCerts(data) 220 common.ZipWriteStr(output_tf_zip, out_info, new_data) 221 elif info.filename in ("SYSTEM/recovery-from-boot.p", 222 "SYSTEM/etc/recovery.img", 223 "SYSTEM/bin/install-recovery.sh"): 224 rebuild_recovery = True 225 elif (OPTIONS.replace_ota_keys and 226 info.filename in ("RECOVERY/RAMDISK/res/keys", 227 "SYSTEM/etc/security/otacerts.zip")): 228 # don't copy these files if we're regenerating them below 229 pass 230 elif (OPTIONS.replace_verity_private_key and 231 info.filename == "META/misc_info.txt"): 232 pass 233 elif (OPTIONS.replace_verity_public_key and 234 info.filename == "BOOT/RAMDISK/verity_key"): 235 pass 236 else: 237 # a non-APK file; copy it verbatim 238 common.ZipWriteStr(output_tf_zip, out_info, data) 239 240 if OPTIONS.replace_ota_keys: 241 new_recovery_keys = ReplaceOtaKeys(input_tf_zip, output_tf_zip, misc_info) 242 if new_recovery_keys: 243 write_to_temp("RECOVERY/RAMDISK/res/keys", 0o755 << 16, new_recovery_keys) 244 245 if rebuild_recovery: 246 recovery_img = common.GetBootableImage( 247 "recovery.img", "recovery.img", tmpdir, "RECOVERY", info_dict=misc_info) 248 boot_img = common.GetBootableImage( 249 "boot.img", "boot.img", tmpdir, "BOOT", info_dict=misc_info) 250 251 def output_sink(fn, data): 252 common.ZipWriteStr(output_tf_zip, "SYSTEM/" + fn, data) 253 254 common.MakeRecoveryPatch(tmpdir, output_sink, recovery_img, boot_img, 255 info_dict=misc_info) 256 257 shutil.rmtree(tmpdir) 258 259 260def ReplaceCerts(data): 261 """Given a string of data, replace all occurences of a set 262 of X509 certs with a newer set of X509 certs and return 263 the updated data string.""" 264 for old, new in OPTIONS.key_map.iteritems(): 265 try: 266 if OPTIONS.verbose: 267 print " Replacing %s.x509.pem with %s.x509.pem" % (old, new) 268 f = open(old + ".x509.pem") 269 old_cert16 = base64.b16encode(common.ParseCertificate(f.read())).lower() 270 f.close() 271 f = open(new + ".x509.pem") 272 new_cert16 = base64.b16encode(common.ParseCertificate(f.read())).lower() 273 f.close() 274 # Only match entire certs. 275 pattern = "\\b"+old_cert16+"\\b" 276 (data, num) = re.subn(pattern, new_cert16, data, flags=re.IGNORECASE) 277 if OPTIONS.verbose: 278 print " Replaced %d occurence(s) of %s.x509.pem with " \ 279 "%s.x509.pem" % (num, old, new) 280 except IOError as e: 281 if e.errno == errno.ENOENT and not OPTIONS.verbose: 282 continue 283 284 print " Error accessing %s. %s. Skip replacing %s.x509.pem " \ 285 "with %s.x509.pem." % (e.filename, e.strerror, old, new) 286 287 return data 288 289 290def EditTags(tags): 291 """Given a string containing comma-separated tags, apply the edits 292 specified in OPTIONS.tag_changes and return the updated string.""" 293 tags = set(tags.split(",")) 294 for ch in OPTIONS.tag_changes: 295 if ch[0] == "-": 296 tags.discard(ch[1:]) 297 elif ch[0] == "+": 298 tags.add(ch[1:]) 299 return ",".join(sorted(tags)) 300 301 302def RewriteProps(data, misc_info): 303 output = [] 304 for line in data.split("\n"): 305 line = line.strip() 306 original_line = line 307 if line and line[0] != '#' and "=" in line: 308 key, value = line.split("=", 1) 309 if (key in ("ro.build.fingerprint", "ro.vendor.build.fingerprint") 310 and misc_info.get("oem_fingerprint_properties") is None): 311 pieces = value.split("/") 312 pieces[-1] = EditTags(pieces[-1]) 313 value = "/".join(pieces) 314 elif (key in ("ro.build.thumbprint", "ro.vendor.build.thumbprint") 315 and misc_info.get("oem_fingerprint_properties") is not None): 316 pieces = value.split("/") 317 pieces[-1] = EditTags(pieces[-1]) 318 value = "/".join(pieces) 319 elif key == "ro.bootimage.build.fingerprint": 320 pieces = value.split("/") 321 pieces[-1] = EditTags(pieces[-1]) 322 value = "/".join(pieces) 323 elif key == "ro.build.description": 324 pieces = value.split(" ") 325 assert len(pieces) == 5 326 pieces[-1] = EditTags(pieces[-1]) 327 value = " ".join(pieces) 328 elif key == "ro.build.tags": 329 value = EditTags(value) 330 elif key == "ro.build.display.id": 331 # change, eg, "JWR66N dev-keys" to "JWR66N" 332 value = value.split() 333 if len(value) > 1 and value[-1].endswith("-keys"): 334 value.pop() 335 value = " ".join(value) 336 line = key + "=" + value 337 if line != original_line: 338 print " replace: ", original_line 339 print " with: ", line 340 output.append(line) 341 return "\n".join(output) + "\n" 342 343 344def ReplaceOtaKeys(input_tf_zip, output_tf_zip, misc_info): 345 try: 346 keylist = input_tf_zip.read("META/otakeys.txt").split() 347 except KeyError: 348 raise common.ExternalError("can't read META/otakeys.txt from input") 349 350 extra_recovery_keys = misc_info.get("extra_recovery_keys", None) 351 if extra_recovery_keys: 352 extra_recovery_keys = [OPTIONS.key_map.get(k, k) + ".x509.pem" 353 for k in extra_recovery_keys.split()] 354 if extra_recovery_keys: 355 print "extra recovery-only key(s): " + ", ".join(extra_recovery_keys) 356 else: 357 extra_recovery_keys = [] 358 359 mapped_keys = [] 360 for k in keylist: 361 m = re.match(r"^(.*)\.x509\.pem$", k) 362 if not m: 363 raise common.ExternalError( 364 "can't parse \"%s\" from META/otakeys.txt" % (k,)) 365 k = m.group(1) 366 mapped_keys.append(OPTIONS.key_map.get(k, k) + ".x509.pem") 367 368 if mapped_keys: 369 print "using:\n ", "\n ".join(mapped_keys) 370 print "for OTA package verification" 371 else: 372 devkey = misc_info.get("default_system_dev_certificate", 373 "build/target/product/security/testkey") 374 mapped_keys.append( 375 OPTIONS.key_map.get(devkey, devkey) + ".x509.pem") 376 print "META/otakeys.txt has no keys; using", mapped_keys[0] 377 378 # recovery uses a version of the key that has been slightly 379 # predigested (by DumpPublicKey.java) and put in res/keys. 380 # extra_recovery_keys are used only in recovery. 381 382 p = common.Run(["java", "-jar", 383 os.path.join(OPTIONS.search_path, "framework", "dumpkey.jar")] 384 + mapped_keys + extra_recovery_keys, 385 stdout=subprocess.PIPE) 386 new_recovery_keys, _ = p.communicate() 387 if p.returncode != 0: 388 raise common.ExternalError("failed to run dumpkeys") 389 common.ZipWriteStr(output_tf_zip, "RECOVERY/RAMDISK/res/keys", 390 new_recovery_keys) 391 392 # SystemUpdateActivity uses the x509.pem version of the keys, but 393 # put into a zipfile system/etc/security/otacerts.zip. 394 # We DO NOT include the extra_recovery_keys (if any) here. 395 396 temp_file = cStringIO.StringIO() 397 certs_zip = zipfile.ZipFile(temp_file, "w") 398 for k in mapped_keys: 399 certs_zip.write(k) 400 certs_zip.close() 401 common.ZipWriteStr(output_tf_zip, "SYSTEM/etc/security/otacerts.zip", 402 temp_file.getvalue()) 403 404 return new_recovery_keys 405 406def ReplaceVerityPublicKey(targetfile_zip, key_path): 407 print "Replacing verity public key with %s" % key_path 408 with open(key_path) as f: 409 data = f.read() 410 common.ZipWriteStr(targetfile_zip, "BOOT/RAMDISK/verity_key", data) 411 return data 412 413def ReplaceVerityPrivateKey(targetfile_input_zip, targetfile_output_zip, 414 misc_info, key_path): 415 print "Replacing verity private key with %s" % key_path 416 current_key = misc_info["verity_key"] 417 original_misc_info = targetfile_input_zip.read("META/misc_info.txt") 418 new_misc_info = original_misc_info.replace(current_key, key_path) 419 common.ZipWriteStr(targetfile_output_zip, "META/misc_info.txt", new_misc_info) 420 misc_info["verity_key"] = key_path 421 422def BuildKeyMap(misc_info, key_mapping_options): 423 for s, d in key_mapping_options: 424 if s is None: # -d option 425 devkey = misc_info.get("default_system_dev_certificate", 426 "build/target/product/security/testkey") 427 devkeydir = os.path.dirname(devkey) 428 429 OPTIONS.key_map.update({ 430 devkeydir + "/testkey": d + "/releasekey", 431 devkeydir + "/devkey": d + "/releasekey", 432 devkeydir + "/media": d + "/media", 433 devkeydir + "/shared": d + "/shared", 434 devkeydir + "/platform": d + "/platform", 435 }) 436 else: 437 OPTIONS.key_map[s] = d 438 439 440def main(argv): 441 442 key_mapping_options = [] 443 444 def option_handler(o, a): 445 if o in ("-e", "--extra_apks"): 446 names, key = a.split("=") 447 names = names.split(",") 448 for n in names: 449 OPTIONS.extra_apks[n] = key 450 elif o in ("-d", "--default_key_mappings"): 451 key_mapping_options.append((None, a)) 452 elif o in ("-k", "--key_mapping"): 453 key_mapping_options.append(a.split("=", 1)) 454 elif o in ("-o", "--replace_ota_keys"): 455 OPTIONS.replace_ota_keys = True 456 elif o in ("-t", "--tag_changes"): 457 new = [] 458 for i in a.split(","): 459 i = i.strip() 460 if not i or i[0] not in "-+": 461 raise ValueError("Bad tag change '%s'" % (i,)) 462 new.append(i[0] + i[1:].strip()) 463 OPTIONS.tag_changes = tuple(new) 464 elif o == "--replace_verity_public_key": 465 OPTIONS.replace_verity_public_key = (True, a) 466 elif o == "--replace_verity_private_key": 467 OPTIONS.replace_verity_private_key = (True, a) 468 else: 469 return False 470 return True 471 472 args = common.ParseOptions(argv, __doc__, 473 extra_opts="e:d:k:ot:", 474 extra_long_opts=["extra_apks=", 475 "default_key_mappings=", 476 "key_mapping=", 477 "replace_ota_keys", 478 "tag_changes=", 479 "replace_verity_public_key=", 480 "replace_verity_private_key="], 481 extra_option_handler=option_handler) 482 483 if len(args) != 2: 484 common.Usage(__doc__) 485 sys.exit(1) 486 487 input_zip = zipfile.ZipFile(args[0], "r") 488 output_zip = zipfile.ZipFile(args[1], "w") 489 490 misc_info = common.LoadInfoDict(input_zip) 491 492 BuildKeyMap(misc_info, key_mapping_options) 493 494 apk_key_map = GetApkCerts(input_zip) 495 CheckAllApksSigned(input_zip, apk_key_map) 496 497 key_passwords = common.GetKeyPasswords(set(apk_key_map.values())) 498 ProcessTargetFiles(input_zip, output_zip, misc_info, 499 apk_key_map, key_passwords) 500 501 common.ZipClose(input_zip) 502 common.ZipClose(output_zip) 503 504 add_img_to_target_files.AddImagesToTargetFiles(args[1]) 505 506 print "done." 507 508 509if __name__ == '__main__': 510 try: 511 main(sys.argv[1:]) 512 except common.ExternalError, e: 513 print 514 print " ERROR: %s" % (e,) 515 print 516 sys.exit(1) 517