| # Copyright 2018 The ChromiumOS Authors | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| """Chrome OS Image file signing.""" | |
| import glob | |
| import logging | |
| import os | |
| import re | |
| import tempfile | |
| from chromite.lib import constants | |
| from chromite.lib import cros_build_lib | |
| from chromite.lib import image_lib | |
| from chromite.lib import kernel_cmdline | |
| from chromite.lib import osutils | |
| from chromite.signing.lib import firmware | |
| from chromite.signing.lib import keys | |
| from chromite.utils import key_value_store | |
| class Error(Exception): | |
| """Base exception for all exceptions in this module.""" | |
| class SignImageError(Error): | |
| """Error occurred within SignImage.""" | |
| def _PathForVbootSigningScripts(path=None): | |
| """Get extra_env for finding vboot_reference scripts. | |
| Args: | |
| path: path to vboot_reference/scripts/image_signing. | |
| Returns: | |
| Dictionary to pass to run's extra_env so that it finds the scripts. | |
| """ | |
| if not path: | |
| path = os.path.join( | |
| constants.SOURCE_ROOT, | |
| "src/platform/vboot_reference/scripts/image_signing", | |
| ) | |
| current_path = os.environ.get("PATH", "").split(":") | |
| if path not in current_path: | |
| current_path.insert(0, path) | |
| return {"PATH": ":".join(current_path)} | |
| def GetKernelConfig(loop_kern, check=True): | |
| """Get the kernel config for |loop_kern|. | |
| Args: | |
| loop_kern: Device file for the partition to inspect. | |
| check: Whether failure to read the command line is acceptable. | |
| Returns: | |
| String containing the kernel arguments, or None. | |
| """ | |
| ret = cros_build_lib.sudo_run( | |
| ["dump_kernel_config", loop_kern], | |
| print_cmd=False, | |
| capture_output=True, | |
| check=check, | |
| encoding="utf-8", | |
| ) | |
| if ret.returncode: | |
| return None | |
| return ret.stdout.strip() | |
| def _GetKernelCmdLine(loop_kern, check=True): | |
| """Get the kernel commandline for |loop_kern|. | |
| Args: | |
| loop_kern: Device file for the partition to inspect. | |
| check: Whether failure to read the command line is acceptable. | |
| Returns: | |
| CommandLine() containing the kernel config. | |
| """ | |
| config = GetKernelConfig(loop_kern, check) | |
| if config is None: | |
| return None | |
| else: | |
| return kernel_cmdline.CommandLine(config) | |
| def SignImage( | |
| image_type, | |
| input_file, | |
| output_file, | |
| kernel_part_id, | |
| keydir, | |
| keyA_prefix="", | |
| vboot_path=None, | |
| ): | |
| """Sign the image file. | |
| A Chromium OS image file (INPUT) always contains 2 partitions (kernel A & | |
| B). This function will rebuild hash data by DM_PARTNO, resign kernel | |
| partitions by their KEYBLOCK and PRIVKEY files, and then write to OUTPUT | |
| file. Note some special images (specified by IMAGE_TYPE, like 'recovery' or | |
| 'factory_install') may have additional steps (ex, tweaking verity hash or | |
| not stripping files) when generating output file. | |
| Args: | |
| image_type: Type of image (e.g., 'factory', 'recovery'). | |
| input_file: Image to sign. (read-only: copied to output_file). | |
| output_file: Signed image. (file is created here) | |
| kernel_part_id: partition number (or name) for the kernel (usually 2, | |
| 4 on recovery media.) | |
| keydir: Path of keyset dir to use. | |
| keyA_prefix: Prefix for kernA key (e.g., 'recovery_'). | |
| vboot_path: Vboot_reference/scripts/image_signing dir path. | |
| Raises: | |
| SignImageException | |
| """ | |
| extra_env = _PathForVbootSigningScripts(vboot_path) | |
| logging.info("Preparing %s image...", image_type) | |
| cros_build_lib.run(["cp", "--sparse=always", input_file, output_file]) | |
| keyset = keys.Keyset(keydir) | |
| firmware.ResignImageFirmware(output_file, keyset) | |
| with osutils.TempDir() as dest_dir: | |
| with image_lib.LoopbackPartitions(output_file, dest_dir) as image: | |
| rootfs_dir = image.Mount(("ROOT-A",))[0] | |
| SignAndroidImage(rootfs_dir, keyset, vboot_path=vboot_path) | |
| SignUefiBinaries(image, rootfs_dir, keyset, vboot_path=vboot_path) | |
| image.Unmount(("ROOT-A",)) | |
| # TODO(lamontjones): From this point on, all we really want at the | |
| # moment is the loopback devicefile names, but that may change as | |
| # we implement more of the shell functions. | |
| # | |
| # We do not actually want to have any filesystems mounted at this | |
| # point. | |
| loop_kernA = image.GetPartitionDevName("KERN-A") | |
| loop_rootfs = image.GetPartitionDevName("ROOT-A") | |
| loop_kern = image.GetPartitionDevName(kernel_part_id) | |
| kernA_cmd = _GetKernelCmdLine(loop_kernA) | |
| if ( | |
| image_type != "factory_install" | |
| and not kernA_cmd.GetKernelParameter("cros_legacy") | |
| and not kernA_cmd.GetKernelParameter("cros_efi") | |
| ): | |
| cros_build_lib.run( | |
| ["strip_boot_from_image.sh", "--image", loop_rootfs], | |
| extra_env=extra_env, | |
| ) | |
| ClearResignFlag(image) | |
| UpdateRootfsHash(image, loop_kern, keyset, keyA_prefix) | |
| UpdateStatefulPartitionVblock(image, keyset) | |
| if image_type == "recovery": | |
| UpdateRecoveryKernelHash(image, keyset) | |
| UpdateLegacyBootloader(image, loop_kern) | |
| logging.info("Signed %s image written to %s", image_type, output_file) | |
| def SignAndroidImage(rootfs_dir, keyset, vboot_path=None): | |
| """If there is an android image, sign it.""" | |
| system_img = os.path.join( | |
| rootfs_dir, "opt/google/containers/android/system.raw.img" | |
| ) | |
| if not os.path.exists(system_img): | |
| logging.info("ARC image not found. Not signing Android APKs.") | |
| return | |
| arc_version = key_value_store.LoadFile( | |
| os.path.join(rootfs_dir, "etc/lsb-release") | |
| ).get("CHROMEOS_ARC_VERSION", "") | |
| if not arc_version: | |
| logging.warning( | |
| "CHROMEOS_ARC_VERSION not found in lsb-release. " | |
| "Not signing Android APKs." | |
| ) | |
| return | |
| extra_env = _PathForVbootSigningScripts(vboot_path) | |
| logging.info("Found ARC image version %s, resigning APKs", arc_version) | |
| # Sign the Android APKs using ${keyset.key_dir}/android keys. | |
| android_keydir = os.path.join(keyset.key_dir, "android") | |
| logging.info("Using %s", android_keydir) | |
| # TODO(lamontjones) migrate sign_android_image.sh. | |
| cros_build_lib.run( | |
| ["sign_android_image.sh", rootfs_dir, android_keydir], | |
| extra_env=extra_env, | |
| ) | |
| def SignUefiBinaries(image, rootfs_dir, keyset, vboot_path=None): | |
| """Sign UEFI binaries if appropriate.""" | |
| # If there are no uefi keys in the keyset, we're done. | |
| uefi_keydir = os.path.join(keyset.key_dir, "uefi") | |
| if not os.path.isdir(uefi_keydir): | |
| logging.info("No UEFI keys in keyset. Skipping.") | |
| return | |
| # Mount the UEFI partition and sign the contents. | |
| try: | |
| uefi_fsdir = image.Mount(("EFI-SYSTEM",))[0] | |
| except KeyError: | |
| # Image has no EFI-SYSTEM partition. | |
| logging.info("No EFI-SYSTEM partition found.") | |
| return | |
| extra_env = _PathForVbootSigningScripts(vboot_path) | |
| # Sign the UEFI binaries on the EFI partition using | |
| # ${keyset.key_dir}/uefi keys. | |
| # TODO(lamontjones): convert install_gsetup_certs.sh to python. | |
| cros_build_lib.run( | |
| ["install_gsetup_certs.sh", uefi_fsdir, uefi_keydir], | |
| extra_env=extra_env, | |
| ) | |
| # TODO(lamontjones): convert sign_uefi.sh to python. | |
| cros_build_lib.run( | |
| ["sign_uefi.sh", uefi_fsdir, uefi_keydir], extra_env=extra_env | |
| ) | |
| # TODO(lamontjones): convert sign_uefi.sh to python. | |
| cros_build_lib.run( | |
| ["sign_uefi.sh", os.path.join(rootfs_dir, "boot"), uefi_keydir], | |
| extra_env=extra_env, | |
| ) | |
| logging.info("Signed UEFI binaries.") | |
| class CalculateRootfsHash: | |
| """Hash info, and other facts about it, suitable for comparison or copying. | |
| Instantiating this class causes it to calculate a new DmConfig and | |
| CommandLine for the given image, as well as creating a file with the | |
| new hashtree for the image. The temporary file is deleted along with the | |
| instance. | |
| Examples: | |
| (See UpdateRootfsHash below) | |
| image = image_lib.LoopbackPartitions(image_path) | |
| rootfs_hash = CalculateRootfsHash( | |
| image, kernel_cmdline.CommandLine( | |
| image.GetPartitionDevName('KERN-A') | |
| ) | |
| ) | |
| <copy or compare updated hashtree, dm_config, | |
| kernel_cmdline to the image> | |
| <do other things, confident that when rootfs_hash is garbage collected, | |
| the underlying new hashtree file will be deleted.> | |
| Attributes: | |
| calculated_dm_config: Updated DmConfig for the kernel | |
| calculated_kernel_cmdline: New kernel_cmdline.CommandLine | |
| hashtree_filename: Name of the temporary file containing the new | |
| hashtree. | |
| """ | |
| def __init__(self, image, cmd_line): | |
| """Create the hash_image for the rootfs. | |
| Args: | |
| image: image_lib.LoopbackPartitions() for the image. | |
| cmd_line: kernel_cmdline.CommandLine for the kernel. | |
| """ | |
| self.image = image | |
| self.cmd_line = cmd_line | |
| loop_rootfs = image.GetPartitionDevName("ROOT-A") | |
| # pylint: disable=consider-using-with | |
| self._file = tempfile.NamedTemporaryFile( | |
| dir=image.destination, delete=False | |
| ) | |
| # pylint: enable=consider-using-with | |
| dm_config = cmd_line.GetDmConfig() | |
| if not dm_config: | |
| logging.warning( | |
| "Couldn't grab dm_config. Aborting rootfs hash calculation." | |
| ) | |
| # TODO(lamontjones): This should probably raise an exception. | |
| return | |
| vroot_dev = dm_config.devices["vroot"] | |
| # Get the verity args from the existing DmConfig. | |
| rootfs_blocks = int(vroot_dev.GetVerityArg("hashstart").value) // 8 | |
| alg = vroot_dev.GetVerityArg("alg").value | |
| root_dev = vroot_dev.GetVerityArg("payload").value | |
| hash_dev = vroot_dev.GetVerityArg("hashtree").value | |
| salt = vroot_dev.GetVerityArg("salt") | |
| cmd = [ | |
| "verity", | |
| "mode=create", | |
| "alg=%s" % alg, | |
| "payload=%s" % loop_rootfs, | |
| "payload_blocks=%d" % rootfs_blocks, | |
| "hashtree=%s" % self._file.name, | |
| ] | |
| if salt: | |
| cmd.append("salt=%s" % salt.value) | |
| target_template = cros_build_lib.sudo_run( | |
| cmd, print_cmd=False, capture_output=True, encoding="utf-8" | |
| ).stdout | |
| # target_template is a templated DmLine string. | |
| target = kernel_cmdline.DmLine( | |
| target_template.replace("ROOT_DEV", root_dev).replace( | |
| "HASH_DEV", hash_dev | |
| ) | |
| ) | |
| vroot_dev.rows[0] = target | |
| self.calculated_dm_config = dm_config | |
| self.calculated_kernel_cmdline = self.cmd_line | |
| self.hashtree_filename = self._file.name | |
| def __del__(self): | |
| if getattr(self, "_file", None): | |
| os.unlink(self._file.name) | |
| del self._file | |
| def ClearResignFlag(image): | |
| """Remove any /root/.need_to_be_signed file from the rootfs. | |
| Args: | |
| image: image_lib.LoopbackPartitions instance for this image. | |
| """ | |
| # Check and clear the need_to_resign tag file. | |
| rootfs_dir = image.Mount(("ROOT-A",), mount_opts=("rw",))[0] | |
| needs_to_be_signed = os.path.join(rootfs_dir, "root/.need_to_be_signed") | |
| if os.path.exists(needs_to_be_signed): | |
| image.Mount(("ROOT-A",), mount_opts=("remount", "rw")) | |
| osutils.SafeUnlink(needs_to_be_signed, sudo=True) | |
| image.Unmount(("ROOT-A",)) | |
| def UpdateRootfsHash(image, loop_kern, keyset, keyA_prefix): | |
| """Update the root filesystem hash. | |
| Args: | |
| image: image_lib.LoopbackPartitions instance for this image. | |
| loop_kern: Device file name for the kernel partition to hash. | |
| keyset: Kernel_cmdline.Keyset to use. | |
| keyA_prefix: Prefix for kernA key (e.g., 'recovery_'). | |
| """ | |
| logging.info( | |
| "Updating rootfs hash and updating cmdline for kernel partitions" | |
| ) | |
| logging.info( | |
| "%s (in %s) keyset=%s keyA_prefix=%s", | |
| loop_kern, | |
| image.path, | |
| keyset.key_dir, | |
| keyA_prefix, | |
| ) | |
| cmd_line = _GetKernelCmdLine(loop_kern, check=False) | |
| if cmd_line: | |
| dm_config = cmd_line.GetDmConfig() | |
| if not cmd_line or not dm_config: | |
| logging.error("Couldn't get dm_config from kernel %s", loop_kern) | |
| logging.error(" (cmdline: %s)", cmd_line) | |
| raise SignImageError("Could not get dm_config from kernel") | |
| loop_rootfs = image.GetPartitionDevName("ROOT-A") | |
| image.DisableRwMount("ROOT-A") | |
| rootfs_hash = CalculateRootfsHash(image, cmd_line) | |
| fsinfo = cros_build_lib.sudo_run( | |
| ["tune2fs", "-l", image.GetPartitionDevName("ROOT-A")], | |
| capture_output=True, | |
| encoding="utf-8", | |
| ).stdout | |
| rootfs_blocks = int( | |
| re.search( | |
| r"^Block count: *([0-9]+)$", fsinfo, flags=re.MULTILINE | |
| ).group(1) | |
| ) | |
| rootfs_sectors = 8 * rootfs_blocks | |
| # Overwrite the appended hashes in the rootfs. | |
| cros_build_lib.sudo_run( | |
| [ | |
| "dd", | |
| "if=%s" % rootfs_hash.hashtree_filename, | |
| "of=%s" % loop_rootfs, | |
| "bs=512", | |
| "seek=%d" % rootfs_sectors, | |
| "conv=notrunc", | |
| ], | |
| stderr=True, | |
| ) | |
| # Update kernel command lines. | |
| for kern in ("KERN-A", "KERN-B"): | |
| loop_kern = image.GetPartitionDevName(kern) | |
| new_cmd_line = _GetKernelCmdLine(loop_kern, check=False) | |
| if not new_cmd_line and kern == "KERN-B": | |
| logging.info("Skipping empty KERN-B partition (legacy images).") | |
| continue | |
| new_cmd_line.SetDmConfig(rootfs_hash.calculated_dm_config) | |
| logging.info("New cmdline for %s partition is: %s", kern, new_cmd_line) | |
| if kern == "KERN-A": | |
| key = keyset.keys["%skernel_data_key" % keyA_prefix] | |
| else: | |
| key = keyset.keys["kernel_data_key"] | |
| _UpdateKernelConfig(loop_kern, new_cmd_line, key) | |
| def _UpdateKernelConfig(loop_kern, cmdline, key): | |
| """Update the kernel config for |loop_kern|. | |
| Args: | |
| loop_kern: Device file for the partition to inspect. | |
| cmdline: CommandLine instance to set. | |
| key: Key to use. | |
| """ | |
| with tempfile.NamedTemporaryFile() as temp: | |
| temp.file.write(cmdline.Format().encode("utf-8")) | |
| temp.file.flush() | |
| cros_build_lib.sudo_run( | |
| [ | |
| "vbutil_kernel", | |
| "--repack", | |
| loop_kern, | |
| "--keyblock", | |
| key.keyblock, | |
| "--signprivate", | |
| key.private, | |
| "--version", | |
| str(key.version), | |
| "--oldblob", | |
| loop_kern, | |
| "--config", | |
| temp.name, | |
| ] | |
| ) | |
| def UpdateStatefulPartitionVblock(image, keyset): | |
| """Update the SSD install-able vblock file on stateful partition. | |
| This is deprecated because all new images should have a SSD boot-able kernel | |
| in partition 4. However, the signer needs to be able to sign new & old | |
| images (crbug.com/449450#c13) so we will probably never remove this. | |
| Args: | |
| image: image_lib.LoopbackPartitions() for the image. | |
| keyset: Keyset to use for signing | |
| """ | |
| with tempfile.NamedTemporaryFile(dir=image.destination) as tmpfile: | |
| loop_kern = image.GetPartitionDevName("KERN-B") | |
| ret = _GetKernelCmdLine(loop_kern, check=False) | |
| if not ret: | |
| logging.info( | |
| "Building vmlinuz_hd.vblock from legacy image partition 2." | |
| ) | |
| loop_kern = image.GetPartitionDevName(2) | |
| kernel_key = keyset.keys["kernel_data_key"] | |
| cros_build_lib.sudo_run( | |
| [ | |
| "vbutil_kernel", | |
| "--repack", | |
| tmpfile.name, | |
| "--keyblock", | |
| kernel_key.keyblock, | |
| "--signprivate", | |
| kernel_key.private, | |
| "--oldblob", | |
| loop_kern, | |
| "--vblockonly", | |
| ] | |
| ) | |
| state_dir = image.Mount(("STATE",), mount_opts=("rw",))[0] | |
| cros_build_lib.sudo_run( | |
| ["cp", tmpfile.name, os.path.join(state_dir, "vmlinuz_hd.vblock")] | |
| ) | |
| image.Unmount(("STATE",)) | |
| def UpdateRecoveryKernelHash(image, keyset): | |
| """Update the recovery kernel hash.""" | |
| loop_kernA = image.GetPartitionDevName("KERN-A") | |
| loop_kernB = image.GetPartitionDevName("KERN-B") | |
| # Update the KERN-B hash in the KERN-A command line. | |
| kernA_cmd = _GetKernelCmdLine(loop_kernA) | |
| old_kernB_hash = kernA_cmd.GetKernelParameter("kern_b_hash") | |
| if old_kernB_hash: | |
| cmd = "sha256sum" if len(old_kernB_hash.value) >= 64 else "sha1sum" | |
| new_kernB_hash = cros_build_lib.sudo_run( | |
| [cmd, loop_kernB], stdout=True, encoding="utf-8" | |
| ).stdout.split()[0] | |
| kernA_cmd.SetKernelParameter("kern_b_hash", new_kernB_hash) | |
| logging.info("New cmdline for kernel A is %s", str(kernA_cmd)) | |
| recovery_key = keyset.keys["recovery"] | |
| _UpdateKernelConfig(loop_kernA, kernA_cmd, recovery_key) | |
| def UpdateLegacyBootloader(image, loop_kern): | |
| """Update the legacy bootloader templates in EFI partition.""" | |
| try: | |
| uefi_dir = image.Mount(("EFI-SYSTEM",))[0] | |
| except KeyError: | |
| # Image has no EFI-SYSTEM partition. | |
| logging.info( | |
| "Could not mount EFI partition for updating legacy bootloader cfg." | |
| ) | |
| raise SignImageError("Could not mount EFI partition") | |
| root_digest = "" | |
| cmd_line = _GetKernelCmdLine(loop_kern) | |
| if cmd_line: | |
| dm_config = cmd_line.GetDmConfig() | |
| if dm_config: | |
| vroot_dev = dm_config.devices["vroot"] | |
| if vroot_dev: | |
| root_digest = vroot_dev.GetVerityArg("root_hexdigest") | |
| if root_digest: | |
| root_digest = root_digest.value | |
| if not root_digest: | |
| logging.error( | |
| "Could not grab root_digest from kernel partition %s", loop_kern | |
| ) | |
| logging.error("cmdline: %s", cmd_line) | |
| raise SignImageError("Could not find root digest") | |
| files = [] | |
| sys_dir = os.path.join(uefi_dir, "syslinux") | |
| if os.path.isdir(sys_dir): | |
| files += glob.glob(os.path.join(sys_dir, "*.cfg")) | |
| grub_cfg = os.path.join(uefi_dir, "efi/boot/grub.cfg") | |
| if os.path.exists(grub_cfg): | |
| files.append(grub_cfg) | |
| if files: | |
| ret = cros_build_lib.sudo_run( | |
| [ | |
| "sed", | |
| "-iE", | |
| r"s/\broot_hexdigest=[a-z0-9]+/root_hexdigest=%s/g" | |
| % root_digest, | |
| ] | |
| + files, | |
| check=False, | |
| ) | |
| if ret.returncode: | |
| logging.error( | |
| "Updating bootloader configs failed: %s", " ".join(files) | |
| ) | |
| raise SignImageError("Updating bootloader configs failed") | |
| def DumpConfig(image_file): | |
| """Dump kernel config for both kernels. | |
| This implements the necessary logic for bin/dump_config, which is intended | |
| primarily for debugging of images. | |
| Args: | |
| image_file: path to the image file from which to dump kernel configs. | |
| """ | |
| with image_lib.LoopbackPartitions(image_file) as image: | |
| for kernel_part in ("KERN-A", "KERN-B"): | |
| loop_kern = image.GetPartitionDevName(kernel_part) | |
| config = GetKernelConfig(loop_kern, check=False) | |
| if config: | |
| logging.info("Partition %s", kernel_part) | |
| logging.info(config) | |
| else: | |
| logging.info("Partition %s has no configuration.", kernel_part) |