sign_target_files_apks.py revision 7ee3a9678e7191c48f0ba4e04792fe97925c1aa1
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, platform_api_level, codename_to_api_level_map):
131  unsigned = tempfile.NamedTemporaryFile()
132  unsigned.write(data)
133  unsigned.flush()
134
135  signed = tempfile.NamedTemporaryFile()
136
137  # For pre-N builds, don't upgrade to SHA-256 JAR signatures based on the APK's
138  # minSdkVersion to avoid increasing incremental OTA update sizes. If an APK
139  # didn't change, we don't want its signature to change due to the switch
140  # from SHA-1 to SHA-256.
141  # By default, APK signer chooses SHA-256 signatures if the APK's minSdkVersion
142  # is 18 or higher. For pre-N builds we disable this mechanism by pretending
143  # that the APK's minSdkVersion is 1.
144  # For N+ builds, we let APK signer rely on the APK's minSdkVersion to
145  # determine whether to use SHA-256.
146  min_api_level = None
147  if platform_api_level > 23:
148    # Let APK signer choose whether to use SHA-1 or SHA-256, based on the APK's
149    # minSdkVersion attribute
150    min_api_level = None
151  else:
152    # Force APK signer to use SHA-1
153    min_api_level = 1
154
155  common.SignFile(unsigned.name, signed.name, keyname, pw,
156      min_api_level=min_api_level,
157      codename_to_api_level_map=codename_to_api_level_map)
158
159  data = signed.read()
160  unsigned.close()
161  signed.close()
162
163  return data
164
165
166def ProcessTargetFiles(input_tf_zip, output_tf_zip, misc_info,
167                       apk_key_map, key_passwords, platform_api_level,
168                       codename_to_api_level_map):
169
170  maxsize = max([len(os.path.basename(i.filename))
171                 for i in input_tf_zip.infolist()
172                 if i.filename.endswith('.apk')])
173  rebuild_recovery = False
174
175  tmpdir = tempfile.mkdtemp()
176  def write_to_temp(fn, attr, data):
177    fn = os.path.join(tmpdir, fn)
178    if fn.endswith("/"):
179      fn = os.path.join(tmpdir, fn)
180      os.mkdir(fn)
181    else:
182      d = os.path.dirname(fn)
183      if d and not os.path.exists(d):
184        os.makedirs(d)
185
186      if attr >> 16 == 0xa1ff:
187        os.symlink(data, fn)
188      else:
189        with open(fn, "wb") as f:
190          f.write(data)
191
192  for info in input_tf_zip.infolist():
193    if info.filename.startswith("IMAGES/"):
194      continue
195
196    data = input_tf_zip.read(info.filename)
197    out_info = copy.copy(info)
198
199    # Replace keys if requested.
200    if (info.filename == "META/misc_info.txt" and
201        OPTIONS.replace_verity_private_key):
202      ReplaceVerityPrivateKey(input_tf_zip, output_tf_zip, misc_info,
203                              OPTIONS.replace_verity_private_key[1])
204    elif (info.filename in ("BOOT/RAMDISK/verity_key",
205                            "BOOT/verity_key") and
206          OPTIONS.replace_verity_public_key):
207      new_data = ReplaceVerityPublicKey(output_tf_zip, info.filename,
208                                        OPTIONS.replace_verity_public_key[1])
209      write_to_temp(info.filename, info.external_attr, new_data)
210    # Copy BOOT/, RECOVERY/, META/, ROOT/ to rebuild recovery patch.
211    elif (info.filename.startswith("BOOT/") or
212          info.filename.startswith("RECOVERY/") or
213          info.filename.startswith("META/") or
214          info.filename.startswith("ROOT/") or
215          info.filename == "SYSTEM/etc/recovery-resource.dat"):
216      write_to_temp(info.filename, info.external_attr, data)
217
218    # Sign APKs.
219    if info.filename.endswith(".apk"):
220      name = os.path.basename(info.filename)
221      key = apk_key_map[name]
222      if key not in common.SPECIAL_CERT_STRINGS:
223        print "    signing: %-*s (%s)" % (maxsize, name, key)
224        signed_data = SignApk(data, key, key_passwords[key], platform_api_level,
225            codename_to_api_level_map)
226        common.ZipWriteStr(output_tf_zip, out_info, signed_data)
227      else:
228        # an APK we're not supposed to sign.
229        print "NOT signing: %s" % (name,)
230        common.ZipWriteStr(output_tf_zip, out_info, data)
231    elif info.filename in ("SYSTEM/build.prop",
232                           "VENDOR/build.prop",
233                           "BOOT/RAMDISK/default.prop",
234                           "RECOVERY/RAMDISK/default.prop"):
235      print "rewriting %s:" % (info.filename,)
236      new_data = RewriteProps(data, misc_info)
237      common.ZipWriteStr(output_tf_zip, out_info, new_data)
238      if info.filename in ("BOOT/RAMDISK/default.prop",
239                           "RECOVERY/RAMDISK/default.prop"):
240        write_to_temp(info.filename, info.external_attr, new_data)
241    elif info.filename.endswith("mac_permissions.xml"):
242      print "rewriting %s with new keys." % (info.filename,)
243      new_data = ReplaceCerts(data)
244      common.ZipWriteStr(output_tf_zip, out_info, new_data)
245    elif info.filename in ("SYSTEM/recovery-from-boot.p",
246                           "SYSTEM/etc/recovery.img",
247                           "SYSTEM/bin/install-recovery.sh"):
248      rebuild_recovery = True
249    elif (OPTIONS.replace_ota_keys and
250          info.filename in ("RECOVERY/RAMDISK/res/keys",
251                            "SYSTEM/etc/security/otacerts.zip")):
252      # don't copy these files if we're regenerating them below
253      pass
254    elif (OPTIONS.replace_verity_private_key and
255          info.filename == "META/misc_info.txt"):
256      pass
257    elif (OPTIONS.replace_verity_public_key and
258          info.filename in ("BOOT/RAMDISK/verity_key",
259                            "BOOT/verity_key")):
260      pass
261    else:
262      # a non-APK file; copy it verbatim
263      common.ZipWriteStr(output_tf_zip, out_info, data)
264
265  if OPTIONS.replace_ota_keys:
266    new_recovery_keys = ReplaceOtaKeys(input_tf_zip, output_tf_zip, misc_info)
267    if new_recovery_keys:
268      write_to_temp("RECOVERY/RAMDISK/res/keys", 0o755 << 16, new_recovery_keys)
269
270  if rebuild_recovery:
271    recovery_img = common.GetBootableImage(
272        "recovery.img", "recovery.img", tmpdir, "RECOVERY", info_dict=misc_info)
273    boot_img = common.GetBootableImage(
274        "boot.img", "boot.img", tmpdir, "BOOT", info_dict=misc_info)
275
276    def output_sink(fn, data):
277      common.ZipWriteStr(output_tf_zip, "SYSTEM/" + fn, data)
278
279    common.MakeRecoveryPatch(tmpdir, output_sink, recovery_img, boot_img,
280                             info_dict=misc_info)
281
282  shutil.rmtree(tmpdir)
283
284
285def ReplaceCerts(data):
286  """Given a string of data, replace all occurences of a set
287  of X509 certs with a newer set of X509 certs and return
288  the updated data string."""
289  for old, new in OPTIONS.key_map.iteritems():
290    try:
291      if OPTIONS.verbose:
292        print "    Replacing %s.x509.pem with %s.x509.pem" % (old, new)
293      f = open(old + ".x509.pem")
294      old_cert16 = base64.b16encode(common.ParseCertificate(f.read())).lower()
295      f.close()
296      f = open(new + ".x509.pem")
297      new_cert16 = base64.b16encode(common.ParseCertificate(f.read())).lower()
298      f.close()
299      # Only match entire certs.
300      pattern = "\\b"+old_cert16+"\\b"
301      (data, num) = re.subn(pattern, new_cert16, data, flags=re.IGNORECASE)
302      if OPTIONS.verbose:
303        print "    Replaced %d occurence(s) of %s.x509.pem with " \
304            "%s.x509.pem" % (num, old, new)
305    except IOError as e:
306      if e.errno == errno.ENOENT and not OPTIONS.verbose:
307        continue
308
309      print "    Error accessing %s. %s. Skip replacing %s.x509.pem " \
310          "with %s.x509.pem." % (e.filename, e.strerror, old, new)
311
312  return data
313
314
315def EditTags(tags):
316  """Given a string containing comma-separated tags, apply the edits
317  specified in OPTIONS.tag_changes and return the updated string."""
318  tags = set(tags.split(","))
319  for ch in OPTIONS.tag_changes:
320    if ch[0] == "-":
321      tags.discard(ch[1:])
322    elif ch[0] == "+":
323      tags.add(ch[1:])
324  return ",".join(sorted(tags))
325
326
327def RewriteProps(data, misc_info):
328  output = []
329  for line in data.split("\n"):
330    line = line.strip()
331    original_line = line
332    if line and line[0] != '#' and "=" in line:
333      key, value = line.split("=", 1)
334      if (key in ("ro.build.fingerprint", "ro.vendor.build.fingerprint")
335          and misc_info.get("oem_fingerprint_properties") is None):
336        pieces = value.split("/")
337        pieces[-1] = EditTags(pieces[-1])
338        value = "/".join(pieces)
339      elif (key in ("ro.build.thumbprint", "ro.vendor.build.thumbprint")
340            and misc_info.get("oem_fingerprint_properties") is not None):
341        pieces = value.split("/")
342        pieces[-1] = EditTags(pieces[-1])
343        value = "/".join(pieces)
344      elif key == "ro.bootimage.build.fingerprint":
345        pieces = value.split("/")
346        pieces[-1] = EditTags(pieces[-1])
347        value = "/".join(pieces)
348      elif key == "ro.build.description":
349        pieces = value.split(" ")
350        assert len(pieces) == 5
351        pieces[-1] = EditTags(pieces[-1])
352        value = " ".join(pieces)
353      elif key == "ro.build.tags":
354        value = EditTags(value)
355      elif key == "ro.build.display.id":
356        # change, eg, "JWR66N dev-keys" to "JWR66N"
357        value = value.split()
358        if len(value) > 1 and value[-1].endswith("-keys"):
359          value.pop()
360        value = " ".join(value)
361      line = key + "=" + value
362    if line != original_line:
363      print "  replace: ", original_line
364      print "     with: ", line
365    output.append(line)
366  return "\n".join(output) + "\n"
367
368
369def ReplaceOtaKeys(input_tf_zip, output_tf_zip, misc_info):
370  try:
371    keylist = input_tf_zip.read("META/otakeys.txt").split()
372  except KeyError:
373    raise common.ExternalError("can't read META/otakeys.txt from input")
374
375  extra_recovery_keys = misc_info.get("extra_recovery_keys", None)
376  if extra_recovery_keys:
377    extra_recovery_keys = [OPTIONS.key_map.get(k, k) + ".x509.pem"
378                           for k in extra_recovery_keys.split()]
379    if extra_recovery_keys:
380      print "extra recovery-only key(s): " + ", ".join(extra_recovery_keys)
381  else:
382    extra_recovery_keys = []
383
384  mapped_keys = []
385  for k in keylist:
386    m = re.match(r"^(.*)\.x509\.pem$", k)
387    if not m:
388      raise common.ExternalError(
389          "can't parse \"%s\" from META/otakeys.txt" % (k,))
390    k = m.group(1)
391    mapped_keys.append(OPTIONS.key_map.get(k, k) + ".x509.pem")
392
393  if mapped_keys:
394    print "using:\n   ", "\n   ".join(mapped_keys)
395    print "for OTA package verification"
396  else:
397    devkey = misc_info.get("default_system_dev_certificate",
398                           "build/target/product/security/testkey")
399    mapped_keys.append(
400        OPTIONS.key_map.get(devkey, devkey) + ".x509.pem")
401    print "META/otakeys.txt has no keys; using", mapped_keys[0]
402
403  # recovery uses a version of the key that has been slightly
404  # predigested (by DumpPublicKey.java) and put in res/keys.
405  # extra_recovery_keys are used only in recovery.
406
407  p = common.Run(["java", "-jar",
408                  os.path.join(OPTIONS.search_path, "framework", "dumpkey.jar")]
409                 + mapped_keys + extra_recovery_keys,
410                 stdout=subprocess.PIPE)
411  new_recovery_keys, _ = p.communicate()
412  if p.returncode != 0:
413    raise common.ExternalError("failed to run dumpkeys")
414  common.ZipWriteStr(output_tf_zip, "RECOVERY/RAMDISK/res/keys",
415                     new_recovery_keys)
416
417  # SystemUpdateActivity uses the x509.pem version of the keys, but
418  # put into a zipfile system/etc/security/otacerts.zip.
419  # We DO NOT include the extra_recovery_keys (if any) here.
420
421  temp_file = cStringIO.StringIO()
422  certs_zip = zipfile.ZipFile(temp_file, "w")
423  for k in mapped_keys:
424    common.ZipWrite(certs_zip, k)
425  common.ZipClose(certs_zip)
426  common.ZipWriteStr(output_tf_zip, "SYSTEM/etc/security/otacerts.zip",
427                     temp_file.getvalue())
428
429  return new_recovery_keys
430
431def ReplaceVerityPublicKey(targetfile_zip, filename, key_path):
432  print "Replacing verity public key with %s" % key_path
433  with open(key_path) as f:
434    data = f.read()
435  common.ZipWriteStr(targetfile_zip, filename, data)
436  return data
437
438def ReplaceVerityPrivateKey(targetfile_input_zip, targetfile_output_zip,
439                            misc_info, key_path):
440  print "Replacing verity private key with %s" % key_path
441  current_key = misc_info["verity_key"]
442  original_misc_info = targetfile_input_zip.read("META/misc_info.txt")
443  new_misc_info = original_misc_info.replace(current_key, key_path)
444  common.ZipWriteStr(targetfile_output_zip, "META/misc_info.txt", new_misc_info)
445  misc_info["verity_key"] = key_path
446
447def BuildKeyMap(misc_info, key_mapping_options):
448  for s, d in key_mapping_options:
449    if s is None:   # -d option
450      devkey = misc_info.get("default_system_dev_certificate",
451                             "build/target/product/security/testkey")
452      devkeydir = os.path.dirname(devkey)
453
454      OPTIONS.key_map.update({
455          devkeydir + "/testkey":  d + "/releasekey",
456          devkeydir + "/devkey":   d + "/releasekey",
457          devkeydir + "/media":    d + "/media",
458          devkeydir + "/shared":   d + "/shared",
459          devkeydir + "/platform": d + "/platform",
460          })
461    else:
462      OPTIONS.key_map[s] = d
463
464
465def GetApiLevelAndCodename(input_tf_zip):
466  data = input_tf_zip.read("SYSTEM/build.prop")
467  api_level = None
468  codename = None
469  for line in data.split("\n"):
470    line = line.strip()
471    original_line = line
472    if line and line[0] != '#' and "=" in line:
473      key, value = line.split("=", 1)
474      key = key.strip()
475      if key == "ro.build.version.sdk":
476        api_level = int(value.strip())
477      elif key == "ro.build.version.codename":
478        codename = value.strip()
479
480  if api_level is None:
481    raise ValueError("No ro.build.version.sdk in SYSTEM/build.prop")
482  if codename is None:
483    raise ValueError("No ro.build.version.codename in SYSTEM/build.prop")
484
485  return (api_level, codename)
486
487
488def GetCodenameToApiLevelMap(input_tf_zip):
489  data = input_tf_zip.read("SYSTEM/build.prop")
490  api_level = None
491  codenames = None
492  for line in data.split("\n"):
493    line = line.strip()
494    original_line = line
495    if line and line[0] != '#' and "=" in line:
496      key, value = line.split("=", 1)
497      key = key.strip()
498      if key == "ro.build.version.sdk":
499        api_level = int(value.strip())
500      elif key == "ro.build.version.all_codenames":
501        codenames = value.strip().split(",")
502
503  if api_level is None:
504    raise ValueError("No ro.build.version.sdk in SYSTEM/build.prop")
505  if codenames is None:
506    raise ValueError("No ro.build.version.all_codenames in SYSTEM/build.prop")
507
508  result = dict()
509  for codename in codenames:
510    codename = codename.strip()
511    if len(codename) > 0:
512      result[codename] = api_level
513  return result
514
515
516def main(argv):
517
518  key_mapping_options = []
519
520  def option_handler(o, a):
521    if o in ("-e", "--extra_apks"):
522      names, key = a.split("=")
523      names = names.split(",")
524      for n in names:
525        OPTIONS.extra_apks[n] = key
526    elif o in ("-d", "--default_key_mappings"):
527      key_mapping_options.append((None, a))
528    elif o in ("-k", "--key_mapping"):
529      key_mapping_options.append(a.split("=", 1))
530    elif o in ("-o", "--replace_ota_keys"):
531      OPTIONS.replace_ota_keys = True
532    elif o in ("-t", "--tag_changes"):
533      new = []
534      for i in a.split(","):
535        i = i.strip()
536        if not i or i[0] not in "-+":
537          raise ValueError("Bad tag change '%s'" % (i,))
538        new.append(i[0] + i[1:].strip())
539      OPTIONS.tag_changes = tuple(new)
540    elif o == "--replace_verity_public_key":
541      OPTIONS.replace_verity_public_key = (True, a)
542    elif o == "--replace_verity_private_key":
543      OPTIONS.replace_verity_private_key = (True, a)
544    else:
545      return False
546    return True
547
548  args = common.ParseOptions(argv, __doc__,
549                             extra_opts="e:d:k:ot:",
550                             extra_long_opts=["extra_apks=",
551                                              "default_key_mappings=",
552                                              "key_mapping=",
553                                              "replace_ota_keys",
554                                              "tag_changes=",
555                                              "replace_verity_public_key=",
556                                              "replace_verity_private_key="],
557                             extra_option_handler=option_handler)
558
559  if len(args) != 2:
560    common.Usage(__doc__)
561    sys.exit(1)
562
563  input_zip = zipfile.ZipFile(args[0], "r")
564  output_zip = zipfile.ZipFile(args[1], "w")
565
566  misc_info = common.LoadInfoDict(input_zip)
567
568  BuildKeyMap(misc_info, key_mapping_options)
569
570  apk_key_map = GetApkCerts(input_zip)
571  CheckAllApksSigned(input_zip, apk_key_map)
572
573  key_passwords = common.GetKeyPasswords(set(apk_key_map.values()))
574  platform_api_level, platform_codename = GetApiLevelAndCodename(input_zip)
575  codename_to_api_level_map = GetCodenameToApiLevelMap(input_zip)
576  # Android N will be API Level 24, but isn't yet.
577  # TODO: Remove this workaround once Android N is officially API Level 24.
578  if platform_api_level == 23 and platform_codename == "N":
579    platform_api_level = 24
580
581  ProcessTargetFiles(input_zip, output_zip, misc_info,
582                     apk_key_map, key_passwords,
583                     platform_api_level,
584                     codename_to_api_level_map)
585
586  common.ZipClose(input_zip)
587  common.ZipClose(output_zip)
588
589  add_img_to_target_files.AddImagesToTargetFiles(args[1])
590
591  print "done."
592
593
594if __name__ == '__main__':
595  try:
596    main(sys.argv[1:])
597  except common.ExternalError, e:
598    print
599    print "   ERROR: %s" % (e,)
600    print
601    sys.exit(1)
602