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