Merge changes I3fa13e3d,I7b7f0017,I2ef318e0

* changes:
  releasetools: Support signing APEXes.
  releasetools: Add apex_utils.py.
  releasetools: check_target_files_signatures.py checks APEXes.
diff --git a/tools/releasetools/apex_utils.py b/tools/releasetools/apex_utils.py
new file mode 100644
index 0000000..d14c94f
--- /dev/null
+++ b/tools/releasetools/apex_utils.py
@@ -0,0 +1,147 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+import os.path
+import re
+import shlex
+import sys
+
+import common
+
+logger = logging.getLogger(__name__)
+
+
+class ApexInfoError(Exception):
+  """An Exception raised during Apex Information command."""
+
+  def __init__(self, message):
+    Exception.__init__(self, message)
+
+
+class ApexSigningError(Exception):
+  """An Exception raised during Apex Payload signing."""
+
+  def __init__(self, message):
+    Exception.__init__(self, message)
+
+
+def SignApexPayload(payload_file, payload_key_path, payload_key_name, algorithm,
+                    salt, signing_args=None):
+  """Signs a given payload_file with the payload key."""
+  # Add the new footer. Old footer, if any, will be replaced by avbtool.
+  cmd = ['avbtool', 'add_hashtree_footer',
+         '--do_not_generate_fec',
+         '--algorithm', algorithm,
+         '--key', payload_key_path,
+         '--prop', 'apex.key:{}'.format(payload_key_name),
+         '--image', payload_file,
+         '--salt', salt]
+  if signing_args:
+    cmd.extend(shlex.split(signing_args))
+
+  try:
+    common.RunAndCheckOutput(cmd)
+  except common.ExternalError as e:
+    raise ApexSigningError, \
+        'Failed to sign APEX payload {} with {}:\n{}'.format(
+            payload_file, payload_key_path, e), sys.exc_info()[2]
+
+  # Verify the signed payload image with specified public key.
+  logger.info('Verifying %s', payload_file)
+  VerifyApexPayload(payload_file, payload_key_path)
+
+
+def VerifyApexPayload(payload_file, payload_key):
+  """Verifies the APEX payload signature with the given key."""
+  cmd = ['avbtool', 'verify_image', '--image', payload_file,
+         '--key', payload_key]
+  try:
+    common.RunAndCheckOutput(cmd)
+  except common.ExternalError as e:
+    raise ApexSigningError, \
+        'Failed to validate payload signing for {} with {}:\n{}'.format(
+            payload_file, payload_key, e), sys.exc_info()[2]
+
+
+def ParseApexPayloadInfo(payload_path):
+  """Parses the APEX payload info.
+
+  Args:
+    payload_path: The path to the payload image.
+
+  Raises:
+    ApexInfoError on parsing errors.
+
+  Returns:
+    A dict that contains payload property-value pairs. The dict should at least
+    contain Algorithm, Salt and apex.key.
+  """
+  if not os.path.exists(payload_path):
+    raise ApexInfoError('Failed to find image: {}'.format(payload_path))
+
+  cmd = ['avbtool', 'info_image', '--image', payload_path]
+  try:
+    output = common.RunAndCheckOutput(cmd)
+  except common.ExternalError as e:
+    raise ApexInfoError, \
+        'Failed to get APEX payload info for {}:\n{}'.format(
+            payload_path, e), sys.exc_info()[2]
+
+  # Extract the Algorithm / Salt / Prop info from payload (i.e. an image signed
+  # with avbtool). For example,
+  # Algorithm:                SHA256_RSA4096
+  PAYLOAD_INFO_PATTERN = (
+      r'^\s*(?P<key>Algorithm|Salt|Prop)\:\s*(?P<value>.*?)$')
+  payload_info_matcher = re.compile(PAYLOAD_INFO_PATTERN)
+
+  payload_info = {}
+  for line in output.split('\n'):
+    line_info = payload_info_matcher.match(line)
+    if not line_info:
+      continue
+
+    key, value = line_info.group('key'), line_info.group('value')
+
+    if key == 'Prop':
+      # Further extract the property key-value pair, from a 'Prop:' line. For
+      # example,
+      #   Prop: apex.key -> 'com.android.runtime'
+      # Note that avbtool writes single or double quotes around values.
+      PROPERTY_DESCRIPTOR_PATTERN = r'^\s*(?P<key>.*?)\s->\s*(?P<value>.*?)$'
+
+      prop_matcher = re.compile(PROPERTY_DESCRIPTOR_PATTERN)
+      prop = prop_matcher.match(value)
+      if not prop:
+        raise ApexInfoError(
+            'Failed to parse prop string {}'.format(value))
+
+      prop_key, prop_value = prop.group('key'), prop.group('value')
+      if prop_key == 'apex.key':
+        # avbtool dumps the prop value with repr(), which contains single /
+        # double quotes that we don't want.
+        payload_info[prop_key] = prop_value.strip('\"\'')
+
+    else:
+      payload_info[key] = value
+
+  # Sanity check.
+  for key in ('Algorithm', 'Salt', 'apex.key'):
+    if key not in payload_info:
+      raise ApexInfoError(
+          'Failed to find {} prop in {}'.format(key, payload_path))
+
+  return payload_info
diff --git a/tools/releasetools/check_target_files_signatures.py b/tools/releasetools/check_target_files_signatures.py
index 9b76954..4b0d4c7 100755
--- a/tools/releasetools/check_target_files_signatures.py
+++ b/tools/releasetools/check_target_files_signatures.py
@@ -168,6 +168,7 @@
 
 
 class APK(object):
