releasetools: Allow skipping PRESIGNED APEXes.

This CL adds support that allows treating an APEX as pre-signed. We can
skip signing an APEX with `-e <apex-name>=` and
`--extra_apex_payload_key <apex-name>=`. Note that the payload_key and
container_key must be in consistent state - either they're both
PRESIGNED or none of them is. CheckApkAndApexKeysAvailable() has been
updated to perform the sanity check.

Bug: 123716522
Test: Run sign_target_files_apks.py with the above flags.
Test: python -m unittest test_sign_target_files_apks
Change-Id: Id1e2f3f2facd4a97a385983cc9b78c028f7e7e73
diff --git a/tools/releasetools/sign_target_files_apks.py b/tools/releasetools/sign_target_files_apks.py
index f1f032d..71598e3 100755
--- a/tools/releasetools/sign_target_files_apks.py
+++ b/tools/releasetools/sign_target_files_apks.py
@@ -166,7 +166,7 @@
 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.
+  Presigned payload / container keys will be set accordingly.
 
   Args:
     keys_info: A dict that maps from APEX filenames to a tuple of (payload_key,
@@ -180,7 +180,8 @@
   # 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)
+    if not key:
+      key = 'PRESIGNED'
     keys_info[apex] = (key, keys_info[apex][1])
 
   # Apply the key remapping to container keys.
@@ -192,7 +193,8 @@
     # Skip non-APEX containers.
     if apex not in keys_info:
       continue
-    assert key, 'Presigned APEX container for {} is not allowed'.format(apex)
+    if not key:
+      key = 'PRESIGNED'
     keys_info[apex] = (keys_info[apex][0], key_map.get(key, key))
 
   return keys_info
@@ -245,7 +247,7 @@
 
 
 def CheckApkAndApexKeysAvailable(input_tf_zip, known_keys,
-                                 compressed_extension):
+                                 compressed_extension, apex_keys):
   """Checks that all the APKs and APEXes have keys specified.
 
   Args:
@@ -253,6 +255,8 @@
     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.
+    apex_keys: A dict that contains the key mapping from APEX name to
+        (payload_key, container_key).
 
   Raises:
     AssertionError: On finding unknown APKs and APEXes.
@@ -284,6 +288,31 @@
        "Use '-e <apkname>=' to specify a key (which may be an empty string to "
        "not sign this apk).".format("\n  ".join(unknown_files)))
 
+  # For all the APEXes, double check that we won't have an APEX that has only
+  # one of the payload / container keys set.
+  if not apex_keys:
+    return
+
+  invalid_apexes = []
+  for info in input_tf_zip.infolist():
+    if (not info.filename.startswith('SYSTEM/apex') or
+        not info.filename.endswith('.apex')):
+      continue
+
+    name = os.path.basename(info.filename)
+    (payload_key, container_key) = apex_keys[name]
+    if ((payload_key in common.SPECIAL_CERT_STRINGS and
+         container_key not in common.SPECIAL_CERT_STRINGS) or
+        (payload_key not in common.SPECIAL_CERT_STRINGS and
+         container_key in common.SPECIAL_CERT_STRINGS)):
+      invalid_apexes.append(
+          "{}: payload_key {}, container_key {}".format(
+              name, payload_key, container_key))
+
+  assert not invalid_apexes, \
+      "Invalid APEX keys specified:\n  {}\n".format(
+          "\n  ".join(invalid_apexes))
+
 
 def SignApk(data, keyname, pw, platform_api_level, codename_to_api_level_map,
             is_compressed):
@@ -468,19 +497,29 @@
       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))
+      # We've asserted not having a case with only one of them PRESIGNED.
+      if (payload_key not in common.SPECIAL_CERT_STRINGS and
+          container_key not in common.SPECIAL_CERT_STRINGS):
+        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)
+        (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
 
-      updated_apex_payload_keys[payload_key_name] = payload_key
+      else:
+        print(
+            "NOT signing: %s\n"
+            "        (skipped due to special cert string)" % (name,))
+        common.ZipWriteStr(output_tf_zip, out_info, data)
 
     # AVB public keys for the installed APEXes, which will be updated later.
     elif (os.path.dirname(filename) == 'SYSTEM/etc/security/apex' and
@@ -557,8 +596,10 @@
       continue
 
     name = os.path.basename(filename)
-    assert name in updated_apex_payload_keys, \
-        'Unsigned APEX payload key: {}'.format(filename)
+
+    # Skip PRESIGNED APEXes.
+    if name not in updated_apex_payload_keys:
+      continue
 
     key_path = updated_apex_payload_keys[name]
     if not os.path.exists(key_path) and not key_path.endswith('.pem'):
@@ -1181,7 +1222,8 @@
   CheckApkAndApexKeysAvailable(
       input_zip,
       set(apk_keys.keys()) | set(apex_keys.keys()),
-      compressed_extension)
+      compressed_extension,
+      apex_keys)
 
   key_passwords = common.GetKeyPasswords(
       set(apk_keys.values()) | set(itertools.chain(*apex_keys.values())))
diff --git a/tools/releasetools/test_sign_target_files_apks.py b/tools/releasetools/test_sign_target_files_apks.py
index 9d21429..6082baf 100644
--- a/tools/releasetools/test_sign_target_files_apks.py
+++ b/tools/releasetools/test_sign_target_files_apks.py
@@ -33,6 +33,7 @@
   <signer signature="{}"><seinfo value="media"/></signer>
 </policy>"""
 
