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