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