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