sign_target_files_apks revision 8ce7c25e905bc14382359e1cd45d41832bcc7ffa
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  -s  (--signapk_jar)  <path>
24      Path of the signapks.jar file used to sign an individual APK
25      file.
26
27  -e  (--extra_apks)  <name,name,...=key>
28      Add extra APK name/key pairs as though they appeared in
29      apkcerts.txt (so mappings specified by -k and -d are applied).
30      Keys specified in -e override any value for that app contained
31      in the apkcerts.txt file.  Option may be repeated to give
32      multiple extra packages.
33
34  -k  (--key_mapping)  <src_key=dest_key>
35      Add a mapping from the key name as specified in apkcerts.txt (the
36      src_key) to the real key you wish to sign the package with
37      (dest_key).  Option may be repeated to give multiple key
38      mappings.
39
40  -d  (--default_key_mappings)  <dir>
41      Set up the following key mappings:
42
43        build/target/product/security/testkey   ==>  $dir/releasekey
44        build/target/product/security/media     ==>  $dir/media
45        build/target/product/security/shared    ==>  $dir/shared
46        build/target/product/security/platform  ==>  $dir/platform
47
48      -d and -k options are added to the set of mappings in the order
49      in which they appear on the command line.
50
51  -o  (--replace_ota_keys)
52      Replace the certificate (public key) used by OTA package
53      verification with the one specified in the input target_files
54      zip (in the META/otakeys.txt file).  Key remapping (-k and -d)
55      is performed on this key.
56
57  -t  (--tag_changes)  <+tag>,<-tag>,...
58      Comma-separated list of changes to make to the set of tags (in
59      the last component of the build fingerprint).  Prefix each with
60      '+' or '-' to indicate whether that tag should be added or
61      removed.  Changes are processed in the order they appear.
62      Default value is "-test-keys,+ota-rel-keys,+release-keys".
63
64"""
65
66import sys
67
68if sys.hexversion < 0x02040000:
69  print >> sys.stderr, "Python 2.4 or newer is required."
70  sys.exit(1)
71
72import cStringIO
73import copy
74import os
75import re
76import subprocess
77import tempfile
78import zipfile
79
80import common
81
82OPTIONS = common.OPTIONS
83
84OPTIONS.extra_apks = {}
85OPTIONS.key_map = {}
86OPTIONS.replace_ota_keys = False
87OPTIONS.tag_changes = ("-test-keys", "+ota-rel-keys", "+release-keys")
88
89def GetApkCerts(tf_zip):
90  certmap = {}
91  for line in tf_zip.read("META/apkcerts.txt").split("\n"):
92    line = line.strip()
93    if not line: continue
94    m = re.match(r'^name="(.*)"\s+certificate="(.*)\.x509\.pem"\s+'
95                 r'private_key="\2\.pk8"$', line)
96    if not m:
97      raise SigningError("failed to parse line from apkcerts.txt:\n" + line)
98    certmap[m.group(1)] = OPTIONS.key_map.get(m.group(2), m.group(2))
99  for apk, cert in OPTIONS.extra_apks.iteritems():
100    certmap[apk] = OPTIONS.key_map.get(cert, cert)
101  return certmap
102
103
104def CheckAllApksSigned(input_tf_zip, apk_key_map):
105  """Check that all the APKs we want to sign have keys specified, and
106  error out if they don't."""
107  unknown_apks = []
108  for info in input_tf_zip.infolist():
109    if info.filename.endswith(".apk"):
110      name = os.path.basename(info.filename)
111      if name not in apk_key_map:
112        unknown_apks.append(name)
113  if unknown_apks:
114    print "ERROR: no key specified for:\n\n ",
115    print "\n  ".join(unknown_apks)
116    print "\nUse '-e <apkname>=' to specify a key (which may be an"
117    print "empty string to not sign this apk)."
118    sys.exit(1)
119
120
121def SharedUserForApk(data):
122  tmp = tempfile.NamedTemporaryFile()
123  tmp.write(data)
124  tmp.flush()
125
126  p = common.Run(["aapt", "dump", "xmltree", tmp.name, "AndroidManifest.xml"],
127                 stdout=subprocess.PIPE)
128  data, _ = p.communicate()
129  if p.returncode != 0:
130    raise ExternalError("failed to run aapt dump")
131  lines = data.split("\n")
132  for i in lines:
133    m = re.match(r'^\s*A: android:sharedUserId\([0-9a-fx]*\)="([^"]*)" .*$', i)
134    if m:
135      return m.group(1)
136  return None
137
138
139def CheckSharedUserIdsConsistent(input_tf_zip, apk_key_map):
140  """Check that all packages that request the same shared user id are
141  going to be signed with the same key."""
142
143  shared_user_apks = {}
144  maxlen = len("(unknown key)")
145
146  for info in input_tf_zip.infolist():
147    if info.filename.endswith(".apk"):
148      data = input_tf_zip.read(info.filename)
149
150      name = os.path.basename(info.filename)
151      shared_user = SharedUserForApk(data)
152      key = apk_key_map[name]
153      maxlen = max(maxlen, len(key))
154
155      if shared_user is not None:
156        shared_user_apks.setdefault(
157            shared_user, {}).setdefault(key, []).append(name)
158
159  errors = []
160  for k, v in shared_user_apks.iteritems():
161    # each shared user should have exactly one key used for all the
162    # apks that want that user.
163    if len(v) > 1:
164      errors.append((k, v))
165
166  if not errors: return
167
168  print "ERROR:  shared user inconsistency.  All apks wanting to use"
169  print "        a given shared user must be signed with the same key."
170  print
171  errors.sort()
172  for user, keys in errors:
173    print 'shared user id "%s":' % (user,)
174    for key, apps in keys.iteritems():
175      print '  %-*s   %s' % (maxlen, key or "(unknown key)", apps[0])
176      for a in apps[1:]:
177        print (' ' * (maxlen+5)) + a
178    print
179
180  sys.exit(1)
181
182
183def SignApk(data, keyname, pw):
184  unsigned = tempfile.NamedTemporaryFile()
185  unsigned.write(data)
186  unsigned.flush()
187
188  signed = tempfile.NamedTemporaryFile()
189
190  common.SignFile(unsigned.name, signed.name, keyname, pw, align=4)
191
192  data = signed.read()
193  unsigned.close()
194  signed.close()
195
196  return data
197
198
199def SignApks(input_tf_zip, output_tf_zip, apk_key_map, key_passwords):
200  maxsize = max([len(os.path.basename(i.filename))
201                 for i in input_tf_zip.infolist()
202                 if i.filename.endswith('.apk')])
203
204  for info in input_tf_zip.infolist():
205    data = input_tf_zip.read(info.filename)
206    out_info = copy.copy(info)
207    if info.filename.endswith(".apk"):
208      name = os.path.basename(info.filename)
209      key = apk_key_map[name]
210      if key:
211        print "    signing: %-*s (%s)" % (maxsize, name, key)
212        signed_data = SignApk(data, key, key_passwords[key])
213        output_tf_zip.writestr(out_info, signed_data)
214      else:
215        # an APK we're not supposed to sign.
216        print "NOT signing: %s" % (name,)
217        output_tf_zip.writestr(out_info, data)
218    elif info.filename in ("SYSTEM/build.prop",
219                           "RECOVERY/RAMDISK/default.prop"):
220      print "rewriting %s:" % (info.filename,)
221      new_data = RewriteProps(data)
222      output_tf_zip.writestr(out_info, new_data)
223    else:
224      # a non-APK file; copy it verbatim
225      output_tf_zip.writestr(out_info, data)
226
227
228def RewriteProps(data):
229  output = []
230  for line in data.split("\n"):
231    line = line.strip()
232    original_line = line
233    if line and line[0] != '#':
234      key, value = line.split("=", 1)
235      if key == "ro.build.fingerprint":
236        pieces = line.split("/")
237        tags = set(pieces[-1].split(","))
238        for ch in OPTIONS.tag_changes:
239          if ch[0] == "-":
240            tags.discard(ch[1:])
241          elif ch[0] == "+":
242            tags.add(ch[1:])
243        line = "/".join(pieces[:-1] + [",".join(sorted(tags))])
244      elif key == "ro.build.description":
245        pieces = line.split(" ")
246        assert len(pieces) == 5
247        tags = set(pieces[-1].split(","))
248        for ch in OPTIONS.tag_changes:
249          if ch[0] == "-":
250            tags.discard(ch[1:])
251          elif ch[0] == "+":
252            tags.add(ch[1:])
253        line = " ".join(pieces[:-1] + [",".join(sorted(tags))])
254    if line != original_line:
255      print "  replace: ", original_line
256      print "     with: ", line
257    output.append(line)
258  return "\n".join(output) + "\n"
259
260
261def ReplaceOtaKeys(input_tf_zip, output_tf_zip):
262  try:
263    keylist = input_tf_zip.read("META/otakeys.txt").split()
264  except KeyError:
265    raise ExternalError("can't read META/otakeys.txt from input")
266
267  mapped_keys = []
268  for k in keylist:
269    m = re.match(r"^(.*)\.x509\.pem$", k)
270    if not m:
271      raise ExternalError("can't parse \"%s\" from META/otakeys.txt" % (k,))
272    k = m.group(1)
273    mapped_keys.append(OPTIONS.key_map.get(k, k) + ".x509.pem")
274
275  print "using:\n   ", "\n   ".join(mapped_keys)
276  print "for OTA package verification"
277
278  # recovery uses a version of the key that has been slightly
279  # predigested (by DumpPublicKey.java) and put in res/keys.
280
281  p = common.Run(["java", "-jar", OPTIONS.dumpkey_jar] + mapped_keys,
282                 stdout=subprocess.PIPE)
283  data, _ = p.communicate()
284  if p.returncode != 0:
285    raise ExternalError("failed to run dumpkeys")
286  output_tf_zip.writestr("RECOVERY/RAMDISK/res/keys", data)
287
288  # SystemUpdateActivity uses the x509.pem version of the keys, but
289  # put into a zipfile system/etc/security/otacerts.zip.
290
291  tempfile = cStringIO.StringIO()
292  certs_zip = zipfile.ZipFile(tempfile, "w")
293  for k in mapped_keys:
294    certs_zip.write(k)
295  certs_zip.close()
296  output_tf_zip.writestr("SYSTEM/etc/security/otacerts.zip",
297                         tempfile.getvalue())
298
299
300def main(argv):
301
302  def option_handler(o, a):
303    if o in ("-s", "--signapk_jar"):
304      OPTIONS.signapk_jar = a
305    elif o in ("-e", "--extra_apks"):
306      names, key = a.split("=")
307      names = names.split(",")
308      for n in names:
309        OPTIONS.extra_apks[n] = key
310    elif o in ("-d", "--default_key_mappings"):
311      OPTIONS.key_map.update({
312          "build/target/product/security/testkey": "%s/releasekey" % (a,),
313          "build/target/product/security/media": "%s/media" % (a,),
314          "build/target/product/security/shared": "%s/shared" % (a,),
315          "build/target/product/security/platform": "%s/platform" % (a,),
316          })
317    elif o in ("-k", "--key_mapping"):
318      s, d = a.split("=")
319      OPTIONS.key_map[s] = d
320    elif o in ("-o", "--replace_ota_keys"):
321      OPTIONS.replace_ota_keys = True
322    elif o in ("-t", "--tag_changes"):
323      new = []
324      for i in a.split(","):
325        i = i.strip()
326        if not i or i[0] not in "-+":
327          raise ValueError("Bad tag change '%s'" % (i,))
328        new.append(i[0] + i[1:].strip())
329      OPTIONS.tag_changes = tuple(new)
330    else:
331      return False
332    return True
333
334  args = common.ParseOptions(argv, __doc__,
335                             extra_opts="s:e:d:k:ot:",
336                             extra_long_opts=["signapk_jar=",
337                                              "extra_apks=",
338                                              "default_key_mappings=",
339                                              "key_mapping=",
340                                              "replace_ota_keys",
341                                              "tag_changes="],
342                             extra_option_handler=option_handler)
343
344  if len(args) != 2:
345    common.Usage(__doc__)
346    sys.exit(1)
347
348  input_zip = zipfile.ZipFile(args[0], "r")
349  output_zip = zipfile.ZipFile(args[1], "w")
350
351  apk_key_map = GetApkCerts(input_zip)
352  CheckAllApksSigned(input_zip, apk_key_map)
353  CheckSharedUserIdsConsistent(input_zip, apk_key_map)
354
355  key_passwords = common.GetKeyPasswords(set(apk_key_map.values()))
356  SignApks(input_zip, output_zip, apk_key_map, key_passwords)
357
358  if OPTIONS.replace_ota_keys:
359    ReplaceOtaKeys(input_zip, output_zip)
360
361  input_zip.close()
362  output_zip.close()
363
364  print "done."
365
366
367if __name__ == '__main__':
368  try:
369    main(sys.argv[1:])
370  except common.ExternalError, e:
371    print
372    print "   ERROR: %s" % (e,)
373    print
374    sys.exit(1)
375