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