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       line = key + "=" + value
    200     if line != original_line:
    201       print "  replace: ", original_line
    202       print "     with: ", line
    203     output.append(line)
    204   return "\n".join(output) + "\n"
    205 
    206 
    207 def ReplaceOtaKeys(input_tf_zip, output_tf_zip, misc_info):
    208   try:
    209     keylist = input_tf_zip.read("META/otakeys.txt").split()
    210   except KeyError:
    211     raise ExternalError("can't read META/otakeys.txt from input")
    212 
    213   extra_recovery_keys = misc_info.get("extra_recovery_keys", None)
    214   if extra_recovery_keys:
    215     extra_recovery_keys = [OPTIONS.key_map.get(k, k) + ".x509.pem"
    216                            for k in extra_recovery_keys.split()]
    217     if extra_recovery_keys:
    218       print "extra recovery-only key(s): " + ", ".join(extra_recovery_keys)
    219   else:
    220     extra_recovery_keys = []
    221 
    222   mapped_keys = []
    223   for k in keylist:
    224     m = re.match(r"^(.*)\.x509\.pem$", k)
    225     if not m:
    226       raise ExternalError("can't parse \"%s\" from META/otakeys.txt" % (k,))
    227     k = m.group(1)
    228     mapped_keys.append(OPTIONS.key_map.get(k, k) + ".x509.pem")
    229 
    230   if mapped_keys:
    231     print "using:\n   ", "\n   ".join(mapped_keys)
    232     print "for OTA package verification"
    233   else:
    234     devkey = misc_info.get("default_system_dev_certificate",
    235                            "build/target/product/security/testkey")
    236     mapped_keys.append(
    237         OPTIONS.key_map.get(devkey, devkey) + ".x509.pem")
    238     print "META/otakeys.txt has no keys; using", mapped_keys[0]
    239 
    240   # recovery uses a version of the key that has been slightly
    241   # predigested (by DumpPublicKey.java) and put in res/keys.
    242   # extra_recovery_keys are used only in recovery.
    243 
    244   p = common.Run(["java", "-jar",
    245                   os.path.join(OPTIONS.search_path, "framework", "dumpkey.jar")]
    246                  + mapped_keys + extra_recovery_keys,
    247                  stdout=subprocess.PIPE)
    248   data, _ = p.communicate()
    249   if p.returncode != 0:
    250     raise ExternalError("failed to run dumpkeys")
    251   common.ZipWriteStr(output_tf_zip, "RECOVERY/RAMDISK/res/keys", data)
    252 
    253   # SystemUpdateActivity uses the x509.pem version of the keys, but
    254   # put into a zipfile system/etc/security/otacerts.zip.
    255   # We DO NOT include the extra_recovery_keys (if any) here.
    256 
    257   tempfile = cStringIO.StringIO()
    258   certs_zip = zipfile.ZipFile(tempfile, "w")
    259   for k in mapped_keys:
    260     certs_zip.write(k)
    261   certs_zip.close()
    262   common.ZipWriteStr(output_tf_zip, "SYSTEM/etc/security/otacerts.zip",
    263                      tempfile.getvalue())
    264 
    265 
    266 def BuildKeyMap(misc_info, key_mapping_options):
    267   for s, d in key_mapping_options:
    268     if s is None:   # -d option
    269       devkey = misc_info.get("default_system_dev_certificate",
    270                              "build/target/product/security/testkey")
    271       devkeydir = os.path.dirname(devkey)
    272 
    273       OPTIONS.key_map.update({
    274           devkeydir + "/testkey":  d + "/releasekey",
    275           devkeydir + "/devkey":   d + "/releasekey",
    276           devkeydir + "/media":    d + "/media",
    277           devkeydir + "/shared":   d + "/shared",
    278           devkeydir + "/platform": d + "/platform",
    279           })
    280     else:
    281       OPTIONS.key_map[s] = d
    282 
    283 
    284 def main(argv):
    285 
    286   key_mapping_options = []
    287 
    288   def option_handler(o, a):
    289     if o in ("-e", "--extra_apks"):
    290       names, key = a.split("=")
    291       names = names.split(",")
    292       for n in names:
    293         OPTIONS.extra_apks[n] = key
    294     elif o in ("-d", "--default_key_mappings"):
    295       key_mapping_options.append((None, a))
    296     elif o in ("-k", "--key_mapping"):
    297       key_mapping_options.append(a.split("=", 1))
    298     elif o in ("-o", "--replace_ota_keys"):
    299       OPTIONS.replace_ota_keys = True
    300     elif o in ("-t", "--tag_changes"):
    301       new = []
    302       for i in a.split(","):
    303         i = i.strip()
    304         if not i or i[0] not in "-+":
    305           raise ValueError("Bad tag change '%s'" % (i,))
    306         new.append(i[0] + i[1:].strip())
    307       OPTIONS.tag_changes = tuple(new)
    308     else:
    309       return False
    310     return True
    311 
    312   args = common.ParseOptions(argv, __doc__,
    313                              extra_opts="e:d:k:ot:",
    314                              extra_long_opts=["extra_apks=",
    315                                               "default_key_mappings=",
    316                                               "key_mapping=",
    317                                               "replace_ota_keys",
    318                                               "tag_changes="],
    319                              extra_option_handler=option_handler)
    320 
    321   if len(args) != 2:
    322     common.Usage(__doc__)
    323     sys.exit(1)
    324 
    325   input_zip = zipfile.ZipFile(args[0], "r")
    326   output_zip = zipfile.ZipFile(args[1], "w")
    327 
    328   misc_info = common.LoadInfoDict(input_zip)
    329 
    330   BuildKeyMap(misc_info, key_mapping_options)
    331 
    332   apk_key_map = GetApkCerts(input_zip)
    333   CheckAllApksSigned(input_zip, apk_key_map)
    334 
    335   key_passwords = common.GetKeyPasswords(set(apk_key_map.values()))
    336   SignApks(input_zip, output_zip, apk_key_map, key_passwords)
    337 
    338   if OPTIONS.replace_ota_keys:
    339     ReplaceOtaKeys(input_zip, output_zip, misc_info)
    340 
    341   input_zip.close()
    342   output_zip.close()
    343 
    344   print "done."
    345 
    346 
    347 if __name__ == '__main__':
    348   try:
    349     main(sys.argv[1:])
    350   except common.ExternalError, e:
    351     print
    352     print "   ERROR: %s" % (e,)
    353     print
    354     sys.exit(1)
    355