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