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, platform_api_level, codename_to_api_level_map): 131 unsigned = tempfile.NamedTemporaryFile() 132 unsigned.write(data) 133 unsigned.flush() 134 135 signed = tempfile.NamedTemporaryFile() 136 137 # For pre-N builds, don't upgrade to SHA-256 JAR signatures based on the APK's 138 # minSdkVersion to avoid increasing incremental OTA update sizes. If an APK 139 # didn't change, we don't want its signature to change due to the switch 140 # from SHA-1 to SHA-256. 141 # By default, APK signer chooses SHA-256 signatures if the APK's minSdkVersion 142 # is 18 or higher. For pre-N builds we disable this mechanism by pretending 143 # that the APK's minSdkVersion is 1. 144 # For N+ builds, we let APK signer rely on the APK's minSdkVersion to 145 # determine whether to use SHA-256. 146 min_api_level = None 147 if platform_api_level > 23: 148 # Let APK signer choose whether to use SHA-1 or SHA-256, based on the APK's 149 # minSdkVersion attribute 150 min_api_level = None 151 else: 152 # Force APK signer to use SHA-1 153 min_api_level = 1 154 155 common.SignFile(unsigned.name, signed.name, keyname, pw, 156 min_api_level=min_api_level, 157 codename_to_api_level_map=codename_to_api_level_map) 158 159 data = signed.read() 160 unsigned.close() 161 signed.close() 162 163 return data 164 165 166def ProcessTargetFiles(input_tf_zip, output_tf_zip, misc_info, 167 apk_key_map, key_passwords, platform_api_level, 168 codename_to_api_level_map): 169 170 maxsize = max([len(os.path.basename(i.filename)) 171 for i in input_tf_zip.infolist() 172 if i.filename.endswith('.apk')]) 173 rebuild_recovery = False 174 175 tmpdir = tempfile.mkdtemp() 176 def write_to_temp(fn, attr, data): 177 fn = os.path.join(tmpdir, fn) 178 if fn.endswith("/"): 179 fn = os.path.join(tmpdir, fn) 180 os.mkdir(fn) 181 else: 182 d = os.path.dirname(fn) 183 if d and not os.path.exists(d): 184 os.makedirs(d) 185 186 if attr >> 16 == 0xa1ff: 187 os.symlink(data, fn) 188 else: 189 with open(fn, "wb") as f: 190 f.write(data) 191 192 for info in input_tf_zip.infolist(): 193 if info.filename.startswith("IMAGES/"): 194 continue 195 196 data = input_tf_zip.read(info.filename) 197 out_info = copy.copy(info) 198 199 # Replace keys if requested. 200 if (info.filename == "META/misc_info.txt" and 201 OPTIONS.replace_verity_private_key): 202 ReplaceVerityPrivateKey(input_tf_zip, output_tf_zip, misc_info, 203 OPTIONS.replace_verity_private_key[1]) 204 elif (info.filename in ("BOOT/RAMDISK/verity_key", 205 "BOOT/verity_key") and 206 OPTIONS.replace_verity_public_key): 207 new_data = ReplaceVerityPublicKey(output_tf_zip, info.filename, 208 OPTIONS.replace_verity_public_key[1]) 209 write_to_temp(info.filename, info.external_attr, new_data) 210 # Copy BOOT/, RECOVERY/, META/, ROOT/ to rebuild recovery patch. 211 elif (info.filename.startswith("BOOT/") or 212 info.filename.startswith("RECOVERY/") or 213 info.filename.startswith("META/") or 214 info.filename.startswith("ROOT/") or 215 info.filename == "SYSTEM/etc/recovery-resource.dat"): 216 write_to_temp(info.filename, info.external_attr, data) 217 218 # Sign APKs. 219 if info.filename.endswith(".apk"): 220 name = os.path.basename(info.filename) 221 key = apk_key_map[name] 222 if key not in common.SPECIAL_CERT_STRINGS: 223 print " signing: %-*s (%s)" % (maxsize, name, key) 224 signed_data = SignApk(data, key, key_passwords[key], platform_api_level, 225 codename_to_api_level_map) 226 common.ZipWriteStr(output_tf_zip, out_info, signed_data) 227 else: 228 # an APK we're not supposed to sign. 229 print "NOT signing: %s" % (name,) 230 common.ZipWriteStr(output_tf_zip, out_info, data) 231 elif info.filename in ("SYSTEM/build.prop", 232 "VENDOR/build.prop", 233 "BOOT/RAMDISK/default.prop", 234 "RECOVERY/RAMDISK/default.prop"): 235 print "rewriting %s:" % (info.filename,) 236 new_data = RewriteProps(data, misc_info) 237 common.ZipWriteStr(output_tf_zip, out_info, new_data) 238 if info.filename in ("BOOT/RAMDISK/default.prop", 239 "RECOVERY/RAMDISK/default.prop"): 240 write_to_temp(info.filename, info.external_attr, new_data) 241 elif info.filename.endswith("mac_permissions.xml"): 242 print "rewriting %s with new keys." % (info.filename,) 243 new_data = ReplaceCerts(data) 244 common.ZipWriteStr(output_tf_zip, out_info, new_data) 245 elif info.filename in ("SYSTEM/recovery-from-boot.p", 246 "SYSTEM/etc/recovery.img", 247 "SYSTEM/bin/install-recovery.sh"): 248 rebuild_recovery = True 249 elif (OPTIONS.replace_ota_keys and 250 info.filename in ("RECOVERY/RAMDISK/res/keys", 251 "SYSTEM/etc/security/otacerts.zip")): 252 # don't copy these files if we're regenerating them below 253 pass 254 elif (OPTIONS.replace_verity_private_key and 255 info.filename == "META/misc_info.txt"): 256 pass 257 elif (OPTIONS.replace_verity_public_key and 258 info.filename in ("BOOT/RAMDISK/verity_key", 259 "BOOT/verity_key")): 260 pass 261 else: 262 # a non-APK file; copy it verbatim 263 common.ZipWriteStr(output_tf_zip, out_info, data) 264 265 if OPTIONS.replace_ota_keys: 266 new_recovery_keys = ReplaceOtaKeys(input_tf_zip, output_tf_zip, misc_info) 267 if new_recovery_keys: 268 write_to_temp("RECOVERY/RAMDISK/res/keys", 0o755 << 16, new_recovery_keys) 269 270 if rebuild_recovery: 271 recovery_img = common.GetBootableImage( 272 "recovery.img", "recovery.img", tmpdir, "RECOVERY", info_dict=misc_info) 273 boot_img = common.GetBootableImage( 274 "boot.img", "boot.img", tmpdir, "BOOT", info_dict=misc_info) 275 276 def output_sink(fn, data): 277 common.ZipWriteStr(output_tf_zip, "SYSTEM/" + fn, data) 278 279 common.MakeRecoveryPatch(tmpdir, output_sink, recovery_img, boot_img, 280 info_dict=misc_info) 281 282 shutil.rmtree(tmpdir) 283 284 285def ReplaceCerts(data): 286 """Given a string of data, replace all occurences of a set 287 of X509 certs with a newer set of X509 certs and return 288 the updated data string.""" 289 for old, new in OPTIONS.key_map.iteritems(): 290 try: 291 if OPTIONS.verbose: 292 print " Replacing %s.x509.pem with %s.x509.pem" % (old, new) 293 f = open(old + ".x509.pem") 294 old_cert16 = base64.b16encode(common.ParseCertificate(f.read())).lower() 295 f.close() 296 f = open(new + ".x509.pem") 297 new_cert16 = base64.b16encode(common.ParseCertificate(f.read())).lower() 298 f.close() 299 # Only match entire certs. 300 pattern = "\\b"+old_cert16+"\\b" 301 (data, num) = re.subn(pattern, new_cert16, data, flags=re.IGNORECASE) 302 if OPTIONS.verbose: 303 print " Replaced %d occurence(s) of %s.x509.pem with " \ 304 "%s.x509.pem" % (num, old, new) 305 except IOError as e: 306 if e.errno == errno.ENOENT and not OPTIONS.verbose: 307 continue 308 309 print " Error accessing %s. %s. Skip replacing %s.x509.pem " \ 310 "with %s.x509.pem." % (e.filename, e.strerror, old, new) 311 312 return data 313 314 315def EditTags(tags): 316 """Given a string containing comma-separated tags, apply the edits 317 specified in OPTIONS.tag_changes and return the updated string.""" 318 tags = set(tags.split(",")) 319 for ch in OPTIONS.tag_changes: 320 if ch[0] == "-": 321 tags.discard(ch[1:]) 322 elif ch[0] == "+": 323 tags.add(ch[1:]) 324 return ",".join(sorted(tags)) 325 326 327def RewriteProps(data, misc_info): 328 output = [] 329 for line in data.split("\n"): 330 line = line.strip() 331 original_line = line 332 if line and line[0] != '#' and "=" in line: 333 key, value = line.split("=", 1) 334 if (key in ("ro.build.fingerprint", "ro.vendor.build.fingerprint") 335 and misc_info.get("oem_fingerprint_properties") is None): 336 pieces = value.split("/") 337 pieces[-1] = EditTags(pieces[-1]) 338 value = "/".join(pieces) 339 elif (key in ("ro.build.thumbprint", "ro.vendor.build.thumbprint") 340 and misc_info.get("oem_fingerprint_properties") is not None): 341 pieces = value.split("/") 342 pieces[-1] = EditTags(pieces[-1]) 343 value = "/".join(pieces) 344 elif key == "ro.bootimage.build.fingerprint": 345 pieces = value.split("/") 346 pieces[-1] = EditTags(pieces[-1]) 347 value = "/".join(pieces) 348 elif key == "ro.build.description": 349 pieces = value.split(" ") 350 assert len(pieces) == 5 351 pieces[-1] = EditTags(pieces[-1]) 352 value = " ".join(pieces) 353 elif key == "ro.build.tags": 354 value = EditTags(value) 355 elif key == "ro.build.display.id": 356 # change, eg, "JWR66N dev-keys" to "JWR66N" 357 value = value.split() 358 if len(value) > 1 and value[-1].endswith("-keys"): 359 value.pop() 360 value = " ".join(value) 361 line = key + "=" + value 362 if line != original_line: 363 print " replace: ", original_line 364 print " with: ", line 365 output.append(line) 366 return "\n".join(output) + "\n" 367 368 369def ReplaceOtaKeys(input_tf_zip, output_tf_zip, misc_info): 370 try: 371 keylist = input_tf_zip.read("META/otakeys.txt").split() 372 except KeyError: 373 raise common.ExternalError("can't read META/otakeys.txt from input") 374 375 extra_recovery_keys = misc_info.get("extra_recovery_keys", None) 376 if extra_recovery_keys: 377 extra_recovery_keys = [OPTIONS.key_map.get(k, k) + ".x509.pem" 378 for k in extra_recovery_keys.split()] 379 if extra_recovery_keys: 380 print "extra recovery-only key(s): " + ", ".join(extra_recovery_keys) 381 else: 382 extra_recovery_keys = [] 383 384 mapped_keys = [] 385 for k in keylist: 386 m = re.match(r"^(.*)\.x509\.pem$", k) 387 if not m: 388 raise common.ExternalError( 389 "can't parse \"%s\" from META/otakeys.txt" % (k,)) 390 k = m.group(1) 391 mapped_keys.append(OPTIONS.key_map.get(k, k) + ".x509.pem") 392 393 if mapped_keys: 394 print "using:\n ", "\n ".join(mapped_keys) 395 print "for OTA package verification" 396 else: 397 devkey = misc_info.get("default_system_dev_certificate", 398 "build/target/product/security/testkey") 399 mapped_keys.append( 400 OPTIONS.key_map.get(devkey, devkey) + ".x509.pem") 401 print "META/otakeys.txt has no keys; using", mapped_keys[0] 402 403 # recovery uses a version of the key that has been slightly 404 # predigested (by DumpPublicKey.java) and put in res/keys. 405 # extra_recovery_keys are used only in recovery. 406 407 p = common.Run(["java", "-jar", 408 os.path.join(OPTIONS.search_path, "framework", "dumpkey.jar")] 409 + mapped_keys + extra_recovery_keys, 410 stdout=subprocess.PIPE) 411 new_recovery_keys, _ = p.communicate() 412 if p.returncode != 0: 413 raise common.ExternalError("failed to run dumpkeys") 414 common.ZipWriteStr(output_tf_zip, "RECOVERY/RAMDISK/res/keys", 415 new_recovery_keys) 416 417 # SystemUpdateActivity uses the x509.pem version of the keys, but 418 # put into a zipfile system/etc/security/otacerts.zip. 419 # We DO NOT include the extra_recovery_keys (if any) here. 420 421 temp_file = cStringIO.StringIO() 422 certs_zip = zipfile.ZipFile(temp_file, "w") 423 for k in mapped_keys: 424 common.ZipWrite(certs_zip, k) 425 common.ZipClose(certs_zip) 426 common.ZipWriteStr(output_tf_zip, "SYSTEM/etc/security/otacerts.zip", 427 temp_file.getvalue()) 428 429 return new_recovery_keys 430 431def ReplaceVerityPublicKey(targetfile_zip, filename, key_path): 432 print "Replacing verity public key with %s" % key_path 433 with open(key_path) as f: 434 data = f.read() 435 common.ZipWriteStr(targetfile_zip, filename, data) 436 return data 437 438def ReplaceVerityPrivateKey(targetfile_input_zip, targetfile_output_zip, 439 misc_info, key_path): 440 print "Replacing verity private key with %s" % key_path 441 current_key = misc_info["verity_key"] 442 original_misc_info = targetfile_input_zip.read("META/misc_info.txt") 443 new_misc_info = original_misc_info.replace(current_key, key_path) 444 common.ZipWriteStr(targetfile_output_zip, "META/misc_info.txt", new_misc_info) 445 misc_info["verity_key"] = key_path 446 447def BuildKeyMap(misc_info, key_mapping_options): 448 for s, d in key_mapping_options: 449 if s is None: # -d option 450 devkey = misc_info.get("default_system_dev_certificate", 451 "build/target/product/security/testkey") 452 devkeydir = os.path.dirname(devkey) 453 454 OPTIONS.key_map.update({ 455 devkeydir + "/testkey": d + "/releasekey", 456 devkeydir + "/devkey": d + "/releasekey", 457 devkeydir + "/media": d + "/media", 458 devkeydir + "/shared": d + "/shared", 459 devkeydir + "/platform": d + "/platform", 460 }) 461 else: 462 OPTIONS.key_map[s] = d 463 464 465def GetApiLevelAndCodename(input_tf_zip): 466 data = input_tf_zip.read("SYSTEM/build.prop") 467 api_level = None 468 codename = None 469 for line in data.split("\n"): 470 line = line.strip() 471 original_line = line 472 if line and line[0] != '#' and "=" in line: 473 key, value = line.split("=", 1) 474 key = key.strip() 475 if key == "ro.build.version.sdk": 476 api_level = int(value.strip()) 477 elif key == "ro.build.version.codename": 478 codename = value.strip() 479 480 if api_level is None: 481 raise ValueError("No ro.build.version.sdk in SYSTEM/build.prop") 482 if codename is None: 483 raise ValueError("No ro.build.version.codename in SYSTEM/build.prop") 484 485 return (api_level, codename) 486 487 488def GetCodenameToApiLevelMap(input_tf_zip): 489 data = input_tf_zip.read("SYSTEM/build.prop") 490 api_level = None 491 codenames = None 492 for line in data.split("\n"): 493 line = line.strip() 494 original_line = line 495 if line and line[0] != '#' and "=" in line: 496 key, value = line.split("=", 1) 497 key = key.strip() 498 if key == "ro.build.version.sdk": 499 api_level = int(value.strip()) 500 elif key == "ro.build.version.all_codenames": 501 codenames = value.strip().split(",") 502 503 if api_level is None: 504 raise ValueError("No ro.build.version.sdk in SYSTEM/build.prop") 505 if codenames is None: 506 raise ValueError("No ro.build.version.all_codenames in SYSTEM/build.prop") 507 508 result = dict() 509 for codename in codenames: 510 codename = codename.strip() 511 if len(codename) > 0: 512 result[codename] = api_level 513 return result 514 515 516def main(argv): 517 518 key_mapping_options = [] 519 520 def option_handler(o, a): 521 if o in ("-e", "--extra_apks"): 522 names, key = a.split("=") 523 names = names.split(",") 524 for n in names: 525 OPTIONS.extra_apks[n] = key 526 elif o in ("-d", "--default_key_mappings"): 527 key_mapping_options.append((None, a)) 528 elif o in ("-k", "--key_mapping"): 529 key_mapping_options.append(a.split("=", 1)) 530 elif o in ("-o", "--replace_ota_keys"): 531 OPTIONS.replace_ota_keys = True 532 elif o in ("-t", "--tag_changes"): 533 new = [] 534 for i in a.split(","): 535 i = i.strip() 536 if not i or i[0] not in "-+": 537 raise ValueError("Bad tag change '%s'" % (i,)) 538 new.append(i[0] + i[1:].strip()) 539 OPTIONS.tag_changes = tuple(new) 540 elif o == "--replace_verity_public_key": 541 OPTIONS.replace_verity_public_key = (True, a) 542 elif o == "--replace_verity_private_key": 543 OPTIONS.replace_verity_private_key = (True, a) 544 else: 545 return False 546 return True 547 548 args = common.ParseOptions(argv, __doc__, 549 extra_opts="e:d:k:ot:", 550 extra_long_opts=["extra_apks=", 551 "default_key_mappings=", 552 "key_mapping=", 553 "replace_ota_keys", 554 "tag_changes=", 555 "replace_verity_public_key=", 556 "replace_verity_private_key="], 557 extra_option_handler=option_handler) 558 559 if len(args) != 2: 560 common.Usage(__doc__) 561 sys.exit(1) 562 563 input_zip = zipfile.ZipFile(args[0], "r") 564 output_zip = zipfile.ZipFile(args[1], "w") 565 566 misc_info = common.LoadInfoDict(input_zip) 567 568 BuildKeyMap(misc_info, key_mapping_options) 569 570 apk_key_map = GetApkCerts(input_zip) 571 CheckAllApksSigned(input_zip, apk_key_map) 572 573 key_passwords = common.GetKeyPasswords(set(apk_key_map.values())) 574 platform_api_level, platform_codename = GetApiLevelAndCodename(input_zip) 575 codename_to_api_level_map = GetCodenameToApiLevelMap(input_zip) 576 # Android N will be API Level 24, but isn't yet. 577 # TODO: Remove this workaround once Android N is officially API Level 24. 578 if platform_api_level == 23 and platform_codename == "N": 579 platform_api_level = 24 580 581 ProcessTargetFiles(input_zip, output_zip, misc_info, 582 apk_key_map, key_passwords, 583 platform_api_level, 584 codename_to_api_level_map) 585 586 common.ZipClose(input_zip) 587 common.ZipClose(output_zip) 588 589 add_img_to_target_files.AddImagesToTargetFiles(args[1]) 590 591 print "done." 592 593 594if __name__ == '__main__': 595 try: 596 main(sys.argv[1:]) 597 except common.ExternalError, e: 598 print 599 print " ERROR: %s" % (e,) 600 print 601 sys.exit(1) 602