+
   def __init__(self, full_filename, filename):
     self.filename = filename
     self.certs = None
@@ -244,12 +245,12 @@
     # must decompress them individually before we perform any analysis.
 
     # This is the list of wildcards of files we extract from |filename|.
-    apk_extensions = ['*.apk']
+    apk_extensions = ['*.apk', '*.apex']
 
     self.certmap, compressed_extension = common.ReadApkCerts(
-        zipfile.ZipFile(filename, "r"))
+        zipfile.ZipFile(filename))
     if compressed_extension:
-      apk_extensions.append("*.apk" + compressed_extension)
+      apk_extensions.append('*.apk' + compressed_extension)
 
     d = common.UnzipTemp(filename, apk_extensions)
     self.apks = {}
@@ -272,7 +273,7 @@
           os.remove(os.path.join(dirpath, fn))
           fn = uncompressed_fn
 
-        if fn.endswith(".apk"):
+        if fn.endswith(('.apk', '.apex')):
           fullname = os.path.join(dirpath, fn)
           displayname = fullname[len(d)+1:]
           apk = APK(fullname, displayname)
diff --git a/tools/releasetools/sign_target_files_apks.py b/tools/releasetools/sign_target_files_apks.py
index 85897c2..a9d2218 100755
--- a/tools/releasetools/sign_target_files_apks.py
+++ b/tools/releasetools/sign_target_files_apks.py
@@ -21,11 +21,17 @@
 Usage:  sign_target_files_apks [flags] input_target_files output_target_files
 
   -e  (--extra_apks)  <name,name,...=key>
-      Add extra APK name/key pairs as though they appeared in
-      apkcerts.txt (so mappings specified by -k and -d are applied).
-      Keys specified in -e override any value for that app contained
-      in the apkcerts.txt file.  Option may be repeated to give
-      multiple extra packages.
+      Add extra APK/APEX name/key pairs as though they appeared in apkcerts.txt
+      or apexkeys.txt (so mappings specified by -k and -d are applied). Keys
+      specified in -e override any value for that app contained in the
+      apkcerts.txt file, or the container key for an APEX. Option may be
+      repeated to give multiple extra packages.
+
+  --extra_apex_payload_key <name=key>
+      Add a mapping for APEX package name to payload signing key, which will
+      override the default payload signing key in apexkeys.txt. Note that the
+      container key should be overridden via the `--extra_apks` flag above.
+      Option may be repeated for multiple APEXes.
 
   --skip_apks_with_path_prefix  <prefix>
       Skip signing an APK if it has the matching prefix in its path. The prefix
@@ -90,7 +96,7 @@
       Use the specified algorithm (e.g. SHA256_RSA4096) and the key to AVB-sign
       the specified image. Otherwise it uses the existing values in info dict.
 
-  --avb_{boot,system,vendor,dtbo,vbmeta}_extra_args <args>
+  --avb_{apex,boot,system,vendor,dtbo,vbmeta}_extra_args <args>
       Specify any additional args that are needed to AVB-sign the image
       (e.g. "--signing_helper /path/to/helper"). The args will be appended to
       the existing ones in info dict.
@@ -102,6 +108,7 @@
 import copy
 import errno
 import gzip
+import itertools
 import logging
 import os
 import re
@@ -114,6 +121,7 @@
 from xml.etree import ElementTree
 
 import add_img_to_target_files
+import apex_utils
 import common
 
 
@@ -127,6 +135,7 @@
 OPTIONS = common.OPTIONS
 
 OPTIONS.extra_apks = {}
+OPTIONS.extra_apex_payload_keys = {}
 OPTIONS.skip_apks_with_path_prefix = set()
 OPTIONS.key_map = {}
 OPTIONS.rebuild_recovery = False
@@ -154,6 +163,41 @@
   return certmap
 
 
