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