Initial commit of certify_bootimg.py

Adding a new script, certify_bootimg.py, to add boot
certificates for a given boot image. The script adds
two certificates: 'boot' and 'generic_kernel'. The former
is to certify the entire boot.img, while the latter
is to certify the kernel packed in the boot.img.

It assumes all boot certificates are within the last 16K
of the boot image, i.e., the boot signature block, before
adding the AVB footer.

It also adds a non-signed AVB hash footer, for device with
AVB to use the output boot image directly if it is unlocked,
where the verification error is allowed.

An usage example:
    certify_bootimg --boot_img boot.img \
	--algorithm SHA256_RSA4096 \
	--key external/avb/test/data/testkey_rsa4096.pem \
	--extra_args "--prop foo:bar" \
	--extra_args "--prop gki:nice" \
        --output boot-certified.img

Bug: 223288963
Test: atest --host certify_bootimg_test
Change-Id: Id03d9967b89d87f3d3e0ce08b886909c68fac18c
diff --git a/gki/Android.bp b/gki/Android.bp
index ac56d38..5173852 100644
--- a/gki/Android.bp
+++ b/gki/Android.bp
@@ -17,6 +17,39 @@
 }
 
 python_binary_host {
+    name: "certify_bootimg",
+    defaults: ["mkbootimg_defaults"],
+    main: "certify_bootimg.py",
+    srcs: [
+        "certify_bootimg.py",
+        "generate_gki_certificate.py",
+    ],
+    required: [
+        "avbtool",
+        "unpack_bootimg",
+    ],
+}
+
+python_test_host {
+    name: "certify_bootimg_test",
+    defaults: ["mkbootimg_defaults"],
+    main: "certify_bootimg_test.py",
+    srcs: [
+        "certify_bootimg_test.py",
+    ],
+    data: [
+        ":avbtool",
+        ":certify_bootimg",
+        ":mkbootimg",
+        ":unpack_bootimg",
+        "testdata/*",
+    ],
+    test_options: {
+        unit_test: true,
+    },
+}
+
+python_binary_host {
     name: "generate_gki_certificate",
     defaults: ["mkbootimg_defaults"],
     srcs: [
diff --git a/gki/certify_bootimg.py b/gki/certify_bootimg.py
new file mode 100755
index 0000000..1543698
--- /dev/null
+++ b/gki/certify_bootimg.py
@@ -0,0 +1,195 @@
+#!/usr/bin/env python3
+#
+# Copyright 2022, 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.
+#
+
+"""Certify a GKI boot image by generating and appending its boot_signature."""
+
+from argparse import ArgumentParser
+import os
+import shutil
+import subprocess
+import tempfile
+
+from generate_gki_certificate import generate_gki_certificate
+
+BOOT_SIGNATURE_SIZE = 16 * 1024
+
+
+def get_kernel(boot_img):
+    """Extracts the kernel from |boot_img| and returns it."""
+    with tempfile.TemporaryDirectory() as unpack_dir:
+        unpack_bootimg_cmd = [
+            'unpack_bootimg',
+            '--boot_img', boot_img,
+            '--out', unpack_dir,
+        ]
+        subprocess.run(unpack_bootimg_cmd, check=True,
+                       stdout=subprocess.DEVNULL)
+
+        with open(os.path.join(unpack_dir, 'kernel'), 'rb') as kernel:
+            kernel_bytes = kernel.read()
+            assert len(kernel_bytes) > 0
+            return kernel_bytes
+
+
+def add_certificate(boot_img, algorithm, key, extra_args):
+    """Appends certificates to the end of the boot image.
+
+    This functions appends two certificates to the end of the |boot_img|:
+    the 'boot' certificate and the 'generic_kernel' certificate. The former
+    is to certify the entire |boot_img|, while the latter is to certify
+    the kernel inside the |boot_img|.
+    """
+
+    def generate_certificate(image, certificate_name):
+        """Generates the certificate and returns the certificate content."""
+        with tempfile.NamedTemporaryFile() as output_certificate:
+            generate_gki_certificate(
+                image=image, avbtool='avbtool', name=certificate_name,
+                algorithm=algorithm, key=key, salt='d00df00d',
+                additional_avb_args=extra_args, output=output_certificate.name)
+            output_certificate.seek(os.SEEK_SET, 0)
+            return output_certificate.read()
+
+    boot_signature_bytes = b''
+    boot_signature_bytes += generate_certificate(boot_img, 'boot')
+
+    with tempfile.NamedTemporaryFile() as kernel_img:
+        kernel_img.write(get_kernel(boot_img))
+        kernel_img.flush()
+        boot_signature_bytes += generate_certificate(kernel_img.name,
+                                                     'generic_kernel')
+
+    if len(boot_signature_bytes) > BOOT_SIGNATURE_SIZE:
+        raise ValueError(
+            f'boot_signature size must be <= {BOOT_SIGNATURE_SIZE}')
+    boot_signature_bytes += (
+        b'\0' * (BOOT_SIGNATURE_SIZE - len(boot_signature_bytes)))
+    assert len(boot_signature_bytes) == BOOT_SIGNATURE_SIZE
+
+    with open(boot_img, 'ab') as f:
+        f.write(boot_signature_bytes)
+
+
+def erase_certificate_and_avb_footer(boot_img):
+    """Erases the boot certificate and avb footer.
+
+    A boot image might already contain a certificate and/or a AVB footer.
+    This function erases these additional metadata from the |boot_img|.
+    """
+    # Tries to erase the AVB footer first, which may or may not exist.
+    avbtool_cmd = ['avbtool', 'erase_footer', '--image', boot_img]
+    subprocess.run(avbtool_cmd, check=False, stderr=subprocess.DEVNULL)
+    assert os.path.getsize(boot_img) > 0
+
+    # No boot signature to erase, just return.
+    if os.path.getsize(boot_img) <= BOOT_SIGNATURE_SIZE:
+        return
+
+    # Checks if the last 16K is a boot signature, then erases it.
+    with open(boot_img, 'rb') as image:
+        image.seek(-BOOT_SIGNATURE_SIZE, os.SEEK_END)
+        boot_signature = image.read(BOOT_SIGNATURE_SIZE)
+        assert len(boot_signature) == BOOT_SIGNATURE_SIZE
+
+    with tempfile.NamedTemporaryFile() as signature_tmpfile:
+        signature_tmpfile.write(boot_signature)
+        signature_tmpfile.flush()
+        avbtool_info_cmd = [
+            'avbtool', 'info_image', '--image', signature_tmpfile.name]
+        result = subprocess.run(avbtool_info_cmd, check=False,
+                                stdout=subprocess.DEVNULL,
+                                stderr=subprocess.DEVNULL)
+        has_boot_signature = (result.returncode == 0)
+
+    if has_boot_signature:
+        new_file_size = os.path.getsize(boot_img) - BOOT_SIGNATURE_SIZE
+        os.truncate(boot_img, new_file_size)
+
+    assert os.path.getsize(boot_img) > 0
+
+
+def get_avb_image_size(image):
+    """Returns the image size if there is a AVB footer, else return zero."""
+
+    avbtool_info_cmd = ['avbtool', 'info_image', '--image', image]
+    result = subprocess.run(avbtool_info_cmd, check=False,
+                            stdout=subprocess.DEVNULL,
+                            stderr=subprocess.DEVNULL)
+
+    if result.returncode == 0:
+        return os.path.getsize(image)
+
+    return 0
+
+
+def add_avb_footer(image, partition_size):
+    """Appends a AVB hash footer to the image."""
+
+    avbtool_cmd = ['avbtool', 'add_hash_footer', '--image', image,
+                   '--partition_name', 'boot']
+
+    if partition_size:
+        avbtool_cmd.extend(['--partition_size', str(partition_size)])
+    else:
+        avbtool_cmd.extend(['--dynamic_partition_size'])
+
+    subprocess.check_call(avbtool_cmd)
+
+
+def parse_cmdline():
+    """Parse command-line options."""
+    parser = ArgumentParser(add_help=True)
+
+    # Required args.
+    parser.add_argument('--boot_img', required=True,
+                        help='path to the boot image to certify')
+    parser.add_argument('--algorithm', required=True,
+                        help='signing algorithm for the certificate')
+    parser.add_argument('--key', required=True,
+                        help='path to the RSA private key')
+    parser.add_argument('-o', '--output', required=True,
+                        help='output file name')
+
+    # Optional args.
+    parser.add_argument('--extra_args', default=[], action='append',
+                        help='extra arguments to be forwarded to avbtool')
+
+    args = parser.parse_args()
+
+    extra_args = []
+    for a in args.extra_args:
+        extra_args.extend(a.split())
+    args.extra_args = extra_args
+
+    return args
+
+
+def main():
+    """Parse arguments and certify the boot image."""
+    args = parse_cmdline()
+
+    shutil.copy2(args.boot_img, args.output)
+    erase_certificate_and_avb_footer(args.output)
+
+    add_certificate(args.output, args.algorithm, args.key, args.extra_args)
+
+    avb_partition_size = get_avb_image_size(args.boot_img)
+    add_avb_footer(args.output, avb_partition_size)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/gki/certify_bootimg_test.py b/gki/certify_bootimg_test.py
new file mode 100644
index 0000000..25cdbff
--- /dev/null
+++ b/gki/certify_bootimg_test.py
@@ -0,0 +1,417 @@
+#!/usr/bin/env python3
+#
+# Copyright 2022, 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.
+
+"""Tests certify_bootimg."""
+
+import logging
+import os
+import random
+import shutil
+import struct
+import subprocess
+import sys
+import tempfile
+import unittest
+
+BOOT_SIGNATURE_SIZE = 16 * 1024
+
+TEST_KERNEL_CMDLINE = (
+    'printk.devkmsg=on firmware_class.path=/vendor/etc/ init=/init '
+    'kfence.sample_interval=500 loop.max_part=7 bootconfig'
+)
+
+
+def generate_test_file(pathname, size, seed=None):
+    """Generates a gibberish-filled test file and returns its pathname."""
+    random.seed(os.path.basename(pathname) if seed is None else seed)
+    with open(pathname, 'wb') as file:
+        file.write(random.randbytes(size))
+    return pathname
+
+
+def generate_test_boot_image(boot_img, avb_partition_size=None):
+    """Generates a test boot.img without a ramdisk."""
+    with tempfile.NamedTemporaryFile() as kernel_tmpfile:
+        generate_test_file(pathname=kernel_tmpfile.name, size=0x1000,
+                           seed='kernel')
+        kernel_tmpfile.flush()
+
+        mkbootimg_cmds = [
+            'mkbootimg',
+            '--header_version', '4',
+            '--kernel', kernel_tmpfile.name,
+            '--cmdline', TEST_KERNEL_CMDLINE,
+            '--os_version', '12.0.0',
+            '--os_patch_level', '2022-03',
+            '--output', boot_img,
+        ]
+        subprocess.check_call(mkbootimg_cmds)
+
+    if avb_partition_size:
+        avbtool_cmd = ['avbtool', 'add_hash_footer', '--image', boot_img,
+                       '--partition_name', 'boot',
+                       '--partition_size', str(avb_partition_size)]
+        subprocess.check_call(avbtool_cmd)
+
+
+def has_avb_footer(image):
+    """Returns true if the image has a avb footer."""
+
+    avbtool_info_cmd = ['avbtool', 'info_image', '--image', image]
+    result = subprocess.run(avbtool_info_cmd, check=False,
+                            stdout=subprocess.DEVNULL,
+                            stderr=subprocess.DEVNULL)
+
+    return result.returncode == 0
+
+
+def get_vbmeta_size(vbmeta_bytes):
+    """Returns the total size of a AvbVBMeta image."""
+
+    # Keep in sync with |AvbVBMetaImageHeader|.
+    AVB_MAGIC = b'AVB0'                        # pylint: disable=C0103
+    AVB_VBMETA_IMAGE_HEADER_SIZE = 256         # pylint: disable=C0103
+    FORMAT_STRING = (                          # pylint: disable=C0103
+        '!4s2L'      # magic, 2 x version.
+        '2Q'         # 2 x block size: Authentication and Auxiliary blocks.
+    )
+
+    if len(vbmeta_bytes) < struct.calcsize(FORMAT_STRING):
+        return 0
+
+    data = vbmeta_bytes[:struct.calcsize(FORMAT_STRING)]
+    (magic, _, _,
+     authentication_block_size,
+     auxiliary_data_block_size) = struct.unpack(FORMAT_STRING, data)
+
+    if magic == AVB_MAGIC:
+        return (AVB_VBMETA_IMAGE_HEADER_SIZE +
+                authentication_block_size +
+                auxiliary_data_block_size)
+    return 0
+
+
+def extract_boot_signatures(boot_img, output_dir):
+    """Extracts the boot signatures of a boot image."""
+
+    boot_img_copy = os.path.join(output_dir, 'boot_image_copy')
+    shutil.copy2(boot_img, boot_img_copy)
+    avbtool_cmd = ['avbtool', 'erase_footer', '--image', boot_img_copy]
+    subprocess.run(avbtool_cmd, check=False, stderr=subprocess.DEVNULL)
+
+    # The boot signature is assumed to be at the end of boot image, after
+    # the AVB footer is erased.
+    with open(boot_img_copy, 'rb') as image:
+        image.seek(-BOOT_SIGNATURE_SIZE, os.SEEK_END)
+        boot_signature_bytes = image.read(BOOT_SIGNATURE_SIZE)
+        assert len(boot_signature_bytes) == BOOT_SIGNATURE_SIZE
+    os.unlink(boot_img_copy)
+
+    num_signatures = 0
+    while True:
+        next_signature_size = get_vbmeta_size(boot_signature_bytes)
+        if next_signature_size <= 0:
+            break
+
+        num_signatures += 1
+        next_signature = boot_signature_bytes[:next_signature_size]
+        output_path = os.path.join(
+            output_dir, 'boot_signature' + str(num_signatures))
+        with open(output_path, 'wb') as output:
+            output.write(next_signature)
+
+        # Moves to the next signature.
+        boot_signature_bytes = boot_signature_bytes[next_signature_size:]
+
+
+class CertifyBootimgTest(unittest.TestCase):
+    """Tests the functionalities of certify_bootimg."""
+
+    def setUp(self):
+        # Saves the test executable directory so that relative path references
+        # to test dependencies don't rely on being manually run from the
+        # executable directory.
+        # With this, we can just open "./testdata/testkey_rsa2048.pem" in the
+        # following tests with subprocess.run(..., cwd=self._exec_dir, ...).
+        self._exec_dir = os.path.abspath(os.path.dirname(sys.argv[0]))
+
+        # Set self.maxDiff to None to see full diff in assertion.
+        # C0103: invalid-name for maxDiff.
+        self.maxDiff = None  # pylint: disable=C0103
+
+        self._EXPECTED_BOOT_SIGNATURE_RSA2048 = (       # pylint: disable=C0103
+            'Minimum libavb version:   1.0\n'
+            'Header Block:             256 bytes\n'
+            'Authentication Block:     320 bytes\n'
+            'Auxiliary Block:          832 bytes\n'
+            'Public key (sha1):        '
+            'cdbb77177f731920bbe0a0f94f84d9038ae0617d\n'
+            'Algorithm:                SHA256_RSA2048\n'
+            'Rollback Index:           0\n'
+            'Flags:                    0\n'
+            'Rollback Index Location:  0\n'
+            "Release String:           'avbtool 1.2.0'\n"
+            'Descriptors:\n'
+            '    Hash descriptor:\n'
+            '      Image Size:            8192 bytes\n'
+            '      Hash Algorithm:        sha256\n'
+            '      Partition Name:        boot\n'           # boot
+            '      Salt:                  d00df00d\n'
+            '      Digest:                '
+            'faf1da72a4fba97ddab0b8f7a410db86'
+            '8fb72392a66d1440ff8bff490c73c771\n'
+            '      Flags:                 0\n'
+            "    Prop: foo -> 'bar'\n"
+            "    Prop: gki -> 'nice'\n"
+        )
+
+        self._EXPECTED_KERNEL_SIGNATURE_RSA2048 = (     # pylint: disable=C0103
+            'Minimum libavb version:   1.0\n'
+            'Header Block:             256 bytes\n'
+            'Authentication Block:     320 bytes\n'
+            'Auxiliary Block:          832 bytes\n'
+            'Public key (sha1):        '
+            'cdbb77177f731920bbe0a0f94f84d9038ae0617d\n'
+            'Algorithm:                SHA256_RSA2048\n'
+            'Rollback Index:           0\n'
+            'Flags:                    0\n'
+            'Rollback Index Location:  0\n'
+            "Release String:           'avbtool 1.2.0'\n"
+            'Descriptors:\n'
+            '    Hash descriptor:\n'
+            '      Image Size:            4096 bytes\n'
+            '      Hash Algorithm:        sha256\n'
+            '      Partition Name:        generic_kernel\n' # generic_kernel
+            '      Salt:                  d00df00d\n'
+            '      Digest:                '
+            '762c877f3af0d50a4a4fbc1385d5c7ce'
+            '52a1288db74b33b72217d93db6f2909f\n'
+            '      Flags:                 0\n'
+            "    Prop: foo -> 'bar'\n"
+            "    Prop: gki -> 'nice'\n"
+        )
+
+        self._EXPECTED_BOOT_SIGNATURE_RSA4096 = (       # pylint: disable=C0103
+            'Minimum libavb version:   1.0\n'
+            'Header Block:             256 bytes\n'
+            'Authentication Block:     576 bytes\n'
+            'Auxiliary Block:          1344 bytes\n'
+            'Public key (sha1):        '
+            '2597c218aae470a130f61162feaae70afd97f011\n'
+            'Algorithm:                SHA256_RSA4096\n'    # RSA4096
+            'Rollback Index:           0\n'
+            'Flags:                    0\n'
+            'Rollback Index Location:  0\n'
+            "Release String:           'avbtool 1.2.0'\n"
+            'Descriptors:\n'
+            '    Hash descriptor:\n'
+            '      Image Size:            8192 bytes\n'
+            '      Hash Algorithm:        sha256\n'
+            '      Partition Name:        boot\n'           # boot
+            '      Salt:                  d00df00d\n'
+            '      Digest:                '
+            'faf1da72a4fba97ddab0b8f7a410db86'
+            '8fb72392a66d1440ff8bff490c73c771\n'
+            '      Flags:                 0\n'
+            "    Prop: foo -> 'bar'\n"
+            "    Prop: gki -> 'nice'\n"
+        )
+
+        self._EXPECTED_KERNEL_SIGNATURE_RSA4096 = (     # pylint: disable=C0103
+            'Minimum libavb version:   1.0\n'
+            'Header Block:             256 bytes\n'
+            'Authentication Block:     576 bytes\n'
+            'Auxiliary Block:          1344 bytes\n'
+            'Public key (sha1):        '
+            '2597c218aae470a130f61162feaae70afd97f011\n'
+            'Algorithm:                SHA256_RSA4096\n'    # RSA4096
+            'Rollback Index:           0\n'
+            'Flags:                    0\n'
+            'Rollback Index Location:  0\n'
+            "Release String:           'avbtool 1.2.0'\n"
+            'Descriptors:\n'
+            '    Hash descriptor:\n'
+            '      Image Size:            4096 bytes\n'
+            '      Hash Algorithm:        sha256\n'
+            '      Partition Name:        generic_kernel\n' # generic_kernel
+            '      Salt:                  d00df00d\n'
+            '      Digest:                '
+            '762c877f3af0d50a4a4fbc1385d5c7ce'
+            '52a1288db74b33b72217d93db6f2909f\n'
+            '      Flags:                 0\n'
+            "    Prop: foo -> 'bar'\n"
+            "    Prop: gki -> 'nice'\n"
+        )
+
+    def _test_boot_signatures(self, signatures_dir, expected_signatures_info):
+        """Tests the info of each boot signature under the signature directory.
+
+        Args:
+            signatures_dir: the directory containing the boot signatures. e.g.,
+                - signatures_dir/boot_signature1
+                - signatures_dir/boot_signature2
+            expected_signatures_info: A dict containing the expected output
+                of `avbtool info_image` for each signature under
+                |signatures_dir|. e.g.,
+                {'boot_signature1': expected_stdout_signature1
+                 'boot_signature2': expected_stdout_signature2}
+        """
+        for signature in expected_signatures_info:
+            avbtool_info_cmds = [
+                'avbtool', 'info_image', '--image',
+                os.path.join(signatures_dir, signature)
+            ]
+            result = subprocess.run(avbtool_info_cmds, check=True,
+                                    capture_output=True, encoding='utf-8')
+            self.assertEqual(result.stdout, expected_signatures_info[signature])
+
+    def test_certify_bootimg_without_avb_footer(self):
+        """Tests certify_bootimg on a boot image without an AVB footer."""
+        with tempfile.TemporaryDirectory() as temp_out_dir:
+            boot_img = os.path.join(temp_out_dir, 'boot.img')
+            generate_test_boot_image(boot_img)
+
+            # Generates the certified boot image, with a RSA2048 key.
+            boot_certified_img = os.path.join(temp_out_dir,
+                                              'boot-certified.img')
+            certify_bootimg_cmds = [
+                'certify_bootimg',
+                '--boot_img', boot_img,
+                '--algorithm', 'SHA256_RSA2048',
+                '--key', './testdata/testkey_rsa2048.pem',
+                '--extra_args', '--prop foo:bar --prop gki:nice',
+                '--output', boot_certified_img,
+            ]
+            subprocess.run(certify_bootimg_cmds, check=True, cwd=self._exec_dir)
+
+            extract_boot_signatures(boot_certified_img, temp_out_dir)
+            self._test_boot_signatures(
+                temp_out_dir,
+                {'boot_signature1': self._EXPECTED_BOOT_SIGNATURE_RSA2048,
+                 'boot_signature2': self._EXPECTED_KERNEL_SIGNATURE_RSA2048})
+
+            # Generates the certified boot image again, with a RSA4096 key.
+            boot_certified2_img = os.path.join(temp_out_dir,
+                                              'boot-certified2.img')
+            certify_bootimg_cmds = [
+                'certify_bootimg',
+                '--boot_img', boot_certified_img,
+                '--algorithm', 'SHA256_RSA4096',
+                '--key', './testdata/testkey_rsa4096.pem',
+                '--extra_args', '--prop foo:bar --prop gki:nice',
+                '--output', boot_certified2_img,
+            ]
+            subprocess.run(certify_bootimg_cmds, check=True, cwd=self._exec_dir)
+
+            extract_boot_signatures(boot_certified2_img, temp_out_dir)
+            self._test_boot_signatures(
+                temp_out_dir,
+                {'boot_signature1': self._EXPECTED_BOOT_SIGNATURE_RSA4096,
+                 'boot_signature2': self._EXPECTED_KERNEL_SIGNATURE_RSA4096})
+
+    def test_certify_bootimg_with_avb_footer(self):
+        """Tests the AVB footer location remains after certify_bootimg."""
+        with tempfile.TemporaryDirectory() as temp_out_dir:
+            boot_img = os.path.join(temp_out_dir, 'boot.img')
+            generate_test_boot_image(boot_img=boot_img,
+                                     avb_partition_size=128 * 1024)
+            self.assertTrue(has_avb_footer(boot_img))
+
+            # Generates the certified boot image, with a RSA2048 key.
+            boot_certified_img = os.path.join(temp_out_dir,
+                                              'boot-certified.img')
+            certify_bootimg_cmds = [
+                'certify_bootimg',
+                '--boot_img', boot_img,
+                '--algorithm', 'SHA256_RSA2048',
+                '--key', './testdata/testkey_rsa2048.pem',
+                '--extra_args', '--prop foo:bar --prop gki:nice',
+                '--output', boot_certified_img,
+            ]
+            subprocess.run(certify_bootimg_cmds, check=True, cwd=self._exec_dir)
+
+            # Checks an AVB footer exists and the image size remains.
+            self.assertTrue(has_avb_footer(boot_certified_img))
+            self.assertEqual(os.path.getsize(boot_img),
+                             os.path.getsize(boot_certified_img))
+
+            extract_boot_signatures(boot_certified_img, temp_out_dir)
+            self._test_boot_signatures(
+                temp_out_dir,
+                {'boot_signature1': self._EXPECTED_BOOT_SIGNATURE_RSA2048,
+                 'boot_signature2': self._EXPECTED_KERNEL_SIGNATURE_RSA2048})
+
+            # Generates the certified boot image again, with a RSA4096 key.
+            boot_certified2_img = os.path.join(temp_out_dir,
+                                              'boot-certified2.img')
+            certify_bootimg_cmds = [
+                'certify_bootimg',
+                '--boot_img', boot_certified_img,
+                '--algorithm', 'SHA256_RSA4096',
+                '--key', './testdata/testkey_rsa4096.pem',
+                '--extra_args', '--prop foo:bar --prop gki:nice',
+                '--output', boot_certified2_img,
+            ]
+            subprocess.run(certify_bootimg_cmds, check=True, cwd=self._exec_dir)
+
+            # Checks an AVB footer exists and the image size remains.
+            self.assertTrue(has_avb_footer(boot_certified2_img))
+            self.assertEqual(os.path.getsize(boot_certified_img),
+                             os.path.getsize(boot_certified2_img))
+
+            extract_boot_signatures(boot_certified2_img, temp_out_dir)
+            self._test_boot_signatures(
+                temp_out_dir,
+                {'boot_signature1': self._EXPECTED_BOOT_SIGNATURE_RSA4096,
+                 'boot_signature2': self._EXPECTED_KERNEL_SIGNATURE_RSA4096})
+
+    def test_certify_bootimg_exceed_size(self):
+        """Tests the boot signature size exceeded max size of the signature."""
+        with tempfile.TemporaryDirectory() as temp_out_dir:
+            boot_img = os.path.join(temp_out_dir, 'boot.img')
+            generate_test_boot_image(boot_img)
+
+            # Certifies the boot.img with many --extra_args, and checks
+            # it will raise the ValueError() exception.
+            boot_certified_img = os.path.join(temp_out_dir,
+                                              'boot-certified.img')
+            certify_bootimg_cmds = [
+                'certify_bootimg',
+                '--boot_img', boot_img,
+                '--algorithm', 'SHA256_RSA2048',
+                '--key', './testdata/testkey_rsa2048.pem',
+                # Makes it exceed the signature max size.
+                '--extra_args', '--prop foo:bar --prop gki:nice ' * 128,
+                '--output', boot_certified_img,
+            ]
+
+            try:
+                subprocess.run(certify_bootimg_cmds, check=True,
+                               capture_output=True, cwd=self._exec_dir,
+                               encoding='utf-8')
+                self.fail('Exceeding signature size assertion is not raised')
+            except subprocess.CalledProcessError as err:
+                self.assertIn('ValueError: boot_signature size must be <= ',
+                              err.stderr)
+
+
+# I don't know how, but we need both the logger configuration and verbosity
+# level > 2 to make atest work. And yes this line needs to be at the very top
+# level, not even in the "__main__" indentation block.
+logging.basicConfig(stream=sys.stdout)
+
+if __name__ == '__main__':
+    unittest.main(verbosity=2)
diff --git a/gki/testdata/testkey_rsa2048.pem b/gki/testdata/testkey_rsa2048.pem
new file mode 100644
index 0000000..867dcff
--- /dev/null
+++ b/gki/testdata/testkey_rsa2048.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAxlVR3TIkouAOvH79vaJTgFhpfvVKQIeVkFRZPVXK/zY0Gvrh
+4JAqGjJoW/PfrQv5sdD36qtHH3a+G5hLZ6Ni+t/mtfjucxZfuLGC3kmJ1T3XqEKZ
+gXXI2IR7vVSoImREvDQGEDyJwtHzLANlkbGg0cghVhWZSCAndO8BenalC2v94/rt
+DfkPekH6dgU3Sf40T0sBSeSY94mOzTaqOR2pfV1rWlLRdWmo33zeHBv52Rlbt0dM
+uXAureXWiHztkm5GCBC1dgM+CaxNtizNEgC91KcD0xuRCCM2WxH+r1lpszyIJDct
+YbrFmVEYl/kjQpafhy7Nsk1fqSTyRdriZSYmTQIDAQABAoIBAQC+kJgaCuX8wYAn
+SXWQ0fmdZlXnMNRpcF0a0pD0SAzGb1RdYBXMaXiqtyhiwc53PPxsCDdNecjayIMd
+jJVXPTwLhTruOgMS/bp3gcgWwV34UHV4LJXGOGAE+jbS0hbDBMiudOYmj6RmVshp
+z9G1zZCSQNMXHaWsEYkX59XpzzoB384nRul2QgEtwzUNR9XlpzgtJBLk3SACkvsN
+mQ/DW8IWHXLg8vLn1LzVJ2e3B16H4MoE2TCHxqfMgr03IDRRJogkenQuQsFhevYT
+o/mJyHSWavVgzMHG9I5m+eepF4Wyhj1Y4WyKAuMI+9dHAX/h7Lt8XFCQCh5DbkVG
+zGr34sWBAoGBAOs7n7YZqNaaguovfIdRRsxxZr1yJAyDsr6w3yGImDZYju4c4WY9
+5esO2kP3FA4p0c7FhQF5oOb1rBuHEPp36cpL4aGeK87caqTfq63WZAujoTZpr9Lp
+BRbkL7w/xG7jpQ/clpA8sHzHGQs/nelxoOtC7E118FiRgvD/jdhlMyL9AoGBANfX
+vyoN1pplfT2xR8QOjSZ+Q35S/+SAtMuBnHx3l0qH2bbBjcvM1MNDWjnRDyaYhiRu
+i+KA7tqfib09+XpB3g5D6Ov7ls/Ldx0S/VcmVWtia2HK8y8iLGtokoBZKQ5AaFX2
+iQU8+tC4h69GnJYQKqNwgCUzh8+gHX5Y46oDiTmRAoGAYpOx8lX+czB8/Da6MNrW
+mIZNT8atZLEsDs2ANEVRxDSIcTCZJId7+m1W+nRoaycLTWNowZ1+2ErLvR10+AGY
+b7Ys79Wg9idYaY9yGn9lnZsMzAiuLeyIvXcSqgjvAKlVWrhOQFOughvNWvFl85Yy
+oWSCMlPiTLtt7CCsCKsgKuECgYBgdIp6GZsIfkgclKe0hqgvRoeU4TR3gcjJlM9A
+lBTo+pKhaBectplx9RxR8AnsPobbqwcaHnIfAuKDzjk5mEvKZjClnFXF4HAHbyAF
+nRzZEy9XkWFhc80T5rRpZO7C7qdxmu2aiKixM3V3L3/0U58qULEDbubHMw9bEhAT
+PudI8QKBgHEEiMm/hr9T41hbQi/LYanWnlFw1ue+osKuF8bXQuxnnHNuFT/c+9/A
+vWhgqG6bOEHu+p/IPrYm4tBMYlwsyh4nXCyGgDJLbLIfzKwKAWCtH9LwnyDVhOow
+GH9shdR+sW3Ew97xef02KAH4VlNANEmBV4sQNqWWvsYrcFm2rOdL
+-----END RSA PRIVATE KEY-----
diff --git a/gki/testdata/testkey_rsa4096.pem b/gki/testdata/testkey_rsa4096.pem
new file mode 100644
index 0000000..26db5c3
--- /dev/null
+++ b/gki/testdata/testkey_rsa4096.pem
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----