+def GetApexKeys(keys_info, key_map):
+  """Gets APEX payload and container signing keys by applying the mapping rules.
+
+  We currently don't allow PRESIGNED payload / container keys.
+
+  Args:
+    keys_info: A dict that maps from APEX filenames to a tuple of (payload_key,
+        container_key).
+    key_map: A dict that overrides the keys, specified via command-line input.
+
+  Returns:
+    A dict that contains the updated APEX key mapping, which should be used for
+    the current signing.
+  """
+  # Apply all the --extra_apex_payload_key options to override the payload
+  # signing keys in the given keys_info.
+  for apex, key in OPTIONS.extra_apex_payload_keys.items():
+    assert key, 'Presigned APEX payload for {} is not allowed'.format(apex)
+    keys_info[apex] = (key, keys_info[apex][1])
+
+  # Apply the key remapping to container keys.
+  for apex, (payload_key, container_key) in keys_info.items():
+    keys_info[apex] = (payload_key, key_map.get(container_key, container_key))
+
+  # Apply all the --extra_apks options to override the container keys.
+  for apex, key in OPTIONS.extra_apks.items():
+    # Skip non-APEX containers.
+    if apex not in keys_info:
+      continue
+    assert key, 'Presigned APEX container for {} is not allowed'.format(apex)
+    keys_info[apex][1] = key_map.get(key, key)
+
+  return keys_info
+
+
 def GetApkFileInfo(filename, compressed_extension, skipped_prefixes):
   """Returns the APK info based on the given filename.
 
@@ -200,34 +244,45 @@
   return (True, is_compressed, should_be_skipped)
 
 
-def CheckAllApksSigned(input_tf_zip, apk_key_map, compressed_extension):
-  """Checks that all the APKs have keys specified, otherwise errors out.
+def CheckApkAndApexKeysAvailable(input_tf_zip, known_keys,
+                                 compressed_extension):
+  """Checks that all the APKs and APEXes have keys specified.
 
   Args:
     input_tf_zip: An open target_files zip file.
-    apk_key_map: A dict of known signing keys key'd by APK names.
+    known_keys: A set of APKs and APEXes that have known signing keys.
     compressed_extension: The extension string of compressed APKs, such as
-        ".gz", or None if there's no compressed APKs.
+        '.gz', or None if there's no compressed APKs.
 
   Raises:
-    AssertionError: On finding unknown APKs.
+    AssertionError: On finding unknown APKs and APEXes.
   """
-  unknown_apks = []
+  unknown_files = []
   for info in input_tf_zip.infolist():
+    # Handle APEXes first, e.g. SYSTEM/apex/com.android.tzdata.apex.
+    if (info.filename.startswith('SYSTEM/apex') and
+        info.filename.endswith('.apex')):
+      name = os.path.basename(info.filename)
+      if name not in known_keys:
+        unknown_files.append(name)
+      continue
+
+    # And APKs.
     (is_apk, is_compressed, should_be_skipped) = GetApkFileInfo(
         info.filename, compressed_extension, OPTIONS.skip_apks_with_path_prefix)
     if not is_apk or should_be_skipped:
       continue
+
     name = os.path.basename(info.filename)
     if is_compressed:
       name = name[:-len(compressed_extension)]
-    if name not in apk_key_map:
-      unknown_apks.append(name)
+    if name not in known_keys:
+      unknown_files.append(name)
 
-  assert not unknown_apks, \
+  assert not unknown_files, \
       ("No key specified for:\n  {}\n"
        "Use '-e <apkname>=' to specify a key (which may be an empty string to "
-       "not sign this apk).".format("\n  ".join(unknown_apks)))
+       "not sign this apk).".format("\n  ".join(unknown_files)))
 
 
 def SignApk(data, keyname, pw, platform_api_level, codename_to_api_level_map,
@@ -293,9 +348,69 @@
   return data
 
 
+def SignApex(apex_data, payload_key, container_key, container_pw,
+             codename_to_api_level_map, signing_args=None):
+  """Signs the current APEX with the given payload/container keys.
+
+  Args:
+    apex_data: Raw APEX data.
+    payload_key: The path to payload signing key (w/o extension).
+    container_key: The path to container signing key (w/o extension).
+    container_pw: The matching password of the container_key, or None.
+    codename_to_api_level_map: A dict that maps from codename to API level.
+    signing_args: Additional args to be passed to the payload signer.
+
+  Returns:
+    (signed_apex, payload_key_name): signed_apex is the path to the signed APEX
+        file; payload_key_name is a str of the payload signing key name (e.g.
+        com.android.tzdata).
+  """
+  apex_file = common.MakeTempFile(prefix='apex-', suffix='.apex')
+  with open(apex_file, 'wb') as apex_fp:
+    apex_fp.write(apex_data)
+
+  APEX_PAYLOAD_IMAGE = 'apex_payload.img'
+
+  # Signing an APEX is a two step process.
+  # 1. Extract and sign the APEX_PAYLOAD_IMAGE entry with the given payload_key.
+  payload_dir = common.MakeTempDir(prefix='apex-payload-')
+  with zipfile.ZipFile(apex_file) as apex_fd:
+    payload_file = apex_fd.extract(APEX_PAYLOAD_IMAGE, payload_dir)
+
+  payload_info = apex_utils.ParseApexPayloadInfo(payload_file)
+  apex_utils.SignApexPayload(
+      payload_file,
+      payload_key,
+      payload_info['apex.key'],
+      payload_info['Algorithm'],
+      payload_info['Salt'],
+      signing_args)
+
+  common.ZipDelete(apex_file, APEX_PAYLOAD_IMAGE)
+  apex_zip = zipfile.ZipFile(apex_file, 'a')
+  common.ZipWrite(apex_zip, payload_file, arcname=APEX_PAYLOAD_IMAGE)
+  common.ZipClose(apex_zip)
+
+  # 2. Sign the overall APEX container with container_key.
+  signed_apex = common.MakeTempFile(prefix='apex-container-', suffix='.apex')
+  common.SignFile(
+      apex_file,
+      signed_apex,
+      container_key,
+      container_pw,
+      codename_to_api_level_map=codename_to_api_level_map)
+
+  signed_and_aligned_apex = common.MakeTempFile(
+      prefix='apex-container-', suffix='.apex')
+  common.RunAndCheckOutput(
+      ['zipalign', '-f', '4096', signed_apex, signed_and_aligned_apex])
+
+  return (signed_and_aligned_apex, payload_info['apex.key'])
+
+
 def ProcessTargetFiles(input_tf_zip, output_tf_zip, misc_info,
-                       apk_key_map, key_passwords, platform_api_level,
-                       codename_to_api_level_map,
+                       apk_keys, apex_keys, key_passwords,
+                       platform_api_level, codename_to_api_level_map,
                        compressed_extension):
   # maxsize measures the maximum filename length, including the ones to be
   # skipped.
@@ -304,6 +419,10 @@
        if GetApkFileInfo(i.filename, compressed_extension, [])[0]])
   system_root_image = misc_info.get("system_root_image") == "true"
 
