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                            "VENDOR/build.prop",
    202                            "RECOVERY/RAMDISK/default.prop"):
    203       print "rewriting %s:" % (info.filename,)
    204       new_data = RewriteProps(data, misc_info)
    205       output_tf_zip.writestr(out_info, new_data)
    206       if info.filename == "RECOVERY/RAMDISK/default.prop":
    207         write_to_temp(info.filename, info.external_attr, new_data)
    208     elif info.filename.endswith("mac_permissions.xml"):
    209       print "rewriting %s with new keys." % (info.filename,)
    210       new_data = ReplaceCerts(data)
    211       output_tf_zip.writestr(out_info, new_data)
    212     elif info.filename in ("SYSTEM/recovery-from-boot.p",
    213                            "SYSTEM/bin/install-recovery.sh"):
    214       rebuild_recovery = True
    215     elif (OPTIONS.replace_ota_keys and
    216           info.filename in ("RECOVERY/RAMDISK/res/keys",
    217                             "SYSTEM/etc/security/otacerts.zip")):
    218       # don't copy these files if we're regenerating them below
    219       pass
    220     elif (OPTIONS.replace_verity_private_key and
    221           info.filename == "META/misc_info.txt"):
    222       pass
    223     elif (OPTIONS.replace_verity_public_key and
    224           info.filename == "BOOT/RAMDISK/verity_key"):
    225       pass
    226     else:
    227       # a non-APK file; copy it verbatim
    228       output_tf_zip.writestr(out_info, data)
    229 
    230   if OPTIONS.replace_ota_keys:
    231     new_recovery_keys = ReplaceOtaKeys(input_tf_zip, output_tf_zip, misc_info)
    232     if new_recovery_keys:
    233       write_to_temp("RECOVERY/RAMDISK/res/keys", 0755 << 16, new_recovery_keys)
    234 
    235   if rebuild_recovery:
    236     recovery_img = common.GetBootableImage(
    237         "recovery.img", "recovery.img", tmpdir, "RECOVERY", info_dict=misc_info)
    238     boot_img = common.GetBootableImage(
    239         "boot.img", "boot.img", tmpdir, "BOOT", info_dict=misc_info)
    240 
    241     def output_sink(fn, data):
    242       output_tf_zip.writestr("SYSTEM/"+fn, data)
    243 
    244     common.MakeRecoveryPatch(tmpdir, output_sink, recovery_img, boot_img,
    245                              info_dict=misc_info)
    246 
    247   shutil.rmtree(tmpdir)
    248 
    249 
    250 def ReplaceCerts(data):
    251   """Given a string of data, replace all occurences of a set
    252   of X509 certs with a newer set of X509 certs and return
    253   the updated data string."""
    254   for old, new in OPTIONS.key_map.iteritems():
    255     try:
    256       if OPTIONS.verbose:
    257         print "    Replacing %s.x509.pem with %s.x509.pem" % (old, new)
    258       f = open(old + ".x509.pem")
    259       old_cert16 = base64.b16encode(common.ParseCertificate(f.read())).lower()
    260       f.close()
    261       f = open(new + ".x509.pem")
    262       new_cert16 = base64.b16encode(common.ParseCertificate(f.read())).lower()
    263       f.close()
    264       # Only match entire certs.
    265       pattern = "\\b"+old_cert16+"\\b"
    266       (data, num) = re.subn(pattern, new_cert16, data, flags=re.IGNORECASE)
    267       if OPTIONS.verbose:
    268         print "    Replaced %d occurence(s) of %s.x509.pem with " \
    269             "%s.x509.pem" % (num, old, new)
    270     except IOError, e:
    271       if (e.errno == errno.ENOENT and not OPTIONS.verbose):
    272         continue
    273 
    274       print "    Error accessing %s. %s. Skip replacing %s.x509.pem " \
    275           "with %s.x509.pem." % (e.filename, e.strerror, old, new)
    276 
    277   return data
    278 
    279 
    280 def EditTags(tags):
    281   """Given a string containing comma-separated tags, apply the edits
    282   specified in OPTIONS.tag_changes and return the updated string."""
    283   tags = set(tags.split(","))
    284   for ch in OPTIONS.tag_changes:
    285     if ch[0] == "-":
    286       tags.discard(ch[1:])
    287     elif ch[0] == "+":
    288       tags.add(ch[1:])
    289   return ",".join(sorted(tags))
    290 
    291 
    292 def RewriteProps(data, misc_info):
    293   output = []
    294   for line in data.split("\n"):
    295     line = line.strip()
    296     original_line = line
    297     if line and line[0] != '#' and "=" in line:
    298       key, value = line.split("=", 1)
    299       if (key in ("ro.build.fingerprint", "ro.vendor.build.fingerprint")
    300           and misc_info.get("oem_fingerprint_properties") is None):
    301         pieces = value.split("/")
    302         pieces[-1] = EditTags(pieces[-1])
    303         value = "/".join(pieces)
    304       elif (key in ("ro.build.thumbprint", "ro.vendor.build.thumbprint")
    305           and misc_info.get("oem_fingerprint_properties") is not None):
    306         pieces = value.split("/")
    307         pieces[-1] = EditTags(pieces[-1])
    308         value = "/".join(pieces)
    309       elif key == "ro.build.description":
    310         pieces = value.split(" ")
    311         assert len(pieces) == 5
    312         pieces[-1] = EditTags(pieces[-1])
    313         value = " ".join(pieces)
    314       elif key == "ro.build.tags":
    315         value = EditTags(value)
    316       elif key == "ro.build.display.id":
    317         # change, eg, "JWR66N dev-keys" to "JWR66N"
    318         value = value.split()
    319         if len(value) > 1 and value[-1].endswith("-keys"):
    320           value.pop()
    321         value = " ".join(value)
    322       line = key + "=" + value
    323     if line != original_line:
    324       print "  replace: ", original_line
    325       print "     with: ", line
    326     output.append(line)
    327   return "\n".join(output) + "\n"
    328 
    329 
    330 def ReplaceOtaKeys(input_tf_zip, output_tf_zip, misc_info):
    331   try:
    332     keylist = input_tf_zip.read("META/otakeys.txt").split()
    333   except KeyError:
    334     raise common.ExternalError("can't read META/otakeys.txt from input")
    335 
    336   extra_recovery_keys = misc_info.get("extra_recovery_keys", None)
    337   if extra_recovery_keys:
    338     extra_recovery_keys = [OPTIONS.key_map.get(k, k) + ".x509.pem"
    339                            for k in extra_recovery_keys.split()]
    340     if extra_recovery_keys:
    341       print "extra recovery-only key(s): " + ", ".join(extra_recovery_keys)
    342   else:
    343     extra_recovery_keys = []
    344 
    345   mapped_keys = []
    346   for k in keylist:
    347     m = re.match(r"^(.*)\.x509\.pem$", k)
    348     if not m:
    349       raise common.ExternalError(
    350           "can't parse \"%s\" from META/otakeys.txt" % (k,))
    351     k = m.group(1)
    352     mapped_keys.append(OPTIONS.key_map.get(k, k) + ".x509.pem")
    353 
    354   if mapped_keys:
    355     print "using:\n   ", "\n   ".join(mapped_keys)
    356     print "for OTA package verification"
    357   else:
    358     devkey = misc_info.get("default_system_dev_certificate",
    359                            "build/target/product/security/testkey")
    360     mapped_keys.append(
    361         OPTIONS.key_map.get(devkey, devkey) + ".x509.pem")
    362     print "META/otakeys.txt has no keys; using", mapped_keys[0]
    363 
    364   # recovery uses a version of the key that has been slightly
    365   # predigested (by DumpPublicKey.java) and put in res/keys.
    366   # extra_recovery_keys are used only in recovery.
    367 
    368   p = common.Run(["java", "-jar",
    369                   os.path.join(OPTIONS.search_path, "framework", "dumpkey.jar")]
    370                  + mapped_keys + extra_recovery_keys,
    371                  stdout=subprocess.PIPE)
    372   new_recovery_keys, _ = p.communicate()
    373   if p.returncode != 0:
    374     raise common.ExternalError("failed to run dumpkeys")
    375   common.ZipWriteStr(output_tf_zip, "RECOVERY/RAMDISK/res/keys",
    376                      new_recovery_keys)
    377 
    378   # SystemUpdateActivity uses the x509.pem version of the keys, but
    379   # put into a zipfile system/etc/security/otacerts.zip.
    380   # We DO NOT include the extra_recovery_keys (if any) here.
    381 
    382   tempfile = cStringIO.StringIO()
    383   certs_zip = zipfile.ZipFile(tempfile, "w")
    384   for k in mapped_keys:
    385     certs_zip.write(k)
    386   certs_zip.close()
    387   common.ZipWriteStr(output_tf_zip, "SYSTEM/etc/security/otacerts.zip",
    388                      tempfile.getvalue())
    389 
    390   return new_recovery_keys
    391 
    392 def ReplaceVerityPublicKey(targetfile_zip, key_path):
    393   print "Replacing verity public key with %s" % key_path
    394   with open(key_path) as f:
    395     common.ZipWriteStr(targetfile_zip, "BOOT/RAMDISK/verity_key", f.read())
    396 
    397 def ReplaceVerityPrivateKey(targetfile_input_zip, targetfile_output_zip, misc_info, key_path):
    398   print "Replacing verity private key with %s" % key_path
    399   current_key = misc_info["verity_key"]
    400   original_misc_info = targetfile_input_zip.read("META/misc_info.txt")
    401   new_misc_info = original_misc_info.replace(current_key, key_path)
    402   common.ZipWriteStr(targetfile_output_zip, "META/misc_info.txt", new_misc_info)
    403 
    404 def BuildKeyMap(misc_info, key_mapping_options):
    405   for s, d in key_mapping_options:
    406     if s is None:   # -d option
    407       devkey = misc_info.get("default_system_dev_certificate",
    408                              "build/target/product/security/testkey")
    409       devkeydir = os.path.dirname(devkey)
    410 
    411       OPTIONS.key_map.update({
    412           devkeydir + "/testkey":  d + "/releasekey",
    413           devkeydir + "/devkey":   d + "/releasekey",
    414           devkeydir + "/media":    d + "/media",
    415           devkeydir + "/shared":   d + "/shared",
    416           devkeydir + "/platform": d + "/platform",
    417           })
    418     else:
    419       OPTIONS.key_map[s] = d
    420 
    421 
    422 def main(argv):
    423 
    424   key_mapping_options = []
    425 
    426   def option_handler(o, a):
    427     if o in ("-e", "--extra_apks"):
    428       names, key = a.split("=")
    429       names = names.split(",")
    430       for n in names:
    431         OPTIONS.extra_apks[n] = key
    432     elif o in ("-d", "--default_key_mappings"):
    433       key_mapping_options.append((None, a))
    434     elif o in ("-k", "--key_mapping"):
    435       key_mapping_options.append(a.split("=", 1))
    436     elif o in ("-o", "--replace_ota_keys"):
    437       OPTIONS.replace_ota_keys = True
    438     elif o in ("-t", "--tag_changes"):
    439       new = []
    440       for i in a.split(","):
    441         i = i.strip()
    442         if not i or i[0] not in "-+":
    443           raise ValueError("Bad tag change '%s'" % (i,))
    444         new.append(i[0] + i[1:].strip())
    445       OPTIONS.tag_changes = tuple(new)
    446     elif o == "--replace_verity_public_key":
    447       OPTIONS.replace_verity_public_key = (True, a)
    448     elif o == "--replace_verity_private_key":
    449       OPTIONS.replace_verity_private_key = (True, a)
    450     else:
    451       return False
    452     return True
    453 
    454   args = common.ParseOptions(argv, __doc__,
    455                              extra_opts="e:d:k:ot:",
    456                              extra_long_opts=["extra_apks=",
    457                                               "default_key_mappings=",
    458                                               "key_mapping=",
    459                                               "replace_ota_keys",
    460                                               "tag_changes=",
    461                                               "replace_verity_public_key=",
    462                                               "replace_verity_private_key="],
    463                              extra_option_handler=option_handler)
    464 
    465   if len(args) != 2:
    466     common.Usage(__doc__)
    467     sys.exit(1)
    468 
    469   input_zip = zipfile.ZipFile(args[0], "r")
    470   output_zip = zipfile.ZipFile(args[1], "w")
    471 
    472   misc_info = common.LoadInfoDict(input_zip)
    473 
    474   BuildKeyMap(misc_info, key_mapping_options)
    475 
    476   apk_key_map = GetApkCerts(input_zip)
    477   CheckAllApksSigned(input_zip, apk_key_map)
    478 
    479   key_passwords = common.GetKeyPasswords(set(apk_key_map.values()))
    480   ProcessTargetFiles(input_zip, output_zip, misc_info,
    481                      apk_key_map, key_passwords)
    482 
    483   input_zip.close()
    484   output_zip.close()
    485 
    486   add_img_to_target_files.AddImagesToTargetFiles(args[1])
    487 
    488   print "done."
    489 
    490 
    491 if __name__ == '__main__':
    492   try:
    493     main(sys.argv[1:])
    494   except common.ExternalError, e:
    495     print
    496     print "   ERROR: %s" % (e,)
    497     print
    498     sys.exit(1)
    499