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