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