+  # A dict of APEX payload public keys that should be updated, i.e. the files
+  # under '/system/etc/security/apex/'.
+  updated_apex_payload_keys = {}
+
   for info in input_tf_zip.infolist():
     filename = info.filename
     if filename.startswith("IMAGES/"):
@@ -331,7 +450,7 @@
       if is_compressed:
         name = name[:-len(compressed_extension)]
 
-      key = apk_key_map[name]
+      key = apk_keys[name]
       if key not in common.SPECIAL_CERT_STRINGS:
         print("    signing: %-*s (%s)" % (maxsize, name, key))
         signed_data = SignApk(data, key, key_passwords[key], platform_api_level,
@@ -344,6 +463,30 @@
             "        (skipped due to special cert string)" % (name,))
         common.ZipWriteStr(output_tf_zip, out_info, data)
 
+    # Sign bundled APEX files.
+    elif filename.startswith("SYSTEM/apex") and filename.endswith(".apex"):
+      name = os.path.basename(filename)
+      payload_key, container_key = apex_keys[name]
+
+      print("    signing: %-*s container (%s)" % (maxsize, name, container_key))
+      print("           : %-*s payload   (%s)" % (maxsize, name, payload_key))
+
+      (signed_apex, payload_key_name) = SignApex(
+          data,
+          payload_key,
+          container_key,
+          key_passwords[container_key],
+          codename_to_api_level_map,
+          OPTIONS.avb_extra_args.get('apex'))
+      common.ZipWrite(output_tf_zip, signed_apex, filename)
+
+      updated_apex_payload_keys[payload_key_name] = payload_key
+
+    # AVB public keys for the installed APEXes, which will be updated later.
+    elif (os.path.dirname(filename) == 'SYSTEM/etc/security/apex' and
+          filename != 'SYSTEM/etc/security/apex/'):
+      continue
+
     # System properties.
     elif filename in ("SYSTEM/build.prop",
                       "VENDOR/build.prop",
@@ -406,6 +549,30 @@
     else:
       common.ZipWriteStr(output_tf_zip, out_info, data)
 
+  # Update APEX payload public keys.
+  for info in input_tf_zip.infolist():
+    filename = info.filename
+    if (os.path.dirname(filename) != 'SYSTEM/etc/security/apex' or
+        filename == 'SYSTEM/etc/security/apex/'):
+      continue
+
+    name = os.path.basename(filename)
+    assert name in updated_apex_payload_keys, \
+        'Unsigned APEX payload key: {}'.format(filename)
+
+    key_path = updated_apex_payload_keys[name]
+    if not os.path.exists(key_path) and not key_path.endswith('.pem'):
+      key_path = '{}.pem'.format(key_path)
+    assert os.path.exists(key_path), \
+        'Failed to find public key file {} for APEX {}'.format(
+            updated_apex_payload_keys[name], name)
+
+    print('Replacing APEX payload public key for {} with {}'.format(
+        name, key_path))
+
+    public_key = common.ExtractAvbPublicKey(key_path)
+    common.ZipWrite(output_tf_zip, public_key, arcname=filename)
+
   if OPTIONS.replace_ota_keys:
     ReplaceOtaKeys(input_tf_zip, output_tf_zip, misc_info)
 
@@ -821,6 +988,67 @@
   return result
 
 
+def ReadApexKeysInfo(tf_zip):
+  """Parses the APEX keys info from a given target-files zip.
+
+  Given a target-files ZipFile, parses the META/apexkeys.txt entry and returns a
+  dict that contains the mapping from APEX names (e.g. com.android.tzdata) to a
+  tuple of (payload_key, container_key).
+
+  Args:
+    tf_zip: The input target_files ZipFile (already open).
+
+  Returns:
+    (payload_key, container_key): payload_key contains the path to the payload
+        signing key; container_key contains the path to the container signing
+        key.
+  """
+  keys = {}
+  for line in tf_zip.read("META/apexkeys.txt").split("\n"):
+    line = line.strip()
+    if not line:
+      continue
+    matches = re.match(
+        r'^name="(?P<NAME>.*)"\s+'
+        r'public_key="(?P<PAYLOAD_PUBLIC_KEY>.*)"\s+'
+        r'private_key="(?P<PAYLOAD_PRIVATE_KEY>.*)"\s+'
+        r'container_certificate="(?P<CONTAINER_CERT>.*)"\s+'
+        r'container_private_key="(?P<CONTAINER_PRIVATE_KEY>.*)"$',
+        line)
+    if not matches:
+      continue
+
+    name = matches.group('NAME')
+    payload_public_key = matches.group("PAYLOAD_PUBLIC_KEY")
+    payload_private_key = matches.group("PAYLOAD_PRIVATE_KEY")
+
+    def CompareKeys(pubkey, pubkey_suffix, privkey, privkey_suffix):
+      pubkey_suffix_len = len(pubkey_suffix)
+      privkey_suffix_len = len(privkey_suffix)
+      return (pubkey.endswith(pubkey_suffix) and
+              privkey.endswith(privkey_suffix) and
+              pubkey[:-pubkey_suffix_len] == privkey[:-privkey_suffix_len])
+
+    PAYLOAD_PUBLIC_KEY_SUFFIX = '.avbpubkey'
+    PAYLOAD_PRIVATE_KEY_SUFFIX = '.pem'
+    if not CompareKeys(
+        payload_public_key, PAYLOAD_PUBLIC_KEY_SUFFIX,
+        payload_private_key, PAYLOAD_PRIVATE_KEY_SUFFIX):
+      raise ValueError("Failed to parse payload keys: \n{}".format(line))
+
+    container_cert = matches.group("CONTAINER_CERT")
+    container_private_key = matches.group("CONTAINER_PRIVATE_KEY")
+    if not CompareKeys(
+        container_cert, OPTIONS.public_key_suffix,
+        container_private_key, OPTIONS.private_key_suffix):
+      raise ValueError("Failed to parse container keys: \n{}".format(line))
+
+    keys[name] = (payload_private_key,
+                  container_cert[:-len(OPTIONS.public_key_suffix)])
+
+  return keys
+
+
 def main(argv):
 
   key_mapping_options = []
@@ -831,6 +1059,9 @@
       names = names.split(",")
       for n in names:
         OPTIONS.extra_apks[n] = key
+    elif o == "--extra_apex_payload_key":
+      apex_name, key = a.split("=")
+      OPTIONS.extra_apex_payload_keys[apex_name] = key
     elif o == "--skip_apks_with_path_prefix":
       # Sanity check the prefix, which must be in all upper case.
       prefix = a.split('/')[0]
@@ -887,6 +1118,8 @@
       OPTIONS.avb_algorithms['vendor'] = a
     elif o == "--avb_vendor_extra_args":
       OPTIONS.avb_extra_args['vendor'] = a
+    elif o == "--avb_apex_extra_args":
+      OPTIONS.avb_extra_args['apex'] = a
     else:
       return False
     return True
@@ -896,6 +1129,7 @@
       extra_opts="e:d:k:ot:",
       extra_long_opts=[
           "extra_apks=",
+          "extra_apex_payload_key=",
           "skip_apks_with_path_prefix=",
           "default_key_mappings=",
           "key_mapping=",
@@ -904,6 +1138,7 @@
           "replace_verity_public_key=",
           "replace_verity_private_key=",
           "replace_verity_keyid=",
+          "avb_apex_extra_args=",
           "avb_vbmeta_algorithm=",
           "avb_vbmeta_key=",
           "avb_vbmeta_extra_args=",
@@ -937,18 +1172,25 @@
 
   BuildKeyMap(misc_info, key_mapping_options)
 
-  certmap, compressed_extension = common.ReadApkCerts(input_zip)
-  apk_key_map = GetApkCerts(certmap)
-  CheckAllApksSigned(input_zip, apk_key_map, compressed_extension)
+  apk_keys_info, compressed_extension = common.ReadApkCerts(input_zip)
+  apk_keys = GetApkCerts(apk_keys_info)
 
-  key_passwords = common.GetKeyPasswords(set(apk_key_map.values()))
+  apex_keys_info = ReadApexKeysInfo(input_zip)
+  apex_keys = GetApexKeys(apex_keys_info, apk_keys)
+
+  CheckApkAndApexKeysAvailable(
+      input_zip,
+      set(apk_keys.keys()) | set(apex_keys.keys()),
+      compressed_extension)
+
+  key_passwords = common.GetKeyPasswords(
+      set(apk_keys.values()) | set(itertools.chain(*apex_keys.values())))
   platform_api_level, _ = GetApiLevelAndCodename(input_zip)
   codename_to_api_level_map = GetCodenameToApiLevelMap(input_zip)
 
   ProcessTargetFiles(input_zip, output_zip, misc_info,
-                     apk_key_map, key_passwords,
-                     platform_api_level,
-                     codename_to_api_level_map,
+                     apk_keys, apex_keys, key_passwords,
+                     platform_api_level, codename_to_api_level_map,
                      compressed_extension)
 
   common.ZipClose(input_zip)
diff --git a/tools/releasetools/test_apex_utils.py b/tools/releasetools/test_apex_utils.py
new file mode 100644
index 0000000..2f8ee49
--- /dev/null
+++ b/tools/releasetools/test_apex_utils.py
@@ -0,0 +1,87 @@
+#
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import os
+import os.path
+
+import apex_utils
+import common
+import test_utils
+
+
+class ApexUtilsTest(test_utils.ReleaseToolsTestCase):
+
+  # echo "foo" | sha256sum
+  SALT = 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c'
+
+  def setUp(self):
+    self.testdata_dir = test_utils.get_testdata_dir()
+    # The default payload signing key.
+    self.payload_key = os.path.join(self.testdata_dir, 'testkey.key')
+
+  @staticmethod
+  def _GetTestPayload():
+    payload_file = common.MakeTempFile(prefix='apex-', suffix='.img')
+    with open(payload_file, 'wb') as payload_fp:
+      payload_fp.write(os.urandom(8192))
+    return payload_file
+
+  def test_ParseApexPayloadInfo(self):
+    payload_file = self._GetTestPayload()
+    apex_utils.SignApexPayload(
+        payload_file, self.payload_key, 'testkey', 'SHA256_RSA2048', self.SALT)
+    payload_info = apex_utils.ParseApexPayloadInfo(payload_file)
+    self.assertEqual('SHA256_RSA2048', payload_info['Algorithm'])
+    self.assertEqual(self.SALT, payload_info['Salt'])
+    self.assertEqual('testkey', payload_info['apex.key'])
+
+  def test_SignApexPayload(self):
+    payload_file = self._GetTestPayload()
+    apex_utils.SignApexPayload(
+        payload_file, self.payload_key, 'testkey', 'SHA256_RSA2048', self.SALT)
+    apex_utils.VerifyApexPayload(payload_file, self.payload_key)
+
+  def test_SignApexPayload_withSignerHelper(self):
+    payload_file = self._GetTestPayload()
+    payload_signer_args = '--signing_helper_with_files {}'.format(
+        os.path.join(self.testdata_dir, 'signing_helper.sh'))
+    apex_utils.SignApexPayload(
+        payload_file,
+        self.payload_key,
+        'testkey', 'SHA256_RSA2048', self.SALT,
+        payload_signer_args)
+    apex_utils.VerifyApexPayload(payload_file, self.payload_key)
+
+  def test_SignApexPayload_invalidKey(self):
+    self.assertRaises(
+        apex_utils.ApexSigningError,
+        apex_utils.SignApexPayload,
+        self._GetTestPayload(),
+        os.path.join(self.testdata_dir, 'testkey.x509.pem'),
+        'testkey',
+        'SHA256_RSA2048',
+        self.SALT)
+
+  def test_VerifyApexPayload_wrongKey(self):
+    payload_file = self._GetTestPayload()
+    apex_utils.SignApexPayload(
+        payload_file, self.payload_key, 'testkey', 'SHA256_RSA2048', self.SALT)
+    apex_utils.VerifyApexPayload(payload_file, self.payload_key)
+    self.assertRaises(
+        apex_utils.ApexSigningError,
+        apex_utils.VerifyApexPayload,
+        payload_file,
+        os.path.join(self.testdata_dir, 'testkey_with_passwd.key'))
diff --git a/tools/releasetools/test_sign_target_files_apks.py b/tools/releasetools/test_sign_target_files_apks.py
index 18762ee..9d21429 100644
--- a/tools/releasetools/test_sign_target_files_apks.py
+++ b/tools/releasetools/test_sign_target_files_apks.py
@@ -21,8 +21,8 @@
 import common
 import test_utils
 from sign_target_files_apks import (
-    CheckAllApksSigned, EditTags, GetApkFileInfo, ReplaceCerts,
-    ReplaceVerityKeyId, RewriteProps)
+    CheckApkAndApexKeysAvailable, EditTags, GetApkFileInfo, ReadApexKeysInfo,
+    ReplaceCerts, ReplaceVerityKeyId, RewriteProps)
 
 
 class SignTargetFilesApksTest(test_utils.ReleaseToolsTestCase):
@@ -33,6 +33,10 @@
   <signer signature="{}"><seinfo value="media"/></signer>
 </policy>"""
 
+  APEX_KEYS_TXT = """name="apex.apexd_test.apex" public_key="system/apex/apexd/apexd_testdata/com.android.apex.test_package.avbpubkey" private_key="system/apex/apexd/apexd_testdata/com.android.apex.test_package.pem" container_certificate="build/target/product/security/testkey.x509.pem" container_private_key="build/target/product/security/testkey.pk8"
+name="apex.apexd_test_different_app.apex" public_key="system/apex/apexd/apexd_testdata/com.android.apex.test_package_2.avbpubkey" private_key="system/apex/apexd/apexd_testdata/com.android.apex.test_package_2.pem" container_certificate="build/target/product/security/testkey.x509.pem" container_private_key="build/target/product/security/testkey.pk8"
+"""
+
   def setUp(self):
     self.testdata_dir = test_utils.get_testdata_dir()
 
@@ -207,7 +211,7 @@
     }
     self.assertEqual(output_xml, ReplaceCerts(input_xml))
 
-  def test_CheckAllApksSigned(self):
+  def test_CheckApkAndApexKeysAvailable(self):
     input_file = common.MakeTempFile(suffix='.zip')
     with zipfile.ZipFile(input_file, 'w') as input_zip:
       input_zip.writestr('SYSTEM/app/App1.apk', "App1-content")
@@ -219,16 +223,17 @@
         'App3.apk' : 'key3',
     }
     with zipfile.ZipFile(input_file) as input_zip:
-      CheckAllApksSigned(input_zip, apk_key_map, None)
-      CheckAllApksSigned(input_zip, apk_key_map, '.gz')
+      CheckApkAndApexKeysAvailable(input_zip, apk_key_map, None)
+      CheckApkAndApexKeysAvailable(input_zip, apk_key_map, '.gz')
 
       # 'App2.apk.gz' won't be considered as an APK.
-      CheckAllApksSigned(input_zip, apk_key_map, None)
-      CheckAllApksSigned(input_zip, apk_key_map, '.xz')
+      CheckApkAndApexKeysAvailable(input_zip, apk_key_map, None)
+      CheckApkAndApexKeysAvailable(input_zip, apk_key_map, '.xz')
 
       del apk_key_map['App2.apk']
       self.assertRaises(
-          AssertionError, CheckAllApksSigned, input_zip, apk_key_map, '.gz')
+          AssertionError, CheckApkAndApexKeysAvailable, input_zip, apk_key_map,
+          '.gz')
 
   def test_GetApkFileInfo(self):
     (is_apk, is_compressed, should_be_skipped) = GetApkFileInfo(
@@ -344,3 +349,62 @@
     self.assertRaises(
         AssertionError, GetApkFileInfo, "SYSTEM_OTHER/preloads/apps/Chats.apk",
         None, None)
+
+  def test_ReadApexKeysInfo(self):
+    target_files = common.MakeTempFile(suffix='.zip')
+    with zipfile.ZipFile(target_files, 'w') as target_files_zip:
+      target_files_zip.writestr('META/apexkeys.txt', self.APEX_KEYS_TXT)
+
+    with zipfile.ZipFile(target_files) as target_files_zip:
+      keys_info = ReadApexKeysInfo(target_files_zip)
+
+    self.assertEqual(
+        {
+          'apex.apexd_test.apex': (
+              'system/apex/apexd/apexd_testdata/com.android.apex.test_package.pem',
+              'build/target/product/security/testkey'),
+          'apex.apexd_test_different_app.apex': (
+              'system/apex/apexd/apexd_testdata/com.android.apex.test_package_2.pem',
+              'build/target/product/security/testkey'),
+        },
+        keys_info)
+
+  def test_ReadApexKeysInfo_mismatchingKeys(self):
+    # Mismatching payload public / private keys.
+    apex_keys = self.APEX_KEYS_TXT + (
+        'name="apex.apexd_test_different_app2.apex" '
+        'public_key="system/apex/apexd/apexd_testdata/com.android.apex.test_package_2.avbpubkey" '
+        'private_key="system/apex/apexd/apexd_testdata/com.android.apex.test_package_3.pem" '
+        'container_certificate="build/target/product/security/testkey.x509.pem" '
+        'container_private_key="build/target/product/security/testkey.pk8"')
+    target_files = common.MakeTempFile(suffix='.zip')
+    with zipfile.ZipFile(target_files, 'w') as target_files_zip:
+      target_files_zip.writestr('META/apexkeys.txt', apex_keys)
+
+    with zipfile.ZipFile(target_files) as target_files_zip:
+      self.assertRaises(ValueError, ReadApexKeysInfo, target_files_zip)
+
+  def test_ReadApexKeysInfo_missingPrivateKey(self):
+    # Invalid lines will be skipped.
+    apex_keys = self.APEX_KEYS_TXT + (
+        'name="apex.apexd_test_different_app2.apex" '
+        'public_key="system/apex/apexd/apexd_testdata/com.android.apex.test_package_2.avbpubkey" '
+        'container_certificate="build/target/product/security/testkey.x509.pem" '
+        'container_private_key="build/target/product/security/testkey.pk8"')
+    target_files = common.MakeTempFile(suffix='.zip')
+    with zipfile.ZipFile(target_files, 'w') as target_files_zip:
+      target_files_zip.writestr('META/apexkeys.txt', apex_keys)
+
+    with zipfile.ZipFile(target_files) as target_files_zip:
+      keys_info = ReadApexKeysInfo(target_files_zip)
+
+    self.assertEqual(
+        {
+          'apex.apexd_test.apex': (
+              'system/apex/apexd/apexd_testdata/com.android.apex.test_package.pem',
+              'build/target/product/security/testkey'),
+          'apex.apexd_test_different_app.apex': (
+              'system/apex/apexd/apexd_testdata/com.android.apex.test_package_2.pem',
+              'build/target/product/security/testkey'),
+        },
+        keys_info)
diff --git a/tools/releasetools/testdata/signing_helper.sh b/tools/releasetools/testdata/signing_helper.sh
new file mode 100755
index 0000000..364e023
--- /dev/null
+++ b/tools/releasetools/testdata/signing_helper.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+#
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+tmpfile=$(mktemp)
+cat $3 | openssl rsautl -sign -inkey $2 -raw > $tmpfile
+cat $tmpfile > $3
+rm $tmpfile
diff --git a/tools/releasetools/testdata/testkey_with_passwd.key b/tools/releasetools/testdata/testkey_with_passwd.key
new file mode 100644
index 0000000..2f0a199
--- /dev/null
+++ b/tools/releasetools/testdata/testkey_with_passwd.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCwaAOHPqgkCmqU
+AFRnJW6LrAuSfy9EzWSRHSkltp811ByMIE0N6/Nttu8ZCL456lzArHNKt/zdoBik
+eLB6gN9CTvQ8n4LMdSEmkRl3uXBtOPJuVObJ6ZUILz6L7WofWcr8DT81j2At7nHi
+Wg8SkCsFXbFfpjljOlpqUG3Szt+48X8rcgG82s97BuRwNxUgfK1/8QzOiH9fDbMU
+h6XI2jo2VwuBYOsJadJJWOf6oRRHZonrts0FXpV46CXykpLvLT2u5GXg1Pxd7i1K
+v1P8bxZOzVbEVfkL2DnUCtUBAnP98r9UyjQDd4blk4Mwl+mzB5otPTacNzEGhmNK
+Et+HB/cdAgMBAAECggEATsn2IXa7tHUuivHmwLb4O8vY01KY8xrleubSVPTPAUS+
+h1t57ujerbcR7VV5WPay/J9JUyr/9qClwPfioqRikwQek+EOk3ERIF+YR1/8tdvE
+c8DZ337DQIeRYP/l8SCyx4bHH43tADbKiLV+m+TmQhxJt5XPdeE/NtK7andZdwkv
+xEoG9l2aONE4z9pY1x+c1SdDSsq92/iLHLgSkQJmWo+lrfeh6gshXgQgDY8n6rgY
+GsCgSawLphvd8Tvo86CL04l0pWtY1gEW3s6sdYo1YDkpWQzSRCtGm0GlhEt2fyq5
+coTK2sLHguE7NL5VZo4zlGtM3QBdvRksTO1mJOt6JQKBgQDaT4oGjZp1rtKdObvn
+ElaUo5EOyJjmXkRBBndrbiG3078eOqTJHXx45DJUv8hj9+g6vSULiIeFk1FiiMQD
+vcnsBEaGaSc886wXY6TQgIIzvVfzDHGYTuQydiYQbLClH6S28HLqdlZjUIlHwxb9
+wBm8JwmTiVeAEvO8LTzeEqfkLwKBgQDO3He8Ei8XDeqtIK0lzcZ83yw9OGP23/gK
+8GDaf8J+cOtOyYkDlcV0rBNFvE8+TzIpIUlo47b2RSaART3iPSfRJTaySZjKWCVo
+s2A0/zQcrj7GgD2gaHRrgI9bmnWW1j95a9n/6AUEyEIJ6K8tYK819Vl4GAyhNHEQ
+sRbxa69qcwKBgQC5F8jxx2tXLdM6JLIQtzabLZcWTrN8Vh5Od3oWpriF0EzxB02h
+ipN3OBsISdZQE+dcrfNTtP0aHo5ZGZX/ihFCP1nAKjVvczXMWtppQRujXHzOABXr
+ya+mrQ+Wy2B1j7+qr3DvI0gZSjYqltjOaeon4X04DrEWUHtAZ6Z8rpqUVwKBgQCB
+o8mmI/8/A4m/Vmss9fke6P5gn6aGYXah5GPOi6Loevv9NHCZvpMwu2aYnZtMAXX+
+MM5A3fUcAdpPKRXPY2RAvoG42kbXCMbpBwGUNRwDnW/aFySIEu5jMP6m+fYXwc2l
+2uGUb2Q1ywsYCqs+VQl5V3nquaewn5z8SP+H7WTR4QKBgQCO5CRpyNOjEwMxTPR1
+GYUKAEiVtmzknHAxUE6drTgGEZSquAXiau0B5+7+/G5gwqxCLGpnstMByI+dhkR6
++ybAc/bzb2aoGK4pZf/PuwxQQsHBnG0oaSFU6RZlbVV20j7FZ04+cYnKHwCYkKjN
+DwA1Ae+H+u95raB4vYhk7IzD4A==
+-----END PRIVATE KEY-----