blob: aaeff5b54a12a71fe4ce798c9149835a01d57c1e [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_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_MISSING_APPROVAL=7
EXIT_CODE_SUCCESS_COLD_REBOOT=8
# Log a diagnostic message to stderr.
dlog() {
echo "tpm-firmware-updater: $*" >&2
}
# Log an error and exit.
fail() {
dlog "$@"
exit "${EXIT_CODE_ERROR}"
}
# 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 -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 "/dev/tpm0" ]; then
dlog "/dev/tpm0 unavailable"
return 1
fi
if ! (exec 3<>/dev/tpm0) ; then
dlog "Can't open /dev/tpm0. Is it claimed by another process? stop tcsd?"
return 1
fi
return 0
}
# Figure out whether we have a newer firmware image and print its path.
locate_firmware() {
local tpmc_version_data="$1"
local tpmc_ifxfui_data="$2"
# Determine firmware package ID and current build number.
local package_id
local build_number
read_key_value_pairs tpmc_ifxfui " " "${tpmc_ifxfui_data}"
if [ "${tpmc_ifxfui_fw0_stale_version}" != "ffffeeee" ]; then
package_id="${tpmc_ifxfui_fw0_package_id}"
build_number="${tpmc_ifxfui_fw0_version}"
else
package_id="${tpmc_ifxfui_fw1_package_id}"
build_number="${tpmc_ifxfui_fw1_version}"
fi
local pattern
if [ "${tpmc_ifxfui_status}" = "5a3c" ]; then
# For bootloader mode, append the target build number.
pattern="${package_id}_${build_number}_${tpmc_ifxfui_process_fw_version}"
else
# For non-bootloader mode, attempt to get the current build number
# from 'tpmc getver' instead. 'getver' provides more reliable information
# about the current version than 'ifxfui'.
read_key_value_pairs tpmc_version " " "${tpmc_version_data}"
if [ "${tpmc_version_vendor_specific}" != "" ]; then
build_number="0000$(echo "${tpmc_version_vendor_specific}" | cut -c5-8)"
fi
pattern="${package_id}_${build_number}_*"
fi
# Pick the latest matching firmware image.
local path="$(ls ${TPM_FIRMWARE_DIR}/ifx/${pattern}.bin |
sort -r | head -n 1)"
if [ -n "${path}" -a -f "${path}" ]; then
echo "${path}"
fi
}
# 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 tpmc_ifxfui_data="$1"
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
dlog "TPM already owned, will not update."
return 1
fi
echo "tpm12-takeownership"
;;
*)
dlog "Unsupported boot mode: ${mainfw_type}"
return 1
esac
# The TPM enforces a maximum number of 64 of updates before it doesn't allow
# further updates. Enforce an artificial safety margin.
read_key_value_pairs tpmc_ifxfui " " "${tpmc_ifxfui_data}"
if [ "${tpmc_ifxfui_field_upgrade_counter}" -le \
"${TPM_FIRMWARE_UPDATE_MIN_REMAINING}" ]; then
dlog "Out of update attempts!"
return 1
fi
# All checks passed, success!
return 0
}
# Checks whether VPD state allows the update to go ahead. Prints the update
# attempt counter on stdout.
approve_update() {
local vpd_params=$1
local fw_image_hash="$2"
local tpmc_ifxfui_data="$3"
# Determine whether a previous update attempt failed.
local previous_failed_update
read_key_value_pairs tpmc_ifxfui " " "${tpmc_ifxfui_data}"
if [ "${tpmc_ifxfui_status}" = "5a3c" ]; then
previous_failed_update=1
fi
# Determine how many times we failed updating according to VPD.
read_key_value_pairs vpd_param ":" "$(echo "${vpd_params}" | tr ',' '\n')"
local previous_attempts=0
if [ -n "${previous_failed_update}" ]; then
previous_attempts="${vpd_param_attempts:-0}"
fi
# See whether we're allowed to update at all. The user's consent to run an
# update is reflected ${vpd_param_mode}.
local mainfw_type=$(crossystem mainfw_type)
case "${mainfw_type}" in
recovery)
if [ -n "${previous_failed_update}" ]; then
# Waive consent for TPMs that failed to update previously. They're
# already in a hosed state so no need to confirm with the user that
# they're OK with the risk of entering that state. Note that we're still
# subject to the hard limit on update attempts below.
:
elif [ "${vpd_param_mode}" != "recovery" -a \
"${vpd_param_mode}" != "first_boot" ]; then
dlog "Update mode '${vpd_param_mode}' not allowed in recovery mode."
return 1
fi
;;
normal|developer)
if [ "${vpd_param_mode}" != "first_boot" ]; then
dlog "Update mode '${vpd_param_mode}' not allowed in normal/dev mode."
return 1
fi
;;
*)
dlog "Unsupported boot mode: ${mainfw_type}"
return 1
;;
esac
# 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 [ -n "${previous_failed_update}" ]; 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 "${vpd_param_image}" -a \
"${fw_image_hash}" != "${vpd_param_image}" ]; 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
fi
echo "$(( previous_attempts + 1 ))"
return 0
}
# Prepares the TPM before installing the update.
prepare_tpm() {
local tpm_auth_mechanism="$1"
case "${tpm_auth_mechanism}" in
# In case of owner-authorized updates:
# Request another TPM reset to take place after installing the update. This
# is important because owner-authorized update carries over some state
# (notably the SRK) which we specifically don't want to happen.
tpm12-takeownership)
crossystem clear_tpm_owner_request=1
;;
# In case of PP-authorized updates:
# The PP update still fails if the TPM is owned. 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.
tpm12-PP)
(
set +e
tpmc ppon
tpmc clear
tpmc enable
tpmc activate
) || :
;;
esac
}
# 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"
(
set +e
infineon-firmware-updater \
-update "${tpm_auth_mechanism}" \
-access-mode 3 /dev/tpm0 \
-firmware "${firmware_file}" \
-log /dev/stderr \
${options}
echo $? >/run/infineon-firmware-updater.status
) | (
# 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 /run/infineon-firmware-updater.status)
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() {
fail <<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 user consented to the update accroding to the tpm_firmware_update_params
value in VPD.
4. 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
}
main() {
if [ "$#" != 0 ]; then
usage
fi
if ! probe_tpm; then
exit "${EXIT_CODE_ERROR}"
fi
# Cache tpm version info.
local tpmc_version_data="$(tpmc getversion)"
read_key_value_pairs tpmc_version " " "${tpmc_version_data}"
if [ "${tpmc_version_vendor}" != "49465800" ]; then
dlog "This tool can only update Infineon hardware."
exit "${EXIT_CODE_NO_UPDATE}"
fi
# Cache vendor-specific field updgrade info.
local tpmc_ifxfui_data="$(tpmc ifxfieldupgradeinfo)"
local firmware_file="$(locate_firmware "${tpmc_version_data}" \
"${tpmc_ifxfui_data}")"
if [ -z "${firmware_file}" ]; then
exit "${EXIT_CODE_NO_UPDATE}"
fi
local tpm_auth_mechanism
if ! tpm_auth_mechanism="$(verify_tpm_state "${tpmc_ifxfui_data}")"; then
exit "${EXIT_CODE_NOT_UPDATABLE}"
fi
# Read R/W VPD. This is slow hence should happen as late as possible.
local vpd_params="$(vpd -i RW_VPD -g tpm_firmware_update_params)"
local fw_image_hash="$(sha256sum "${firmware_file}" | cut -d ' ' -f 1)"
if ! attempts="$(approve_update "${vpd_params}" "${fw_image_hash}" \
"${tpmc_ifxfui_data}")"; then
exit "${EXIT_CODE_MISSING_APPROVAL}"
fi
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."
exit "${EXIT_CODE_LOW_BATTERY}"
fi
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
exit "${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}"
exit "${success_code}"
}
if [ "$(basename "$0")" = "tpm-firmware-updater" ]; then
main "$@"
fi