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