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