+  # pylint: disable=line-too-long
   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"
 """
@@ -223,17 +224,50 @@
         'App3.apk' : 'key3',
     }
     with zipfile.ZipFile(input_file) as input_zip:
-      CheckApkAndApexKeysAvailable(input_zip, apk_key_map, None)
-      CheckApkAndApexKeysAvailable(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.
-      CheckApkAndApexKeysAvailable(input_zip, apk_key_map, None)
-      CheckApkAndApexKeysAvailable(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, CheckApkAndApexKeysAvailable, input_zip, apk_key_map,
-          '.gz')
+          '.gz', {})
+
+  def test_CheckApkAndApexKeysAvailable_invalidApexKeys(self):
+    input_file = common.MakeTempFile(suffix='.zip')
+    with zipfile.ZipFile(input_file, 'w') as input_zip:
+      input_zip.writestr('SYSTEM/apex/Apex1.apex', "Apex1-content")
+      input_zip.writestr('SYSTEM/apex/Apex2.apex', "Apex2-content")
+
+    apk_key_map = {
+        'Apex1.apex' : 'key1',
+        'Apex2.apex' : 'key2',
+        'Apex3.apex' : 'key3',
+    }
+    apex_keys = {
+        'Apex1.apex' : ('payload-key1', 'container-key1'),
+        'Apex2.apex' : ('payload-key2', 'container-key2'),
+    }
+    with zipfile.ZipFile(input_file) as input_zip:
+      CheckApkAndApexKeysAvailable(input_zip, apk_key_map, None, apex_keys)
+
+      # Fine to have both keys as PRESIGNED.
+      apex_keys['Apex2.apex'] = ('PRESIGNED', 'PRESIGNED')
+      CheckApkAndApexKeysAvailable(input_zip, apk_key_map, None, apex_keys)
+
+      # Having only one of them as PRESIGNED is not allowed.
+      apex_keys['Apex2.apex'] = ('payload-key2', 'PRESIGNED')
+      self.assertRaises(
+          AssertionError, CheckApkAndApexKeysAvailable, input_zip, apk_key_map,
+          None, apex_keys)
+
+      apex_keys['Apex2.apex'] = ('PRESIGNED', 'container-key1')
+      self.assertRaises(
+          AssertionError, CheckApkAndApexKeysAvailable, input_zip, apk_key_map,
+          None, apex_keys)
 
   def test_GetApkFileInfo(self):
     (is_apk, is_compressed, should_be_skipped) = GetApkFileInfo(
@@ -358,16 +392,14 @@
     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)
+    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.
@@ -398,13 +430,11 @@
     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)
+    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)