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