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