blob: ffcec5e89fb784e9384ce896b00da0e19f2c34d0 [file] [log] [blame]
#!/bin/sh
#
# Copyright 2017 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.
#
# Driver script for updating Infineon TPM firmware. Checks system state to see
# whether an update should be installed and invokes the updater if that is the
# case. Note that this script must run both in regular mode and during recovery
# implying that the stateful partition can't be used.
set -e
: "${TPM_DEVICE_NODE:=/dev/tpm0}"
: "${TPM_FIRMWARE_DIR:=/lib/firmware/tpm}"
: "${TPM_FIRMWARE_UPDATE_MAX_ATTEMPTS:=3}"
: "${TPM_FIRMWARE_UPDATE_MIN_BATTERY:=10}"
: "${TPM_FIRMWARE_UPDATE_MIN_REMAINING:=32}"
EXIT_CODE_SUCCESS=0
EXIT_CODE_ERROR=1
EXIT_CODE_NO_UPDATE=3
EXIT_CODE_UPDATE_FAILED=4
EXIT_CODE_LOW_BATTERY=5
EXIT_CODE_NOT_UPDATABLE=6
EXIT_CODE_SUCCESS_COLD_REBOOT=8
EXIT_CODE_BAD_RETRY=9
# Log a diagnostic message to stderr.
dlog() {
echo "tpm-firmware-updater: $*" >&2
}
# Parse key-value pairs and assign corresponding shell variables. Keys and
# values are sanitized before assigning.
read_key_value_pairs() {
local prefix="$1"
local field_separator="$2"
local input="$3"
local key
local value
while IFS="${field_separator}" read -r key value; do
local sanitized_key=$(
echo "${key}" | tr '[:upper:] ' '[:lower:]_' | tr -cd '[:alnum:]_-')
local sanitized_value=$(
echo "${value}" | tr '[:upper:] ' '[:lower:]_' | tr -cd '[:alnum:]._-')
export "${prefix}_${sanitized_key}=${sanitized_value}"
done <<EOF
${input}
EOF
}
# See whether /dev/tpm0 exists and can be opened.
probe_tpm() {
if [ ! -c "${TPM_DEVICE_NODE}" ]; then
dlog "${TPM_DEVICE_NODE} unavailable"
return 1
fi
if ! (exec 3<>"${TPM_DEVICE_NODE}") ; then
dlog "Can't open ${TPM_DEVICE_NODE}. Opened by another process? stop tcsd?"
return 1
fi
return 0
}
# Checks whether the TPM is in a state that allows updating and prints the
# TPM auth mechanism (physical presence or owner auth) to use to stdout.
verify_tpm_state() {
local mainfw_type=$(crossystem mainfw_type)
case "${mainfw_type}" in
recovery)
# Ideally, we'd verify that we have physical presence enabled. This isn't
# so simple however: Physical presence information is normally part of
# TPM_PERMANENT_FLAGS which are accessible via tpmc, but IFX TPMs in
# bootloader mode fail the GetCapability command to retrieve flags.
# Unfortunately, there doesn't seem to be an alternative way to query TPMs
# in bootloader mode for physical presence information, so we'll just
# assume that physical presence is available. The firmware updater will
# fail if it isn't.
echo "tpm12-PP"
;;
normal|developer)
if [ "$(tpmc getownership)" = "Owned: no" ]; then
echo "tpm12-takeownership"
elif tpmc checkownerauth; then
echo "tpm12-ownerauth"
else
dlog "TPM already owned, will not update."
return 1
fi
;;
*)
dlog "Unsupported boot mode: ${mainfw_type}"
return 1
esac
# All checks passed, success!
return 0
}
# Performs sanity checks to determine whether it makes sense to go ahead with
# this update attempt. Prints the update attempt counter on stdout.
validate_attempt() {
local previous_attempts="${1:-0}"
local previous_image_hash="$2"
local tpm_status="$3"
local upgrade_counter="$4"
local fw_image_hash="$5"
# If there are previous failed updates, we only have a very limited number of
# attempts to recover. Run some additional checks to make sure we're not
# burning update attempts carelessly in case of bugs.
if [ "${tpm_status}" = "5a3c" ]; then
# If we've previously attempted an update we need to send the exact same
# image again on retry or the update will fail and burn an attempt.
if [ -n "${previous_image_hash}" -a \
"${fw_image_hash}" != "${previous_image_hash}" ]; then
dlog "Firmware image hash mismatch on retry."
return 1
fi
# Artificially limit the number of attempts we're willing to burn in case of
# systematic bugs, so we have a safety margin.
if [ "${previous_attempts}" -ge \
"${TPM_FIRMWARE_UPDATE_MAX_ATTEMPTS}" ]; then
dlog "Too many attempts at updating firmware, giving up."
return 1
fi
else
# If the TPM is not in bootloader mode, then by definition this is the first
# attempt to install the update.
previous_attempts=0
fi
# The TPM enforces a maximum number of 64 of updates before it doesn't allow
# further updates. Enforce an artificial safety margin.
if [ "${upgrade_counter}" -le "${TPM_FIRMWARE_UPDATE_MIN_REMAINING}" ]; then
dlog "Out of update attempts!"
return 1
fi
echo "$(( previous_attempts + 1 ))"
return 0
}
# Prepares the TPM before installing the update.
prepare_tpm() {
local tpm_auth_mechanism="$1"
# Request another TPM reset to take place after installing the update. This
# is important because the update carries over some state (notably the SRK)
# which we specifically don't want to happen.
#
# This is necessary for owner-authorized updating where the SRK is known to be
# present and must be cleared. It's also necessary for PP-authorized update
# retries in recovery mode, because the retry will keep TPM ownership intact
# and the TPM clear attempt below fails if the TPM is in failed selftest mode.
crossystem clear_tpm_owner_request=1
# If the TPM is owned, the PP-authorized update fails, so clear the TPM owner
# before the update. Do it on a "best effort" basis, ignoring any errors. If
# the TPM is in failed selftest mode, the commands will actually fail, but the
# firmware update can still be installed. If anything else fails and the TPM
# is left in non-updatable state, infineon-firmware-updater will detect the
# problem and report an error, which causes an error screen to be shown to the
# user.
if [ "${tpm_auth_mechanism}" = "tpm12-PP" ]; then
(
set +e
tpmc ppon
tpmc clear
tpmc enable
tpmc activate
) || :
fi
}
# Fires off the firmware updater to install the update. Writes progress
# information (decimal numbers indicating completion in percentage) to stdout.
install_update() {
local tpm_auth_mechanism="$1"
local firmware_file="$2"
local options="$3"
local status_file="$(mktemp -t infineon-firmware-updater-status.XXXXXX)"
(
set +e
infineon-firmware-updater \
-update "${tpm_auth_mechanism}" \
-access-mode 3 "${TPM_DEVICE_NODE}" \
-firmware "${firmware_file}" \
-log /dev/stderr \
${options}
echo $? >"${status_file}"
) | (
# The updater writes progress indication to stdout. We post-process the
# output as follows to obtain integer percentage numbers:
# 1. Replace \r with \n to put each progress update on a new line.
# 2. Filter the lines containing progress updates. This drops other
# diagnostic output (which also appears on the stderr log anyways).
# 3. Drop everything on the line except the actual number.
# NB: Use stdbuf to avoid updates getting held in pipe buffers and make them
# appear immediately on stdout.
stdbuf -oL tr '\r' '\n' |
stdbuf -oL grep '^\s*Completion:' |
stdbuf -oL tr -dc '0-9\n'
)
local status=$(cat "${status_file}")
return "${status:-1}"
}
# Updates a value in the vpd_params string.
update_vpd_params() {
local params=$1
local key=$2
local value=$3
local stripped="$(printf "${params}" | tr ',' '\n' | grep -v "^${key}:")"
printf "${key}:${value}\n${stripped}" | tr '\n' ','
}
usage() {
echo 1>&2 <<EOL
Usage: $0
Installs Infineon TPM firmware updates. Checks the following prerequisites:
1. Updated firmware for the TPM chip in the device is available.
2. The TPM is in a state that allows updating.
3. The battery (if present) is sufficiently charged.
If all of the checks above succeed, the script will install the update and write
progress information to stdout.
EOL
exit "${EXIT_CODE_ERROR}"
}
main() {
if [ "$#" != 0 ]; then
usage
fi
if ! probe_tpm; then
return "${EXIT_CODE_ERROR}"
fi
# Read relevant system information.
local tpmc_version_data="$(tpmc getversion)"
read_key_value_pairs tpmc_version " " "${tpmc_version_data}"
local tpmc_ifxfui_data="$(tpmc ifxfieldupgradeinfo)"
read_key_value_pairs tpmc_ifxfui " " "${tpmc_ifxfui_data}"
if [ "${tpmc_version_vendor}" != "49465800" ]; then
dlog "This tool can only update Infineon hardware."
return "${EXIT_CODE_NO_UPDATE}"
fi
# Check whether we have a firmware binary with an update.
local firmware_file="$(tpm-firmware-locate-update "${tpmc_version_data}" \
"${tpmc_ifxfui_data}")"
if [ -z "${firmware_file}" ]; then
return "${EXIT_CODE_NO_UPDATE}"
fi
local fw_image_hash="$(sha256sum "${firmware_file}" | cut -d ' ' -f 1)"
# Check whether the TPM is in updateable state in this boot cycle.
local tpm_auth_mechanism
if ! tpm_auth_mechanism="$(verify_tpm_state)"; then
return "${EXIT_CODE_NOT_UPDATABLE}"
fi
# Run a couple of safety checks to prevent doomed retries.
local vpd_params="$(vpd -i RW_VPD -g tpm_firmware_update_params)"
read_key_value_pairs vpd_param ":" "$(echo "${vpd_params}" | tr ',' '\n')"
local attempts
if ! attempts="$(validate_attempt "${vpd_param_attempts}" \
"${vpd_param_image}" \
"${tpmc_ifxfui_status}" \
"${tpmc_ifxfui_field_upgrade_counter}" \
"${fw_image_hash}")"; then
return "${EXIT_CODE_BAD_RETRY}"
fi
# Check that the battery (if present) is sufficiently charged to prevent
# failed updates due to power loss while sending the update.
read_key_value_pairs power_status " " "$(dump_power_status)"
local battery_percent="${power_status_battery_display_percent%%.*}"
if [ "${power_status_battery_present}" = "1" -a \
"${battery_percent}" -lt "${TPM_FIRMWARE_UPDATE_MIN_BATTERY}" ]; then
dlog "Insufficient battery charge level."
return "${EXIT_CODE_LOW_BATTERY}"
fi
# Figure out updater options.
local success_code="${EXIT_CODE_SUCCESS}"
local updater_options
read_key_value_pairs vpd_param ":" "$(echo "${vpd_params}" | tr ',' '\n')"
if [ "${vpd_param_dryrun}" = "1" ]; then
updater_options="-dry-run"
fi
if [ -e "${TPM_FIRMWARE_DIR}/ifx/.ignore_error_on_complete" ]; then
updater_options="${updater_options} -ignore-error-on-complete"
success_code="${EXIT_CODE_SUCCESS_COLD_REBOOT}"
fi
prepare_tpm "${tpm_auth_mechanism}"
# This is the point of no return. Fingers crossed!
# We write 0 and 100 before and after the updater runs as progress indicators
# so consuming code can depend on them always showing up.
echo 0
vpd_params="$(update_vpd_params "${vpd_params}" 'attempts' "${attempts}")"
vpd_params="$(update_vpd_params "${vpd_params}" 'image' "${fw_image_hash}")"
vpd -i RW_VPD -s "tpm_firmware_update_params=${vpd_params}"
dlog "Installing TPM firmware update ${firmware_file}"
if ! install_update "${tpm_auth_mechanism}" "${firmware_file}" \
"${updater_options}"; then
return "${EXIT_CODE_UPDATE_FAILED}"
fi
echo 100
# Record success in the VPD parameters. We intentionally do not clear the
# value here so we can report metrics once we're booted in normal mode again.
vpd_params="$(update_vpd_params "${vpd_params}" 'mode' 'complete')"
vpd -i RW_VPD -s "tpm_firmware_update_params=${vpd_params}"
return "${success_code}"
}
if [ "$(basename "$0")" = "tpm-firmware-updater" ]; then
main "$@"
fi