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 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