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