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