releasetools: Support validating Verified Boot images.

For a given (signed) target-files.zip, this CLs allows verifying the
Verified Boot related images. It works with both of VB 1.0 and VB 2.0
images.

As part of the CL, it also moves validate_target_files.py to argparse,
which is more flexible than the traditional getopt module.

Also add unittests for the VB 1.0 path. VB 2.0 tests will be added in
follow-up CL.

Example usage:

- Run the script on aosp_bullhead target-files.zip.

$ ./build/make/tools/releasetools/validate_target_files.py \
    --verity_key build/target/product/security/verity.x509.pem \
    --verity_key_mincrypt build/target/product/security/verity_key \
    aosp_bullhead-target_files-4522605.zip

- Run the script on aosp_walleye target-files.zip.

$ ./build/make/tools/releasetools/validate_target_files.py \
    --verity_key external/avb/test/data/testkey_rsa4096.pem \
    aosp_walleye-target_files-4627254.zip

Bug: 63706333
Bug: 65486807
Test: Run validate_target_files.py on target_files.zip files.
Test: PYTHONPATH=build/make/tools/releasetools python -m unittest \
          test_validate_target_files
Change-Id: I170f14d5828d15f3687d8af0a89a816968069057
diff --git a/tools/releasetools/test_validate_target_files.py b/tools/releasetools/test_validate_target_files.py
new file mode 100644
index 0000000..bae648f
--- /dev/null
+++ b/tools/releasetools/test_validate_target_files.py
@@ -0,0 +1,180 @@
+#
+# Copyright (C) 2018 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.
+#
+
+"""Unittests for validate_target_files.py.
+
+Note: This file calls functions in build_image.py that hard-code the path in
+relative to ANDROID_BUILD_TOP (e.g.
+system/extras/verity/build_verity_metadata.py). So the test needs to be
+triggered under ANDROID_BUILD_TOP or the top-level OTA tools directory (i.e.
+the one after unzipping otatools.zip).
+
+  (from ANDROID_BUILD_TOP)
+  $ PYTHONPATH=build/make/tools/releasetools python -m unittest \\
+      test_validate_target_files
+
+  (from OTA tools directory)
+  $ PYTHONPATH=releasetools python -m unittest test_validate_target_files
+"""
+
+from __future__ import print_function
+
+import os
+import os.path
+import shutil
+import subprocess
+import unittest
+
+import build_image
+import common
+import test_utils
+from validate_target_files import ValidateVerifiedBootImages
+
+
+class ValidateTargetFilesTest(unittest.TestCase):
+
+  def setUp(self):
+    self.testdata_dir = test_utils.get_testdata_dir()
+
+  def tearDown(self):
+    common.Cleanup()
+
+  def _generate_boot_image(self, output_file):
+    kernel = common.MakeTempFile(prefix='kernel-')
+    with open(kernel, 'wb') as kernel_fp:
+      kernel_fp.write(os.urandom(10))
+
+    cmd = ['mkbootimg', '--kernel', kernel, '-o', output_file]
+    proc = common.Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+    stdoutdata, _ = proc.communicate()
+    self.assertEqual(
+        0, proc.returncode,
+        "Failed to run mkbootimg: {}".format(stdoutdata))
+
+    cmd = ['boot_signer', '/boot', output_file,
+           os.path.join(self.testdata_dir, 'testkey.pk8'),
+           os.path.join(self.testdata_dir, 'testkey.x509.pem'), output_file]
+    proc = common.Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+    stdoutdata, _ = proc.communicate()
+    self.assertEqual(
+        0, proc.returncode,
+        "Failed to sign boot image with boot_signer: {}".format(stdoutdata))
+
+  def test_ValidateVerifiedBootImages_bootImage(self):
+    input_tmp = common.MakeTempDir()
+    os.mkdir(os.path.join(input_tmp, 'IMAGES'))
+    boot_image = os.path.join(input_tmp, 'IMAGES', 'boot.img')
+    self._generate_boot_image(boot_image)
+
+    info_dict = {
+        'boot_signer' : 'true',
+    }
+    options = {
+        'verity_key' : os.path.join(self.testdata_dir, 'testkey.x509.pem'),
+    }
+    ValidateVerifiedBootImages(input_tmp, info_dict, options)
+
+  def test_ValidateVerifiedBootImages_bootImage_wrongKey(self):
+    input_tmp = common.MakeTempDir()
+    os.mkdir(os.path.join(input_tmp, 'IMAGES'))
+    boot_image = os.path.join(input_tmp, 'IMAGES', 'boot.img')
+    self._generate_boot_image(boot_image)
+
+    info_dict = {
+        'boot_signer' : 'true',
+    }
+    options = {
+        'verity_key' : os.path.join(self.testdata_dir, 'verity.x509.pem'),
+    }
+    self.assertRaises(
+        AssertionError, ValidateVerifiedBootImages, input_tmp, info_dict,
+        options)
+
+  def test_ValidateVerifiedBootImages_bootImage_corrupted(self):
+    input_tmp = common.MakeTempDir()
+    os.mkdir(os.path.join(input_tmp, 'IMAGES'))
+    boot_image = os.path.join(input_tmp, 'IMAGES', 'boot.img')
+    self._generate_boot_image(boot_image)
+
+    # Corrupt the late byte of the image.
+    with open(boot_image, 'r+b') as boot_fp:
+      boot_fp.seek(-1, os.SEEK_END)
+      last_byte = boot_fp.read(1)
+      last_byte = chr(255 - ord(last_byte))
+      boot_fp.seek(-1, os.SEEK_END)
+      boot_fp.write(last_byte)
+
+    info_dict = {
+        'boot_signer' : 'true',
+    }
+    options = {
+        'verity_key' : os.path.join(self.testdata_dir, 'testkey.x509.pem'),
+    }
+    self.assertRaises(
+        AssertionError, ValidateVerifiedBootImages, input_tmp, info_dict,
+        options)
+
+  def _generate_system_image(self, output_file):
+    verity_fec = True
+    partition_size = 1024 * 1024
+    adjusted_size, verity_size = build_image.AdjustPartitionSizeForVerity(
+        partition_size, verity_fec)
+
+    # Use an empty root directory.
+    system_root = common.MakeTempDir()
+    cmd = ['mkuserimg_mke2fs.sh', '-s', system_root, output_file, 'ext4',
+           '/system', str(adjusted_size), '-j', '0']
+    proc = common.Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+    stdoutdata, _ = proc.communicate()
+    self.assertEqual(
+        0, proc.returncode,
+        "Failed to create system image with mkuserimg_mke2fs.sh: {}".format(
+            stdoutdata))
+
+    # Append the verity metadata.
+    prop_dict = {
+        'original_partition_size' : str(partition_size),
+        'partition_size' : str(adjusted_size),
+        'verity_block_device' : '/dev/block/system',
+        'verity_key' : os.path.join(self.testdata_dir, 'testkey'),
+        'verity_signer_cmd' : 'verity_signer',
+        'verity_size' : str(verity_size),
+    }
+    self.assertTrue(
+        build_image.MakeVerityEnabledImage(output_file, verity_fec, prop_dict))
+
+  def test_ValidateVerifiedBootImages_systemImage(self):
+    input_tmp = common.MakeTempDir()
+    os.mkdir(os.path.join(input_tmp, 'IMAGES'))
+    system_image = os.path.join(input_tmp, 'IMAGES', 'system.img')
+    self._generate_system_image(system_image)
+
+    # Pack the verity key.
+    verity_key_mincrypt = os.path.join(
+        input_tmp, 'BOOT', 'RAMDISK', 'verity_key')
+    os.makedirs(os.path.dirname(verity_key_mincrypt))
+    shutil.copyfile(
+        os.path.join(self.testdata_dir, 'testkey_mincrypt'),
+        verity_key_mincrypt)
+
+    info_dict = {
+        'verity' : 'true',
+    }
+    options = {
+        'verity_key' : os.path.join(self.testdata_dir, 'testkey.x509.pem'),
+        'verity_key_mincrypt' : verity_key_mincrypt,
+    }
+    ValidateVerifiedBootImages(input_tmp, info_dict, options)
diff --git a/tools/releasetools/testdata/testkey_mincrypt b/tools/releasetools/testdata/testkey_mincrypt
new file mode 100644
index 0000000..7f5d31b
--- /dev/null
+++ b/tools/releasetools/testdata/testkey_mincrypt
Binary files differ
diff --git a/tools/releasetools/validate_target_files.py b/tools/releasetools/validate_target_files.py
index db1ba2e..e8cea29 100755
--- a/tools/releasetools/validate_target_files.py
+++ b/tools/releasetools/validate_target_files.py
@@ -17,16 +17,25 @@
 """
 Validate a given (signed) target_files.zip.
 
-It performs checks to ensure the integrity of the input zip.
+It performs the following checks to assert the integrity of the input zip.
+
  - It verifies the file consistency between the ones in IMAGES/system.img (read
    via IMAGES/system.map) and the ones under unpacked folder of SYSTEM/. The
    same check also applies to the vendor image if present.
+
+ - It verifies the install-recovery script consistency, by comparing the
+   checksums in the script against the ones of IMAGES/{boot,recovery}.img.
+
+ - It verifies the signed Verified Boot related images, for both of Verified
+   Boot 1.0 and 2.0 (aka AVB).
 """
 
