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