| #!/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 |