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