| # Copyright (c) 2011 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. |
| # |
| # This consists of functions sourced by the /init script and used |
| # exclusively for recovery images. Note that this code uses the |
| # busybox shell (not bash, not dash). |
| |
| # Include disk information. |
| . /usr/sbin/write_gpt.sh |
| |
| # Minimum (default) version in firmware space |
| FW_VER_MIN=0x10001 |
| # Starting kernel rollback version. |
| KERNEL_VER_MIN=0x10001 |
| # TPM NVRAM index where the rollback firmware version is stored. |
| FW_VER_TPM_NV_SPACE=0x1007 |
| # TPM NVRAM index where the rollback kernel version is stored. |
| KERNEL_VER_TPM_NV_SPACE=0x1008 |
| # TPM NVRAM index for the lockbox. |
| LOCKBOX_TPM_NV_SPACE=0x20000004 |
| # TPM NVRAM index for OOBE autoconfig public key hashes. |
| OOBE_AUTO_CONFIG_KEY_HASH_NV_SPACE=0x100c |
| # Recovery version is the top nibble of the firmware version. Lock out the |
| # recovery image if the version is greater than this. |
| REC_VER_MAX=0 |
| # Where chromeos-install will store any related hardware diagnostics data. |
| LOG_HARDWARE_DIAGNOSTICS=hardware_diagnostics.log |
| # Minimum battery charge required for TPM firmware update. |
| MIN_BATTERY_CHARGE_PERCENT=10 |
| # Whether we should proceed with OOBE auto-setup. |
| OOBE_AUTO_CONFIG= |
| # PEM-format keys for signing OOBE configuration data. |
| OOBE_PRIVATE_KEY_PEM= |
| OOBE_PUBLIC_KEY_PEM= |
| # Domain specified for OOBE auto-enrollment. |
| OOBE_ENROLLMENT_DOMAIN= |
| |
| |
| # Installation Target. |
| # DST_DEV_BASE: A device path for concatenate partition number. |
| # Sample: /dev/mmcblk0p /dev/sda |
| # Usage: "${DST_DEV_BASE}${PART_NUM}" |
| # DST: A device path for the block device itself (similar to rootdev -d). |
| # Sample: /dev/mmcblk0 /dev/sda |
| DST_DEV_BASE= |
| DST= |
| |
| |
| # Error codes used as return code by functions to indicate installation |
| # should be aborted for the corresponding reason. |
| |
| # Indicates a failure validating the kernel. |
| ERR_INVALID_INSTALL_KERNEL=2 |
| |
| # The image failed validation on a device with block_devmode=1. |
| ERR_DEV_MODE_BLOCKED=3 |
| |
| |
| # Check whether the device owner has configured the device to block |
| # developer mode. Note that the check works regardless of whether the |
| # device is currently booted in developer mode or not. |
| # |
| # This check is used to enforce the block_devmode flag. The regular boot |
| # path checks the flag in chromeos_startup, which resides in the root. |
| # Hence, only official kernels and roots that are guaranteed to perform the |
| # check can be allowed to be installed - otherwise developer mode blocking |
| # is easily circumvented by installing an unofficial kernel or root that |
| # doesn't perform the block_devmode check. |
| is_developer_mode_blocked() { |
| # The device should refuse operation in developer mode if the device |
| # owner has flipped the block_devmode flag to 1. We still block |
| # even if firmware write protection is disabled, because removing a |
| # screw is simpler than hooking up a different disk or using a |
| # dediprog to reprogram the flash directly. |
| if crossystem 'block_devmode?1'; then |
| return 0 |
| fi |
| [ "$(vpd -i RW_VPD -g block_devmode)" = "1" ] |
| } |
| |
| # Verifies the recovery root by reading all blocks of the dm-verity |
| # block device. Return code indicates whether verification succeeded. |
| verify_recovery_root() { |
| local usb_base=$(basename "$USB_DEV") |
| local size=$(( 512 * $(cat /sys/block/$usb_base/size) )) |
| |
| # Ensure the verified rootfs is fully intact or fail with no USB_DEV. |
| # REAL_USB_DEV is left intact. |
| # |
| # Correctness wins over speed for this. Correctness also wins |
| # over readability. :-( |
| # |
| # High level summary: The 'pv -n' command copies bytes from stdin |
| # to stdout, and periodically prints *to stderr* the amount read |
| # as a percentage of the size in the '-s' option. The pipeline |
| # redirects the stderr from 'pv' into stdin of 'progress_bar'. |
| # This usage is covered in the pv(1) man page, which rightly warns |
| # that "it may cause the programmer to overheat." |
| ( |
| # The 'set -x' output goes to stderr, which would become input |
| # to 'progress_bar', below. Turn it off in a subshell so as not |
| # to affect the main process. |
| set +x |
| ( |
| ( |
| # This 'dd' is the actual validation. We must capture the |
| # exit code in order to figure out whether it passed or not. |
| dd if="$USB_DEV" bs=$((16 * 1024 * 1024)) 2>/dev/null |
| echo $? >/tmp/verification_status |
| ) | pv -n -s $size >/dev/null |
| ) 2>&1 | ( set -x ; progress_bar ) >$LOG_DIR/progress.log 2>&1 |
| ) |
| if [ "$(cat /tmp/verification_status)" != "0" ]; then |
| dlog "Included root filesystem could not be verified." |
| return 1 |
| fi |
| return 0 |
| } |
| |
| # Checks that we have a valid recovery root before handing it off to the |
| # installer. |
| validate_recovery_root() { |
| # Allow test recovery roots that are unverified unless developer mode |
| # is blocked by the device owner. |
| if [ "$USB_DEV" != "/dev/dm-0" ] || is_unofficial_root; then |
| is_developer_mode_blocked && return $ERR_DEV_MODE_BLOCKED |
| return 0 |
| fi |
| |
| # Perform a full device mapper root validation to avoid any unexpected |
| # failures during postinst. It also allows us to detect if the root |
| # is intentionally mismatched - such as during Chromium OS recovery |
| # with a Chrome OS recovery kernel. |
| verify_recovery_root && return 0 |
| |
| # Verification failed. Only proceed in developer mode. |
| is_developer_mode || return 1 |
| |
| # If developer mode is blocked, don't allow an unverified root. |
| is_developer_mode_blocked && return $ERR_DEV_MODE_BLOCKED |
| |
| # The root we just mounted looked like an official recovery image, but |
| # it didn't pass validation. Try to fall back to installing a |
| # developer image. |
| umount "${USB_MNT}" |
| dmsetup remove "$DM_NAME" # Free up the real root for use. |
| USB_DEV= |
| |
| find_developer_root || return 1 |
| get_stateful_dev || return 1 |
| |
| message developer_image |
| |
| return 0 |
| } |
| |
| get_dst() { |
| load_base_vars |
| DST="$(get_fixed_dst_drive)" |
| if [ -z "${DST}" ]; then |
| dlog "SSD for installation not specified" |
| return 1 |
| fi |
| if [ "${DST%[0-9]}" = "${DST}" ]; then |
| # ex, sda => sda1, sdb1 |
| DST_DEV_BASE="${DST}" |
| else |
| # ex, mmcblk0 => mmcblk0p1 |
| DST_DEV_BASE="${DST}p" |
| fi |
| local src_dev_base="${REAL_USB_DEV%[0-9]*}" |
| if [ "${src_dev_base}" = "${DST_DEV_BASE}" ]; then |
| dlog "Cannot find SSD for installation." |
| return 1 |
| fi |
| SRC_DEV_BASE="${src_dev_base}" |
| } |
| |
| # Checks whether a given key block is also a valid key block used to |
| # sign a kernel currently installed on the destination block device. |
| check_install_kernel_key_match() { |
| dlogf "Searching the system disk for a matching kernel key . . ." |
| if ! cgpt find -t kernel -M "$1" "$DST"; then |
| dlog " failed." |
| return 1 |
| fi |
| dlog " found." |
| |
| dlogf "Validating matching signature(s) . . ." |
| # If we found a keyblock, at the right offset, make sure it actually signed |
| # the subsequent payload. |
| local kdev= |
| for kdev in $(cgpt find -t kernel -M "$1" "${DST}"); do |
| dlogf " ." |
| verify_kernel_signature "$kdev" "/tmp/kern.keyblock" || continue |
| dlog " done." |
| return 0 |
| done |
| |
| dlog " failed." |
| return 1 |
| } |
| |
| # Get the firmware version by directly reading the TPM NVRAM spaces. |
| # Needed when crossystem doesn't work (e.g. Mario systems). |
| get_fwver_from_tpmc () { |
| if [ $# -ne 0 ]; then |
| dlog "ERROR: get_fwver_from_tpmc() doesn't take any args" |
| return |
| fi |
| # Example output: |
| # |
| # 2 3 3 0 1 0 |
| # |
| # The first 2 bytes of the output are internal version and flags and |
| # can be ignored. The full firmware version is stored as a 32-bit |
| # integer in little endian format. |
| local out |
| if ! out=$(tpmc read ${FW_VER_TPM_NV_SPACE} 6); then |
| dlog "ERROR: tpmc read failed" |
| echo "${FW_VER_MIN}" |
| return |
| fi |
| set -- ${out} |
| if [ $# != 6 ]; then |
| # The TPM is uninitialized or corrupt. Return a default |
| # version. Don't lock out recovery because we might be what's |
| # supposed to fix it. |
| echo "${FW_VER_MIN}" |
| return |
| fi |
| |
| echo "$(( 0x$6 << 24 | 0x$5 << 16 | 0x$4 << 8 | 0x$3 ))" |
| } |
| |
| # Get the kernel version by directly reading the TPM NVRAM spaces. |
| # Needed when crossystem doesn't work (e.g. Mario systems). |
| get_kernelver_from_tpmc () { |
| set -- $(tpmc read $KERNEL_VER_TPM_NV_SPACE 9) |
| # Example output: |
| # |
| # 1 4c 57 52 47 1 0 1 0 |
| # |
| # The first 5 bytes of the output are a canary value and can be |
| # ignored. The full kernel version is stored as a 32-bit integer |
| # in little endian format. |
| echo "$(( $9 << 24 | $8 << 16 | $7 << 8 | $6 ))" |
| } |
| |
| verify_kernel_signature() { |
| local kern_dev="$1" |
| local keyblock="$2" |
| |
| if ! dd if="$kern_dev" of="/tmp/kern.bin"; then |
| return 1 |
| fi |
| |
| # Validates the signature and outputs a keyblock. |
| if ! vbutil_kernel --verify "/tmp/kern.bin" \ |
| --keyblock "$keyblock"; then |
| return 1 |
| fi |
| return 0 |
| } |
| |
| verify_fw_version() { |
| local fwver recver |
| fwver=$(crossystem tpm_fwver || get_fwver_from_tpmc) |
| : $(( recver = fwver >> 28 )) |
| echo "FW version from TPM: ${fwver}" |
| echo "Recovery version from top nibble: ${recver}" |
| |
| if [ ${recver} -gt ${REC_VER_MAX} ]; then |
| printf "Recovery script locked out due to firmware version 0x%x\n" \ |
| "${fwver}" |
| return 1 |
| fi |
| return 0 |
| } |
| |
| verify_kernel_version() { |
| local kern_dev="$1" |
| local minversion="$KERNEL_VER_MIN" |
| |
| if ! dd if="$kern_dev" of="/tmp/kern.bin"; then |
| return 1 |
| fi |
| |
| # Get the currently set TPM NVRAM rollback versions. |
| minversion=$(crossystem tpm_kernver || get_kernelver_from_tpmc) |
| dlog "Rollback version stored in the TPM: $minversion" |
| |
| # Validate the signature and rollback versions. |
| if ! vbutil_kernel --verify "/tmp/kern.bin" --minversion "$minversion"; then |
| return 1 |
| fi |
| return 0 |
| } |
| |
| verify_install_kernel() { |
| # TODO(wad) check signatures from stateful on kern b using the |
| # root of trust instead of using a baked in cmdline. |
| if [ "$REAL_KERN_B_HASH" != "$KERN_ARG_KERN_B_HASH" ]; then |
| # Assertion: we have to be in developer mode; this was verified in the |
| # course of the general init process. |
| is_developer_mode || return 1 |
| |
| # Only allow verified kernels when developer mode blocking is on. |
| is_developer_mode_blocked && return $ERR_DEV_MODE_BLOCKED |
| |
| message developer_image |
| |
| # Extract the kernel so that vbutil_kernel will happily consume it. |
| dlog "Checking the install kernel for a valid developer signature . . ." |
| verify_kernel_signature "$KERN_B_DEV" "/tmp/kern_b.keyblock" || return 1 |
| check_install_kernel_key_match /tmp/kern_b.keyblock || message key_change |
| return 0 |
| fi |
| |
| # Looks like we have an official recovery image. Check for version rollback. |
| dlog "Checking the install kernel for valid versions and signature . . ." |
| if ! verify_kernel_version "$KERN_B_DEV"; then |
| # Rollback version check failure is fatal if we are not in developer mode. |
| is_developer_mode || return $ERR_INVALID_INSTALL_KERNEL |
| |
| # Don't allow version downgrades when developer mode blocking is on. |
| is_developer_mode_blocked && return $ERR_DEV_MODE_BLOCKED |
| |
| message warn_invalid_install_kernel |
| fi |
| |
| return 0 |
| } |
| |
| setup_install_mounts() { |
| mount -t tmpfs -o mode=1777 none "${USB_MNT}/tmp" || return 1 |
| mount -t tmpfs -o mode=0755 run "${USB_MNT}/run" || return 1 |
| mkdir -p -m 0755 "${USB_MNT}/run/lock" || return 1 |
| |
| dlog "Re-binding $BASE_MOUNTS for $NEWROOT_MNT" |
| for mnt in $BASE_MOUNTS; do |
| # $mnt is a full path (leading '/'), so no '/' joiner |
| mkdir -p "$NEWROOT_MNT$mnt" |
| mount -n -o bind "$mnt" "$NEWROOT_MNT$mnt" || return 1 |
| done |
| dlog "Done." |
| return 0 |
| } |
| |
| cleanup_install_mounts() { |
| dlog "Unmounting $BASE_MOUNTS in $NEWROOT_MNT" |
| for mnt in $BASE_MOUNTS; do |
| # $mnt is a full path (leading '/'), so no '/' joiner |
| umount "$NEWROOT_MNT$mnt" |
| done |
| dlog "Done." |
| umount "${USB_MNT}/run" |
| umount "${USB_MNT}/tmp" |
| return 0 |
| } |
| |
| # If the recovery media was provisioned for OOBE auto-setup, we have to ask the |
| # user for confirmation before proceeding. |
| request_oobe_autoconfig_acknowledgment() { |
| if ! is_oobe_autoconfig; then |
| return |
| fi |
| |
| if [ -n "${OOBE_ENROLLMENT_DOMAIN}" ]; then |
| message oobe_autoconfig_domain |
| showtext "${OOBE_ENROLLMENT_DOMAIN}" |
| fi |
| |
| # Check to see if we have a physical recovery switch. This is fairly tricky: |
| # * recoverysw_is_virtual queries the RECSW_IS_VIRT bit in vboot shared data, |
| # which will be set for all devices more recent than 2014 that do *not* have |
| # a physical recovery switch. |
| # * For boards older than 2014 that are not EOL, the bit will always be 0 |
| # regardless of whether a switch exists or not. But if it doesn't, |
| # recoverysw_cur will return an error code. |
| # Note that recoverysw_cur returning without error is not sufficient to |
| # determine if a board has a recovery switch, e.g. veyron_minnie returns 0 |
| # but is a Chromebook without a recovery switch. |
| if crossystem 'recoverysw_is_virtual?0' && crossystem recoverysw_cur; then |
| message oobe_autoconfig_acknowledgment_switch |
| |
| dlog "Waiting for user to confirm OOBE auto-setup by recovery switch..." |
| |
| # Wait until the switch goes low in case the user kept the recovery switch |
| # pressed too long on entering recovery, so that we actually give them a |
| # chance to confirm. |
| until crossystem 'recoverysw_cur?0'; do |
| sleep 0.1 |
| done |
| |
| # Wait until user confirms by pressing their recovery switch. |
| until crossystem 'recoverysw_cur?1'; do |
| sleep 0.1 |
| done |
| |
| # User has no buttons with which to proceed with recovery without accepting |
| # OOBE auto-setup, so if we reach here it means the user has not cancelled |
| # by turning off the device. |
| OOBE_AUTO_CONFIG=1 |
| else |
| # evdev mappings for keys determined by `evtest`. |
| local KEY_ESC=1 |
| local KEY_ENTER=28 |
| local KEY_VOLUMEDOWN=114 |
| local KEY_VOLUMEUP=115 |
| local key= |
| |
| # Board-specific flag specified in board overlay and created in ramfs |
| # Makefile. |
| if [ -f "/.is_detachable" ]; then |
| if evwaitkey --check --keys ${KEY_VOLUMEUP}:${KEY_VOLUMEDOWN}; then |
| message oobe_autoconfig_acknowledgment_volume |
| dlog "Waiting for user to confirm OOBE auto-setup by volume keys..." |
| key=$(evwaitkey --keys ${KEY_VOLUMEUP}:${KEY_VOLUMEDOWN}) |
| fi |
| elif evwaitkey --check --keys ${KEY_ESC}:${KEY_ENTER}; then |
| message oobe_autoconfig_acknowledgment_keyboard |
| dlog "Waiting for user to confirm OOBE auto-setup by keyboard..." |
| key=$(evwaitkey --keys ${KEY_ESC}:${KEY_ENTER}) |
| fi |
| |
| # If we've obtained user acknowledgment, mark ready for OOBE autoconfig. |
| if [ ${key} -eq ${KEY_ENTER} ] || [ ${key} -eq ${KEY_VOLUMEUP} ]; then |
| OOBE_AUTO_CONFIG=1 |
| fi |
| |
| # This shouldn't ever happen, but provide some feedback just in case. |
| if [ -z "${key}" ]; then |
| dlog "WARNING!!! Could not find method for confirming OOBE auto-setup!" |
| dlog "Recovery will proceed without auto-setup." |
| fi |
| fi |
| } |
| |
| call_image_recovery_script() { |
| dlog "Installing software; this will take some time." |
| dlog "See the debug log on VT3 for the full output." |
| |
| # Prevent accidentally loading modules from the usb mount |
| echo 1 >/proc/sys/kernel/modules_disabled |
| |
| chroot "${USB_MNT}" /usr/sbin/chromeos-recovery "$@" |
| local install_status=$? |
| if [ $install_status -ne 0 ]; then |
| dlog "WARNING!!! Installation of software failed. Displaying hw diagnostics" |
| local diagnostics_file="${USB_MNT}/tmp/$LOG_HARDWARE_DIAGNOSTICS" |
| if [ -f "$diagnostics_file" ]; then |
| cp "$diagnostics_file" "$LOG_DIR" |
| dlog \ |
| "============================ HARDWARE DIAGNOSTICS ==========================" |
| dlog $(cat "$diagnostics_file") |
| dlog "See recovery log for more information." |
| dlog \ |
| "============================================================================" |
| dlog |
| else |
| dlog "Missing hardware diagnostics." |
| fi |
| fi |
| |
| return $install_status |
| } |
| |
| clobber_lockbox_space() { |
| # Clobber the lockbox space, by defining a new space at the same index. |
| # Protection flags and size of the new space don't matter, as it will get |
| # recreated again by cryptohome. |
| local ppwrite_permission=0x1 |
| local temporary_lockbox_size=1 |
| tpmc def $LOCKBOX_TPM_NV_SPACE $temporary_lockbox_size $ppwrite_permission |
| } |
| |
| clear_tpm() { |
| dlogf "Resetting security device . . ." |
| # TODO(wad) should we fail on this? |
| tpmc ppon || dlog "tpmc ppon error: $?" |
| tpmc clear || dlog "tpmc clear error: $?" |
| tpmc enable || dlog "tpmc enable error: $?" |
| tpmc activate || dlog "tpmc activate error: $?" |
| clobber_lockbox_space || dlog "error clobbering lockbox space: $?" |
| tpmc pplock || dlog "tpmc pplock error: $?" |
| dlog " done." |
| return 0 |
| } |
| |
| verify_rw_vpd() { |
| local tmpfile="$(mktemp ${TMPDIR:-/tmp}/rw_vpd.XXXXXX)" |
| local rc=0 |
| |
| dlog "Verifying RW_VPD" |
| |
| # First method: Check if RW VPD entries have been populated in sysfs by the |
| # kernel. If so, assume everything is good. Otherwise fall back to using |
| # flashrom and vpd utilities (slower). |
| ls -1A "/sys/firmware/vpd/rw" | grep -q . |
| if [ $? -eq 0 ]; then |
| dlog "Found RW VPD in sysfs." |
| return 0 |
| fi |
| |
| # Test if RW_VPD region exists on system by attempting to read it |
| # using flashrom. |
| flashrom -p host -i RW_VPD:${tmpfile} -r |
| if [ $? -ne 0 ]; then |
| dlog "RW_VPD does not exist on this system, skipping." |
| return 0 |
| else |
| rm -f "${tmpfile}" |
| fi |
| |
| # vpd utility exit codes defined in vpd/include/lib/lib_vpd.h: |
| # 0: VPD_OK |
| # 9: VPD_ERR_NOT_FOUND, meaning VPD was not found. |
| # 10: VPD_ERR_OVERFLOW, meaning VPD is likely corrupt. |
| # 11: VPD_ERR_INVALID, meaning VPD is corrupt. |
| # |
| # All others will be treated as generic failures. |
| vpd -i RW_VPD -l |
| rc=$? |
| case $rc in |
| 0) |
| dlog "Successfully read VPD from RW_VPD." |
| return 0 |
| ;; |
| 9) |
| dlog "VPD not found in RW_VPD." |
| ;; |
| 10) |
| dlog "Overflow detected in VPD. May be corrupted." |
| ;; |
| 11) |
| dlog "VPD found in RW_VPD, but is corrupted." |
| ;; |
| *) |
| dlog "Unspecified error when reading VPD from RW_VPD: $rc" |
| return 1 |
| esac |
| |
| # From here, erase the RW_VPD region and re-initialize. If erase fails then |
| # try initializing anyway and hope it works well enough. |
| dlog "Erasing RW_VPD region and re-initializing the VPD." |
| |
| flashrom -p host -i RW_VPD -E |
| if [ $? -ne 0 ]; then |
| dlog "Failed to erase RW_VPD region, continuing anyway." |
| fi |
| |
| vpd -i RW_VPD -O |
| rc=$? |
| if [ $rc -ne 0 ]; then |
| dlog "Error re-formatting VPD region: $rc" |
| return 1 |
| fi |
| |
| return 0 |
| } |
| |
| recover_system() { |
| local source=$(strip_partition "$REAL_USB_DEV") |
| dlog "Beginning system recovery from $source" |
| |
| # If we're not running a developer script then we're either |
| # installing a developer image or an official one. If we're |
| # in normal recovery mode, then we require that the KERN-B |
| # on the recovery image matches the hash on the command line. |
| # In developer mode, we will just check the keys. |
| verify_install_kernel || return $? |
| |
| # Only clear on full installs. Shim scripts can call tpmc if they |
| # like. Only bGlobalLock will be in place in advance. |
| clear_tpm || return 1 |
| |
| # Check if RW_VPD is valid and reinitialize if not. This is intended to |
| # ensure certain functionality such as re-enrollment works. Refer to |
| # crbug.com/660121 for details. |
| verify_rw_vpd || dlog "Could not verify or re-format RW_VPD" |
| |
| local extra_flags= |
| if [ -n "${OOBE_AUTO_CONFIG}" ]; then |
| # If we're running for OOBE auto-config, write our keys into the chroot |
| # and add flags to the invocation of chromeos-recovery pointing to them. |
| local pub_key_file="/tmp/oobe_config_key.pub" |
| local priv_key_file="/tmp/oobe_config_key.pem" |
| ( |
| # +x in subshell to prevent logging private key. |
| set +x |
| echo "${OOBE_PUBLIC_KEY_PEM}" > "${USB_MNT}${pub_key_file}" |
| echo "${OOBE_PRIVATE_KEY_PEM}" > "${USB_MNT}${priv_key_file}" |
| ) |
| extra_flags="--oobe_pub_key=${pub_key_file} |
| --oobe_priv_key=${priv_key_file}" |
| else |
| # Normal recovery: the progress bar is full, and it is the user's last |
| # chance to cancel. The power button has to be held for a full 8 seconds |
| # to power off, so we allow a short grace period in case the user started |
| # pressing the power button late in the cycle. |
| sleep 2 |
| fi |
| |
| message recovery_start |
| |
| call_image_recovery_script "$source" ${extra_flags} || return 1 |
| |
| return 0 |
| } |
| |
| # Return the path to the node under /sys/block associated with |
| # the USB stick. The existence of that path is used to test whether |
| # the user has removed the stick and we can reboot. |
| # |
| # For certain non-interactive test cases, the stateful partition on |
| # the USB stick may be flagged to request that we bypass the |
| # interactive removal of the USB stick. If we detect that |
| # condition, we signal it by returning an empty string instead |
| # of a path. |
| # |
| # In the case of OOBE auto-configuration, the presence of OOBE auto-config data |
| # on the stateful partition of the recovery USB also flags that the end of |
| # recovery should be non-interactive. |
| get_usb_node_dir() { |
| local usb_node_dir=/sys/block/$(strip_partition "${REAL_USB_DEV##*/}") |
| if [ "$INTERACTIVE_COMPLETE" = false ] || \ |
| [ -n "${OOBE_AUTO_CONFIG}" ]; then |
| usb_node_dir="" |
| elif mount -n -o sync,rw "${REAL_USB_DEV%[0-9]*}1" /tmp; then |
| if [ -f /tmp/non_interactive ]; then |
| usb_node_dir="" |
| fi |
| umount /tmp |
| fi |
| echo "$usb_node_dir" |
| } |
| |
| get_usb_debugging_flag() { |
| local decrypt="" |
| if get_stateful_dev && mount -n -o sync,ro "${STATE_DEV}" /tmp; then |
| if [ -f /tmp/decrypt_stateful ]; then |
| decrypt=$(cat /tmp/decrypt_stateful) |
| fi |
| umount /tmp |
| fi |
| echo "$decrypt" |
| } |
| |
| is_oobe_autoconfig() { |
| local has_config=1 |
| if get_stateful_dev && mount -n -o sync,ro "${STATE_DEV}" "${STATEFUL_MNT}" |
| then |
| local oobe_dir="${STATEFUL_MNT}/unencrypted/oobe_auto_config" |
| local config_path="${oobe_dir}/config.json" |
| if [ -f "${config_path}" ]; then |
| has_config=0 |
| fi |
| |
| local domain_path="${oobe_dir}/enrollment_domain" |
| if [ -f "${domain_path}" ]; then |
| # Sanitize characters that are not allowed in punycoded domains. Also, |
| # truncate it off at 253 characters so we don't read a potentially massive |
| # string in. |
| OOBE_ENROLLMENT_DOMAIN="$(tr -dc 'a-z0-9.-' < "${domain_path}" | \ |
| cut -b-253)" |
| fi |
| |
| umount "${STATEFUL_MNT}" |
| fi |
| return ${has_config} |
| } |
| |
| maybe_get_debugging_logs() { |
| local state=$(get_usb_debugging_flag) |
| if [ -z "$state" ]; then |
| return 0 |
| fi |
| log "Stateful recovery requested." |
| |
| dlog "Attempting to find the destination stateful . . ." |
| get_dst || return 0 |
| |
| log "Please wait (this may take up to 2 and a half minutes). . ." |
| sleep 150 # Five minutes in half. |
| if ! mount -n -o sync,rw "${DST_DEV_BASE}1" "${STATEFUL_MNT}"; then |
| log "Unable to perform stateful recovery." |
| dlog "mount failed for ${DST_DEV_BASE}1" |
| sleep 1d |
| reboot -f |
| fi |
| |
| local flagfile="${STATEFUL_MNT}/decrypt_stateful" |
| local decrypted_dir="${STATEFUL_MNT}/decrypted" |
| local tarball_dir="/tmp/recovery" |
| local tarball="${tarball_dir}/extracted.tgz" |
| # Check if the files exist already or if recovery needs to be requested. |
| if [ -f "$flagfile" ] && [ ! -d "$decrypted_dir" ]; then |
| log "Prior recovery request incomplete. Starting over . . ." |
| fi |
| if [ -f "$flagfile" ] && [ -d "$decrypted_dir" ]; then |
| log "Prior recovery request exists. Attempting to extract files." |
| if ! mount -n -o sync,rw "${STATE_DEV}" /tmp 2>"${TTY_LOG}"; then |
| log "Failed to mount recovery stateful partition." |
| on_error |
| fi |
| mkdir -p "$tarball_dir" || on_error |
| log "Copying files . . ." |
| # Due to the tty redirection that this script runs under, without the |
| # explicit stdin and stdout redirects, tar will fail with "Broken pipe". |
| if ! tar -czf "${tarball}" -C "${STATEFUL_MNT}" \ |
| --exclude './decrypt_stateful' \ |
| --exclude './encrypted*' \ |
| --exclude './home/.shadow/*/*' \ |
| --exclude './dev_image' \ |
| . >"${TTY_LOG}" 2>"${TTY_LOG}" </dev/null ; then |
| log "Extraction failed. See debug log for more details." |
| crossystem recovery_request=1 |
| umount /tmp |
| umount "${STATEFUL_MNT}" |
| on_error |
| fi |
| log "Removing the request file and old data . . ." |
| rm -f "${flagfile}" |
| rm -rf "${decrypted_dir}" |
| umount /tmp |
| umount "${STATEFUL_MNT}" |
| log "Operation complete, you can now remove your recovery media." |
| log "The requested data can be found in the /recovery folder." |
| sleep 1d |
| reboot -f |
| fi |
| |
| log "Stateful recovery requires authentication." |
| log "Your username and salted password will be temporarily written to the" |
| log "device for the duration of this recovery process. Once finished," |
| log "please be sure to change your password as a precautionary measure." |
| |
| local username |
| read -p Username: username <"${TTY_CONSOLE}" 2>"${TTY_CONSOLE}" |
| |
| if [ -n "${username}" ]; then |
| local password |
| read -p Password: -s password <"${TTY_CONSOLE}" 2>"${TTY_CONSOLE}" |
| # Force a newline for sane output after silent input. |
| echo "" >"${TTY_CONSOLE}" |
| |
| local saltfile="${STATEFUL_MNT}"/home/.shadow/salt |
| if [ ! -f "${saltfile}" ]; then |
| log "Target has no system salt file. Giving up." |
| sleep 1d |
| reboot -f |
| fi |
| |
| local salt=$(hexdump -v -e '/1 "%02x"' <"$saltfile") |
| local passkey=$(printf '%s' "${salt}${password}" | sha256sum | cut -c-32) |
| |
| log "Installing v2 request file . . ." |
| cat > "${flagfile}" <<EOM |
| 2 |
| $username |
| $passkey |
| EOM |
| else |
| log "Installing v1 request file . . ." |
| printf "1" > "${flagfile}" |
| fi |
| umount "${STATEFUL_MNT}" |
| |
| log "Stateful recovery initiation completed." |
| log "In 60 seconds, the system will reboot to the local OS." |
| log "Once the files are prepared, it will return back to recovery mode." |
| log "Please use this same recovery media." |
| sleep 60 |
| reboot -f # Back to the system! |
| return 1 |
| } |
| |
| # Shows the appropriate error screen for the specified error code and |
| # stops further action, i.e. doesn't return. |
| handle_error() { |
| case "$1" in |
| $ERR_DEV_MODE_BLOCKED) |
| message block_developer_mode |
| ;; |
| $ERR_INVALID_INSTALL_KERNEL) |
| message invalid_install_kernel |
| ;; |
| *) |
| # Show the generic error screen by default. |
| on_error |
| ;; |
| esac |
| } |
| |
| wait_for_battery_to_charge() { |
| while true; do |
| local power_status="$(chroot "${USB_MNT}" /usr/bin/dump_power_status)" |
| |
| # Recheck whether charge level is sufficient. |
| local battery_charge=$(echo "${power_status}" | |
| grep "^battery_display_percent " | |
| cut -d ' ' -f 2) |
| if [ "${battery_charge%%.*}" -ge "${MIN_BATTERY_CHARGE_PERCENT}" ]; then |
| break |
| fi |
| |
| # Display the appropriate low battery message. |
| if echo "${power_status}" | grep -Fqx "line_power_connected 1"; then |
| message update_firmware_low_battery_charging |
| else |
| message update_firmware_low_battery |
| fi |
| |
| sleep 1 |
| done |
| } |
| |
| tpm_firmware_update_applicable() { |
| # Only allow the firmware updater to run from an official root. We don't want |
| # arbitrary code to run with TPM physical presence asserted! If we're running |
| # an unofficial root, return success so it looks like we don't have an update. |
| if is_unofficial_root; then |
| return 1 |
| fi |
| |
| # Images for boards that we don't ship TPM firmware updates for don't carry |
| # the updater. Nothing to do in that case. |
| if ! [ -x "${USB_MNT}/usr/sbin/tpm-firmware-updater" ]; then |
| return 1 |
| fi |
| |
| # The TPM is in failed selftest mode after a failed previous TPM firmware |
| # update. Devices in this condition don't boot in normal mode, and recovery is |
| # the supported way to fix this via a TPM firmware update retry. |
| if [ -n "${TPM_FAILED_SELFTEST}" ]; then |
| return 0 |
| fi |
| |
| # Attempt a TPM firmware update when the VPD key tpm_firmware_update_params |
| # contains mode:recovery. This is mainly useful for testing behavior of the |
| # firmware updater in recovery mode without having to put a device into failed |
| # selftest mode. |
| local vpd_params="$(vpd -i RW_VPD -g tpm_firmware_update_params)" |
| if echo ",${vpd_params}," | grep -qF ',mode:recovery,'; then |
| return 0 |
| fi |
| |
| # Current default is not to attempt updating, even if an update is available. |
| return 1 |
| } |
| |
| cold_reset() { |
| # Issue cold reset via Chromium EC if ectool is available. |
| if [ -x "${USB_MNT}/usr/sbin/ectool" ]; then |
| chroot "${USB_MNT}" /usr/sbin/ectool reboot_ec cold |
| fi |
| } |
| |
| # Checks whether the root includes a TPM firmware update and installs it if |
| # applicable. Note that this must happen before locking TPM physical presence, |
| # because this is the only way to recover from a failed previous firmware |
| # update. |
| update_tpm_firmware() { |
| if ! tpm_firmware_update_applicable; then |
| return 0 |
| fi |
| |
| while true; do |
| ( |
| set +e |
| # TODO(mnissler): Reading the VPD from flash requires CAP_SYS_ADMIN and |
| # CAP_SYS_RAWIO. Figure out whether there's a way around that. |
| TPM_FIRMWARE_UPDATE_MIN_BATTERY="${MIN_BATTERY_CHARGE_PERCENT}" \ |
| chroot "${USB_MNT}" \ |
| /sbin/minijail0 -c 0x220000 --ambient -e -l -n -p -r -v --uts -- \ |
| /bin/sh -x /usr/sbin/tpm-firmware-updater |
| echo $? > /tmp/tpm-firmware-updater.status |
| ) 2>>"${LOG_DIR}/tpm-firmware-updater.log" | ( |
| # The dummy read is so we only show the update_firmware message to the |
| # user in case the update actually takes place. |
| if read progress; then |
| message update_firmware |
| progress_bar |
| fi |
| ) |
| |
| local EXIT_CODE_SUCCESS=0 |
| local EXIT_CODE_ERROR=1 |
| local EXIT_CODE_NO_UPDATE=3 |
| local EXIT_CODE_UPDATE_FAILED=4 |
| local EXIT_CODE_LOW_BATTERY=5 |
| local EXIT_CODE_NOT_UPDATABLE=6 |
| local EXIT_CODE_SUCCESS_COLD_REBOOT=8 |
| local EXIT_CODE_BAD_RETRY=9 |
| |
| local status="$(cat /tmp/tpm-firmware-updater.status)" |
| case "${status}" in |
| ${EXIT_CODE_SUCCESS}|${EXIT_CODE_SUCCESS_COLD_REBOOT}) |
| # We need to reboot to get the TPM back into operational state. |
| save_log_files |
| [ "${status}" = "${EXIT_CODE_SUCCESS_COLD_REBOOT}" ] && cold_reset |
| reboot -f |
| exit 0 |
| ;; |
| ${EXIT_CODE_NO_UPDATE}|${EXIT_CODE_BAD_RETRY}) |
| # Let the recovery process continue. |
| return 0 |
| ;; |
| ${EXIT_CODE_UPDATE_FAILED}) |
| # Fail and let the caller handle the error. |
| return 1 |
| ;; |
| ${EXIT_CODE_LOW_BATTERY}) |
| wait_for_battery_to_charge |
| ;; |
| ${EXIT_CODE_NOT_UPDATABLE}) |
| # No physical presence? This can only happen if we're not booted in |
| # recovery mode. |
| dlog "Attempted TPM firmware update, but no physical presence." |
| return 0 |
| ;; |
| ${EXIT_CODE_ERROR}|*) |
| return 1 |
| ;; |
| esac |
| done |
| |
| # Never reached. |
| return 0 |
| } |
| |
| # Generate a key pair for OOBE auto-setup validation, and write a 32-byte SHA256 |
| # digest of the public key into a TPM space defined with PPWRITE permissions. |
| # The private and public keys are kept in memory as OOBE_PRIVATE_KEY_PEM and |
| # OOBE_PUBLIC_KEY_PEM. |
| setup_oobe_auto_config_keys() { |
| # +x to prevent logging private key. |
| set +x |
| local key_data="$(cros_oobe_crypto)" |
| # We will need the keys after we've completed installation. |
| OOBE_PRIVATE_KEY_PEM=$(echo "${key_data}" | \ |
| sed -n "/BEGIN EC PRIVATE KEY/,/END EC PRIVATE KEY/p") |
| OOBE_PUBLIC_KEY_PEM=$(echo "${key_data}" | \ |
| sed -n "/BEGIN PUBLIC KEY/,/END PUBLIC KEY/p") |
| # Restore logging. |
| set -x |
| |
| # Split up the hash of the public key into 32 hexadecimal bytes to feed into |
| # `tpmc`. |
| local pub_bytes=$(echo "${OOBE_PUBLIC_KEY_PEM}" | sha256sum | \ |
| grep -Eo '[a-zA-Z0-9]{2}' | head -n 32) |
| |
| # Define a 32-byte space with the same PPWRITE permission as the vboot kernel |
| # rollback space. This prevents a compromised OS from altering it to load an |
| # arbitrary configuration file without going through recovery. |
| # |
| # For TPM 1.2 set bits: |
| # - 0: TPM_NV_PER_PPWRITE - Requires physical presence to write. |
| # |
| # For TPM 2.0 set bits: |
| # - 0: TPM_NV_PPWRITE - Requires physical presence to write. |
| # - 18: TPM_NV_PER_AUTHREAD - Requires authorization to read (in this case |
| # with empty password) |
| # - 25: TPMA_NV_NO_DA - To avoid TPM lockouts in case of bugs in reading |
| # logic. |
| # - 30: TPMA_NV_PLATFORMCREATE - The index can be undefined with platform |
| # authorization but not with owner authorization. |
| local permissions= |
| if [ "$(tpmc tpmver)" == "2.0" ]; then |
| permissions=0x42040001 |
| else |
| permissions=0x1 |
| fi |
| tpmc definespace "${OOBE_AUTO_CONFIG_KEY_HASH_NV_SPACE}" 0x20 "${permissions}" |
| # No quotes; this is intended to split up the bytes. |
| tpmc write "${OOBE_AUTO_CONFIG_KEY_HASH_NV_SPACE}" ${pub_bytes} |
| } |
| |
| recovery_install() { |
| NEWROOT_MNT="${USB_MNT}" |
| |
| # Always lock the TPM. If a NVRAM reset is ever needed, we can change it. |
| lock_tpm || on_error |
| |
| # Check if we're really doing debugging log extraction. |
| maybe_get_debugging_logs || return 1 |
| |
| # Check if we have a verified recovery root. |
| validate_recovery_root || handle_error $? |
| |
| setup_install_mounts || on_error |
| |
| # Install a TPM firmware update included in the rootfs if applicable. |
| update_tpm_firmware || message security_module_failure |
| |
| # Do not install the system image if the TPM is in failed selftest mode. The |
| # installer is not prepared to handle this state and installing a fresh system |
| # image will not fix things anyways, so err on the safe side and abort. |
| [ -z "${TPM_FAILED_SELFTEST}" ] || message security_module_failure |
| |
| get_dst || on_error |
| |
| recover_system || handle_error $? |
| |
| # No error check here: Clean up doesn't need to be successful. |
| cleanup_install_mounts |
| |
| stop_log_file |
| # Save the recovery log to the target on success and the USB. |
| save_log_files "${DST_DEV_BASE}"1 ext4 |
| save_log_files "${SRC_DEV_BASE}"12 vfat |
| save_log_files "${SRC_DEV_BASE}"1 ext4 |
| |
| # This assignment depends on the stateful partition on the USB |
| # stick, so we must do it before unmount_usb and the |
| # "recovery_complete" message, because the user could remove the |
| # USB stick from that moment forward. |
| local usb_node_dir=$(get_usb_node_dir) |
| |
| unmount_usb |
| |
| if [ -n "$usb_node_dir" ]; then |
| # Default (interactive) case; wait till the user removes the USB stick. |
| message recovery_complete |
| while [ -d "$usb_node_dir" ]; do |
| sleep 1 |
| done |
| elif [ $(cat /sys/devices/virtual/tty/tty0/active) != tty1 ]; then |
| # Special (non-interactive) test case, and the user is watching |
| # VT2 or VT3. Stay up a long time to allow for debugging. |
| message recovery_complete |
| dlog "Recovery is complete! The system will remain up to allow" |
| dlog "you to examine VT screen contents. You must manually" |
| dlog "power off when you're done." |
| sleep 1d |
| else |
| # Special (non-interactive) test case; reboot after a nominal |
| # pause to allow a human to observe the screen. |
| if [ -n "${OOBE_AUTO_CONFIG}" ]; then |
| message recovery_complete_oobe_autoconfig |
| else |
| message recovery_complete_noninteractive |
| fi |
| fi |
| |
| reboot -f |
| exit 0 |
| } |