Snap for 8414339 from 2fe7d1583e874b45eb678d10e1450a6e70ff4cb4 to tm-qpr1-release
Change-Id: I618a10d5f9b9d640b13c7036557267447ef400ad
diff --git a/Android.bp b/Android.bp
index 77a5a32..65b6ac2 100644
--- a/Android.bp
+++ b/Android.bp
@@ -91,6 +91,20 @@
],
}
+python_binary_host {
+ name: "certify_bootimg",
+ defaults: ["mkbootimg_defaults"],
+ main: "gki/certify_bootimg.py",
+ srcs: [
+ "gki/certify_bootimg.py",
+ "gki/generate_gki_certificate.py",
+ "unpack_bootimg.py",
+ ],
+ required: [
+ "avbtool",
+ ],
+}
+
python_test_host {
name: "mkbootimg_test",
defaults: ["mkbootimg_defaults"],
diff --git a/gki/Android.bp b/gki/Android.bp
index 5173852..c62e7d8 100644
--- a/gki/Android.bp
+++ b/gki/Android.bp
@@ -16,20 +16,6 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
-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"],
diff --git a/gki/certify_bootimg.py b/gki/certify_bootimg.py
index 1543698..9a7b058 100755
--- a/gki/certify_bootimg.py
+++ b/gki/certify_bootimg.py
@@ -18,12 +18,15 @@
"""Certify a GKI boot image by generating and appending its boot_signature."""
from argparse import ArgumentParser
+import glob
import os
+import shlex
import shutil
import subprocess
import tempfile
-from generate_gki_certificate import generate_gki_certificate
+from gki.generate_gki_certificate import generate_gki_certificate
+from unpack_bootimg import unpack_bootimg
BOOT_SIGNATURE_SIZE = 16 * 1024
@@ -31,14 +34,7 @@
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)
-
+ unpack_bootimg(boot_img, unpack_dir)
with open(os.path.join(unpack_dir, 'kernel'), 'rb') as kernel:
kernel_bytes = kernel.read()
assert len(kernel_bytes) > 0
@@ -150,13 +146,31 @@
subprocess.check_call(avbtool_cmd)
+def load_dict_from_file(path):
+ """Loads key=value pairs from |path| and returns a dict."""
+ d = {}
+ with open(path, 'r', encoding='utf-8') as f:
+ for line in f:
+ line = line.strip()
+ if not line or line.startswith('#'):
+ continue
+ if '=' in line:
+ name, value = line.split('=', 1)
+ d[name] = value
+ return d
+
+
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')
+ input_group = parser.add_mutually_exclusive_group(required=True)
+ input_group.add_argument(
+ '--boot_img', help='path to the boot image to certify')
+ input_group.add_argument(
+ '--boot_img_zip', help='path to the boot-img-*.zip archive to certify')
+
parser.add_argument('--algorithm', required=True,
help='signing algorithm for the certificate')
parser.add_argument('--key', required=True,
@@ -172,23 +186,60 @@
extra_args = []
for a in args.extra_args:
- extra_args.extend(a.split())
+ extra_args.extend(shlex.split(a))
args.extra_args = extra_args
return args
+def certify_bootimg(boot_img, output_img, algorithm, key, extra_args):
+ """Certify a GKI boot image by generating and appending a boot_signature."""
+ with tempfile.TemporaryDirectory() as temp_dir:
+ boot_tmp = os.path.join(temp_dir, 'boot.tmp')
+ shutil.copy2(boot_img, boot_tmp)
+
+ erase_certificate_and_avb_footer(boot_tmp)
+ add_certificate(boot_tmp, algorithm, key, extra_args)
+
+ avb_partition_size = get_avb_image_size(boot_img)
+ add_avb_footer(boot_tmp, avb_partition_size)
+
+ # We're done, copy the temp image to the final output.
+ shutil.copy2(boot_tmp, output_img)
+
+
+def certify_bootimg_zip(boot_img_zip, output_zip, algorithm, key, extra_args):
+ """Similar to certify_bootimg(), but for a zip archive of boot images."""
+ with tempfile.TemporaryDirectory() as unzip_dir:
+ shutil.unpack_archive(boot_img_zip, unzip_dir)
+
+ gki_info_file = os.path.join(unzip_dir, 'gki-info.txt')
+ if os.path.exists(gki_info_file):
+ info_dict = load_dict_from_file(gki_info_file)
+ if 'certify_bootimg_extra_args' in info_dict:
+ extra_args.extend(
+ shlex.split(info_dict['certify_bootimg_extra_args']))
+
+ for boot_img in glob.glob(os.path.join(unzip_dir, 'boot-*.img')):
+ print(f'Certifying {os.path.basename(boot_img)} ...')
+ certify_bootimg(boot_img=boot_img, output_img=boot_img,
+ algorithm=algorithm, key=key, extra_args=extra_args)
+
+ print(f'Making certified archive: {output_zip}')
+ archive_base_name = os.path.splitext(output_zip)[0]
+ shutil.make_archive(archive_base_name, 'zip', unzip_dir)
+
+
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 args.boot_img_zip:
+ certify_bootimg_zip(args.boot_img_zip, args.output, args.algorithm,
+ args.key, args.extra_args)
+ else:
+ certify_bootimg(args.boot_img, args.output, args.algorithm,
+ args.key, args.extra_args)
if __name__ == '__main__':
diff --git a/gki/certify_bootimg_test.py b/gki/certify_bootimg_test.py
index 25cdbff..8c7c4d3 100644
--- a/gki/certify_bootimg_test.py
+++ b/gki/certify_bootimg_test.py
@@ -17,6 +17,7 @@
"""Tests certify_bootimg."""
import logging
+import glob
import os
import random
import shutil
@@ -42,11 +43,11 @@
return pathname
-def generate_test_boot_image(boot_img, avb_partition_size=None):
+def generate_test_boot_image(boot_img, kernel_size=4096, seed='kernel',
+ 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')
+ generate_test_file(kernel_tmpfile.name, kernel_size, seed)
kernel_tmpfile.flush()
mkbootimg_cmds = [
@@ -67,6 +68,38 @@
subprocess.check_call(avbtool_cmd)
+def generate_test_boot_image_archive(output_zip, boot_img_info, gki_info=None):
+ """Generates a zip archive of test boot images.
+
+ It also adds a file gki-info.txt, which contains additional settings for
+ for `certify_bootimg --extra_args`.
+
+ Args:
+ output_zip: the output zip archive, e.g., /path/to/boot-img.zip.
+ boot_img_info: a list of (boot_image_name, kernel_size,
+ partition_size) tuples. e.g.,
+ [('boot-1.0.img', 4096, 4 * 1024),
+ ('boot-2.0.img', 8192, 8 * 1024)].
+ gki_info: the file content to be written into 'gki-info.txt' in the
+ |output_zip|.
+ """
+ with tempfile.TemporaryDirectory() as temp_out_dir:
+ for name, kernel_size, partition_size in boot_img_info:
+ boot_img = os.path.join(temp_out_dir, name)
+ generate_test_boot_image(boot_img=boot_img,
+ kernel_size=kernel_size,
+ seed=name,
+ avb_partition_size=partition_size)
+
+ if gki_info:
+ gki_info_path = os.path.join(temp_out_dir, 'gki-info.txt')
+ with open(gki_info_path, 'w', encoding='utf-8') as f:
+ f.write(gki_info)
+
+ archive_base_name = os.path.splitext(output_zip)[0]
+ shutil.make_archive(archive_base_name, 'zip', temp_out_dir)
+
+
def has_avb_footer(image):
"""Returns true if the image has a avb footer."""
@@ -105,7 +138,12 @@
def extract_boot_signatures(boot_img, output_dir):
- """Extracts the boot signatures of a boot image."""
+ """Extracts the boot signatures of a boot image.
+
+ This functions extracts the boot signatures of |boot_img| as:
+ - |output_dir|/boot_signature1
+ - |output_dir|/boot_signature2
+ """
boot_img_copy = os.path.join(output_dir, 'boot_image_copy')
shutil.copy2(boot_img, boot_img_copy)
@@ -137,6 +175,27 @@
boot_signature_bytes = boot_signature_bytes[next_signature_size:]
+def extract_boot_archive_with_signatures(boot_img_zip, output_dir):
+ """Extracts boot images and signatures of a boot images archive.
+
+ Suppose there are two boot images in |boot_img_zip|: boot-1.0.img
+ and boot-2.0.img. This function then extracts each boot-*.img and
+ their signatures as:
+ - |output_dir|/boot-1.0.img
+ - |output_dir|/boot-2.0.img
+ - |output_dir|/boot-1.0/boot_signature1
+ - |output_dir|/boot-1.0/boot_signature2
+ - |output_dir|/boot-2.0/boot_signature1
+ - |output_dir|/boot-2.0/boot_signature2
+ """
+ shutil.unpack_archive(boot_img_zip, output_dir)
+ for boot_img in glob.glob(os.path.join(output_dir, 'boot-*.img')):
+ img_name = os.path.splitext(os.path.basename(boot_img))[0]
+ signature_output_dir = os.path.join(output_dir, img_name)
+ os.mkdir(signature_output_dir, 0o777)
+ extract_boot_signatures(boot_img, signature_output_dir)
+
+
class CertifyBootimgTest(unittest.TestCase):
"""Tests the functionalities of certify_bootimg."""
@@ -174,8 +233,8 @@
'faf1da72a4fba97ddab0b8f7a410db86'
'8fb72392a66d1440ff8bff490c73c771\n'
' Flags: 0\n'
- " Prop: foo -> 'bar'\n"
" Prop: gki -> 'nice'\n"
+ " Prop: space -> 'nice to meet you'\n"
)
self._EXPECTED_KERNEL_SIGNATURE_RSA2048 = ( # pylint: disable=C0103
@@ -200,8 +259,8 @@
'762c877f3af0d50a4a4fbc1385d5c7ce'
'52a1288db74b33b72217d93db6f2909f\n'
' Flags: 0\n'
- " Prop: foo -> 'bar'\n"
" Prop: gki -> 'nice'\n"
+ " Prop: space -> 'nice to meet you'\n"
)
self._EXPECTED_BOOT_SIGNATURE_RSA4096 = ( # pylint: disable=C0103
@@ -226,8 +285,8 @@
'faf1da72a4fba97ddab0b8f7a410db86'
'8fb72392a66d1440ff8bff490c73c771\n'
' Flags: 0\n'
- " Prop: foo -> 'bar'\n"
" Prop: gki -> 'nice'\n"
+ " Prop: space -> 'nice to meet you'\n"
)
self._EXPECTED_KERNEL_SIGNATURE_RSA4096 = ( # pylint: disable=C0103
@@ -252,8 +311,184 @@
'762c877f3af0d50a4a4fbc1385d5c7ce'
'52a1288db74b33b72217d93db6f2909f\n'
' Flags: 0\n'
- " Prop: foo -> 'bar'\n"
" Prop: gki -> 'nice'\n"
+ " Prop: space -> 'nice to meet you'\n"
+ )
+
+ self._EXPECTED_BOOT_1_0_SIGNATURE1_RSA4096 = ( # pylint: disable=C0103
+ 'Minimum libavb version: 1.0\n'
+ 'Header Block: 256 bytes\n'
+ 'Authentication Block: 576 bytes\n'
+ 'Auxiliary Block: 1600 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: 12288 bytes\n'
+ ' Hash Algorithm: sha256\n'
+ ' Partition Name: boot\n' # boot
+ ' Salt: d00df00d\n'
+ ' Digest: '
+ '88465e463bffb9f7dfc0c1f46d01bcf3'
+ '15f7693e19bd188a0ca1feca2ed7b9df\n'
+ ' Flags: 0\n'
+ " Prop: gki -> 'nice'\n"
+ " Prop: space -> 'nice to meet you'\n"
+ " Prop: KERNEL_RELEASE -> '5.10.42-android13-0-00544-"
+ "ged21d463f856'\n"
+ " Prop: BRANCH -> 'android13-5.10-2022-05'\n"
+ " Prop: BUILD_NUMBER -> 'ab8295296'\n"
+ " Prop: SPACE -> 'nice to meet you'\n"
+ )
+
+ self._EXPECTED_BOOT_1_0_SIGNATURE2_RSA4096 = ( # pylint: disable=C0103
+ 'Minimum libavb version: 1.0\n'
+ 'Header Block: 256 bytes\n'
+ 'Authentication Block: 576 bytes\n'
+ 'Auxiliary Block: 1600 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: generic_kernel\n' # generic_kernel
+ ' Salt: d00df00d\n'
+ ' Digest: '
+ '14ac8d0d233e57a317acd05cd458f2bb'
+ 'cc78725ef9f66c1b38e90697fb09d943\n'
+ ' Flags: 0\n'
+ " Prop: gki -> 'nice'\n"
+ " Prop: space -> 'nice to meet you'\n"
+ " Prop: KERNEL_RELEASE -> '5.10.42-android13-0-00544-"
+ "ged21d463f856'\n"
+ " Prop: BRANCH -> 'android13-5.10-2022-05'\n"
+ " Prop: BUILD_NUMBER -> 'ab8295296'\n"
+ " Prop: SPACE -> 'nice to meet you'\n"
+ )
+
+ self._EXPECTED_BOOT_2_0_SIGNATURE1_RSA4096 = ( # pylint: disable=C0103
+ 'Minimum libavb version: 1.0\n'
+ 'Header Block: 256 bytes\n'
+ 'Authentication Block: 576 bytes\n'
+ 'Auxiliary Block: 1600 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: 20480 bytes\n'
+ ' Hash Algorithm: sha256\n'
+ ' Partition Name: boot\n' # boot
+ ' Salt: d00df00d\n'
+ ' Digest: '
+ '3e6a9854a9d2350a7071083bc3f37376'
+ '37573fd87b1c72b146cb4870ac6af36f\n'
+ ' Flags: 0\n'
+ " Prop: gki -> 'nice'\n"
+ " Prop: space -> 'nice to meet you'\n"
+ " Prop: KERNEL_RELEASE -> '5.10.42-android13-0-00544-"
+ "ged21d463f856'\n"
+ " Prop: BRANCH -> 'android13-5.10-2022-05'\n"
+ " Prop: BUILD_NUMBER -> 'ab8295296'\n"
+ " Prop: SPACE -> 'nice to meet you'\n"
+ )
+
+ self._EXPECTED_BOOT_2_0_SIGNATURE2_RSA4096 = ( # pylint: disable=C0103
+ 'Minimum libavb version: 1.0\n'
+ 'Header Block: 256 bytes\n'
+ 'Authentication Block: 576 bytes\n'
+ 'Auxiliary Block: 1600 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: 16384 bytes\n'
+ ' Hash Algorithm: sha256\n'
+ ' Partition Name: generic_kernel\n' # generic_kernel
+ ' Salt: d00df00d\n'
+ ' Digest: '
+ '92fb8443cd284b67a4cbf5ce00348b50'
+ '1c657e0aedf4e2181c92ad7fc8b5224f\n'
+ ' Flags: 0\n'
+ " Prop: gki -> 'nice'\n"
+ " Prop: space -> 'nice to meet you'\n"
+ " Prop: KERNEL_RELEASE -> '5.10.42-android13-0-00544-"
+ "ged21d463f856'\n"
+ " Prop: BRANCH -> 'android13-5.10-2022-05'\n"
+ " Prop: BUILD_NUMBER -> 'ab8295296'\n"
+ " Prop: SPACE -> 'nice to meet you'\n"
+ )
+
+ self._EXPECTED_BOOT_3_0_SIGNATURE1_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: 12288 bytes\n'
+ ' Hash Algorithm: sha256\n'
+ ' Partition Name: boot\n' # boot
+ ' Salt: d00df00d\n'
+ ' Digest: '
+ '9b9cd845a367d7fc9b61d6ac02b0e7c9'
+ 'dc3d3b219abf60dd6e19359f0353c917\n'
+ ' Flags: 0\n'
+ " Prop: gki -> 'nice'\n"
+ " Prop: space -> 'nice to meet you'\n"
+ )
+
+ self._EXPECTED_BOOT_3_0_SIGNATURE2_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: generic_kernel\n' # generic_kernel
+ ' Salt: d00df00d\n'
+ ' Digest: '
+ '0cd7d331ed9b32dcd92f00e2cac75595'
+ '52199170afe788a8fcf1954f9ea072d0\n'
+ ' Flags: 0\n'
+ " Prop: gki -> 'nice'\n"
+ " Prop: space -> 'nice to meet you'\n"
)
def _test_boot_signatures(self, signatures_dir, expected_signatures_info):
@@ -292,7 +527,8 @@
'--boot_img', boot_img,
'--algorithm', 'SHA256_RSA2048',
'--key', './testdata/testkey_rsa2048.pem',
- '--extra_args', '--prop foo:bar --prop gki:nice',
+ '--extra_args', '--prop gki:nice '
+ '--prop space:"nice to meet you"',
'--output', boot_certified_img,
]
subprocess.run(certify_bootimg_cmds, check=True, cwd=self._exec_dir)
@@ -311,7 +547,8 @@
'--boot_img', boot_certified_img,
'--algorithm', 'SHA256_RSA4096',
'--key', './testdata/testkey_rsa4096.pem',
- '--extra_args', '--prop foo:bar --prop gki:nice',
+ '--extra_args', '--prop gki:nice '
+ '--prop space:"nice to meet you"',
'--output', boot_certified2_img,
]
subprocess.run(certify_bootimg_cmds, check=True, cwd=self._exec_dir)
@@ -338,7 +575,8 @@
'--boot_img', boot_img,
'--algorithm', 'SHA256_RSA2048',
'--key', './testdata/testkey_rsa2048.pem',
- '--extra_args', '--prop foo:bar --prop gki:nice',
+ '--extra_args', '--prop gki:nice '
+ '--prop space:"nice to meet you"',
'--output', boot_certified_img,
]
subprocess.run(certify_bootimg_cmds, check=True, cwd=self._exec_dir)
@@ -362,7 +600,8 @@
'--boot_img', boot_certified_img,
'--algorithm', 'SHA256_RSA4096',
'--key', './testdata/testkey_rsa4096.pem',
- '--extra_args', '--prop foo:bar --prop gki:nice',
+ '--extra_args', '--prop gki:nice '
+ '--prop space:"nice to meet you"',
'--output', boot_certified2_img,
]
subprocess.run(certify_bootimg_cmds, check=True, cwd=self._exec_dir)
@@ -407,6 +646,123 @@
self.assertIn('ValueError: boot_signature size must be <= ',
err.stderr)
+ def test_certify_bootimg_archive(self):
+ """Tests certify_bootimg for a boot-img.zip."""
+ with tempfile.TemporaryDirectory() as temp_out_dir:
+ boot_img_zip = os.path.join(temp_out_dir, 'boot-img.zip')
+ gki_info = ('certify_bootimg_extra_args='
+ '--prop KERNEL_RELEASE:5.10.42'
+ '-android13-0-00544-ged21d463f856 '
+ '--prop BRANCH:android13-5.10-2022-05 '
+ '--prop BUILD_NUMBER:ab8295296 '
+ '--prop SPACE:"nice to meet you"\n')
+ generate_test_boot_image_archive(
+ boot_img_zip,
+ # A list of (boot_img_name, kernel_size, partition_size).
+ [('boot-1.0.img', 8 * 1024, 128 * 1024),
+ ('boot-2.0.img', 16 * 1024, 256 * 1024)],
+ gki_info)
+
+ # Certify the boot image archive, with a RSA4096 key.
+ boot_certified_img_zip = os.path.join(temp_out_dir,
+ 'boot-certified-img.zip')
+ certify_bootimg_cmds = [
+ 'certify_bootimg',
+ '--boot_img_zip', boot_img_zip,
+ '--algorithm', 'SHA256_RSA4096',
+ '--key', './testdata/testkey_rsa4096.pem',
+ '--extra_args', '--prop gki:nice '
+ '--prop space:"nice to meet you"',
+ '--output', boot_certified_img_zip,
+ ]
+ subprocess.run(certify_bootimg_cmds, check=True, cwd=self._exec_dir)
+
+ extract_boot_archive_with_signatures(boot_certified_img_zip,
+ temp_out_dir)
+
+ # Checks an AVB footer exists and the image size remains.
+ boot_1_img = os.path.join(temp_out_dir, 'boot-1.0.img')
+ self.assertTrue(has_avb_footer(boot_1_img))
+ self.assertEqual(os.path.getsize(boot_1_img), 128 * 1024)
+
+ boot_2_img = os.path.join(temp_out_dir, 'boot-2.0.img')
+ self.assertTrue(has_avb_footer(boot_2_img))
+ self.assertEqual(os.path.getsize(boot_2_img), 256 * 1024)
+
+ self._test_boot_signatures(
+ temp_out_dir,
+ {'boot-1.0/boot_signature1':
+ self._EXPECTED_BOOT_1_0_SIGNATURE1_RSA4096,
+ 'boot-1.0/boot_signature2':
+ self._EXPECTED_BOOT_1_0_SIGNATURE2_RSA4096,
+ 'boot-2.0/boot_signature1':
+ self._EXPECTED_BOOT_2_0_SIGNATURE1_RSA4096,
+ 'boot-2.0/boot_signature2':
+ self._EXPECTED_BOOT_2_0_SIGNATURE2_RSA4096})
+
+ def test_certify_bootimg_archive_without_gki_info(self):
+ """Tests certify_bootimg for a boot-img.zip."""
+ with tempfile.TemporaryDirectory() as temp_out_dir:
+ boot_img_zip = os.path.join(temp_out_dir, 'boot-img.zip')
+
+ # Checks ceritfy_bootimg works for a boot-img.zip without a
+ # gki-info.txt.
+ generate_test_boot_image_archive(
+ boot_img_zip,
+ # A list of (boot_img_name, kernel_size, partition_size).
+ [('boot-3.0.img', 8 * 1024, 128 * 1024)],
+ gki_info=None)
+ # Certify the boot image archive, with a RSA4096 key.
+ boot_certified_img_zip = os.path.join(temp_out_dir,
+ 'boot-certified-img.zip')
+ certify_bootimg_cmds = [
+ 'certify_bootimg',
+ '--boot_img_zip', boot_img_zip,
+ '--algorithm', 'SHA256_RSA4096',
+ '--key', './testdata/testkey_rsa4096.pem',
+ '--extra_args', '--prop gki:nice '
+ '--prop space:"nice to meet you"',
+ '--output', boot_certified_img_zip,
+ ]
+ subprocess.run(certify_bootimg_cmds, check=True, cwd=self._exec_dir)
+
+ # Checks ceritfy_bootimg works for a boot-img.zip with a special
+ # gki-info.txt.
+ generate_test_boot_image_archive(
+ boot_img_zip,
+ # A list of (boot_img_name, kernel_size, partition_size).
+ [('boot-3.0.img', 8 * 1024, 128 * 1024)],
+ gki_info='a=b\n'
+ 'c=d\n')
+ # Certify the boot image archive, with a RSA4096 key.
+ boot_certified_img_zip = os.path.join(temp_out_dir,
+ 'boot-certified-img.zip')
+ certify_bootimg_cmds = [
+ 'certify_bootimg',
+ '--boot_img_zip', boot_img_zip,
+ '--algorithm', 'SHA256_RSA4096',
+ '--key', './testdata/testkey_rsa4096.pem',
+ '--extra_args', '--prop gki:nice '
+ '--prop space:"nice to meet you"',
+ '--output', boot_certified_img_zip,
+ ]
+ subprocess.run(certify_bootimg_cmds, check=True, cwd=self._exec_dir)
+
+ extract_boot_archive_with_signatures(boot_certified_img_zip,
+ temp_out_dir)
+
+ # Checks an AVB footer exists and the image size remains.
+ boot_3_img = os.path.join(temp_out_dir, 'boot-3.0.img')
+ self.assertTrue(has_avb_footer(boot_3_img))
+ self.assertEqual(os.path.getsize(boot_3_img), 128 * 1024)
+
+ self._test_boot_signatures(
+ temp_out_dir,
+ {'boot-3.0/boot_signature1':
+ self._EXPECTED_BOOT_3_0_SIGNATURE1_RSA4096,
+ 'boot-3.0/boot_signature2':
+ self._EXPECTED_BOOT_3_0_SIGNATURE2_RSA4096})
+
# 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
diff --git a/gki/generate_gki_certificate.py b/gki/generate_gki_certificate.py
index 0765c08..2797cca 100755
--- a/gki/generate_gki_certificate.py
+++ b/gki/generate_gki_certificate.py
@@ -18,6 +18,7 @@
"""Generate a Generic Boot Image certificate suitable for VTS verification."""
from argparse import ArgumentParser
+import shlex
import subprocess
@@ -73,7 +74,7 @@
additional_avb_args = []
for a in args.additional_avb_args:
- additional_avb_args.extend(a.split())
+ additional_avb_args.extend(shlex.split(a))
args.additional_avb_args = additional_avb_args
return args
diff --git a/mkbootimg.py b/mkbootimg.py
index 5c65e2c..ec29581 100755
--- a/mkbootimg.py
+++ b/mkbootimg.py
@@ -141,9 +141,6 @@
def write_vendor_boot_header(args):
- if filesize(args.dtb) == 0:
- raise ValueError('DTB image must not be empty.')
-
if args.header_version > 3:
vendor_ramdisk_size = args.vendor_ramdisk_total_size
vendor_boot_header_size = VENDOR_BOOT_IMAGE_HEADER_V4_SIZE
diff --git a/tests/mkbootimg_test.py b/tests/mkbootimg_test.py
index 28f47f0..e691e30 100644
--- a/tests/mkbootimg_test.py
+++ b/tests/mkbootimg_test.py
@@ -760,6 +760,80 @@
self.assertEqual(raw_vendor_cmdline,
vendor_cmdline.encode() + b'\x00')
+ def test_vendor_boot_v4_without_dtb(self):
+ """Tests building vendor_boot version 4 without dtb image."""
+ with tempfile.TemporaryDirectory() as temp_out_dir:
+ vendor_boot_img = os.path.join(temp_out_dir, 'vendor_boot.img')
+ ramdisk = generate_test_file(
+ os.path.join(temp_out_dir, 'ramdisk'), 0x1000)
+ mkbootimg_cmds = [
+ 'mkbootimg',
+ '--header_version', '4',
+ '--vendor_boot', vendor_boot_img,
+ '--vendor_ramdisk', ramdisk,
+ ]
+ unpack_bootimg_cmds = [
+ 'unpack_bootimg',
+ '--boot_img', vendor_boot_img,
+ '--out', os.path.join(temp_out_dir, 'out'),
+ ]
+ expected_output = [
+ 'boot magic: VNDRBOOT',
+ 'vendor boot image header version: 4',
+ 'dtb size: 0',
+ ]
+
+ subprocess.run(mkbootimg_cmds, check=True)
+ result = subprocess.run(unpack_bootimg_cmds, check=True,
+ capture_output=True, encoding='utf-8')
+ output = [line.strip() for line in result.stdout.splitlines()]
+ if not subsequence_of(expected_output, output):
+ msg = '\n'.join([
+ 'Unexpected unpack_bootimg output:',
+ 'Expected:',
+ ' ' + '\n '.join(expected_output),
+ '',
+ 'Actual:',
+ ' ' + '\n '.join(output),
+ ])
+ self.fail(msg)
+
+ def test_unpack_vendor_boot_image_v4_without_dtb(self):
+ """Tests that mkbootimg(unpack_bootimg(image)) is an identity when no dtb image."""
+ with tempfile.TemporaryDirectory() as temp_out_dir:
+ vendor_boot_img = os.path.join(temp_out_dir, 'vendor_boot.img')
+ vendor_boot_img_reconstructed = os.path.join(
+ temp_out_dir, 'vendor_boot.img.reconstructed')
+ ramdisk = generate_test_file(
+ os.path.join(temp_out_dir, 'ramdisk'), 0x121212)
+
+ mkbootimg_cmds = [
+ 'mkbootimg',
+ '--header_version', '4',
+ '--vendor_boot', vendor_boot_img,
+ '--vendor_ramdisk', ramdisk,
+ ]
+ unpack_bootimg_cmds = [
+ 'unpack_bootimg',
+ '--boot_img', vendor_boot_img,
+ '--out', os.path.join(temp_out_dir, 'out'),
+ '--format=mkbootimg',
+ ]
+ subprocess.run(mkbootimg_cmds, check=True)
+ result = subprocess.run(unpack_bootimg_cmds, check=True,
+ capture_output=True, encoding='utf-8')
+ mkbootimg_cmds = [
+ 'mkbootimg',
+ '--vendor_boot', vendor_boot_img_reconstructed,
+ ]
+ unpack_format_args = shlex.split(result.stdout)
+ mkbootimg_cmds.extend(unpack_format_args)
+
+ subprocess.run(mkbootimg_cmds, check=True)
+ self.assertTrue(
+ filecmp.cmp(vendor_boot_img, vendor_boot_img_reconstructed),
+ 'reconstructed vendor_boot image differ from the original')
+
# 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
diff --git a/unpack_bootimg.py b/unpack_bootimg.py
index 437408f..462190f 100755
--- a/unpack_bootimg.py
+++ b/unpack_bootimg.py
@@ -19,7 +19,7 @@
Extracts the kernel, ramdisk, second bootloader, dtb and recovery dtbo images.
"""
-from argparse import ArgumentParser, FileType, RawDescriptionHelpFormatter
+from argparse import ArgumentParser, RawDescriptionHelpFormatter
from struct import unpack
import os
import shlex
@@ -58,7 +58,7 @@
a = os_version >> 14
b = os_version >> 7 & ((1<<7) - 1)
c = os_version & ((1<<7) - 1)
- return '{}.{}.{}'.format(a, b, c)
+ return f'{a}.{b}.{c}'
def format_os_patch_level(os_patch_level):
@@ -67,7 +67,7 @@
y = os_patch_level >> 4
y += 2000
m = os_patch_level & ((1<<4) - 1)
- return '{:04d}-{:02d}'.format(y, m)
+ return f'{y:04d}-{m:02d}'
def decode_os_version_patch_level(os_version_patch_level):
@@ -181,12 +181,12 @@
return args
-def unpack_boot_image(args):
+def unpack_boot_image(boot_img, output_dir):
"""extracts kernel, ramdisk, second bootloader and recovery dtbo"""
info = BootImageInfoFormatter()
- info.boot_magic = unpack('8s', args.boot_img.read(8))[0].decode()
+ info.boot_magic = unpack('8s', boot_img.read(8))[0].decode()
- kernel_ramdisk_second_info = unpack('9I', args.boot_img.read(9 * 4))
+ kernel_ramdisk_second_info = unpack('9I', boot_img.read(9 * 4))
# header_version is always at [8] regardless of the value of header_version.
info.header_version = kernel_ramdisk_second_info[8]
@@ -199,7 +199,7 @@
info.second_load_address = kernel_ramdisk_second_info[5]
info.tags_load_address = kernel_ramdisk_second_info[6]
info.page_size = kernel_ramdisk_second_info[7]
- os_version_patch_level = unpack('I', args.boot_img.read(1 * 4))[0]
+ os_version_patch_level = unpack('I', boot_img.read(1 * 4))[0]
else:
info.kernel_size = kernel_ramdisk_second_info[0]
info.ramdisk_size = kernel_ramdisk_second_info[1]
@@ -212,31 +212,31 @@
if info.header_version < 3:
info.product_name = cstr(unpack('16s',
- args.boot_img.read(16))[0].decode())
- info.cmdline = cstr(unpack('512s', args.boot_img.read(512))[0].decode())
- args.boot_img.read(32) # ignore SHA
+ boot_img.read(16))[0].decode())
+ info.cmdline = cstr(unpack('512s', boot_img.read(512))[0].decode())
+ boot_img.read(32) # ignore SHA
info.extra_cmdline = cstr(unpack('1024s',
- args.boot_img.read(1024))[0].decode())
+ boot_img.read(1024))[0].decode())
else:
info.cmdline = cstr(unpack('1536s',
- args.boot_img.read(1536))[0].decode())
+ boot_img.read(1536))[0].decode())
if info.header_version in {1, 2}:
- info.recovery_dtbo_size = unpack('I', args.boot_img.read(1 * 4))[0]
- info.recovery_dtbo_offset = unpack('Q', args.boot_img.read(8))[0]
- info.boot_header_size = unpack('I', args.boot_img.read(4))[0]
+ info.recovery_dtbo_size = unpack('I', boot_img.read(1 * 4))[0]
+ info.recovery_dtbo_offset = unpack('Q', boot_img.read(8))[0]
+ info.boot_header_size = unpack('I', boot_img.read(4))[0]
else:
info.recovery_dtbo_size = 0
if info.header_version == 2:
- info.dtb_size = unpack('I', args.boot_img.read(4))[0]
- info.dtb_load_address = unpack('Q', args.boot_img.read(8))[0]
+ info.dtb_size = unpack('I', boot_img.read(4))[0]
+ info.dtb_load_address = unpack('Q', boot_img.read(8))[0]
else:
info.dtb_size = 0
info.dtb_load_address = 0
if info.header_version >= 4:
- info.boot_signature_size = unpack('I', args.boot_img.read(4))[0]
+ info.boot_signature_size = unpack('I', boot_img.read(4))[0]
else:
info.boot_signature_size = 0
@@ -284,10 +284,10 @@
image_info_list.append((boot_signature_offset, info.boot_signature_size,
'boot_signature'))
- create_out_dir(args.out)
+ create_out_dir(output_dir)
for offset, size, name in image_info_list:
- extract_image(offset, size, args.boot_img, os.path.join(args.out, name))
- info.image_dir = args.out
+ extract_image(offset, size, boot_img, os.path.join(output_dir, name))
+ info.image_dir = output_dir
return info
@@ -353,7 +353,8 @@
args.extend(['--vendor_cmdline', self.cmdline])
args.extend(['--board', self.product_name])
- args.extend(['--dtb', os.path.join(self.image_dir, 'dtb')])
+ if self.dtb_size > 0:
+ args.extend(['--dtb', os.path.join(self.image_dir, 'dtb')])
if self.header_version > 3:
args.extend(['--vendor_bootconfig',
@@ -377,20 +378,20 @@
return args
-def unpack_vendor_boot_image(args):
+def unpack_vendor_boot_image(boot_img, output_dir):
info = VendorBootImageInfoFormatter()
- info.boot_magic = unpack('8s', args.boot_img.read(8))[0].decode()
- info.header_version = unpack('I', args.boot_img.read(4))[0]
- info.page_size = unpack('I', args.boot_img.read(4))[0]
- info.kernel_load_address = unpack('I', args.boot_img.read(4))[0]
- info.ramdisk_load_address = unpack('I', args.boot_img.read(4))[0]
- info.vendor_ramdisk_size = unpack('I', args.boot_img.read(4))[0]
- info.cmdline = cstr(unpack('2048s', args.boot_img.read(2048))[0].decode())
- info.tags_load_address = unpack('I', args.boot_img.read(4))[0]
- info.product_name = cstr(unpack('16s', args.boot_img.read(16))[0].decode())
- info.header_size = unpack('I', args.boot_img.read(4))[0]
- info.dtb_size = unpack('I', args.boot_img.read(4))[0]
- info.dtb_load_address = unpack('Q', args.boot_img.read(8))[0]
+ info.boot_magic = unpack('8s', boot_img.read(8))[0].decode()
+ info.header_version = unpack('I', boot_img.read(4))[0]
+ info.page_size = unpack('I', boot_img.read(4))[0]
+ info.kernel_load_address = unpack('I', boot_img.read(4))[0]
+ info.ramdisk_load_address = unpack('I', boot_img.read(4))[0]
+ info.vendor_ramdisk_size = unpack('I', boot_img.read(4))[0]
+ info.cmdline = cstr(unpack('2048s', boot_img.read(2048))[0].decode())
+ info.tags_load_address = unpack('I', boot_img.read(4))[0]
+ info.product_name = cstr(unpack('16s', boot_img.read(16))[0].decode())
+ info.header_size = unpack('I', boot_img.read(4))[0]
+ info.dtb_size = unpack('I', boot_img.read(4))[0]
+ info.dtb_load_address = unpack('Q', boot_img.read(8))[0]
# Convenient shorthand.
page_size = info.page_size
@@ -407,10 +408,10 @@
(ramdisk_offset_base, info.vendor_ramdisk_size, 'vendor_ramdisk'))
if info.header_version > 3:
- info.vendor_ramdisk_table_size = unpack('I', args.boot_img.read(4))[0]
- vendor_ramdisk_table_entry_num = unpack('I', args.boot_img.read(4))[0]
- vendor_ramdisk_table_entry_size = unpack('I', args.boot_img.read(4))[0]
- info.vendor_bootconfig_size = unpack('I', args.boot_img.read(4))[0]
+ info.vendor_ramdisk_table_size = unpack('I', boot_img.read(4))[0]
+ vendor_ramdisk_table_entry_num = unpack('I', boot_img.read(4))[0]
+ vendor_ramdisk_table_entry_size = unpack('I', boot_img.read(4))[0]
+ info.vendor_bootconfig_size = unpack('I', boot_img.read(4))[0]
num_vendor_ramdisk_table_pages = get_number_of_pages(
info.vendor_ramdisk_table_size, page_size)
vendor_ramdisk_table_offset = page_size * (
@@ -421,16 +422,16 @@
for idx in range(vendor_ramdisk_table_entry_num):
entry_offset = vendor_ramdisk_table_offset + (
vendor_ramdisk_table_entry_size * idx)
- args.boot_img.seek(entry_offset)
- ramdisk_size = unpack('I', args.boot_img.read(4))[0]
- ramdisk_offset = unpack('I', args.boot_img.read(4))[0]
- ramdisk_type = unpack('I', args.boot_img.read(4))[0]
+ boot_img.seek(entry_offset)
+ ramdisk_size = unpack('I', boot_img.read(4))[0]
+ ramdisk_offset = unpack('I', boot_img.read(4))[0]
+ ramdisk_type = unpack('I', boot_img.read(4))[0]
ramdisk_name = cstr(unpack(
f'{VENDOR_RAMDISK_NAME_SIZE}s',
- args.boot_img.read(VENDOR_RAMDISK_NAME_SIZE))[0].decode())
+ boot_img.read(VENDOR_RAMDISK_NAME_SIZE))[0].decode())
board_id = unpack(
f'{VENDOR_RAMDISK_TABLE_ENTRY_BOARD_ID_SIZE}I',
- args.boot_img.read(
+ boot_img.read(
4 * VENDOR_RAMDISK_TABLE_ENTRY_BOARD_ID_SIZE))
output_ramdisk_name = f'vendor_ramdisk{idx:02}'
@@ -451,16 +452,17 @@
dtb_offset = page_size * (num_boot_header_pages + num_boot_ramdisk_pages
) # header + vendor_ramdisk
- image_info_list.append((dtb_offset, info.dtb_size, 'dtb'))
+ if info.dtb_size > 0:
+ image_info_list.append((dtb_offset, info.dtb_size, 'dtb'))
- create_out_dir(args.out)
+ create_out_dir(output_dir)
for offset, size, name in image_info_list:
- extract_image(offset, size, args.boot_img, os.path.join(args.out, name))
- info.image_dir = args.out
+ extract_image(offset, size, boot_img, os.path.join(output_dir, name))
+ info.image_dir = output_dir
if info.header_version > 3:
vendor_ramdisk_by_name_dir = os.path.join(
- args.out, 'vendor-ramdisk-by-name')
+ output_dir, 'vendor-ramdisk-by-name')
create_out_dir(vendor_ramdisk_by_name_dir)
for src, dst in vendor_ramdisk_symlinks:
src_pathname = os.path.join('..', src)
@@ -473,19 +475,26 @@
return info
-def unpack_image(args):
- boot_magic = unpack('8s', args.boot_img.read(8))[0].decode()
- args.boot_img.seek(0)
- if boot_magic == 'ANDROID!':
- info = unpack_boot_image(args)
- elif boot_magic == 'VNDRBOOT':
- info = unpack_vendor_boot_image(args)
- else:
- raise ValueError(f'Not an Android boot image, magic: {boot_magic}')
+def unpack_bootimg(boot_img, output_dir):
+ """Unpacks the |boot_img| to |output_dir|, and returns the 'info' object."""
+ with open(boot_img, 'rb') as image_file:
+ boot_magic = unpack('8s', image_file.read(8))[0].decode()
+ image_file.seek(0)
+ if boot_magic == 'ANDROID!':
+ info = unpack_boot_image(image_file, output_dir)
+ elif boot_magic == 'VNDRBOOT':
+ info = unpack_vendor_boot_image(image_file, output_dir)
+ else:
+ raise ValueError(f'Not an Android boot image, magic: {boot_magic}')
- if args.format == 'mkbootimg':
+ return info
+
+
+def print_bootimg_info(info, output_format, null_separator):
+ """Format and print boot image info."""
+ if output_format == 'mkbootimg':
mkbootimg_args = info.format_mkbootimg_argument()
- if args.null:
+ if null_separator:
print('\0'.join(mkbootimg_args) + '\0', end='')
else:
print(shlex.join(mkbootimg_args))
@@ -531,7 +540,7 @@
description='Unpacks boot, recovery or vendor_boot image.',
epilog=get_unpack_usage(),
)
- parser.add_argument('--boot_img', type=FileType('rb'), required=True,
+ parser.add_argument('--boot_img', required=True,
help='path to the boot, recovery or vendor_boot image')
parser.add_argument('--out', default='out',
help='output directory of the unpacked images')
@@ -546,7 +555,8 @@
def main():
"""parse arguments and unpack boot image"""
args = parse_cmdline()
- unpack_image(args)
+ info = unpack_bootimg(args.boot_img, args.out)
+ print_bootimg_info(info, args.format, args.null)
if __name__ == '__main__':