Home | History | Annotate | Download | only in releasetools
      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