+import argparse
+import filecmp
 import logging
 import os.path
 import re
-import sys
+import subprocess
 import zipfile
 
 import common
@@ -177,33 +186,152 @@
   logging.info('Done checking %s', script_path)
 
 
-def main(argv):
-  def option_handler():
-    return True
+def ValidateVerifiedBootImages(input_tmp, info_dict, options):
+  """Validates the Verified Boot related images.
 
-  args = common.ParseOptions(
-      argv, __doc__, extra_opts="",
-      extra_long_opts=[],
-      extra_option_handler=option_handler)
+  For Verified Boot 1.0, it verifies the signatures of the bootable images
+  (boot/recovery etc), as well as the dm-verity metadata in system images
+  (system/vendor/product). For Verified Boot 2.0, it calls avbtool to verify
+  vbmeta.img, which in turn verifies all the descriptors listed in vbmeta.
 
-  if len(args) != 1:
-    common.Usage(__doc__)
-    sys.exit(1)
+  Args:
+    input_tmp: The top-level directory of unpacked target-files.zip.
+    info_dict: The loaded info dict.
+    options: A dict that contains the user-supplied public keys to be used for
+        image verification. In particular, 'verity_key' is used to verify the
+        bootable images in VB 1.0, and the vbmeta image in VB 2.0, where
+        applicable. 'verity_key_mincrypt' will be used to verify the system
+        images in VB 1.0.
+
+  Raises:
+    AssertionError: On any verification failure.
+  """
+  # Verified boot 1.0 (images signed with boot_signer and verity_signer).
+  if info_dict.get('boot_signer') == 'true':
+    logging.info('Verifying Verified Boot images...')
+
+    # Verify the boot/recovery images (signed with boot_signer), against the
+    # given X.509 encoded pubkey (or falling back to the one in the info_dict if
+    # none given).
+    verity_key = options['verity_key']
+    if verity_key is None:
+      verity_key = info_dict['verity_key'] + '.x509.pem'
+    for image in ('boot.img', 'recovery.img', 'recovery-two-step.img'):
+      image_path = os.path.join(input_tmp, 'IMAGES', image)
+      if not os.path.exists(image_path):
+        continue
+
+      cmd = ['boot_signer', '-verify', image_path, '-certificate', verity_key]
+      proc = common.Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+      stdoutdata, _ = proc.communicate()
+      assert proc.returncode == 0, \
+          'Failed to verify {} with boot_signer:\n{}'.format(image, stdoutdata)
+      logging.info(
+          'Verified %s with boot_signer (key: %s):\n%s', image, verity_key,
+          stdoutdata.rstrip())
+
+  # Verify verity signed system images in Verified Boot 1.0. Note that not using
+  # 'elif' here, since 'boot_signer' and 'verity' are not bundled in VB 1.0.
+  if info_dict.get('verity') == 'true':
+    # First verify that the verity key that's built into the root image (as
+    # /verity_key) matches the one given via command line, if any.
+    if info_dict.get("system_root_image") == "true":
+      verity_key_mincrypt = os.path.join(input_tmp, 'ROOT', 'verity_key')
+    else:
+      verity_key_mincrypt = os.path.join(
+          input_tmp, 'BOOT', 'RAMDISK', 'verity_key')
+    assert os.path.exists(verity_key_mincrypt), 'Missing verity_key'
+
+    if options['verity_key_mincrypt'] is None:
+      logging.warn(
+          'Skipped checking the content of /verity_key, as the key file not '
+          'provided. Use --verity_key_mincrypt to specify.')
+    else:
+      expected_key = options['verity_key_mincrypt']
+      assert filecmp.cmp(expected_key, verity_key_mincrypt, shallow=False), \
+          "Mismatching mincrypt verity key files"
+      logging.info('Verified the content of /verity_key')
+
+    # Then verify the verity signed system/vendor/product images, against the
+    # verity pubkey in mincrypt format.
+    for image in ('system.img', 'vendor.img', 'product.img'):
+      image_path = os.path.join(input_tmp, 'IMAGES', image)
+
+      # We are not checking if the image is actually enabled via info_dict (e.g.
+      # 'system_verity_block_device=...'). Because it's most likely a bug that
+      # skips signing some of the images in signed target-files.zip, while
+      # having the top-level verity flag enabled.
+      if not os.path.exists(image_path):
+        continue
+
+      cmd = ['verity_verifier', image_path, '-mincrypt', verity_key_mincrypt]
+      proc = common.Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+      stdoutdata, _ = proc.communicate()
+      assert proc.returncode == 0, \
+          'Failed to verify {} with verity_verifier (key: {}):\n{}'.format(
+              image, verity_key_mincrypt, stdoutdata)
+      logging.info(
+          'Verified %s with verity_verifier (key: %s):\n%s', image,
+          verity_key_mincrypt, stdoutdata.rstrip())
+
+  # Handle the case of Verified Boot 2.0 (AVB).
+  if info_dict.get("avb_enable") == "true":
+    logging.info('Verifying Verified Boot 2.0 (AVB) images...')
+
+    key = options['verity_key']
+    if key is None:
+      key = info_dict['avb_vbmeta_key_path']
+    # avbtool verifies all the images that have descriptors listed in vbmeta.
+    image = os.path.join(input_tmp, 'IMAGES', 'vbmeta.img')
+    cmd = ['avbtool', 'verify_image', '--image', image, '--key', key]
+    proc = common.Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+    stdoutdata, _ = proc.communicate()
+    assert proc.returncode == 0, \
+        'Failed to verify {} with verity_verifier (key: {}):\n{}'.format(
+            image, key, stdoutdata)
+
+    logging.info(
+        'Verified %s with avbtool (key: %s):\n%s', image, key,
+        stdoutdata.rstrip())
+
+
+def main():
+  parser = argparse.ArgumentParser(
+      description=__doc__,
+      formatter_class=argparse.RawDescriptionHelpFormatter)
+  parser.add_argument(
+      'target_files',
+      help='the input target_files.zip to be validated')
+  parser.add_argument(
+      '--verity_key',
+      help='the verity public key to verify the bootable images (Verified '
+           'Boot 1.0), or the vbmeta image (Verified Boot 2.0), where '
+           'applicable')
+  parser.add_argument(
+      '--verity_key_mincrypt',
+      help='the verity public key in mincrypt format to verify the system '
+           'images, if target using Verified Boot 1.0')
+  args = parser.parse_args()
+
+  # Unprovided args will have 'None' as the value.
+  options = vars(args)
 
   logging_format = '%(asctime)s - %(filename)s - %(levelname)-8s: %(message)s'
   date_format = '%Y/%m/%d %H:%M:%S'
   logging.basicConfig(level=logging.INFO, format=logging_format,
                       datefmt=date_format)
 
-  logging.info("Unzipping the input target_files.zip: %s", args[0])
-  input_tmp = common.UnzipTemp(args[0])
+  logging.info("Unzipping the input target_files.zip: %s", args.target_files)
+  input_tmp = common.UnzipTemp(args.target_files)
 
-  with zipfile.ZipFile(args[0], 'r') as input_zip:
+  with zipfile.ZipFile(args.target_files, 'r') as input_zip:
     ValidateFileConsistency(input_zip, input_tmp)
 
   info_dict = common.LoadInfoDict(input_tmp)
   ValidateInstallRecoveryScript(input_tmp, info_dict)
 
+  ValidateVerifiedBootImages(input_tmp, info_dict, options)
+
   # TODO: Check if the OTA keys have been properly updated (the ones on /system,
   # in recovery image).
 
@@ -212,6 +340,6 @@
 
 if __name__ == '__main__':
   try:
-    main(sys.argv[1:])
+    main()
   finally:
     common.Cleanup()