sign_target_files_apks.py revision 3c84f569487c4e59baa332be33b5430fdefb76b3
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.tag_changes = ("-test-keys", "-dev-keys", "+release-keys") 94 95def GetApkCerts(tf_zip): 96 certmap = common.ReadApkCerts(tf_zip) 97 98 # apply the key remapping to the contents of the file 99 for apk, cert in certmap.iteritems(): 100 certmap[apk] = OPTIONS.key_map.get(cert, cert) 101 102 # apply all the -e options, overriding anything in the file 103 for apk, cert in OPTIONS.extra_apks.iteritems(): 104 if not cert: 105 cert = "PRESIGNED" 106 certmap[apk] = OPTIONS.key_map.get(cert, cert) 107 108 return certmap 109 110 111def CheckAllApksSigned(input_tf_zip, apk_key_map): 112 """Check that all the APKs we want to sign have keys specified, and 113 error out if they don't.""" 114 unknown_apks = [] 115 for info in input_tf_zip.infolist(): 116 if info.filename.endswith(".apk"): 117 name = os.path.basename(info.filename) 118 if name not in apk_key_map: 119 unknown_apks.append(name) 120 if unknown_apks: 121 print "ERROR: no key specified for:\n\n ", 122 print "\n ".join(unknown_apks) 123 print "\nUse '-e <apkname>=' to specify a key (which may be an" 124 print "empty string to not sign this apk)." 125 sys.exit(1) 126 127 128def SignApk(data, keyname, pw): 129 unsigned = tempfile.NamedTemporaryFile() 130 unsigned.write(data) 131 unsigned.flush() 132 133 signed = tempfile.NamedTemporaryFile() 134 135 common.SignFile(unsigned.name, signed.name, keyname, pw, align=4) 136 137 data = signed.read() 138 unsigned.close() 139 signed.close() 140 141 return data 142 143 144def ProcessTargetFiles(input_tf_zip, output_tf_zip, misc_info, 145 apk_key_map, key_passwords): 146 147 maxsize = max([len(os.path.basename(i.filename)) 148 for i in input_tf_zip.infolist() 149 if i.filename.endswith('.apk')]) 150 rebuild_recovery = False 151 152 tmpdir = tempfile.mkdtemp() 153 def write_to_temp(fn, attr, data): 154 fn = os.path.join(tmpdir, fn) 155 if fn.endswith("/"): 156 fn = os.path.join(tmpdir, fn) 157 os.mkdir(fn) 158 else: 159 d = os.path.dirname(fn) 160 if d and not os.path.exists(d): 161 os.makedirs(d) 162 163 if attr >> 16 == 0xa1ff: 164 os.symlink(data, fn) 165 else: 166 with open(fn, "wb") as f: 167 f.write(data) 168 169 for info in input_tf_zip.infolist(): 170 if info.filename.startswith("IMAGES/"): continue 171 172 data = input_tf_zip.read(info.filename) 173 out_info = copy.copy(info) 174 175 if (info.filename.startswith("BOOT/") or 176 info.filename.startswith("RECOVERY/") or 177 info.filename.startswith("META/") or 178 info.filename == "SYSTEM/etc/recovery-resource.dat"): 179 write_to_temp(info.filename, info.external_attr, data) 180 181 if info.filename.endswith(".apk"): 182 name = os.path.basename(info.filename) 183 key = apk_key_map[name] 184 if key not in common.SPECIAL_CERT_STRINGS: 185 print " signing: %-*s (%s)" % (maxsize, name, key) 186 signed_data = SignApk(data, key, key_passwords[key]) 187 output_tf_zip.writestr(out_info, signed_data) 188 else: 189 # an APK we're not supposed to sign. 190 print "NOT signing: %s" % (name,) 191 output_tf_zip.writestr(out_info, data) 192 elif info.filename in ("SYSTEM/build.prop", 193 "RECOVERY/RAMDISK/default.prop"): 194 print "rewriting %s:" % (info.filename,) 195 new_data = RewriteProps(data, misc_info) 196 output_tf_zip.writestr(out_info, new_data) 197 if info.filename == "RECOVERY/RAMDISK/default.prop": 198 write_to_temp(info.filename, info.external_attr, new_data) 199 elif info.filename.endswith("mac_permissions.xml"): 200 print "rewriting %s with new keys." % (info.filename,) 201 new_data = ReplaceCerts(data) 202 output_tf_zip.writestr(out_info, new_data) 203 elif info.filename in ("SYSTEM/recovery-from-boot.p", 204 "SYSTEM/bin/install-recovery.sh"): 205 rebuild_recovery = True 206 elif (OPTIONS.replace_ota_keys and 207 info.filename in ("RECOVERY/RAMDISK/res/keys", 208 "SYSTEM/etc/security/otacerts.zip")): 209 # don't copy these files if we're regenerating them below 210 pass 211 else: 212 # a non-APK file; copy it verbatim 213 output_tf_zip.writestr(out_info, data) 214 215 if OPTIONS.replace_ota_keys: 216 new_recovery_keys = ReplaceOtaKeys(input_tf_zip, output_tf_zip, misc_info) 217 if new_recovery_keys: 218 write_to_temp("RECOVERY/RAMDISK/res/keys", 0755 << 16, new_recovery_keys) 219 220 if rebuild_recovery: 221 recovery_img = common.GetBootableImage( 222 "recovery.img", "recovery.img", tmpdir, "RECOVERY", info_dict=misc_info) 223 boot_img = common.GetBootableImage( 224 "boot.img", "boot.img", tmpdir, "BOOT", info_dict=misc_info) 225 226 def output_sink(fn, data): 227 output_tf_zip.writestr("SYSTEM/"+fn, data) 228 229 common.MakeRecoveryPatch(tmpdir, output_sink, recovery_img, boot_img, 230 info_dict=misc_info) 231 232 shutil.rmtree(tmpdir) 233 234 235def ReplaceCerts(data): 236 """Given a string of data, replace all occurences of a set 237 of X509 certs with a newer set of X509 certs and return 238 the updated data string.""" 239 for old, new in OPTIONS.key_map.iteritems(): 240 try: 241 if OPTIONS.verbose: 242 print " Replacing %s.x509.pem with %s.x509.pem" % (old, new) 243 f = open(old + ".x509.pem") 244 old_cert16 = base64.b16encode(common.ParseCertificate(f.read())).lower() 245 f.close() 246 f = open(new + ".x509.pem") 247 new_cert16 = base64.b16encode(common.ParseCertificate(f.read())).lower() 248 f.close() 249 # Only match entire certs. 250 pattern = "\\b"+old_cert16+"\\b" 251 (data, num) = re.subn(pattern, new_cert16, data, flags=re.IGNORECASE) 252 if OPTIONS.verbose: 253 print " Replaced %d occurence(s) of %s.x509.pem with " \ 254 "%s.x509.pem" % (num, old, new) 255 except IOError, e: 256 if (e.errno == errno.ENOENT and not OPTIONS.verbose): 257 continue 258 259 print " Error accessing %s. %s. Skip replacing %s.x509.pem " \ 260 "with %s.x509.pem." % (e.filename, e.strerror, old, new) 261 262 return data 263 264 265def EditTags(tags): 266 """Given a string containing comma-separated tags, apply the edits 267 specified in OPTIONS.tag_changes and return the updated string.""" 268 tags = set(tags.split(",")) 269 for ch in OPTIONS.tag_changes: 270 if ch[0] == "-": 271 tags.discard(ch[1:]) 272 elif ch[0] == "+": 273 tags.add(ch[1:]) 274 return ",".join(sorted(tags)) 275 276 277def RewriteProps(data, misc_info): 278 output = [] 279 for line in data.split("\n"): 280 line = line.strip() 281 original_line = line 282 if line and line[0] != '#' and "=" in line: 283 key, value = line.split("=", 1) 284 if (key == "ro.build.fingerprint" 285 and misc_info.get("oem_fingerprint_properties") is None): 286 pieces = value.split("/") 287 pieces[-1] = EditTags(pieces[-1]) 288 value = "/".join(pieces) 289 elif (key == "ro.build.thumbprint" 290 and misc_info.get("oem_fingerprint_properties") is not None): 291 pieces = value.split("/") 292 pieces[-1] = EditTags(pieces[-1]) 293 value = "/".join(pieces) 294 elif key == "ro.build.description": 295 pieces = value.split(" ") 296 assert len(pieces) == 5 297 pieces[-1] = EditTags(pieces[-1]) 298 value = " ".join(pieces) 299 elif key == "ro.build.tags": 300 value = EditTags(value) 301 elif key == "ro.build.display.id": 302 # change, eg, "JWR66N dev-keys" to "JWR66N" 303 value = value.split() 304 if len(value) > 1 and value[-1].endswith("-keys"): 305 value.pop() 306 value = " ".join(value) 307 line = key + "=" + value 308 if line != original_line: 309 print " replace: ", original_line 310 print " with: ", line 311 output.append(line) 312 return "\n".join(output) + "\n" 313 314 315def ReplaceOtaKeys(input_tf_zip, output_tf_zip, misc_info): 316 try: 317 keylist = input_tf_zip.read("META/otakeys.txt").split() 318 except KeyError: 319 raise common.ExternalError("can't read META/otakeys.txt from input") 320 321 extra_recovery_keys = misc_info.get("extra_recovery_keys", None) 322 if extra_recovery_keys: 323 extra_recovery_keys = [OPTIONS.key_map.get(k, k) + ".x509.pem" 324 for k in extra_recovery_keys.split()] 325 if extra_recovery_keys: 326 print "extra recovery-only key(s): " + ", ".join(extra_recovery_keys) 327 else: 328 extra_recovery_keys = [] 329 330 mapped_keys = [] 331 for k in keylist: 332 m = re.match(r"^(.*)\.x509\.pem$", k) 333 if not m: 334 raise common.ExternalError( 335 "can't parse \"%s\" from META/otakeys.txt" % (k,)) 336 k = m.group(1) 337 mapped_keys.append(OPTIONS.key_map.get(k, k) + ".x509.pem") 338 339 if mapped_keys: 340 print "using:\n ", "\n ".join(mapped_keys) 341 print "for OTA package verification" 342 else: 343 devkey = misc_info.get("default_system_dev_certificate", 344 "build/target/product/security/testkey") 345 mapped_keys.append( 346 OPTIONS.key_map.get(devkey, devkey) + ".x509.pem") 347 print "META/otakeys.txt has no keys; using", mapped_keys[0] 348 349 # recovery uses a version of the key that has been slightly 350 # predigested (by DumpPublicKey.java) and put in res/keys. 351 # extra_recovery_keys are used only in recovery. 352 353 p = common.Run(["java", "-jar", 354 os.path.join(OPTIONS.search_path, "framework", "dumpkey.jar")] 355 + mapped_keys + extra_recovery_keys, 356 stdout=subprocess.PIPE) 357 new_recovery_keys, _ = p.communicate() 358 if p.returncode != 0: 359 raise common.ExternalError("failed to run dumpkeys") 360 common.ZipWriteStr(output_tf_zip, "RECOVERY/RAMDISK/res/keys", 361 new_recovery_keys) 362 363 # SystemUpdateActivity uses the x509.pem version of the keys, but 364 # put into a zipfile system/etc/security/otacerts.zip. 365 # We DO NOT include the extra_recovery_keys (if any) here. 366 367 tempfile = cStringIO.StringIO() 368 certs_zip = zipfile.ZipFile(tempfile, "w") 369 for k in mapped_keys: 370 certs_zip.write(k) 371 certs_zip.close() 372 common.ZipWriteStr(output_tf_zip, "SYSTEM/etc/security/otacerts.zip", 373 tempfile.getvalue()) 374 375 return new_recovery_keys 376 377 378def BuildKeyMap(misc_info, key_mapping_options): 379 for s, d in key_mapping_options: 380 if s is None: # -d option 381 devkey = misc_info.get("default_system_dev_certificate", 382 "build/target/product/security/testkey") 383 devkeydir = os.path.dirname(devkey) 384 385 OPTIONS.key_map.update({ 386 devkeydir + "/testkey": d + "/releasekey", 387 devkeydir + "/devkey": d + "/releasekey", 388 devkeydir + "/media": d + "/media", 389 devkeydir + "/shared": d + "/shared", 390 devkeydir + "/platform": d + "/platform", 391 }) 392 else: 393 OPTIONS.key_map[s] = d 394 395 396def main(argv): 397 398 key_mapping_options = [] 399 400 def option_handler(o, a): 401 if o in ("-e", "--extra_apks"): 402 names, key = a.split("=") 403 names = names.split(",") 404 for n in names: 405 OPTIONS.extra_apks[n] = key 406 elif o in ("-d", "--default_key_mappings"): 407 key_mapping_options.append((None, a)) 408 elif o in ("-k", "--key_mapping"): 409 key_mapping_options.append(a.split("=", 1)) 410 elif o in ("-o", "--replace_ota_keys"): 411 OPTIONS.replace_ota_keys = True 412 elif o in ("-t", "--tag_changes"): 413 new = [] 414 for i in a.split(","): 415 i = i.strip() 416 if not i or i[0] not in "-+": 417 raise ValueError("Bad tag change '%s'" % (i,)) 418 new.append(i[0] + i[1:].strip()) 419 OPTIONS.tag_changes = tuple(new) 420 else: 421 return False 422 return True 423 424 args = common.ParseOptions(argv, __doc__, 425 extra_opts="e:d:k:ot:", 426 extra_long_opts=["extra_apks=", 427 "default_key_mappings=", 428 "key_mapping=", 429 "replace_ota_keys", 430 "tag_changes="], 431 extra_option_handler=option_handler) 432 433 if len(args) != 2: 434 common.Usage(__doc__) 435 sys.exit(1) 436 437 input_zip = zipfile.ZipFile(args[0], "r") 438 output_zip = zipfile.ZipFile(args[1], "w") 439 440 misc_info = common.LoadInfoDict(input_zip) 441 442 BuildKeyMap(misc_info, key_mapping_options) 443 444 apk_key_map = GetApkCerts(input_zip) 445 CheckAllApksSigned(input_zip, apk_key_map) 446 447 key_passwords = common.GetKeyPasswords(set(apk_key_map.values())) 448 ProcessTargetFiles(input_zip, output_zip, misc_info, 449 apk_key_map, key_passwords) 450 451 input_zip.close() 452 output_zip.close() 453 454 add_img_to_target_files.AddImagesToTargetFiles(args[1]) 455 456 print "done." 457 458 459if __name__ == '__main__': 460 try: 461 main(sys.argv[1:]) 462 except common.ExternalError, e: 463 print 464 print " ERROR: %s" % (e,) 465 print 466 sys.exit(1) 467