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