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