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