#!/bin/bash # Copyright 2011 The ChromiumOS Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. # Sign the final build image using the "official" keys. # # Prerequisite tools needed in the system path: # # futility (from src/platform/vboot_reference) # verity (from src/platform2/verity) # load_kernel_test (from src/platform/vboot_reference) # dumpe2fs # e2fsck # sha1sum # Load common constants and variables. . "$(dirname "$0")/common.sh" . "$(dirname "$0")/lib/keycfg.sh" # Abort on errors. set -e # Our random local constants. MINIOS_KERNEL_GUID="09845860-705F-4BB5-B16C-8A8A099CAF52" FIRMWARE_VERSION=1 KERNEL_VERSION=1 V1_SUFFIX=".v1" # Print usage string usage() { cat < input_image /path/to/keys/dir [output_image] \ [version_file] [--cloud-signing] where is one of: base (sign a base image) recovery (sign a USB recovery image) factory (sign a factory install image) update_payload (sign a delta update hash) firmware (sign a firmware image) verify (verify an image including rootfs hashes) accessory_usbpd (sign USB-PD accessory firmware) accessory_rwsig (sign accessory RW firmware) gsc_firmware (sign a GSC firmware image) uefi_kernel (sign a UEFI kernel image) output_image: File name of the signed output image version_file: File name of where to read the kernel and firmware versions. --cloud-signing: Instead of relying on a local key directory, retrieve keys from Cloud KMS. --debug: Show more information for debugging purpose. If you are signing an image, you must specify an [output_image] and optionally, a [version_file]. EOF if [[ $# -gt 0 ]]; then error "$*" exit 1 fi exit 0 } # Verify we have as many arguments as we expect, else show usage & quit. # Usage: # check_argc # check_argc check_argc() { case $# in 2) if [[ $1 -ne $2 ]]; then usage "command takes exactly $2 args" fi ;; 3) if [[ $1 -lt $2 || $1 -gt $3 ]]; then usage "command takes $2 to $3 args" fi ;; *) die "check_argc: incorrect number of arguments" esac } # Run futility as root with some preserved environment variables. sudo_futility() { sudo "KMS_PKCS11_CONFIG=${KMS_PKCS11_CONFIG}" "${FUTILITY}" ${FUTILITY_EXTRA_FLAGS} "$@" } do_futility() { "${FUTILITY}" ${FUTILITY_EXTRA_FLAGS} "$@" } # TODO(gauravsh): These are duplicated from chromeos-setimage. We need # to move all signing and rootfs code to one single place where it can be # reused. crosbug.com/19543 # get_verity_arg -> get_verity_arg() { echo "$1" | sed -n "s/.*\b$2=\([^ \"]*\).*/\1/p" } # Get the dmparams parameters from a kernel config. get_dmparams_from_config() { local kernel_config=$1 echo "${kernel_config}" | sed -nre 's/.*dm="([^"]*)".*/\1/p' } # Get the verity root digest hash from a kernel config command line. get_hash_from_config() { local kernel_config=$1 local dm_config dm_config=$(get_dmparams_from_config "${kernel_config}") local vroot_dev vroot_dev=$(get_dm_device "${dm_config}" vroot) get_verity_arg "${vroot_dev}" root_hexdigest } # Get the mapped device and its args. # Usage: # get_dm_device $dm_config [vboot|vroot] # Assumes we have only one mapped device per device. get_dm_device() { local dm=$1 local device=$2 echo "${dm}" | sed -nre "s/.*${device}[^,]*,([^,]*).*/\1/p" } # Set the mapped device and its args for a device. # Usage: # set_dm_device $dm_config [vboot|vroot] args # Assumes we have only one mapped device per device. set_dm_device() { local dm=$1 local device=$2 local args=$3 echo "${dm}" | sed -nre "s#(.*${device}[^,]*,)([^,]*)(.*)#\1${args}\3#p" } CALCULATED_KERNEL_CONFIG= CALCULATED_DM_ARGS= # Calculate rootfs hash of an image # Args: ROOTFS_IMAGE KERNEL_CONFIG HASH_IMAGE # # rootfs calculation parameters are grabbed from KERNEL_CONFIG # # Updated dm-verity arguments (to be replaced in kernel config command line) # with the new hash is stored in $CALCULATED_DM_ARGS and the new hash image is # written to the file HASH_IMAGE. calculate_rootfs_hash() { local rootfs_image=$1 local kernel_config=$2 local hash_image=$3 local dm_config dm_config=$(get_dmparams_from_config "${kernel_config}") if [ -z "${dm_config}" ]; then warn "Couldn't grab dm_config. Aborting rootfs hash calculation." return 1 fi local vroot_dev vroot_dev=$(get_dm_device "${dm_config}" vroot) # Extract the key-value parameters from the kernel command line. local rootfs_sectors rootfs_sectors=$(get_verity_arg "${vroot_dev}" hashstart) local verity_algorithm verity_algorithm=$(get_verity_arg "${vroot_dev}" alg) local root_dev root_dev=$(get_verity_arg "${vroot_dev}" payload) local hash_dev hash_dev=$(get_verity_arg "${vroot_dev}" hashtree) local salt salt=$(get_verity_arg "${vroot_dev}" salt) local salt_arg if [ -n "${salt}" ]; then salt_arg="salt=${salt}" fi # Run the verity tool on the rootfs partition. local table table=$(sudo verity mode=create \ alg="${verity_algorithm}" \ payload="${rootfs_image}" \ payload_blocks=$((rootfs_sectors / 8)) \ hashtree="${hash_image}" "${salt_arg}") # Reconstruct new kernel config command line and replace placeholders. table="$(echo "${table}" | sed -s "s|ROOT_DEV|${root_dev}|g;s|HASH_DEV|${hash_dev}|")" CALCULATED_DM_ARGS="$(set_dm_device "${dm_config}" vroot "${table}")" # shellcheck disable=SC2001 CALCULATED_KERNEL_CONFIG="$(echo "${kernel_config}" | sed -e 's#\(.*dm="\)\([^"]*\)\(".*\)'"#\1${CALCULATED_DM_ARGS}\3#g")" } # Re-calculate rootfs hash, update rootfs and kernel command line(s). # Args: LOOPDEV KERNEL \ # KERN_A_KEYBLOCK KERN_A_PRIVKEY \ # KERN_B_KEYBLOCK KERN_B_PRIVKEY SHOULD_SIGN_KERN_B \ # KERN_C_KEYBLOCK KERN_C_PRIVKEY SHOULD_SIGN_KERN_C # # The rootfs is hashed by tool 'verity', and the hash data is stored after the # rootfs. A hash of those hash data (also known as final verity hash) may be # contained in kernel 2 or kernel 4 command line. # # This function reads dm-verity configuration from KERNEL, rebuilds the rootfs # hash, and then resigns kernel A & B (& C if needed) by their keyblock and # private key files. update_rootfs_hash() { local loopdev="$1" # Input image. local loop_kern="$2" # Kernel that contains verity args. local kern_a_keyblock="$3" # Keyblock file for kernel A. local kern_a_privkey="$4" # Private key file for kernel A. local kern_b_keyblock="$5" # Keyblock file for kernel B. local kern_b_privkey="$6" # Private key file for kernel B. local should_sign_kern_b="$7" local kern_c_keyblock="$8" # Keyblock file for kernel C. local kern_c_privkey="$9" # Private key file for kernel C. local should_sign_kern_c="${10}" local loop_rootfs="${loopdev}p3" # Note even though there are two kernels, there is one place (after rootfs) # for hash data, so we must assume both kernel use same hash algorithm (i.e., # DM config). info "Updating rootfs hash and updating config for Kernel partitions" # If we can't find dm parameters in the kernel config, bail out now. local kernel_config kernel_config=$(sudo_futility dump_kernel_config "${loop_kern}") local dm_config dm_config=$(get_dmparams_from_config "${kernel_config}") if [ -z "${dm_config}" ]; then error "Couldn't grab dm_config from kernel ${loop_kern}" error " (config: ${kernel_config})" return 1 fi # check and clear need_to_resign tag local rootfs_dir rootfs_dir=$(make_temp_dir) sudo mount -o ro "${loop_rootfs}" "${rootfs_dir}" if has_needs_to_be_resigned_tag "${rootfs_dir}"; then # remount as RW sudo mount -o remount,rw "${rootfs_dir}" sudo rm -f "${rootfs_dir}/${TAG_NEEDS_TO_BE_SIGNED}" fi sudo umount "${rootfs_dir}" local hash_image hash_image=$(make_temp_file) # Disable rw mount support prior to hashing. disable_rw_mount "${loop_rootfs}" if ! calculate_rootfs_hash "${loop_rootfs}" "${kernel_config}" \ "${hash_image}"; then error "calculate_rootfs_hash failed!" error "Aborting rootfs hash update!" return 1 fi local rootfs_blocks rootfs_blocks=$(sudo dumpe2fs "${loop_rootfs}" 2> /dev/null | grep "Block count" | tr -d ' ' | cut -f2 -d:) local rootfs_sectors=$((rootfs_blocks * 8)) # Overwrite the appended hashes in the rootfs sudo dd if="${hash_image}" of="${loop_rootfs}" bs=512 \ seek="${rootfs_sectors}" conv=notrunc 2>/dev/null # Update kernel command lines local dm_args="${CALCULATED_DM_ARGS}" local temp_config temp_config=$(make_temp_file) local kernelpart= local keyblock= local priv_key= local new_kernel_config= for kernelpart in 2 4 6; do loop_kern="${loopdev}p${kernelpart}" if ! new_kernel_config="$( sudo_futility dump_kernel_config "${loop_kern}" 2>/dev/null)" && [[ "${kernelpart}" == 4 ]]; then # Legacy images don't have partition 4. info "Skipping empty kernel partition 4 (legacy images)." continue fi if [[ "${should_sign_kern_b}" == "false" && "${kernelpart}" == 4 ]]; then info "Skip signing kernel B." continue fi if [[ "${should_sign_kern_c}" == "false" && "${kernelpart}" == 6 ]]; then info "Skip signing kernel C." continue fi # shellcheck disable=SC2001 new_kernel_config="$(echo "${new_kernel_config}" | sed -e 's#\(.*dm="\)\([^"]*\)\(".*\)'"#\1${dm_args}\3#g")" info "New config for kernel partition ${kernelpart} is:" echo "${new_kernel_config}" | tee "${temp_config}" # Re-calculate kernel partition signature and command line. if [[ "${kernelpart}" == 2 ]]; then keyblock="${kern_a_keyblock}" priv_key="${kern_a_privkey}" elif [[ "${kernelpart}" == 4 ]]; then keyblock="${kern_b_keyblock}" priv_key="${kern_b_privkey}" else keyblock="${kern_c_keyblock}" priv_key="${kern_c_privkey}" fi sudo_futility vbutil_kernel --repack "${loop_kern}" \ --keyblock "${keyblock}" \ --signprivate "${priv_key}" \ --version "${KERNEL_VERSION}" \ --oldblob "${loop_kern}" \ --config "${temp_config}" done } # Update the SSD install-able vblock file on stateful partition. # ARGS: Loopdev # 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. update_stateful_partition_vblock() { local loopdev="$1" local temp_out_vb temp_out_vb="$(make_temp_file)" local loop_kern="${loopdev}p4" if [[ -z "$(sudo_futility dump_kernel_config "${loop_kern}" \ 2>/dev/null)" ]]; then info "Building vmlinuz_hd.vblock from legacy image partition 2." loop_kern="${loopdev}p2" fi # vblock should always use kernel keyblock. sudo_futility vbutil_kernel --repack "${temp_out_vb}" \ --keyblock "${KEYCFG_KERNEL_KEYBLOCK}" \ --signprivate "${KEYCFG_KERNEL_VBPRIVK}" \ --oldblob "${loop_kern}" \ --vblockonly # Copy the installer vblock to the stateful partition. local stateful_dir stateful_dir=$(make_temp_dir) sudo mount "${loopdev}p1" "${stateful_dir}" sudo cp "${temp_out_vb}" "${stateful_dir}"/vmlinuz_hd.vblock sudo umount "${stateful_dir}" } # Do a validity check on the image's rootfs # ARGS: Image verify_image_rootfs() { local rootfs=$1 # This flips the read-only compatibility flag, so that e2fsck does not # complain about unknown file system capabilities. enable_rw_mount "${rootfs}" info "Running e2fsck to check root file system for errors" sudo e2fsck -fn "${rootfs}" || die "Root file system has errors!" # Flip the bit back so we don't break hashes. disable_rw_mount "${rootfs}" } # Repacks firmware updater bundle content from given folder. # Args: INPUT_DIR TARGET_SCRIPT repack_firmware_bundle() { local input_dir="$1" local target target="$(readlink -f "$2")" if [ ! -s "${target}" ]; then return 1 elif grep -q '^##CUTHERE##' "${target}"; then # Bundle supports repacking (--repack, --sb_repack) # Workaround issue crosbug.com/p/33719 sed -i \ 's/shar -Q -q -x -m -w/shar -Q -q -x -m --no-character-count/' \ "${target}" "${target}" --repack "${input_dir}" || "${target}" --sb_repack "${input_dir}" || die "Updating firmware autoupdate (--repack) failed." else # Legacy bundle using uuencode + tar.gz. # Replace MD5 checksum in the firmware update payload. local newfd_checksum newfd_checksum="$(md5sum "${input_dir}"/bios.bin | cut -f 1 -d ' ')" local temp_version temp_version="$(make_temp_file)" cat "${input_dir}"/VERSION | sed -e "s#\(.*\)\ \(.*bios.bin.*\)#${newfd_checksum}\ \2#" > \ "${temp_version}" mv "${temp_version}" "${input_dir}"/VERSION # Re-generate firmware_update.tgz and copy over encoded archive in # the original shell ball. sed -ine '/^begin .*firmware_package/,/end/D' "${target}" tar zcf - -C "${input_dir}" . | uuencode firmware_package.tgz >>"${target}" fi } # Sign a firmware in-place with the given keys. # Args: FIRMWARE_IMAGE KEY_DIR FIRMWARE_VERSION [LOEM_OUTPUT_DIR] sign_firmware() { local image=$1 local key_dir=$2 local firmware_version=$3 local loem_output_dir=${4:-} # Resign the firmware with new keys, also replacing the root and recovery # public keys in the GBB. "${SCRIPT_DIR}/sign_firmware.sh" "${image}" "${key_dir}" "${image}" \ "${firmware_version}" "${loem_output_dir}" info "Signed firmware image output to ${image}" } # Sign a delta update payload (usually created by paygen). # Args: INPUT_IMAGE KEY_DIR OUTPUT_IMAGE sign_update_payload() { local image=$1 local key_info=$2 local output=$3 local key_output key_size if [[ "${key_info}" == "remote:"* ]]; then # get label from key_info with format "remote:::