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