blob: 4fba061e39045acc6edb652177099614dd0cc950 [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright 2018 The Chromium OS Authors. All rights reserved.
# 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."""
from __future__ import print_function
import glob
import os
import re
import tempfile
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import cros_logging as logging
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.output.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(object):
"""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')
self._file = tempfile.NamedTemporaryFile(
dir=image.destination, delete=False)
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)
verity = cros_build_lib.sudo_run(
cmd, print_cmd=False, capture_output=True, encoding='utf-8').stdout
# verity is a templated DmLine string.
slave = kernel_cmdline.DmLine(
verity.replace('ROOT_DEV', root_dev).replace('HASH_DEV', hash_dev))
vroot_dev.rows[0] = slave
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)