diff --git a/tools/releasetools/Android.bp b/tools/releasetools/Android.bp index 5f99f6c573..ee266b7d91 100644 --- a/tools/releasetools/Android.bp +++ b/tools/releasetools/Android.bp @@ -168,6 +168,7 @@ python_defaults { "apexd_host", "brillo_update_payload", "checkvintf", + "generate_gki_certificate", "lz4", "toybox", "unpack_bootimg", @@ -244,6 +245,7 @@ python_library_host { "boot_signer", "brotli", "bsdiff", + "generate_gki_certificate", "imgdiff", "lz4", "mkbootfs", @@ -308,6 +310,7 @@ python_defaults { "brotli", "bsdiff", "deapexer", + "generate_gki_certificate", "imgdiff", "lz4", "mkbootfs", diff --git a/tools/releasetools/common.py b/tools/releasetools/common.py index a4c92ae02a..8ce6083f44 100644 --- a/tools/releasetools/common.py +++ b/tools/releasetools/common.py @@ -1575,6 +1575,50 @@ def GetAvbChainedPartitionArg(partition, info_dict, key=None): pubkey_path=pubkey_path) +def _HasGkiCertificationArgs(): + return ("gki_signing_key_path" in OPTIONS.info_dict and + "gki_signing_algorithm" in OPTIONS.info_dict) + + +def _GenerateGkiCertificate(image, image_name): + key_path = OPTIONS.info_dict.get("gki_signing_key_path") + algorithm = OPTIONS.info_dict.get("gki_signing_algorithm") + + key_path = ResolveAVBSigningPathArgs(key_path) + + # Checks key_path exists, before processing --gki_signing_* args. + if not os.path.exists(key_path): + raise ExternalError( + 'gki_signing_key_path: "{}" not found'.format(key_path)) + + output_certificate = tempfile.NamedTemporaryFile() + cmd = [ + "generate_gki_certificate", + "--name", image_name, + "--algorithm", algorithm, + "--key", key_path, + "--output", output_certificate.name, + image, + ] + + signature_args = OPTIONS.info_dict.get("gki_signing_signature_args", "") + signature_args = signature_args.strip() + if signature_args: + cmd.extend(["--additional_avb_args", signature_args]) + + args = OPTIONS.info_dict.get("avb_boot_add_hash_footer_args", "") + args = args.strip() + if args: + cmd.extend(["--additional_avb_args", args]) + + RunAndCheckOutput(cmd) + + output_certificate.seek(os.SEEK_SET, 0) + data = output_certificate.read() + output_certificate.close() + return data + + def BuildVBMeta(image_path, partitions, name, needed_partitions, resolve_rollback_index_location_conflict=False): """Creates a VBMeta image. @@ -1797,6 +1841,29 @@ def _BuildBootableImage(image_name, sourcedir, fs_config_file, RunAndCheckOutput(cmd) + if _HasGkiCertificationArgs(): + if not os.path.exists(img.name): + raise ValueError("Cannot find GKI boot.img") + if kernel_path is None or not os.path.exists(kernel_path): + raise ValueError("Cannot find GKI kernel.img") + + # Certify GKI images. + boot_signature_bytes = b'' + boot_signature_bytes += _GenerateGkiCertificate(img.name, "boot") + boot_signature_bytes += _GenerateGkiCertificate( + kernel_path, "generic_kernel") + + BOOT_SIGNATURE_SIZE = 16 * 1024 + if len(boot_signature_bytes) > BOOT_SIGNATURE_SIZE: + raise ValueError( + f"GKI 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(img.name, 'ab') as f: + f.write(boot_signature_bytes) + # Sign the image if vboot is non-empty. if info_dict.get("vboot"): path = "/" + partition_name @@ -1910,6 +1977,9 @@ def HasRamdisk(partition_name, info_dict=None): if info_dict.get("recovery_as_boot") == "true": return True # the recovery-as-boot boot.img has a RECOVERY ramdisk. + if info_dict.get("gki_boot_image_without_ramdisk") == "true": + return False # A GKI boot.img has no ramdisk since Android-13. + if info_dict.get("system_root_image") == "true": # The ramdisk content is merged into the system.img, so there is NO # ramdisk in the boot.img or boot-.img. diff --git a/tools/releasetools/sign_target_files_apks.py b/tools/releasetools/sign_target_files_apks.py index 2fbb3b05c2..2b45825ae5 100755 --- a/tools/releasetools/sign_target_files_apks.py +++ b/tools/releasetools/sign_target_files_apks.py @@ -123,6 +123,17 @@ Usage: sign_target_files_apks [flags] input_target_files output_target_files mounted on the partition (e.g. "--signing_helper /path/to/helper"). The args will be appended to the existing ones in info dict. + --gki_signing_algorithm + --gki_signing_key + Use the specified algorithm (e.g. SHA256_RSA4096) and the key to generate + 'boot signature' in a v4 boot.img. Otherwise it uses the existing values + in info dict. + + --gki_signing_extra_args + Specify any additional args that are needed to generate 'boot signature' + (e.g. --prop foo:bar). The args will be appended to the existing ones + in info dict. + --android_jar_path Path to the android.jar to repack the apex file. @@ -182,6 +193,9 @@ OPTIONS.tag_changes = ("-test-keys", "-dev-keys", "+release-keys") OPTIONS.avb_keys = {} OPTIONS.avb_algorithms = {} OPTIONS.avb_extra_args = {} +OPTIONS.gki_signing_key = None +OPTIONS.gki_signing_algorithm = None +OPTIONS.gki_signing_extra_args = None OPTIONS.android_jar_path = None OPTIONS.vendor_partitions = set() OPTIONS.vendor_otatools = None @@ -538,7 +552,7 @@ def ProcessTargetFiles(input_tf_zip, output_tf_zip, misc_info, [len(os.path.basename(i.filename)) for i in input_tf_zip.infolist() if GetApkFileInfo(i.filename, compressed_extension, [])[0]]) except ValueError: - # Sets this to zero for targets without APK files. + # Sets this to zero for targets without APK files, e.g., gki_arm64. maxsize = 0 system_root_image = misc_info.get("system_root_image") == "true" @@ -754,6 +768,9 @@ def ProcessTargetFiles(input_tf_zip, output_tf_zip, misc_info, if misc_info.get('avb_enable') == 'true': RewriteAvbProps(misc_info) + # Replace the GKI signing key for boot.img, if any. + ReplaceGkiSigningKey(misc_info) + # Write back misc_info with the latest values. ReplaceMiscInfoTxt(input_tf_zip, output_tf_zip, misc_info) @@ -1035,6 +1052,27 @@ def RewriteAvbProps(misc_info): misc_info[args_key] = result +def ReplaceGkiSigningKey(misc_info): + """Replaces the GKI signing key.""" + + key = OPTIONS.gki_signing_key + if not key: + return + + algorithm = OPTIONS.gki_signing_algorithm + if not algorithm: + raise ValueError("Missing --gki_signing_algorithm") + + print('Replacing GKI signing key with "%s" (%s)' % (key, algorithm)) + misc_info["gki_signing_algorithm"] = algorithm + misc_info["gki_signing_key_path"] = key + + extra_args = OPTIONS.gki_signing_extra_args + if extra_args: + print('Setting GKI signing args: "%s"' % (extra_args)) + misc_info["gki_signing_signature_args"] = extra_args + + def BuildKeyMap(misc_info, key_mapping_options): for s, d in key_mapping_options: if s is None: # -d option @@ -1388,6 +1426,12 @@ def main(argv): # 'oem=--signing_helper_with_files=/tmp/avbsigner.sh'. partition, extra_args = a.split("=", 1) OPTIONS.avb_extra_args[partition] = extra_args + elif o == "--gki_signing_key": + OPTIONS.gki_signing_key = a + elif o == "--gki_signing_algorithm": + OPTIONS.gki_signing_algorithm = a + elif o == "--gki_signing_extra_args": + OPTIONS.gki_signing_extra_args = a elif o == "--vendor_otatools": OPTIONS.vendor_otatools = a elif o == "--vendor_partitions": @@ -1451,6 +1495,9 @@ def main(argv): "avb_extra_custom_image_key=", "avb_extra_custom_image_algorithm=", "avb_extra_custom_image_extra_args=", + "gki_signing_key=", + "gki_signing_algorithm=", + "gki_signing_extra_args=", "vendor_partitions=", "vendor_otatools=", "allow_gsi_debug_sepolicy", diff --git a/tools/releasetools/test_common.py b/tools/releasetools/test_common.py index c61c29038f..14f0e88f91 100644 --- a/tools/releasetools/test_common.py +++ b/tools/releasetools/test_common.py @@ -1636,6 +1636,40 @@ class CommonUtilsTest(test_utils.ReleaseToolsTestCase): self.assertEqual(3, chained_partition_args.rollback_index_location) self.assertTrue(os.path.exists(chained_partition_args.pubkey_path)) + def test_GenerateGkiCertificate_KeyPathNotFound(self): + pubkey = os.path.join(self.testdata_dir, 'no_testkey_gki.pem') + self.assertFalse(os.path.exists(pubkey)) + + common.OPTIONS.info_dict = { + 'gki_signing_key_path': pubkey, + 'gki_signing_algorithm': 'SHA256_RSA4096', + 'gki_signing_signature_args': '--prop foo:bar', + } + common.OPTIONS.search_path = None + test_file = tempfile.NamedTemporaryFile() + self.assertRaises(common.ExternalError, common._GenerateGkiCertificate, + test_file.name, 'generic_kernel') + + def test_GenerateGkiCertificate_SearchKeyPathNotFound(self): + pubkey = 'no_testkey_gki.pem' + self.assertFalse(os.path.exists(pubkey)) + + # Tests it should raise ExternalError if no key found under + # OPTIONS.search_path. + search_path_dir = common.MakeTempDir() + search_pubkey = os.path.join(search_path_dir, pubkey) + self.assertFalse(os.path.exists(search_pubkey)) + + common.OPTIONS.search_path = search_path_dir + common.OPTIONS.info_dict = { + 'gki_signing_key_path': pubkey, + 'gki_signing_algorithm': 'SHA256_RSA4096', + 'gki_signing_signature_args': '--prop foo:bar', + } + test_file = tempfile.NamedTemporaryFile() + self.assertRaises(common.ExternalError, common._GenerateGkiCertificate, + test_file.name, 'generic_kernel') + class InstallRecoveryScriptFormatTest(test_utils.ReleaseToolsTestCase): """Checks the format of install-recovery.sh. diff --git a/tools/releasetools/test_sign_target_files_apks.py b/tools/releasetools/test_sign_target_files_apks.py index 9cc6df428c..0cd7dac184 100644 --- a/tools/releasetools/test_sign_target_files_apks.py +++ b/tools/releasetools/test_sign_target_files_apks.py @@ -23,7 +23,8 @@ import common import test_utils from sign_target_files_apks import ( CheckApkAndApexKeysAvailable, EditTags, GetApkFileInfo, ReadApexKeysInfo, - ReplaceCerts, RewriteAvbProps, RewriteProps, WriteOtacerts) + ReplaceCerts, ReplaceGkiSigningKey, RewriteAvbProps, RewriteProps, + WriteOtacerts) class SignTargetFilesApksTest(test_utils.ReleaseToolsTestCase): @@ -535,3 +536,52 @@ name="apex.apexd_test_different_app.apex" public_key="system/apex/apexd/apexd_te 'system/apex/apexd/apexd_testdata/com.android.apex.test_package_2.pem', 'build/make/target/product/security/testkey', None), }, keys_info) + + def test_ReplaceGkiSigningKey(self): + common.OPTIONS.gki_signing_key = 'release_gki_key' + common.OPTIONS.gki_signing_algorithm = 'release_gki_algorithm' + common.OPTIONS.gki_signing_extra_args = 'release_gki_signature_extra_args' + + misc_info = { + 'gki_signing_key_path': 'default_gki_key', + 'gki_signing_algorithm': 'default_gki_algorithm', + 'gki_signing_signature_args': 'default_gki_signature_args', + } + expected_dict = { + 'gki_signing_key_path': 'release_gki_key', + 'gki_signing_algorithm': 'release_gki_algorithm', + 'gki_signing_signature_args': 'release_gki_signature_extra_args', + } + ReplaceGkiSigningKey(misc_info) + self.assertDictEqual(expected_dict, misc_info) + + def test_ReplaceGkiSigningKey_MissingSigningAlgorithm(self): + common.OPTIONS.gki_signing_key = 'release_gki_key' + common.OPTIONS.gki_signing_algorithm = None + common.OPTIONS.gki_signing_extra_args = 'release_gki_signature_extra_args' + + misc_info = { + 'gki_signing_key_path': 'default_gki_key', + 'gki_signing_algorithm': 'default_gki_algorithm', + 'gki_signing_signature_args': 'default_gki_signature_args', + } + self.assertRaises(ValueError, ReplaceGkiSigningKey, misc_info) + + def test_ReplaceGkiSigningKey_MissingSigningKeyNop(self): + common.OPTIONS.gki_signing_key = None + common.OPTIONS.gki_signing_algorithm = 'release_gki_algorithm' + common.OPTIONS.gki_signing_extra_args = 'release_gki_signature_extra_args' + + # No change to misc_info if common.OPTIONS.gki_signing_key is missing. + misc_info = { + 'gki_signing_key_path': 'default_gki_key', + 'gki_signing_algorithm': 'default_gki_algorithm', + 'gki_signing_signature_args': 'default_gki_signature_args', + } + expected_dict = { + 'gki_signing_key_path': 'default_gki_key', + 'gki_signing_algorithm': 'default_gki_algorithm', + 'gki_signing_signature_args': 'default_gki_signature_args', + } + ReplaceGkiSigningKey(misc_info) + self.assertDictEqual(expected_dict, misc_info) diff --git a/tools/releasetools/validate_target_files.py b/tools/releasetools/validate_target_files.py index 84a2f7e261..82b31076f7 100755 --- a/tools/releasetools/validate_target_files.py +++ b/tools/releasetools/validate_target_files.py @@ -132,7 +132,7 @@ def ValidateFileConsistency(input_zip, input_tmp, info_dict): return # Verify IMAGES/system.img if applicable. - # Some targets are system.img-less. + # Some targets, e.g., gki_arm64, gki_x86_64, etc., are system.img-less. if 'IMAGES/system.img' in input_zip.namelist(): CheckAllFiles('system')