sign_target_files_apks revision 817c574d753191c52acd5787da02bb853d4ac090
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      line = key + "=" + value
236    if line != original_line:
237      print "  replace: ", original_line
238      print "     with: ", line
239    output.append(line)
240  return "\n".join(output) + "\n"
241
242
243def ReplaceOtaKeys(input_tf_zip, output_tf_zip, misc_info):
244  try:
245    keylist = input_tf_zip.read("META/otakeys.txt").split()
246  except KeyError:
247    raise common.ExternalError("can't read META/otakeys.txt from input")
248
249  extra_recovery_keys = misc_info.get("extra_recovery_keys", None)
250  if extra_recovery_keys:
251    extra_recovery_keys = [OPTIONS.key_map.get(k, k) + ".x509.pem"
252                           for k in extra_recovery_keys.split()]
253    if extra_recovery_keys:
254      print "extra recovery-only key(s): " + ", ".join(extra_recovery_keys)
255  else:
256    extra_recovery_keys = []
257
258  mapped_keys = []
259  for k in keylist:
260    m = re.match(r"^(.*)\.x509\.pem$", k)
261    if not m:
262      raise common.ExternalError("can't parse \"%s\" from META/otakeys.txt" % (k,))
263    k = m.group(1)
264    mapped_keys.append(OPTIONS.key_map.get(k, k) + ".x509.pem")
265
266  if mapped_keys:
267    print "using:\n   ", "\n   ".join(mapped_keys)
268    print "for OTA package verification"
269  else:
270    devkey = misc_info.get("default_system_dev_certificate",
271                           "build/target/product/security/testkey")
272    mapped_keys.append(
273        OPTIONS.key_map.get(devkey, devkey) + ".x509.pem")
274    print "META/otakeys.txt has no keys; using", mapped_keys[0]
275
276  # recovery uses a version of the key that has been slightly
277  # predigested (by DumpPublicKey.java) and put in res/keys.
278  # extra_recovery_keys are used only in recovery.
279
280  p = common.Run(["java", "-jar",
281                  os.path.join(OPTIONS.search_path, "framework", "dumpkey.jar")]
282                 + mapped_keys + extra_recovery_keys,
283                 stdout=subprocess.PIPE)
284  data, _ = p.communicate()
285  if p.returncode != 0:
286    raise common.ExternalError("failed to run dumpkeys")
287  common.ZipWriteStr(output_tf_zip, "RECOVERY/RAMDISK/res/keys", data)
288
289  # SystemUpdateActivity uses the x509.pem version of the keys, but
290  # put into a zipfile system/etc/security/otacerts.zip.
291  # We DO NOT include the extra_recovery_keys (if any) here.
292
293  tempfile = cStringIO.StringIO()
294  certs_zip = zipfile.ZipFile(tempfile, "w")
295  for k in mapped_keys:
296    certs_zip.write(k)
297  certs_zip.close()
298  common.ZipWriteStr(output_tf_zip, "SYSTEM/etc/security/otacerts.zip",
299                     tempfile.getvalue())
300
301
302def BuildKeyMap(misc_info, key_mapping_options):
303  for s, d in key_mapping_options:
304    if s is None:   # -d option
305      devkey = misc_info.get("default_system_dev_certificate",
306                             "build/target/product/security/testkey")
307      devkeydir = os.path.dirname(devkey)
308
309      OPTIONS.key_map.update({
310          devkeydir + "/testkey":  d + "/releasekey",
311          devkeydir + "/devkey":   d + "/releasekey",
312          devkeydir + "/media":    d + "/media",
313          devkeydir + "/shared":   d + "/shared",
314          devkeydir + "/platform": d + "/platform",
315          })
316    else:
317      OPTIONS.key_map[s] = d
318
319
320def main(argv):
321
322  key_mapping_options = []
323
324  def option_handler(o, a):
325    if o in ("-e", "--extra_apks"):
326      names, key = a.split("=")
327      names = names.split(",")
328      for n in names:
329        OPTIONS.extra_apks[n] = key
330    elif o in ("-d", "--default_key_mappings"):
331      key_mapping_options.append((None, a))
332    elif o in ("-k", "--key_mapping"):
333      key_mapping_options.append(a.split("=", 1))
334    elif o in ("-o", "--replace_ota_keys"):
335      OPTIONS.replace_ota_keys = True
336    elif o in ("-t", "--tag_changes"):
337      new = []
338      for i in a.split(","):
339        i = i.strip()
340        if not i or i[0] not in "-+":
341          raise ValueError("Bad tag change '%s'" % (i,))
342        new.append(i[0] + i[1:].strip())
343      OPTIONS.tag_changes = tuple(new)
344    else:
345      return False
346    return True
347
348  args = common.ParseOptions(argv, __doc__,
349                             extra_opts="e:d:k:ot:",
350                             extra_long_opts=["extra_apks=",
351                                              "default_key_mappings=",
352                                              "key_mapping=",
353                                              "replace_ota_keys",
354                                              "tag_changes="],
355                             extra_option_handler=option_handler)
356
357  if len(args) != 2:
358    common.Usage(__doc__)
359    sys.exit(1)
360
361  input_zip = zipfile.ZipFile(args[0], "r")
362  output_zip = zipfile.ZipFile(args[1], "w")
363
364  misc_info = common.LoadInfoDict(input_zip)
365
366  BuildKeyMap(misc_info, key_mapping_options)
367
368  apk_key_map = GetApkCerts(input_zip)
369  CheckAllApksSigned(input_zip, apk_key_map)
370
371  key_passwords = common.GetKeyPasswords(set(apk_key_map.values()))
372  SignApks(input_zip, output_zip, apk_key_map, key_passwords)
373
374  if OPTIONS.replace_ota_keys:
375    ReplaceOtaKeys(input_zip, output_zip, misc_info)
376
377  input_zip.close()
378  output_zip.close()
379
380  print "done."
381
382
383if __name__ == '__main__':
384  try:
385    main(sys.argv[1:])
386  except common.ExternalError, e:
387    print
388    print "   ERROR: %s" % (e,)
389    print
390    sys.exit(1)
391