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