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 < 0x02070000:
     71   print >> sys.stderr, "Python 2.7 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 shutil
     81 import subprocess
     82 import tempfile
     83 import zipfile
     84 
     85 import add_img_to_target_files
     86 import common
     87 
     88 OPTIONS = common.OPTIONS
     89 
     90 OPTIONS.extra_apks = {}
     91 OPTIONS.key_map = {}
     92 OPTIONS.replace_ota_keys = False
     93 OPTIONS.replace_verity_public_key = False
     94 OPTIONS.replace_verity_private_key = False
     95 OPTIONS.tag_changes = ("-test-keys", "-dev-keys", "+release-keys")
     96 
     97 def GetApkCerts(tf_zip):
     98   certmap = common.ReadApkCerts(tf_zip)
     99 
    100   # apply the key remapping to the contents of the file
    101   for apk, cert in certmap.iteritems():
    102     certmap[apk] = OPTIONS.key_map.get(cert, cert)
    103 
    104   # apply all the -e options, overriding anything in the file
    105   for apk, cert in OPTIONS.extra_apks.iteritems():
    106     if not cert:
    107       cert = "PRESIGNED"
    108     certmap[apk] = OPTIONS.key_map.get(cert, cert)
    109 
    110   return certmap
    111 
    112 
    113 def CheckAllApksSigned(input_tf_zip, apk_key_map):
    114   """Check that all the APKs we want to sign have keys specified, and
    115   error out if they don't."""
    116   unknown_apks = []
    117   for info in input_tf_zip.infolist():
    118     if info.filename.endswith(".apk"):
    119       name = os.path.basename(info.filename)
    120       if name not in apk_key_map:
    121         unknown_apks.append(name)
    122   if unknown_apks:
    123     print "ERROR: no key specified for:\n\n ",
    124     print "\n  ".join(unknown_apks)
    125     print "\nUse '-e <apkname>=' to specify a key (which may be an"
    126     print "empty string to not sign this apk)."
    127     sys.exit(1)
    128 
    129 
    130 def SignApk(data, keyname, pw):
    131   unsigned = tempfile.NamedTemporaryFile()
    132   unsigned.write(data)
    133   unsigned.flush()
    134 
    135   signed = tempfile.NamedTemporaryFile()
    136 
    137   common.SignFile(unsigned.name, signed.name, keyname, pw, align=4)
    138 
    139   data = signed.read()
    140   unsigned.close()
    141   signed.close()
    142 
    143   return data
    144 
    145 
    146 def ProcessTargetFiles(input_tf_zip, output_tf_zip, misc_info,
    147                        apk_key_map, key_passwords):
    148 
    149   maxsize = max([len(os.path.basename(i.filename))
    150                  for i in input_tf_zip.infolist()
    151                  if i.filename.endswith('.apk')])
    152   rebuild_recovery = False
    153 
    154   tmpdir = tempfile.mkdtemp()
    155   def write_to_temp(fn, attr, data):
    156     fn = os.path.join(tmpdir, fn)
    157     if fn.endswith("/"):
    158       fn = os.path.join(tmpdir, fn)
    159       os.mkdir(fn)
    160     else:
    161       d = os.path.dirname(fn)
    162       if d and not os.path.exists(d):
    163         os.makedirs(d)
    164 
    165       if attr >> 16 == 0xa1ff:
    166         os.symlink(data, fn)
    167       else:
    168         with open(fn, "wb") as f:
    169           f.write(data)
    170 
    171   for info in input_tf_zip.infolist():
    172     if info.filename.startswith("IMAGES/"): continue
    173 
    174     data = input_tf_zip.read(info.filename)
    175     out_info = copy.copy(info)
    176 
    177     if (info.filename == "META/misc_info.txt" and
    178         OPTIONS.replace_verity_private_key):
    179       ReplaceVerityPrivateKey(input_tf_zip, output_tf_zip, misc_info, OPTIONS.replace_verity_private_key[1])
    180     elif (info.filename == "BOOT/RAMDISK/verity_key" and
    181         OPTIONS.replace_verity_public_key):
    182       ReplaceVerityPublicKey(output_tf_zip, OPTIONS.replace_verity_public_key[1])
    183     elif (info.filename.startswith("BOOT/") or
    184         info.filename.startswith("RECOVERY/") or
    185         info.filename.startswith("META/") or
    186         info.filename == "SYSTEM/etc/recovery-resource.dat"):
    187       write_to_temp(info.filename, info.external_attr, data)
    188 
    189     if info.filename.endswith(".apk"):
    190       name = os.path.basename(info.filename)
    191       key = apk_key_map[name]
    192       if key not in common.SPECIAL_CERT_STRINGS:
    193         print "    signing: %-*s (%s)" % (maxsize, name, key)
    194         signed_data = SignApk(data, key, key_passwords[key])
    195         output_tf_zip.writestr(out_info, signed_data)
    196       else:
    197         # an APK we're not supposed to sign.
    198         print "NOT signing: %s" % (name,)
    199         output_tf_zip.writestr(out_info, data)
    200     elif info.filename in ("SYSTEM/build.prop",
    201                            "RECOVERY/RAMDISK/default.prop"):
    202       print "rewriting %s:" % (info.filename,)
    203       new_data = RewriteProps(data, misc_info)
    204       output_tf_zip.writestr(out_info, new_data)
    205       if info.filename == "RECOVERY/RAMDISK/default.prop":
    206         write_to_temp(info.filename, info.external_attr, new_data)
    207     elif info.filename.endswith("mac_permissions.xml"):
    208       print "rewriting %s with new keys." % (info.filename,)
    209       new_data = ReplaceCerts(data)
    210       output_tf_zip.writestr(out_info, new_data)
    211     elif info.filename in ("SYSTEM/recovery-from-boot.p",
    212                            "SYSTEM/bin/install-recovery.sh"):
    213       rebuild_recovery = True
    214     elif (OPTIONS.replace_ota_keys and
    215           info.filename in ("RECOVERY/RAMDISK/res/keys",
    216                             "SYSTEM/etc/security/otacerts.zip")):
    217       # don't copy these files if we're regenerating them below
    218       pass
    219     elif (OPTIONS.replace_verity_private_key and
    220           info.filename == "META/misc_info.txt"):
    221       pass
    222     elif (OPTIONS.replace_verity_public_key and
    223           info.filename == "BOOT/RAMDISK/verity_key"):
    224       pass
    225     else:
    226       # a non-APK file; copy it verbatim
    227       output_tf_zip.writestr(out_info, data)
    228 
    229   if OPTIONS.replace_ota_keys:
    230     new_recovery_keys = ReplaceOtaKeys(input_tf_zip, output_tf_zip, misc_info)
    231     if new_recovery_keys:
    232       write_to_temp("RECOVERY/RAMDISK/res/keys", 0755 << 16, new_recovery_keys)
    233 
    234   if rebuild_recovery:
    235     recovery_img = common.GetBootableImage(
    236         "recovery.img", "recovery.img", tmpdir, "RECOVERY", info_dict=misc_info)
    237     boot_img = common.GetBootableImage(
    238         "boot.img", "boot.img", tmpdir, "BOOT", info_dict=misc_info)
    239 
    240     def output_sink(fn, data):
    241       output_tf_zip.writestr("SYSTEM/"+fn, data)
    242 
    243     common.MakeRecoveryPatch(tmpdir, output_sink, recovery_img, boot_img,
    244                              info_dict=misc_info)
    245 
    246   shutil.rmtree(tmpdir)
    247 
    248 
    249 def ReplaceCerts(data):
    250   """Given a string of data, replace all occurences of a set
    251   of X509 certs with a newer set of X509 certs and return
    252   the updated data string."""
    253   for old, new in OPTIONS.key_map.iteritems():
    254     try:
    255       if OPTIONS.verbose:
    256         print "    Replacing %s.x509.pem with %s.x509.pem" % (old, new)
    257       f = open(old + ".x509.pem")
    258       old_cert16 = base64.b16encode(common.ParseCertificate(f.read())).lower()
    259       f.close()
    260       f = open(new + ".x509.pem")
    261       new_cert16 = base64.b16encode(common.ParseCertificate(f.read())).lower()
    262       f.close()
    263       # Only match entire certs.
    264       pattern = "\\b"+old_cert16+"\\b"
    265       (data, num) = re.subn(pattern, new_cert16, data, flags=re.IGNORECASE)
    266       if OPTIONS.verbose:
    267         print "    Replaced %d occurence(s) of %s.x509.pem with " \
    268             "%s.x509.pem" % (num, old, new)
    269     except IOError, e:
    270       if (e.errno == errno.ENOENT and not OPTIONS.verbose):
    271         continue
    272 
    273       print "    Error accessing %s. %s. Skip replacing %s.x509.pem " \
    274           "with %s.x509.pem." % (e.filename, e.strerror, old, new)
    275 
    276   return data
    277 
    278 
    279 def EditTags(tags):
    280   """Given a string containing comma-separated tags, apply the edits
    281   specified in OPTIONS.tag_changes and return the updated string."""
    282   tags = set(tags.split(","))
    283   for ch in OPTIONS.tag_changes:
    284     if ch[0] == "-":
    285       tags.discard(ch[1:])
    286     elif ch[0] == "+":
    287       tags.add(ch[1:])
    288   return ",".join(sorted(tags))
    289 
    290 
    291 def RewriteProps(data, misc_info):
    292   output = []
    293   for line in data.split("\n"):
    294     line = line.strip()
    295     original_line = line
    296     if line and line[0] != '#' and "=" in line:
    297       key, value = line.split("=", 1)
    298       if (key == "ro.build.fingerprint"
    299           and misc_info.get("oem_fingerprint_properties") is None):
    300         pieces = value.split("/")
    301         pieces[-1] = EditTags(pieces[-1])
    302         value = "/".join(pieces)
    303       elif (key == "ro.build.thumbprint"
    304           and misc_info.get("oem_fingerprint_properties") is not None):
    305         pieces = value.split("/")
    306         pieces[-1] = EditTags(pieces[-1])
    307         value = "/".join(pieces)
    308       elif key == "ro.build.description":
    309         pieces = value.split(" ")
    310         assert len(pieces) == 5
    311         pieces[-1] = EditTags(pieces[-1])
    312         value = " ".join(pieces)
    313       elif key == "ro.build.tags":
    314         value = EditTags(value)
    315       elif key == "ro.build.display.id":
    316         # change, eg, "JWR66N dev-keys" to "JWR66N"
    317         value = value.split()
    318         if len(value) > 1 and value[-1].endswith("-keys"):
    319           value.pop()
    320         value = " ".join(value)
    321       line = key + "=" + value
    322     if line != original_line:
    323       print "  replace: ", original_line
    324       print "     with: ", line
    325     output.append(line)
    326   return "\n".join(output) + "\n"
    327 
    328 
    329 def ReplaceOtaKeys(input_tf_zip, output_tf_zip, misc_info):
    330   try:
    331     keylist = input_tf_zip.read("META/otakeys.txt").split()
    332   except KeyError:
    333     raise common.ExternalError("can't read META/otakeys.txt from input")
    334 
    335   extra_recovery_keys = misc_info.get("extra_recovery_keys", None)
    336   if extra_recovery_keys:
    337     extra_recovery_keys = [OPTIONS.key_map.get(k, k) + ".x509.pem"
    338                            for k in extra_recovery_keys.split()]
    339     if extra_recovery_keys:
    340       print "extra recovery-only key(s): " + ", ".join(extra_recovery_keys)
    341   else:
    342     extra_recovery_keys = []
    343 
    344   mapped_keys = []
    345   for k in keylist:
    346     m = re.match(r"^(.*)\.x509\.pem$", k)
    347     if not m:
    348       raise common.ExternalError(
    349           "can't parse \"%s\" from META/otakeys.txt" % (k,))
    350     k = m.group(1)
    351     mapped_keys.append(OPTIONS.key_map.get(k, k) + ".x509.pem")
    352 
    353   if mapped_keys:
    354     print "using:\n   ", "\n   ".join(mapped_keys)
    355     print "for OTA package verification"
    356   else:
    357     devkey = misc_info.get("default_system_dev_certificate",
    358                            "build/target/product/security/testkey")
    359     mapped_keys.append(
    360         OPTIONS.key_map.get(devkey, devkey) + ".x509.pem")
    361     print "META/otakeys.txt has no keys; using", mapped_keys[0]
    362 
    363   # recovery uses a version of the key that has been slightly
    364   # predigested (by DumpPublicKey.java) and put in res/keys.
    365   # extra_recovery_keys are used only in recovery.
    366 
    367   p = common.Run(["java", "-jar",
    368                   os.path.join(OPTIONS.search_path, "framework", "dumpkey.jar")]
    369                  + mapped_keys + extra_recovery_keys,
    370                  stdout=subprocess.PIPE)
    371   new_recovery_keys, _ = p.communicate()
    372   if p.returncode != 0:
    373     raise common.ExternalError("failed to run dumpkeys")
    374   common.ZipWriteStr(output_tf_zip, "RECOVERY/RAMDISK/res/keys",
    375                      new_recovery_keys)
    376 
    377   # SystemUpdateActivity uses the x509.pem version of the keys, but
    378   # put into a zipfile system/etc/security/otacerts.zip.
    379   # We DO NOT include the extra_recovery_keys (if any) here.
    380 
    381   tempfile = cStringIO.StringIO()
    382   certs_zip = zipfile.ZipFile(tempfile, "w")
    383   for k in mapped_keys:
    384     certs_zip.write(k)
    385   certs_zip.close()
    386   common.ZipWriteStr(output_tf_zip, "SYSTEM/etc/security/otacerts.zip",
    387                      tempfile.getvalue())
    388 
    389   return new_recovery_keys
    390 
    391 def ReplaceVerityPublicKey(targetfile_zip, key_path):
    392   print "Replacing verity public key with %s" % key_path
    393   with open(key_path) as f:
    394     common.ZipWriteStr(targetfile_zip, "BOOT/RAMDISK/verity_key", f.read())
    395 
    396 def ReplaceVerityPrivateKey(targetfile_input_zip, targetfile_output_zip, misc_info, key_path):
    397   print "Replacing verity private key with %s" % key_path
    398   current_key = misc_info["verity_key"]
    399   original_misc_info = targetfile_input_zip.read("META/misc_info.txt")
    400   new_misc_info = original_misc_info.replace(current_key, key_path)
    401   common.ZipWriteStr(targetfile_output_zip, "META/misc_info.txt", new_misc_info)
    402 
    403 def BuildKeyMap(misc_info, key_mapping_options):
    404   for s, d in key_mapping_options:
    405     if s is None:   # -d option
    406       devkey = misc_info.get("default_system_dev_certificate",
    407                              "build/target/product/security/testkey")
    408       devkeydir = os.path.dirname(devkey)
    409 
    410       OPTIONS.key_map.update({
    411           devkeydir + "/testkey":  d + "/releasekey",
    412           devkeydir + "/devkey":   d + "/releasekey",
    413           devkeydir + "/media":    d + "/media",
    414           devkeydir + "/shared":   d + "/shared",
    415           devkeydir + "/platform": d + "/platform",
    416           })
    417     else:
    418       OPTIONS.key_map[s] = d
    419 
    420 
    421 def main(argv):
    422 
    423   key_mapping_options = []
    424 
    425   def option_handler(o, a):
    426     if o in ("-e", "--extra_apks"):
    427       names, key = a.split("=")
    428       names = names.split(",")
    429       for n in names:
    430         OPTIONS.extra_apks[n] = key
    431     elif o in ("-d", "--default_key_mappings"):
    432       key_mapping_options.append((None, a))
    433     elif o in ("-k", "--key_mapping"):
    434       key_mapping_options.append(a.split("=", 1))
    435     elif o in ("-o", "--replace_ota_keys"):
    436       OPTIONS.replace_ota_keys = True
    437     elif o in ("-t", "--tag_changes"):
    438       new = []
    439       for i in a.split(","):
    440         i = i.strip()
    441         if not i or i[0] not in "-+":
    442           raise ValueError("Bad tag change '%s'" % (i,))
    443         new.append(i[0] + i[1:].strip())
    444       OPTIONS.tag_changes = tuple(new)
    445     elif o == "--replace_verity_public_key":
    446       OPTIONS.replace_verity_public_key = (True, a)
    447     elif o == "--replace_verity_private_key":
    448       OPTIONS.replace_verity_private_key = (True, a)
    449     else:
    450       return False
    451     return True
    452 
    453   args = common.ParseOptions(argv, __doc__,
    454                              extra_opts="e:d:k:ot:",
    455                              extra_long_opts=["extra_apks=",
    456                                               "default_key_mappings=",
    457                                               "key_mapping=",
    458                                               "replace_ota_keys",
    459                                               "tag_changes=",
    460                                               "replace_verity_public_key=",
    461                                               "replace_verity_private_key="],
    462                              extra_option_handler=option_handler)
    463 
    464   if len(args) != 2:
    465     common.Usage(__doc__)
    466     sys.exit(1)
    467 
    468   input_zip = zipfile.ZipFile(args[0], "r")
    469   output_zip = zipfile.ZipFile(args[1], "w")
    470 
    471   misc_info = common.LoadInfoDict(input_zip)
    472 
    473   BuildKeyMap(misc_info, key_mapping_options)
    474 
    475   apk_key_map = GetApkCerts(input_zip)
    476   CheckAllApksSigned(input_zip, apk_key_map)
    477 
    478   key_passwords = common.GetKeyPasswords(set(apk_key_map.values()))
    479   ProcessTargetFiles(input_zip, output_zip, misc_info,
    480                      apk_key_map, key_passwords)
    481 
    482   input_zip.close()
    483   output_zip.close()
    484 
    485   add_img_to_target_files.AddImagesToTargetFiles(args[1])
    486 
    487   print "done."
    488 
    489 
    490 if __name__ == '__main__':
    491   try:
    492     main(sys.argv[1:])
    493   except common.ExternalError, e:
    494     print
    495     print "   ERROR: %s" % (e,)
    496     print
    497     sys.exit(1)
    498