From c4c9e2bc436198ee8f4544e8091c0067c4b4d9be Mon Sep 17 00:00:00 2001 From: Abhi Singh Date: Wed, 6 Nov 2024 11:11:11 -0600 Subject: [PATCH 1/9] feat(rpi3): implement mboot for rpi3 Add Measured Boot support using the Event Log backend for the rpi3 platform. -Implement measured boot infrastructure in BL1 & BL2, including the init, measure image, and finish phases. -Pass the eventlog addr and size from BL1 to BL2 using the image entry point args. -dump the eventlog after measuring BL2, and after all images are measured in BL2. Signed-off-by: Tushar Khandelwal Signed-off-by: Abhi Singh Change-Id: I7c040c4a2d001a933fefb0b16f0fdf2a43a11be9 --- plat/rpi/rpi3/include/platform_def.h | 7 +- plat/rpi/rpi3/include/rpi3_measured_boot.h | 16 +++++ plat/rpi/rpi3/platform.mk | 21 +++++- plat/rpi/rpi3/rpi3_bl1_mboot.c | 77 ++++++++++++++++++++++ plat/rpi/rpi3/rpi3_bl2_mboot.c | 77 ++++++++++++++++++++++ plat/rpi/rpi3/rpi3_bl2_setup.c | 19 +++++- 6 files changed, 213 insertions(+), 4 deletions(-) create mode 100644 plat/rpi/rpi3/include/rpi3_measured_boot.h create mode 100644 plat/rpi/rpi3/rpi3_bl1_mboot.c create mode 100644 plat/rpi/rpi3/rpi3_bl2_mboot.c diff --git a/plat/rpi/rpi3/include/platform_def.h b/plat/rpi/rpi3/include/platform_def.h index 757c64ad9..b439d6876 100644 --- a/plat/rpi/rpi3/include/platform_def.h +++ b/plat/rpi/rpi3/include/platform_def.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2024, Arm Limited and Contributors. All rights reserved. + * Copyright (c) 2015-2025, Arm Limited and Contributors. All rights reserved. * * SPDX-License-Identifier: BSD-3-Clause */ @@ -261,4 +261,9 @@ */ #define SYS_COUNTER_FREQ_IN_TICKS ULL(19200000) +/* + * TCG Event Log + */ +#define PLAT_ARM_EVENT_LOG_MAX_SIZE UL(0x400) + #endif /* PLATFORM_DEF_H */ diff --git a/plat/rpi/rpi3/include/rpi3_measured_boot.h b/plat/rpi/rpi3/include/rpi3_measured_boot.h new file mode 100644 index 000000000..91ba8833c --- /dev/null +++ b/plat/rpi/rpi3/include/rpi3_measured_boot.h @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2025, Arm Limited and Contributors. All rights reserved. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef RPI3_MEASURED_BOOT_H +#define RPI3_MEASURED_BOOT_H + +#include + +#include + +void rpi3_mboot_fetch_eventlog_info(uint8_t **eventlog_addr, size_t *eventlog_size); + +#endif /* RPI3_MEASURED_BOOT_H */ diff --git a/plat/rpi/rpi3/platform.mk b/plat/rpi/rpi3/platform.mk index fc51bec67..7ed13669d 100644 --- a/plat/rpi/rpi3/platform.mk +++ b/plat/rpi/rpi3/platform.mk @@ -1,5 +1,5 @@ # -# Copyright (c) 2013-2024, Arm Limited and Contributors. All rights reserved. +# Copyright (c) 2013-2025, Arm Limited and Contributors. All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause # @@ -20,6 +20,25 @@ PLAT_BL_COMMON_SOURCES := drivers/ti/uart/aarch64/16550_console.S \ plat/rpi/common/rpi3_console_dual.c \ ${XLAT_TABLES_LIB_SRCS} +ifeq (${MEASURED_BOOT},1) +MEASURED_BOOT_MK := drivers/measured_boot/event_log/event_log.mk +$(info Including ${MEASURED_BOOT_MK}) +include ${MEASURED_BOOT_MK} + +PLAT_BL_COMMON_SOURCES += ${EVENT_LOG_SOURCES} + +BL1_SOURCES += plat/rpi/rpi3/rpi3_bl1_mboot.c +BL2_SOURCES += plat/rpi/rpi3/rpi3_bl2_mboot.c + +CRYPTO_SOURCES := drivers/auth/crypto_mod.c + +BL1_SOURCES += ${CRYPTO_SOURCES} +BL2_SOURCES += ${CRYPTO_SOURCES} + +include drivers/auth/mbedtls/mbedtls_crypto.mk + +endif + BL1_SOURCES += drivers/io/io_fip.c \ drivers/io/io_memmap.c \ drivers/io/io_storage.c \ diff --git a/plat/rpi/rpi3/rpi3_bl1_mboot.c b/plat/rpi/rpi3/rpi3_bl1_mboot.c new file mode 100644 index 000000000..4f6b52a18 --- /dev/null +++ b/plat/rpi/rpi3/rpi3_bl1_mboot.c @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2025, Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +/* Event Log data */ +uint8_t event_log[PLAT_ARM_EVENT_LOG_MAX_SIZE]; + +/* RPI3 table with platform specific image IDs, names and PCRs */ +const event_log_metadata_t rpi3_event_log_metadata[] = { + { FW_CONFIG_ID, MBOOT_FW_CONFIG_STRING, PCR_0 }, + { TB_FW_CONFIG_ID, MBOOT_TB_FW_CONFIG_STRING, PCR_0 }, + { BL2_IMAGE_ID, MBOOT_BL2_IMAGE_STRING, PCR_0 }, + + { EVLOG_INVALID_ID, NULL, (unsigned int)(-1) } /* Terminator */ +}; + +void bl1_plat_mboot_init(void) +{ + event_log_init(event_log, event_log + sizeof(event_log)); + event_log_write_header(); +} + +void bl1_plat_mboot_finish(void) +{ + size_t event_log_cur_size; + image_desc_t *image_desc; + entry_point_info_t *ep_info; + + event_log_cur_size = event_log_get_cur_size(event_log); + image_desc = bl1_plat_get_image_desc(BL2_IMAGE_ID); + assert(image_desc != NULL); + + /* Get the entry point info */ + ep_info = &image_desc->ep_info; + ep_info->args.arg2 = (uint64_t) event_log; + ep_info->args.arg3 = (uint32_t) event_log_cur_size; +} + +int plat_mboot_measure_image(unsigned int image_id, image_info_t *image_data) +{ + int rc = 0; + unsigned char hash_data[CRYPTO_MD_MAX_SIZE]; + const event_log_metadata_t *metadata_ptr = rpi3_event_log_metadata; + + rc = event_log_measure(image_data->image_base, image_data->image_size, hash_data); + if (rc != 0) { + return rc; + } + + while ((metadata_ptr->id != EVLOG_INVALID_ID) && + (metadata_ptr->id != image_id)) { + metadata_ptr++; + } + assert(metadata_ptr->id != EVLOG_INVALID_ID); + + event_log_record(hash_data, EV_POST_CODE, metadata_ptr); + + /* Dump Event Log for user view */ + dump_event_log((uint8_t *)event_log, event_log_get_cur_size(event_log)); + + return rc; +} diff --git a/plat/rpi/rpi3/rpi3_bl2_mboot.c b/plat/rpi/rpi3/rpi3_bl2_mboot.c new file mode 100644 index 000000000..07aa40034 --- /dev/null +++ b/plat/rpi/rpi3/rpi3_bl2_mboot.c @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2025, Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include +#include +#include + +#include "./include/rpi3_measured_boot.h" + +#include +#include +#include +#include +#include +#include +#include + +/* RPI3 table with platform specific image IDs, names and PCRs */ +const event_log_metadata_t rpi3_event_log_metadata[] = { + { BL31_IMAGE_ID, MBOOT_BL31_IMAGE_STRING, PCR_0 }, + { BL33_IMAGE_ID, MBOOT_BL33_IMAGE_STRING, PCR_0 }, + { NT_FW_CONFIG_ID, MBOOT_NT_FW_CONFIG_STRING, PCR_0 }, + + { EVLOG_INVALID_ID, NULL, (unsigned int)(-1) } /* Terminator */ +}; + +static uint8_t *event_log_start; +static size_t event_log_size; + +void bl2_plat_mboot_init(void) +{ + uint8_t *bl2_event_log_start; + uint8_t *bl2_event_log_finish; + + rpi3_mboot_fetch_eventlog_info(&event_log_start, &event_log_size); + bl2_event_log_start = event_log_start + event_log_size; + bl2_event_log_finish = event_log_start + PLAT_ARM_EVENT_LOG_MAX_SIZE; + event_log_init(bl2_event_log_start, bl2_event_log_finish); +} + +void bl2_plat_mboot_finish(void) +{ + /* Event Log filled size */ + size_t event_log_cur_size; + + event_log_cur_size = event_log_get_cur_size((uint8_t *)event_log_start); + + /* Dump Event Log for user view */ + dump_event_log((uint8_t *)event_log_start, event_log_cur_size); +} + +int plat_mboot_measure_image(unsigned int image_id, image_info_t *image_data) +{ + int rc = 0; + + unsigned char hash_data[CRYPTO_MD_MAX_SIZE]; + const event_log_metadata_t *metadata_ptr = rpi3_event_log_metadata; + + /* Measure the payload with algorithm selected by EventLog driver */ + rc = event_log_measure(image_data->image_base, image_data->image_size, hash_data); + if (rc != 0) { + return rc; + } + + while ((metadata_ptr->id != EVLOG_INVALID_ID) && + (metadata_ptr->id != image_id)) { + metadata_ptr++; + } + assert(metadata_ptr->id != EVLOG_INVALID_ID); + + event_log_record(hash_data, EV_POST_CODE, metadata_ptr); + + return rc; +} diff --git a/plat/rpi/rpi3/rpi3_bl2_setup.c b/plat/rpi/rpi3/rpi3_bl2_setup.c index 80e4d8d8d..2f57b32a2 100644 --- a/plat/rpi/rpi3/rpi3_bl2_setup.c +++ b/plat/rpi/rpi3/rpi3_bl2_setup.c @@ -1,12 +1,12 @@ /* - * Copyright (c) 2015-2019, ARM Limited and Contributors. All rights reserved. + * Copyright (c) 2015-2025, ARM Limited and Contributors. All rights reserved. * * SPDX-License-Identifier: BSD-3-Clause */ #include -#include +#include "./include/rpi3_measured_boot.h" #include #include @@ -18,6 +18,7 @@ #include #include #include +#include #include @@ -27,6 +28,10 @@ static meminfo_t bl2_tzram_layout __aligned(CACHE_WRITEBACK_GRANULE); /* Data structure which holds the MMC info */ static struct mmc_device_info mmc_info; +/* Variables that hold the eventlog addr and size for use in BL2 Measured Boot */ +static uint8_t *event_log_start; +static size_t event_log_size; + static void rpi3_sdhost_setup(void) { struct rpi3_sdhost_params params; @@ -41,6 +46,12 @@ static void rpi3_sdhost_setup(void) rpi3_sdhost_init(¶ms, &mmc_info); } +void rpi3_mboot_fetch_eventlog_info(uint8_t **eventlog_addr, size_t *eventlog_size) +{ + *eventlog_addr = event_log_start; + *eventlog_size = event_log_size; +} + /******************************************************************************* * BL1 has passed the extents of the trusted SRAM that should be visible to BL2 * in x0. This memory layout is sitting at the base of the free trusted SRAM. @@ -67,6 +78,10 @@ void bl2_early_platform_setup2(u_register_t arg0, u_register_t arg1, /* Setup SDHost driver */ rpi3_sdhost_setup(); + /* populate eventlog addr and size for use in bl2 mboot */ + event_log_start = (uint8_t *)(uintptr_t)arg2; + event_log_size = arg3; + plat_rpi3_io_setup(); } From 6dfcf4e1df3b4690fdf2629815d2a91294f34493 Mon Sep 17 00:00:00 2001 From: Abhi Singh Date: Thu, 7 Nov 2024 16:40:57 -0600 Subject: [PATCH 2/9] feat(rpi3): implement eventlog handoff to BL33 At the end of BL2 measured boot, write the address and size of the TCG Event Log to NT_FW_CONFIG so that the log can be consumed later by BL33. -add dynamic configuration helpers for the fdt -write the eventlog address and size to the fdt Change-Id: I099dd9cc96d740ae13cb8b8e8c6b9f2e6c02accc Signed-off-by: Abhi Singh --- plat/rpi/rpi3/include/platform_def.h | 6 + plat/rpi/rpi3/include/rpi3_measured_boot.h | 2 + plat/rpi/rpi3/platform.mk | 8 +- plat/rpi/rpi3/rpi3_bl2_mboot.c | 25 +++ plat/rpi/rpi3/rpi3_dyn_cfg_helpers.c | 198 +++++++++++++++++++++ 5 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 plat/rpi/rpi3/rpi3_dyn_cfg_helpers.c diff --git a/plat/rpi/rpi3/include/platform_def.h b/plat/rpi/rpi3/include/platform_def.h index b439d6876..37aceafc7 100644 --- a/plat/rpi/rpi3/include/platform_def.h +++ b/plat/rpi/rpi3/include/platform_def.h @@ -266,4 +266,10 @@ */ #define PLAT_ARM_EVENT_LOG_MAX_SIZE UL(0x400) +/* + * NT_FW_CONFIG magic dram addr and max size + */ +#define PLAT_RPI3_DTO_BASE ULL(0x11530000) +#define PLAT_RPI3_DTO_MAX_SIZE ULL(0x001000) + #endif /* PLATFORM_DEF_H */ diff --git a/plat/rpi/rpi3/include/rpi3_measured_boot.h b/plat/rpi/rpi3/include/rpi3_measured_boot.h index 91ba8833c..67ad47947 100644 --- a/plat/rpi/rpi3/include/rpi3_measured_boot.h +++ b/plat/rpi/rpi3/include/rpi3_measured_boot.h @@ -13,4 +13,6 @@ void rpi3_mboot_fetch_eventlog_info(uint8_t **eventlog_addr, size_t *eventlog_size); +int rpi3_set_nt_fw_info(size_t log_size, uintptr_t *ns_log_addr); + #endif /* RPI3_MEASURED_BOOT_H */ diff --git a/plat/rpi/rpi3/platform.mk b/plat/rpi/rpi3/platform.mk index 7ed13669d..2072877ea 100644 --- a/plat/rpi/rpi3/platform.mk +++ b/plat/rpi/rpi3/platform.mk @@ -8,7 +8,8 @@ include lib/libfdt/libfdt.mk include lib/xlat_tables_v2/xlat_tables.mk PLAT_INCLUDES := -Iplat/rpi/common/include \ - -Iplat/rpi/rpi3/include + -Iplat/rpi/rpi3/include \ + -Iinclude/lib/libfdt PLAT_BL_COMMON_SOURCES := drivers/ti/uart/aarch64/16550_console.S \ drivers/arm/pl011/aarch64/pl011_console.S \ @@ -28,7 +29,10 @@ include ${MEASURED_BOOT_MK} PLAT_BL_COMMON_SOURCES += ${EVENT_LOG_SOURCES} BL1_SOURCES += plat/rpi/rpi3/rpi3_bl1_mboot.c -BL2_SOURCES += plat/rpi/rpi3/rpi3_bl2_mboot.c +BL2_SOURCES += plat/rpi/rpi3/rpi3_bl2_mboot.c \ + plat/rpi/rpi3/rpi3_dyn_cfg_helpers.c \ + common/fdt_wrappers.c \ + common/fdt_fixup.c CRYPTO_SOURCES := drivers/auth/crypto_mod.c diff --git a/plat/rpi/rpi3/rpi3_bl2_mboot.c b/plat/rpi/rpi3/rpi3_bl2_mboot.c index 07aa40034..3abc5b80d 100644 --- a/plat/rpi/rpi3/rpi3_bl2_mboot.c +++ b/plat/rpi/rpi3/rpi3_bl2_mboot.c @@ -43,11 +43,36 @@ void bl2_plat_mboot_init(void) void bl2_plat_mboot_finish(void) { + int rc; + + /* Event Log address in Non-Secure memory */ + uintptr_t ns_log_addr; + /* Event Log filled size */ size_t event_log_cur_size; event_log_cur_size = event_log_get_cur_size((uint8_t *)event_log_start); + /* write the eventlog addr and size to NT_FW_CONFIG TPM entry */ + rc = rpi3_set_nt_fw_info(event_log_cur_size, &ns_log_addr); + if (rc != 0) { + ERROR("%s(): Unable to update %s_FW_CONFIG\n", + __func__, "NT"); + /* + * fatal error due to Bl33 maintaining the assumption + * that the eventlog is successfully passed via + * NT_FW_CONFIG. + */ + panic(); + } + + /* Copy Event Log to Non-secure memory */ + (void)memcpy((void *)ns_log_addr, (const void *)event_log_start, + event_log_cur_size); + + /* Ensure that the Event Log is visible in Non-secure memory */ + flush_dcache_range(ns_log_addr, event_log_cur_size); + /* Dump Event Log for user view */ dump_event_log((uint8_t *)event_log_start, event_log_cur_size); } diff --git a/plat/rpi/rpi3/rpi3_dyn_cfg_helpers.c b/plat/rpi/rpi3/rpi3_dyn_cfg_helpers.c new file mode 100644 index 000000000..7c2e6e737 --- /dev/null +++ b/plat/rpi/rpi3/rpi3_dyn_cfg_helpers.c @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2025, ARM Limited and Contributors. All rights reserved. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include + +#include +#include +#include +#include +#include + +#define DTB_PROP_HW_LOG_ADDR "tpm_event_log_addr" +#define DTB_PROP_HW_LOG_SIZE "tpm_event_log_size" + +static int rpi3_event_log_fdt_init_overlay(uintptr_t dt_base, int dt_size) +{ + int ret; + int offset; + void *dtb = (void *)dt_base; + + ret = fdt_create_empty_tree(dtb, dt_size); + if (ret < 0) { + ERROR("cannot create empty dtb tree: %s\n", + fdt_strerror(ret)); + return ret; + } + + offset = fdt_path_offset(dtb, "/"); + if (offset < 0) { + ERROR("cannot find root of the tree: %s\n", + fdt_strerror(offset)); + return offset; + } + + offset = fdt_add_subnode(dtb, offset, "fragment@0"); + if (offset < 0) { + ERROR("cannot add fragment node: %s\n", + fdt_strerror(offset)); + return offset; + } + + ret = fdt_setprop_string(dtb, offset, "target-path", "/"); + if (ret < 0) { + ERROR("cannot set target-path property: %s\n", + fdt_strerror(ret)); + return ret; + } + + offset = fdt_add_subnode(dtb, offset, "__overlay__"); + if (offset < 0) { + ERROR("cannot add __overlay__ node: %s\n", + fdt_strerror(offset)); + return ret; + } + + offset = fdt_add_subnode(dtb, offset, "tpm_event_log"); + if (offset < 0) { + ERROR("cannot add tpm_event_log node: %s\n", + fdt_strerror(offset)); + return offset; + } + + ret = fdt_setprop_string(dtb, offset, "compatible", + "arm,tpm_event_log"); + if (ret < 0) { + ERROR("cannot set compatible property: %s\n", + fdt_strerror(ret)); + return ret; + } + + ret = fdt_setprop_u64(dtb, offset, "tpm_event_log_addr", 0); + if (ret < 0) { + ERROR("cannot set tpm_event_log_addr property: %s\n", + fdt_strerror(ret)); + return ret; + } + + ret = fdt_setprop_u32(dtb, offset, "tpm_event_log_size", 0); + if (ret < 0) { + ERROR("cannot set tpm_event_log_size property: %s\n", + fdt_strerror(ret)); + return ret; + } + + return ret; +} + +/* + * Write the Event Log address and its size in the DTB. + * + * This function is supposed to be called only by BL2. + * + * Returns: + * 0 = success + * < 0 = error + */ +static int rpi3_set_event_log_info(uintptr_t config_base, + uintptr_t log_addr, size_t log_size) +{ + /* As libfdt uses void *, we can't avoid this cast */ + void *dtb = (void *)config_base; + /* compatible is set based on the following tpm_tis_spi guidelines from + * https://www.kernel.org/doc/Documentation/devicetree/bindings + * /security/tpm/tpm_tis_spi.txt + */ + const char *compatible_tpm = "arm,tpm_event_log"; + uint64_t base = cpu_to_fdt64(log_addr); + uint32_t sz = cpu_to_fdt32(log_size); + int err, node; + + err = fdt_open_into(dtb, dtb, PLAT_RPI3_DTO_MAX_SIZE); + if (err < 0) { + ERROR("Invalid Device Tree at %p: error %d\n", dtb, err); + return err; + } + + /* + * Verify that the DTB is valid, before attempting to write to it, + * and get the DTB root node. + */ + + /* Check if the pointer to DT is correct */ + err = fdt_check_header(dtb); + if (err < 0) { + WARN("Invalid DTB file passed\n"); + return err; + } + + /* + * Find the TPM node in device tree. + */ + node = fdt_node_offset_by_compatible(dtb, -1, compatible_tpm); + if (node < 0) { + ERROR("The compatible property '%s' not%s", compatible_tpm, + " found in the config\n"); + return node; + } + + err = fdt_setprop(dtb, node, DTB_PROP_HW_LOG_ADDR, &base, 8); + if (err < 0) { + ERROR("Failed to add log addr err %d\n", err); + return err; + } + + err = fdt_setprop(dtb, node, DTB_PROP_HW_LOG_SIZE, &sz, 4); + if (err < 0) { + ERROR("Failed to add log size err %d\n", err); + return err; + } + + err = fdt_pack(dtb); + if (err < 0) { + ERROR("Failed to pack Device Tree at %p: error %d\n", dtb, err); + return err; + } + + /* + * Ensure that the info written to the DTB is visible + * to other images. + */ + flush_dcache_range(config_base, fdt_totalsize(dtb)); + + return err; +} + +/* + * This function writes the Event Log address and its size + * in the RPi3 DTB. + * + * This function is supposed to be called only by BL2. + * + * Returns: + * 0 = success + * < 0 = error + */ +int rpi3_set_nt_fw_info(size_t log_size, uintptr_t *ns_log_addr) +{ + uintptr_t ns_addr; + int err; + + assert(ns_log_addr != NULL); + + ns_addr = PLAT_RPI3_DTO_BASE + PLAT_RPI3_DTO_MAX_SIZE; + + rpi3_event_log_fdt_init_overlay(PLAT_RPI3_DTO_BASE, + PLAT_RPI3_DTO_MAX_SIZE); + + /* Write the Event Log address and its size in the DTB */ + err = rpi3_set_event_log_info(PLAT_RPI3_DTO_BASE, + ns_addr, log_size); + + /* Return Event Log address in Non-secure memory */ + *ns_log_addr = (err < 0) ? 0UL : ns_addr; + return err; +} From 3c54570afca013e050db3d01a4c948ae938d908a Mon Sep 17 00:00:00 2001 From: Abhi Singh Date: Mon, 18 Nov 2024 10:29:36 -0600 Subject: [PATCH 3/9] feat(io): add generic gpio spi bit-bang driver When using a tpm breakout board with rpi3, we elected to bit-bang gpio pins to emulate a spi interface, this implementation required a driver to interface with the platform specific pins and emulate spi functionality. The generic driver provides the ability to pass in a gpio_spi_data structure that contains the necessary gpio pins in order to simulate spi operations (get_access, start, stop, xfer). Change-Id: I88919e8a294c05e0cabb8224e35ae5c1ba5f2413 Signed-off-by: Tushar Khandelwal Signed-off-by: Abhi Singh --- drivers/gpio/gpio_spi.c | 130 +++++++++++++++++++++++++++++++++++++ include/drivers/gpio_spi.h | 32 +++++++++ 2 files changed, 162 insertions(+) create mode 100644 drivers/gpio/gpio_spi.c create mode 100644 include/drivers/gpio_spi.h diff --git a/drivers/gpio/gpio_spi.c b/drivers/gpio/gpio_spi.c new file mode 100644 index 000000000..2913b4192 --- /dev/null +++ b/drivers/gpio/gpio_spi.c @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2025, ARM Limited and Contributors. All rights reserved. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include +#include + +#include +#include +#include +#include +#include + +static struct spi_plat gpio_spidev; + +static void gpio_spi_delay_us(void) +{ + udelay(gpio_spidev.gpio_data.spi_delay_us); +} + +static int gpio_spi_miso(void) +{ + return gpio_get_value(gpio_spidev.gpio_data.miso_gpio); +} + +static void gpio_spi_sclk(int bit) +{ + gpio_set_value(gpio_spidev.gpio_data.sclk_gpio, bit); +} + +static void gpio_spi_mosi(int bit) +{ + gpio_set_value(gpio_spidev.gpio_data.mosi_gpio, bit); +} + +static void gpio_spi_cs(int bit) +{ + gpio_set_value(gpio_spidev.gpio_data.cs_gpio, bit); +} + +static void gpio_spi_start(void) +{ + gpio_spi_cs(1); + gpio_spi_sclk(0); + gpio_spi_cs(0); +} + +static void gpio_spi_stop(void) +{ + gpio_spi_cs(1); +} + +/* set sclk to a known state (0) before performing any further action */ +static void gpio_spi_get_access(void) +{ + gpio_spi_sclk(0); +} + +static void xfer(unsigned int bytes, const void *out, void *in, int cpol, int cpha) +{ + for (unsigned int j = 0U; j < bytes; j++) { + unsigned char in_byte = 0U; + unsigned char out_byte = (out != NULL) ? *(const uint8_t *)out++ : 0xFF; + + for (int i = 7; i >= 0; i--) { + if (cpha) { + gpio_spi_sclk(!cpol); + } + + gpio_spi_mosi(!!(out_byte & (1 << i))); + + gpio_spi_delay_us(); + gpio_spi_sclk(cpha ? cpol : !cpol); + gpio_spi_delay_us(); + + in_byte |= gpio_spi_miso() << i; + + if (!cpha) { + gpio_spi_sclk(cpol); + } + } + + if (in != NULL) { + *(uint8_t *)in++ = in_byte; + } + } +} + +static int gpio_spi_xfer(unsigned int bytes, const void *out, void *in) +{ + if ((out == NULL) && (in == NULL)) { + return -1; + } + + switch (gpio_spidev.gpio_data.spi_mode) { + case 0: + xfer(bytes, out, in, 0, 0); + break; + case 1: + xfer(bytes, out, in, 0, 1); + break; + case 2: + xfer(bytes, out, in, 1, 0); + break; + case 3: + xfer(bytes, out, in, 1, 1); + break; + default: + return -1; + } + + return 0; +} + +struct spi_ops gpio_spidev_ops = { + .get_access = gpio_spi_get_access, + .start = gpio_spi_start, + .stop = gpio_spi_stop, + .xfer = gpio_spi_xfer, +}; + +struct spi_plat *gpio_spi_init(struct gpio_spi_data *gpio_spi_data) +{ + gpio_spidev.gpio_data = *gpio_spi_data; + gpio_spidev.ops = &gpio_spidev_ops; + + return &gpio_spidev; +} diff --git a/include/drivers/gpio_spi.h b/include/drivers/gpio_spi.h new file mode 100644 index 000000000..a92655384 --- /dev/null +++ b/include/drivers/gpio_spi.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2025, Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef GPIO_SPI_H +#define GPIO_SPI_H + +#include + +struct gpio_spi_data { + uint8_t cs_gpio, sclk_gpio, mosi_gpio, miso_gpio, reset_gpio; + uint32_t spi_delay_us; + unsigned int spi_mode; +}; + +struct spi_ops { + void (*get_access)(void); + void (*start)(void); + void (*stop)(void); + int (*xfer)(unsigned int bitlen, const void *dout, void *din); +}; + +struct spi_plat { + struct gpio_spi_data gpio_data; + const struct spi_ops *ops; +}; + +struct spi_plat *gpio_spi_init(struct gpio_spi_data *gpio_spi_data); + +#endif /* GPIO_SPI_H */ From 36e3d877cd6caf51155a74936f15b461cc9b814c Mon Sep 17 00:00:00 2001 From: "Abhi.Singh" Date: Wed, 28 Aug 2024 14:17:52 -0500 Subject: [PATCH 4/9] feat(tpm): add tpm drivers and framework Add tpm2 drivers to tf-a with adequate framework -implement a fifo spi interface that works with discrete tpm chip. -implement tpm command layer interfaces that are used to initialize, start and make measurements and close the interface. -tpm drivers are built using their own make file to allow for ease in porting across platforms, and across different interfaces. Signed-off-by: Tushar Khandelwal Signed-off-by: Abhi Singh Change-Id: Ie1a189f45c80f26f4dea16c3bd71b1503709e0ea --- Makefile | 2 + changelog.yaml | 3 + drivers/measured_boot/event_log/event_log.mk | 17 +- drivers/tpm/tpm2.mk | 26 ++ drivers/tpm/tpm2_chip.c | 21 ++ drivers/tpm/tpm2_cmds.c | 222 +++++++++++++ drivers/tpm/tpm2_fifo.c | 322 +++++++++++++++++++ drivers/tpm/tpm2_fifo_spi.c | 161 ++++++++++ include/drivers/tpm/tpm2.h | 103 ++++++ include/drivers/tpm/tpm2_chip.h | 24 ++ include/drivers/tpm/tpm2_interface.h | 28 ++ make_helpers/defaults.mk | 3 + 12 files changed, 926 insertions(+), 6 deletions(-) create mode 100644 drivers/tpm/tpm2.mk create mode 100644 drivers/tpm/tpm2_chip.c create mode 100644 drivers/tpm/tpm2_cmds.c create mode 100644 drivers/tpm/tpm2_fifo.c create mode 100644 drivers/tpm/tpm2_fifo_spi.c create mode 100644 include/drivers/tpm/tpm2.h create mode 100644 include/drivers/tpm/tpm2_chip.h create mode 100644 include/drivers/tpm/tpm2_interface.h diff --git a/Makefile b/Makefile index e111be20a..99e91890b 100644 --- a/Makefile +++ b/Makefile @@ -1232,6 +1232,7 @@ $(eval $(call assert_booleans,\ HARDEN_SLS \ HW_ASSISTED_COHERENCY \ MEASURED_BOOT \ + DISCRETE_TPM \ DICE_PROTECTION_ENVIRONMENT \ RMMD_ENABLE_EL3_TOKEN_SIGN \ DRTM_SUPPORT \ @@ -1418,6 +1419,7 @@ $(eval $(call add_defines,\ HW_ASSISTED_COHERENCY \ LOG_LEVEL \ MEASURED_BOOT \ + DISCRETE_TPM \ DICE_PROTECTION_ENVIRONMENT \ DRTM_SUPPORT \ NS_TIMER_SWITCH \ diff --git a/changelog.yaml b/changelog.yaml index 422b9dafe..3f0636941 100644 --- a/changelog.yaml +++ b/changelog.yaml @@ -962,6 +962,9 @@ subsections: - drivers/scmi-msg - scmi-msg + - title: TPM + scope: tpm + - title: UFS scope: ufs diff --git a/drivers/measured_boot/event_log/event_log.mk b/drivers/measured_boot/event_log/event_log.mk index 5ea4c554a..9e0d6c4f6 100644 --- a/drivers/measured_boot/event_log/event_log.mk +++ b/drivers/measured_boot/event_log/event_log.mk @@ -1,5 +1,5 @@ # -# Copyright (c) 2020-2022, Arm Limited. All rights reserved. +# Copyright (c) 2020-2025, Arm Limited. All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause # @@ -7,15 +7,20 @@ # Default log level to dump the event log (LOG_LEVEL_INFO) EVENT_LOG_LEVEL ?= 40 -# Measured Boot hash algorithm. -# SHA-256 (or stronger) is required for all devices that are TPM 2.0 compliant. -ifdef TPM_HASH_ALG - $(warning "TPM_HASH_ALG is deprecated. Please use MBOOT_EL_HASH_ALG instead.") - MBOOT_EL_HASH_ALG := ${TPM_HASH_ALG} +# When using a TPM, adopt the TPM's hash algorithm for +# measurements through the Event Log mechanism, ensuring +# the TPM uses the same algorithm for measurements and +# extends the PCR accordingly, allowing for comparison +# between PCR value and Event Log measurements required +# for attestation. +ifdef MBOOT_TPM_HASH_ALG + MBOOT_EL_HASH_ALG := ${MBOOT_TPM_HASH_ALG} else MBOOT_EL_HASH_ALG := sha256 endif +# Measured Boot hash algorithm. +# SHA-256 (or stronger) is required for all devices that are TPM 2.0 compliant. ifeq (${MBOOT_EL_HASH_ALG}, sha512) TPM_ALG_ID := TPM_ALG_SHA512 TCG_DIGEST_SIZE := 64U diff --git a/drivers/tpm/tpm2.mk b/drivers/tpm/tpm2.mk new file mode 100644 index 000000000..4418b97a9 --- /dev/null +++ b/drivers/tpm/tpm2.mk @@ -0,0 +1,26 @@ +# +# Copyright (c) 2025, Arm Limited. All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause +# + +TPM2_SRC_DIR := drivers/tpm/ + +TPM2_SOURCES := ${TPM2_SRC_DIR}tpm2_cmds.c \ + ${TPM2_SRC_DIR}tpm2_chip.c + +# TPM Hash algorithm, used during Measured Boot +# currently only accepts SHA-256 +ifeq (${MBOOT_TPM_HASH_ALG}, sha256) + TPM_ALG_ID := TPM_ALG_SHA256 + TCG_DIGEST_SIZE := 32U +else + $(error "The selected MBOOT_TPM_HASH_ALG is invalid.") +endif #MBOOT_TPM_HASH_ALG + +ifeq (${TPM_INTERFACE}, FIFO_SPI) + TPM2_SOURCES += ${TPM2_SRC_DIR}tpm2_fifo.c \ + ${TPM2_SRC_DIR}tpm2_fifo_spi.c +else + $(error "The selected TPM_INTERFACE is invalid.") +endif #TPM_INTERFACE diff --git a/drivers/tpm/tpm2_chip.c b/drivers/tpm/tpm2_chip.c new file mode 100644 index 000000000..537ce92a9 --- /dev/null +++ b/drivers/tpm/tpm2_chip.c @@ -0,0 +1,21 @@ + +/* + * Copyright (c) 2025, Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include + +/* + * TPM timeout values + * Reference: TCG PC Client Platform TPM Profile (PTP) Specification v1.05 + */ +struct tpm_chip_data tpm_chip_data = { + .locality = -1, + .timeout_msec_a = 750, + .timeout_msec_b = 2000, + .timeout_msec_c = 200, + .timeout_msec_d = 30, + .address = 0, +}; diff --git a/drivers/tpm/tpm2_cmds.c b/drivers/tpm/tpm2_cmds.c new file mode 100644 index 000000000..b6422a8db --- /dev/null +++ b/drivers/tpm/tpm2_cmds.c @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2025, Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +#include + +#include +#include +#include +#include + +#define CMD_SIZE_OFFSET 6 + +#define SINGLE_BYTE 1 +#define TWO_BYTES 2 +#define FOUR_BYTES 4 + +static struct interface_ops *interface; + +static int tpm_xfer(struct tpm_chip_data *chip_data, const tpm_cmd *send, tpm_cmd *receive) +{ + int ret; + + ret = interface->send(chip_data, send); + if (ret < 0) { + return ret; + } + + ret = interface->receive(chip_data, receive); + if (ret < 0) { + return ret; + } + + return TPM_SUCCESS; +} + +int tpm_interface_init(struct tpm_chip_data *chip_data, uint8_t locality) +{ + int err; + + interface = tpm_interface_getops(chip_data, locality); + + err = interface->request_access(chip_data, locality); + if (err != 0) { + return err; + } + + return interface->get_info(chip_data, locality); +} + +int tpm_interface_close(struct tpm_chip_data *chip_data, uint8_t locality) +{ + return interface->release_locality(chip_data, locality); +} + +static int tpm_update_buffer(tpm_cmd *buf, uint32_t new_data, size_t new_len) +{ + int i, j, start; + uint32_t command_size; + + union { + uint8_t var8; + uint16_t var16; + uint32_t var32; + uint8_t array[4]; + } tpm_new_data; + + command_size = be32toh(buf->header.cmd_size); + + if (command_size + new_len > MAX_SIZE_CMDBUF) { + ERROR("%s: buf size exceeded, increase MAX_SIZE_CMDBUF\n", + __func__); + return TPM_INVALID_PARAM; + } + /* + * Subtract the cmd header size from the current command size + * so the data buffer is written to starting at index 0. + */ + start = command_size - TPM_HEADER_SIZE; + + /* + * The TPM, according to the TCG spec, processes data in BE byte order, + * in the case where the Host is LE, htobe correctly handles the byte order. + * When updating the buffer, keep in mind to only pass sizeof(new_data) or + * the variable type size for the new_len function parameter. This ensures + * there is only the possiblility of writing 1, 2, or 4 bytes to the buffer, + * and that the correct number of bytes are written to data[i]. + */ + if (new_len == SINGLE_BYTE) { + tpm_new_data.var8 = new_data & 0xFF; + } else if (new_len == TWO_BYTES) { + tpm_new_data.var16 = htobe16(new_data & 0xFFFF); + } else if (new_len == FOUR_BYTES) { + tpm_new_data.var32 = htobe32(new_data); + } else { + ERROR("%s: Invalid data length\n", __func__); + return TPM_INVALID_PARAM; + } + + for (i = start, j = 0; i < start + new_len; i++, j++) { + buf->data[i] = tpm_new_data.array[j]; + } + buf->header.cmd_size = htobe32(command_size + new_len); + + return TPM_SUCCESS; +} + + +int tpm_startup(struct tpm_chip_data *chip_data, uint16_t mode) +{ + tpm_cmd startup_cmd, startup_response; + uint32_t tpm_rc; + int ret; + + memset(&startup_cmd, 0, sizeof(startup_cmd)); + memset(&startup_response, 0, sizeof(startup_response)); + + startup_cmd.header.tag = htobe16(TPM_ST_NO_SESSIONS); + startup_cmd.header.cmd_size = htobe32(sizeof(tpm_cmd_hdr)); + startup_cmd.header.cmd_code = htobe32(TPM_CMD_STARTUP); + + ret = tpm_update_buffer(&startup_cmd, mode, sizeof(mode)); + if (ret < 0) { + return ret; + } + + ret = tpm_xfer(chip_data, &startup_cmd, &startup_response); + if (ret < 0) { + return ret; + } + + tpm_rc = be32toh(startup_response.header.cmd_code); + if (tpm_rc != TPM_RESPONSE_SUCCESS) { + ERROR("%s: response code contains error = %X\n", __func__, tpm_rc); + return TPM_ERR_RESPONSE; + } + + return TPM_SUCCESS; +} + +int tpm_pcr_extend(struct tpm_chip_data *chip_data, uint32_t index, + uint16_t algorithm, const uint8_t *digest, + uint32_t digest_len) +{ + tpm_cmd pcr_extend_cmd, pcr_extend_response; + uint32_t tpm_rc; + int ret; + + memset(&pcr_extend_cmd, 0, sizeof(pcr_extend_cmd)); + memset(&pcr_extend_response, 0, sizeof(pcr_extend_response)); + + if (digest == NULL) { + return TPM_INVALID_PARAM; + } + pcr_extend_cmd.header.tag = htobe16(TPM_ST_SESSIONS); + pcr_extend_cmd.header.cmd_size = htobe32(sizeof(tpm_cmd_hdr)); + pcr_extend_cmd.header.cmd_code = htobe32(TPM_CMD_PCR_EXTEND); + + /* handle (PCR Index)*/ + ret = tpm_update_buffer(&pcr_extend_cmd, index, sizeof(index)); + if (ret < 0) { + return ret; + } + + /* authorization size , session handle, nonce size, attributes*/ + ret = tpm_update_buffer(&pcr_extend_cmd, TPM_MIN_AUTH_SIZE, sizeof(uint32_t)); + if (ret < 0) { + return ret; + } + ret = tpm_update_buffer(&pcr_extend_cmd, TPM_RS_PW, sizeof(uint32_t)); + if (ret < 0) { + return ret; + } + ret = tpm_update_buffer(&pcr_extend_cmd, TPM_ZERO_NONCE_SIZE, sizeof(uint16_t)); + if (ret < 0) { + return ret; + } + ret = tpm_update_buffer(&pcr_extend_cmd, TPM_ATTRIBUTES_DISABLE, sizeof(uint8_t)); + if (ret < 0) { + return ret; + } + + /* hmac/password size */ + ret = tpm_update_buffer(&pcr_extend_cmd, TPM_ZERO_HMAC_SIZE, sizeof(uint16_t)); + if (ret < 0) { + return ret; + } + + /* hashes count */ + ret = tpm_update_buffer(&pcr_extend_cmd, TPM_SINGLE_HASH_COUNT, sizeof(uint32_t)); + if (ret < 0) { + return ret; + } + + /* hash algorithm */ + ret = tpm_update_buffer(&pcr_extend_cmd, algorithm, sizeof(algorithm)); + if (ret < 0) { + return ret; + } + + /* digest */ + for (int i = 0; i < digest_len; i++) { + ret = tpm_update_buffer(&pcr_extend_cmd, digest[i], sizeof(uint8_t)); + if (ret < 0) { + return ret; + } + } + + ret = tpm_xfer(chip_data, &pcr_extend_cmd, &pcr_extend_response); + if (ret < 0) { + return ret; + } + + tpm_rc = be32toh(pcr_extend_response.header.cmd_code); + if (tpm_rc != TPM_RESPONSE_SUCCESS) { + ERROR("%s: response code contains error = %X\n", __func__, tpm_rc); + return TPM_ERR_RESPONSE; + } + + return TPM_SUCCESS; +} diff --git a/drivers/tpm/tpm2_fifo.c b/drivers/tpm/tpm2_fifo.c new file mode 100644 index 000000000..7c4b9d8b4 --- /dev/null +++ b/drivers/tpm/tpm2_fifo.c @@ -0,0 +1,322 @@ +/* + * Copyright (c) 2025, Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +#include + +#include +#include +#include +#include + +#define LOCALITY_START_ADDRESS(x, y) \ + ((uint16_t)(x->address + (0x1000 * y))) + +static int tpm2_get_info(struct tpm_chip_data *chip_data, uint8_t locality) +{ + uint16_t tpm_base_addr = LOCALITY_START_ADDRESS(chip_data, locality); + uint32_t vid_did; + uint8_t revision; + int err; + + err = tpm2_fifo_read_chunk(tpm_base_addr + TPM_FIFO_REG_VENDID, DWORD, &vid_did); + if (err < 0) { + return err; + } + + err = tpm2_fifo_read_byte(tpm_base_addr + TPM_FIFO_REG_REVID, &revision); + if (err < 0) { + return err; + } + + INFO("TPM Chip: vendor-id 0x%x, device-id 0x%x, revision-id: 0x%x\n", + 0xFFFF & vid_did, vid_did >> 16, revision); + + return TPM_SUCCESS; +} + +static int tpm2_wait_reg_bits(uint16_t reg, uint8_t set, unsigned long timeout, uint8_t *status) +{ + int err; + uint64_t timeout_delay = timeout_init_us(timeout * 1000); + + do { + err = tpm2_fifo_read_byte(reg, status); + if (err < 0) { + return err; + } + if ((*status & set) == set) { + return TPM_SUCCESS; + } + } while (!timeout_elapsed(timeout_delay)); + + return TPM_ERR_TIMEOUT; +} + +static int tpm2_fifo_request_access(struct tpm_chip_data *chip_data, uint8_t locality) +{ + uint16_t tpm_base_addr = LOCALITY_START_ADDRESS(chip_data, locality); + uint8_t status; + int err; + + err = tpm2_fifo_write_byte(tpm_base_addr + TPM_FIFO_REG_ACCESS, TPM_ACCESS_REQUEST_USE); + if (err < 0) { + return err; + } + + err = tpm2_wait_reg_bits(tpm_base_addr + TPM_FIFO_REG_ACCESS, + TPM_ACCESS_ACTIVE_LOCALITY, + chip_data->timeout_msec_a, &status); + if (err == 0) { + chip_data->locality = locality; + return TPM_SUCCESS; + } + + return err; +} + +static int tpm2_fifo_release_locality(struct tpm_chip_data *chip_data, uint8_t locality) +{ + uint16_t tpm_base_addr = LOCALITY_START_ADDRESS(chip_data, locality); + uint8_t buf; + int err; + + err = tpm2_fifo_read_byte(tpm_base_addr + TPM_FIFO_REG_ACCESS, &buf); + if (err < 0) { + return err; + } + + if (buf & (TPM_ACCESS_REQUEST_PENDING | TPM_ACCESS_VALID)) { + return tpm2_fifo_write_byte(tpm_base_addr + TPM_FIFO_REG_ACCESS, + TPM_ACCESS_RELINQUISH_LOCALITY); + } + + ERROR("%s: Unable to release locality\n", __func__); + return TPM_ERR_RESPONSE; +} + +static int tpm2_fifo_prepare(struct tpm_chip_data *chip_data) +{ + uint16_t tpm_base_addr = LOCALITY_START_ADDRESS(chip_data, chip_data->locality); + uint8_t status; + int err; + + err = tpm2_fifo_write_byte(tpm_base_addr + TPM_FIFO_REG_STATUS, TPM_STAT_COMMAND_READY); + if (err < 0) { + return err; + } + + err = tpm2_wait_reg_bits(tpm_base_addr + TPM_FIFO_REG_STATUS, + TPM_STAT_COMMAND_READY, + chip_data->timeout_msec_b, &status); + if (err < 0) { + ERROR("%s: TPM Status Busy\n", __func__); + return err; + } + + return TPM_SUCCESS; +} + +static int tpm2_fifo_get_burstcount(struct tpm_chip_data *chip_data, uint16_t *burstcount) +{ + uint16_t tpm_base_addr = LOCALITY_START_ADDRESS(chip_data, chip_data->locality); + uint64_t timeout_delay = timeout_init_us(chip_data->timeout_msec_a * 1000); + int err; + + if (burstcount == NULL) { + return TPM_INVALID_PARAM; + } + + do { + uint8_t byte0, byte1; + + err = tpm2_fifo_read_byte(tpm_base_addr + TPM_FIFO_REG_BURST_COUNT_LO, &byte0); + if (err < 0) { + return err; + } + + err = tpm2_fifo_read_byte(tpm_base_addr + TPM_FIFO_REG_BURST_COUNT_HI, &byte1); + if (err < 0) { + return err; + } + + *burstcount = (uint16_t)((byte1 << 8) + byte0); + if (*burstcount != 0U) { + return TPM_SUCCESS; + } + } while (!timeout_elapsed(timeout_delay)); + + return TPM_ERR_TIMEOUT; +} + +static int tpm2_fifo_send(struct tpm_chip_data *chip_data, const tpm_cmd *buf) +{ + uint16_t tpm_base_addr = LOCALITY_START_ADDRESS(chip_data, chip_data->locality); + uint8_t status; + uint16_t burstcnt; + int err; + uint32_t len, index; + + if (sizeof(buf->header) != TPM_HEADER_SIZE) { + ERROR("%s: invalid command header size.\n", __func__); + return TPM_INVALID_PARAM; + } + + err = tpm2_fifo_read_byte(tpm_base_addr + TPM_FIFO_REG_STATUS, &status); + if (err < 0) { + return err; + } + + if (!(status & TPM_STAT_COMMAND_READY)) { + err = tpm2_fifo_prepare(chip_data); + if (err < 0) { + return err; + } + } + + /* write the command header to the TPM first */ + const uint8_t *header_data = (const uint8_t *)&buf->header; + + for (index = 0; index < TPM_HEADER_SIZE; index++) { + err = tpm2_fifo_write_byte(tpm_base_addr + TPM_FIFO_REG_DATA_FIFO, + header_data[index]); + if (err < 0) { + return err; + } + } + + len = be32toh(buf->header.cmd_size); + + while (index < len) { + err = tpm2_fifo_get_burstcount(chip_data, &burstcnt); + if (err < 0) { + return err; + } + + for (; burstcnt > 0U && index < len; burstcnt--) { + err = tpm2_fifo_write_byte(tpm_base_addr + TPM_FIFO_REG_DATA_FIFO, + buf->data[index - TPM_HEADER_SIZE]); + if (err < 0) { + return err; + } + index++; + } + } + + err = tpm2_wait_reg_bits(tpm_base_addr + TPM_FIFO_REG_STATUS, + TPM_STAT_VALID, + chip_data->timeout_msec_c, + &status); + if (err < 0) { + return err; + } + + if (status & TPM_STAT_EXPECT) { + ERROR("%s: TPM is still expecting data after command buffer is sent\n", __func__); + return TPM_ERR_TRANSFER; + } + + err = tpm2_fifo_write_byte(tpm_base_addr + TPM_FIFO_REG_STATUS, TPM_STAT_GO); + if (err < 0) { + return err; + } + + return TPM_SUCCESS; +} + +static int tpm2_fifo_read_data(struct tpm_chip_data *chip_data, tpm_cmd *buf, + uint16_t tpm_base_addr, uint8_t *status, int *size, int bytes_expected) +{ + int err, read_size, loop_index; + uint16_t burstcnt; + uint8_t *read_data; + + if (bytes_expected == TPM_READ_HEADER) { + /* read the response header from the TPM first */ + read_data = (uint8_t *)&buf->header; + read_size = TPM_HEADER_SIZE; + loop_index = *size; + } else { + /* process the rest of the mssg with bytes_expected */ + read_data = buf->data; + read_size = bytes_expected; + loop_index = *size - TPM_HEADER_SIZE; + } + + err = tpm2_wait_reg_bits(tpm_base_addr + TPM_FIFO_REG_STATUS, + TPM_STAT_AVAIL, + chip_data->timeout_msec_c, + status); + if (err < 0) { + return err; + } + + while (*size < read_size) { + err = tpm2_fifo_get_burstcount(chip_data, &burstcnt); + if (err < 0) { + ERROR("%s: TPM burst count error\n", __func__); + return err; + } + + for (; burstcnt > 0U && loop_index < read_size; + burstcnt--, loop_index++, (*size)++) { + err = tpm2_fifo_read_byte( + tpm_base_addr + TPM_FIFO_REG_DATA_FIFO, + (void *)&read_data[loop_index]); + if (err < 0) { + return err; + } + } + } + + return TPM_SUCCESS; +} + +static int tpm2_fifo_receive(struct tpm_chip_data *chip_data, tpm_cmd *buf) +{ + uint16_t tpm_base_addr = LOCALITY_START_ADDRESS(chip_data, chip_data->locality); + int size = 0, bytes_expected, err; + uint8_t status; + + err = tpm2_fifo_read_data(chip_data, buf, tpm_base_addr, &status, &size, TPM_READ_HEADER); + if (err < 0) { + return err; + } + + bytes_expected = be32toh(buf->header.cmd_size); + if (bytes_expected > sizeof(*buf)) { + ERROR("%s: tpm response buffer cannot store expected response\n", __func__); + return TPM_INVALID_PARAM; + } + + if (size == bytes_expected) { + return size; + } + + err = tpm2_fifo_read_data(chip_data, buf, tpm_base_addr, &status, &size, bytes_expected); + if (err < 0) { + return err; + } + + if (size < bytes_expected) { + ERROR("%s: response buffer size is less than expected\n", __func__); + return TPM_ERR_RESPONSE; + } + + return TPM_SUCCESS; +} + +static interface_ops_t fifo_ops = { + .get_info = tpm2_get_info, + .send = tpm2_fifo_send, + .receive = tpm2_fifo_receive, + .request_access = tpm2_fifo_request_access, + .release_locality = tpm2_fifo_release_locality, +}; + +struct interface_ops * +tpm_interface_getops(struct tpm_chip_data *chip_data, uint8_t locality) +{ + return &fifo_ops; +} diff --git a/drivers/tpm/tpm2_fifo_spi.c b/drivers/tpm/tpm2_fifo_spi.c new file mode 100644 index 000000000..16d87d918 --- /dev/null +++ b/drivers/tpm/tpm2_fifo_spi.c @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2025, Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#define ENCODE_LIMIT 128 +#define CS_ASSERT_OFFSET 0xD4 +#define RETRY_COUNT 50 + +#define TPM_READ false +#define TPM_WRITE true + +extern struct spi_plat *spidev; + +static int tpm2_spi_transfer(const void *data_out, void *data_in, uint8_t len) +{ + return spidev->ops->xfer(len, data_out, data_in); +} + +/* + * Reference: TCG PC Client Platform TPM Profile (PTP) Specification v1.05 + */ +static int tpm2_spi_start_transaction(uint16_t tpm_reg, bool write, uint8_t len) +{ + int rc; + uint8_t header[4]; + uint8_t header_response[4]; + uint8_t zero = 0, byte; + int retries; + + /* check to make sure len does not exceed the encoding limit */ + if (len > ENCODE_LIMIT) { + return TPM_INVALID_PARAM; + } + + /* + * 7.4.6 TPM SPI Bit protocol calls for the following header + * to be sent to the TPM at the start of every attempted read/write. + */ + + /* header[0] contains the r/w and the xfer size, if the msb is not + * set, the operation is write, if it is set then it is read. + * The size of the transfer is encoded, and must not overwrite + * the msb, therefore an ENCODE LIMIT of 128 is present. + */ + header[0] = ((write) ? 0x00 : 0x80) | (len - 1); + + /* + * header[1] contains the address offset 0xD4_xxxx as defined + * in the TPM spec, since the CS# is asserted. + */ + header[1] = CS_ASSERT_OFFSET; + + /* + * header[2] and header[3] contain the address of the register + * to be read/written. + */ + header[2] = tpm_reg >> 8; + header[3] = tpm_reg; + + rc = tpm2_spi_transfer(header, header_response, 4); + if (rc != 0) { + return TPM_ERR_TRANSFER; + } + + /* + * 7.4.5 Flow Control defines a wait state in order to accommodate + * the TPM in case it needs to free its buffer. + */ + if ((header_response[3] & 0x01) != 0U) { + return TPM_SUCCESS; + } + + /* + * if the wait state over bit is not set in the initial header_response, + * poll for the wait state over by sending a zeroed byte, if the + * RETRY_COUNT is exceeded the transfer fails. + */ + for (retries = RETRY_COUNT; retries > 0; retries--) { + rc = tpm2_spi_transfer(&zero, &byte, 1); + if (rc != 0) { + return TPM_ERR_TRANSFER; + } + if ((byte & 0x01) != 0U) { + return TPM_SUCCESS; + } + } + + if (retries == 0) { + ERROR("%s: TPM Timeout\n", __func__); + return TPM_ERR_TIMEOUT; + } + + return TPM_SUCCESS; +} + +static void tpm2_spi_end_transaction(void) +{ + spidev->ops->stop(); +} + +static void tpm2_spi_init(void) +{ + spidev->ops->get_access(); + spidev->ops->start(); +} + +static int tpm2_fifo_io(uint16_t tpm_reg, bool is_write, uint8_t len, void *val) +{ + int rc; + + tpm2_spi_init(); + rc = tpm2_spi_start_transaction(tpm_reg, is_write, len); + if (rc != 0) { + tpm2_spi_end_transaction(); + return rc; + } + + rc = tpm2_spi_transfer( + is_write ? val : NULL, + is_write ? NULL : val, + len); + if (rc != 0) { + tpm2_spi_end_transaction(); + return rc; + } + + tpm2_spi_end_transaction(); + + return TPM_SUCCESS; +} + +int tpm2_fifo_write_byte(uint16_t tpm_reg, uint8_t val) +{ + return tpm2_fifo_io(tpm_reg, TPM_WRITE, BYTE, &val); +} + +int tpm2_fifo_read_byte(uint16_t tpm_reg, uint8_t *val) +{ + return tpm2_fifo_io(tpm_reg, TPM_READ, BYTE, val); +} + +int tpm2_fifo_read_chunk(uint16_t tpm_reg, uint8_t len, void *val) +{ + if ((len != BYTE) && (len != WORD) && (len != DWORD)) { + return TPM_INVALID_PARAM; + } + + return tpm2_fifo_io(tpm_reg, TPM_READ, len, val); +} diff --git a/include/drivers/tpm/tpm2.h b/include/drivers/tpm/tpm2.h new file mode 100644 index 000000000..c91acf8e9 --- /dev/null +++ b/include/drivers/tpm/tpm2.h @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2025, Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef TPM2_H +#define TPM2_H + +#include +#include +#include +#include + +#include + +/* Return values */ +enum tpm_ret_value { + TPM_SUCCESS = 0, + TPM_ERR_RESPONSE = -1, + TPM_INVALID_PARAM = -2, + TPM_ERR_TIMEOUT = -3, + TPM_ERR_TRANSFER = -4, +}; + +/* + * TPM FIFO register space address offsets + */ +#define TPM_FIFO_REG_ACCESS 0x00 +#define TPM_FIFO_REG_INTR_ENABLE 0x08 +#define TPM_FIFO_REG_INTR_VECTOR 0x0C +#define TPM_FIFO_REG_INTR_STS 0x10 +#define TPM_FIFO_REG_INTF_CAPS 0x14 +#define TPM_FIFO_REG_STATUS 0x18 +#define TPM_FIFO_REG_BURST_COUNT_LO 0x19 +#define TPM_FIFO_REG_BURST_COUNT_HI 0x20 +#define TPM_FIFO_REG_DATA_FIFO 0x24 +#define TPM_FIFO_REG_VENDID 0xF00 +#define TPM_FIFO_REG_DEVID 0xF02 +#define TPM_FIFO_REG_REVID 0xF04 + +#define TPM_ST_NO_SESSIONS U(0x8001) +#define TPM_ST_SESSIONS U(0x8002) + +#define TPM_SU_CLEAR U(0x0000) +#define TPM_SU_STATE U(0x0001) + +#define TPM_MIN_AUTH_SIZE 9 +#define TPM_RS_PW 0x40000009 +#define TPM_ZERO_NONCE_SIZE 0 +#define TPM_ATTRIBUTES_DISABLE 0 +#define TPM_ZERO_HMAC_SIZE 0 +#define TPM_SINGLE_HASH_COUNT 1 + + +#define TPM_CMD_STARTUP U(0x0144) +#define TPM_CMD_PCR_READ U(0x017E) +#define TPM_CMD_PCR_EXTEND U(0x0182) + +#define TPM_RESPONSE_SUCCESS U(0x0000) + +#define TPM_ACCESS_ACTIVE_LOCALITY U(1 << 5) +#define TPM_ACCESS_VALID U(1 << 7) +#define TPM_ACCESS_RELINQUISH_LOCALITY U(1 << 5) +#define TPM_ACCESS_REQUEST_USE U(1 << 1) +#define TPM_ACCESS_REQUEST_PENDING U(1 << 2) + +#define TPM_STAT_VALID U(1 << 7) +#define TPM_STAT_COMMAND_READY U(1 << 6) +#define TPM_STAT_GO U(1 << 5) +#define TPM_STAT_AVAIL U(1 << 4) +#define TPM_STAT_EXPECT U(1 << 3) + +#define TPM_READ_HEADER -1 + +#define TPM_HEADER_SIZE 10 +#define MAX_SIZE_CMDBUF 256 +#define MAX_CMD_DATA (MAX_SIZE_CMDBUF - TPM_HEADER_SIZE) + +#pragma pack(1) +typedef struct tpm_cmd_hdr { + uint16_t tag; + uint32_t cmd_size; + uint32_t cmd_code; +} tpm_cmd_hdr; + +typedef struct tpm_cmd { + tpm_cmd_hdr header; + uint8_t data[MAX_CMD_DATA]; +} tpm_cmd; +#pragma pack() + +int tpm_interface_init(struct tpm_chip_data *chip_data, uint8_t locality); + +int tpm_interface_close(struct tpm_chip_data *chip_data, uint8_t locality); + +int tpm_startup(struct tpm_chip_data *chip_data, uint16_t mode); + +int tpm_pcr_extend(struct tpm_chip_data *chip_data, uint32_t index, + uint16_t algorithm, const uint8_t *digest, + uint32_t digest_len); + +#endif /* TPM2_H */ diff --git a/include/drivers/tpm/tpm2_chip.h b/include/drivers/tpm/tpm2_chip.h new file mode 100644 index 000000000..ce052ad98 --- /dev/null +++ b/include/drivers/tpm/tpm2_chip.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025, Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include +#include + +#ifndef TPM2_CHIP_H +#define TPM2_CHIP_H + +#define BYTE U(0x1) +#define WORD U(0x2) +#define DWORD U(0x4) + +struct tpm_chip_data { + uint8_t locality; + unsigned long timeout_msec_a, timeout_msec_b; + unsigned long timeout_msec_c, timeout_msec_d; + uint16_t address; +}; + +#endif /* TPM2_CHIP_H */ diff --git a/include/drivers/tpm/tpm2_interface.h b/include/drivers/tpm/tpm2_interface.h new file mode 100644 index 000000000..6bfbf6ced --- /dev/null +++ b/include/drivers/tpm/tpm2_interface.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025, Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef TPM2_INTERFACE_H +#define TPM2_INTERFACE_H + +#include "tpm2_chip.h" + +typedef struct interface_ops { + int (*get_info)(struct tpm_chip_data *chip_data, uint8_t locality); + int (*send)(struct tpm_chip_data *chip_data, const tpm_cmd *buf); + int (*receive)(struct tpm_chip_data *chip_data, tpm_cmd *buf); + int (*request_access)(struct tpm_chip_data *chip_data, uint8_t locality); + int (*release_locality)(struct tpm_chip_data *chip_data, uint8_t locality); +} interface_ops_t; + +struct interface_ops *tpm_interface_getops(struct tpm_chip_data *chip_data, uint8_t locality); + +int tpm2_fifo_write_byte(uint16_t tpm_reg, uint8_t val); + +int tpm2_fifo_read_byte(uint16_t tpm_reg, uint8_t *val); + +int tpm2_fifo_read_chunk(uint16_t tpm_reg, uint8_t len, void *val); + +#endif /* TPM2_INTERFACE_H */ diff --git a/make_helpers/defaults.mk b/make_helpers/defaults.mk index ec2aa1bfb..906e5d706 100644 --- a/make_helpers/defaults.mk +++ b/make_helpers/defaults.mk @@ -192,6 +192,9 @@ endif # Option to build TF with Measured Boot support MEASURED_BOOT := 0 +# Option to build TF with Discrete TPM support +DISCRETE_TPM := 0 + # Option to enable the DICE Protection Environmnet as a Measured Boot backend DICE_PROTECTION_ENVIRONMENT :=0 From 6fa56e93679631b0d23c88e962b9e40c97971942 Mon Sep 17 00:00:00 2001 From: Abhi Singh Date: Tue, 3 Dec 2024 13:19:34 -0600 Subject: [PATCH 5/9] feat(tpm): add Infineon SLB9670 GPIO SPI config add the Infineon Optiga SLB9670 TPM2.0 GPIO SPI configuration data, as well as chip reset and the GPIO SPI bitbang driver initialization. This code supports use with the rpi3 platform, with availibility to add configuration parameters for other platforms Change-Id: Ibdffb28fa0b3b5a18dff2ba5d4ea305633740763 Signed-off-by: Abhi Singh --- drivers/tpm/tpm2_slb9670/slb9670_gpio.c | 75 +++++++++++++++++++ .../drivers/tpm/tpm2_slb9670/slb9670_gpio.h | 16 ++++ 2 files changed, 91 insertions(+) create mode 100644 drivers/tpm/tpm2_slb9670/slb9670_gpio.c create mode 100644 include/drivers/tpm/tpm2_slb9670/slb9670_gpio.h diff --git a/drivers/tpm/tpm2_slb9670/slb9670_gpio.c b/drivers/tpm/tpm2_slb9670/slb9670_gpio.c new file mode 100644 index 000000000..993387da4 --- /dev/null +++ b/drivers/tpm/tpm2_slb9670/slb9670_gpio.c @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2025, Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include +#include +#include + +/* + * Infineon SLB9670 Chip Reset Parameters + */ +#define t_WRST 2 /* Warm Reset Time (us) */ +#define t_RSTIN 60 /* Reset Inactive Time (ms) */ + +/* + * RPi3 GPIO pin configuration for TPM via bit-bang SPI + * References: https://pinout.xyz/pinout/spi + * - docs/design_documents/measured_boot_dtpm_poc.rst + */ +const struct gpio_spi_data tpm_rpi3_gpio_data = { + .cs_gpio = 7, + .sclk_gpio = 11, + .mosi_gpio = 10, + .miso_gpio = 9, + .reset_gpio = 24, + .spi_delay_us = 0, + .spi_mode = 0 +}; + +/* + * When RST is asserted at certain points in time, then this + * triggers the TPM's security functions, in the case where + * multiple resets need to be asserted, there must be a wait + * of at least t_RSTIN between the resets + * + * In most cases this is not needed since RST is only being asserted + * once, ie for TPM initialization at the beginning of TFA. + */ +void tpm2_slb9670_reset_chip(struct gpio_spi_data *tpm_gpio_data) +{ + /* + * since we don't know the value of the pin before it was init to 1 + * it is best to assume the state was 0, and account for that by + * adding an initial RST inactive delay + */ + mdelay(t_RSTIN); + /* pull #RST pin to active low for 2us */ + gpio_set_value(tpm_gpio_data->reset_gpio, 0); + udelay(t_WRST); + /* wait 60ms after warm reset before sending TPM commands */ + gpio_set_value(tpm_gpio_data->reset_gpio, 1); + mdelay(t_RSTIN); +} + +/* + * init GPIO pins for the Infineon slb9670 TPM + */ +void tpm2_slb9670_gpio_init(struct gpio_spi_data *tpm_gpio_data) +{ + gpio_set_value(tpm_gpio_data->cs_gpio, 1); + gpio_set_direction(tpm_gpio_data->cs_gpio, GPIO_DIR_OUT); + + gpio_set_value(tpm_gpio_data->sclk_gpio, 0); + gpio_set_direction(tpm_gpio_data->sclk_gpio, GPIO_DIR_OUT); + + gpio_set_value(tpm_gpio_data->mosi_gpio, 1); + gpio_set_direction(tpm_gpio_data->mosi_gpio, GPIO_DIR_OUT); + + gpio_set_direction(tpm_gpio_data->miso_gpio, GPIO_DIR_IN); + + gpio_set_value(tpm_gpio_data->reset_gpio, 1); + gpio_set_direction(tpm_gpio_data->reset_gpio, GPIO_DIR_OUT); +} diff --git a/include/drivers/tpm/tpm2_slb9670/slb9670_gpio.h b/include/drivers/tpm/tpm2_slb9670/slb9670_gpio.h new file mode 100644 index 000000000..59ae125ad --- /dev/null +++ b/include/drivers/tpm/tpm2_slb9670/slb9670_gpio.h @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2025, Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include "drivers/gpio_spi.h" + +#ifndef SLB9670_GPIO_H +#define SLB9670_GPIO_H + +void tpm2_slb9670_reset_chip(struct gpio_spi_data *tpm_gpio_data); + +void tpm2_slb9670_gpio_init(struct gpio_spi_data *tpm_gpio_data); + +#endif /* SLB9670_GPIO_H */ From 4f9894db3572b6e375c7369bc2619cc690169604 Mon Sep 17 00:00:00 2001 From: Abhi Singh Date: Thu, 7 Nov 2024 17:39:38 -0600 Subject: [PATCH 6/9] feat(rpi3): add dTPM backed measured boot In BL1 and BL2 add support for the use of an Infineon Optiga SLB 9670 TPM2.0. The platform utilizes the gpio_spi.c driver to bit-bang gpio pins in order to send commands and receive responses to/from the TPM. In BL1 & BL2: -utilize TPM commands to initialize the gpio pins for "spi" communication, and extend image hashes to the TPM's PCR 0, at the end of the measured boot phase for the bootloader, the TPM locality is released. -Bl1 executes a tpm_startup command in order to flush the TPM. Change-Id: I2f2fa28f60a262a0aa25a674c72a9904b3cf4d8a Signed-off-by: Tushar Khandelwal Signed-off-by: Abhi Singh --- plat/rpi/rpi3/platform.mk | 14 +++++++- plat/rpi/rpi3/rpi3_bl1_mboot.c | 60 ++++++++++++++++++++++++++++++++++ plat/rpi/rpi3/rpi3_bl2_mboot.c | 49 +++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) diff --git a/plat/rpi/rpi3/platform.mk b/plat/rpi/rpi3/platform.mk index 2072877ea..5297177b4 100644 --- a/plat/rpi/rpi3/platform.mk +++ b/plat/rpi/rpi3/platform.mk @@ -21,12 +21,24 @@ PLAT_BL_COMMON_SOURCES := drivers/ti/uart/aarch64/16550_console.S \ plat/rpi/common/rpi3_console_dual.c \ ${XLAT_TABLES_LIB_SRCS} +ifeq (${DISCRETE_TPM},1) +TPM2_MK := drivers/tpm/tpm2.mk +$(info Including ${TPM2_MK}) +include ${TPM2_MK} +endif + +ifeq (${TPM_INTERFACE},FIFO_SPI) +PLAT_BL_COMMON_SOURCES += drivers/gpio/gpio_spi.c \ + drivers/tpm/tpm2_slb9670/slb9670_gpio.c +endif + ifeq (${MEASURED_BOOT},1) MEASURED_BOOT_MK := drivers/measured_boot/event_log/event_log.mk $(info Including ${MEASURED_BOOT_MK}) include ${MEASURED_BOOT_MK} -PLAT_BL_COMMON_SOURCES += ${EVENT_LOG_SOURCES} +PLAT_BL_COMMON_SOURCES += $(TPM2_SOURCES) \ + ${EVENT_LOG_SOURCES} BL1_SOURCES += plat/rpi/rpi3/rpi3_bl1_mboot.c BL2_SOURCES += plat/rpi/rpi3/rpi3_bl2_mboot.c \ diff --git a/plat/rpi/rpi3/rpi3_bl1_mboot.c b/plat/rpi/rpi3/rpi3_bl1_mboot.c index 4f6b52a18..42943658e 100644 --- a/plat/rpi/rpi3/rpi3_bl1_mboot.c +++ b/plat/rpi/rpi3/rpi3_bl1_mboot.c @@ -11,12 +11,18 @@ #include #include #include +#include #include #include +#include +#include +#include #include #include #include +#include + /* Event Log data */ uint8_t event_log[PLAT_ARM_EVENT_LOG_MAX_SIZE]; @@ -29,8 +35,43 @@ const event_log_metadata_t rpi3_event_log_metadata[] = { { EVLOG_INVALID_ID, NULL, (unsigned int)(-1) } /* Terminator */ }; +#if DISCRETE_TPM +extern struct tpm_chip_data tpm_chip_data; +#if (TPM_INTERFACE == FIFO_SPI) +extern struct gpio_spi_data tpm_rpi3_gpio_data; +struct spi_plat *spidev; +#endif + +static void rpi3_bl1_tpm_early_interface_setup(void) +{ +#if (TPM_INTERFACE == FIFO_SPI) + tpm2_slb9670_gpio_init(&tpm_rpi3_gpio_data); + + tpm2_slb9670_reset_chip(&tpm_rpi3_gpio_data); + + spidev = gpio_spi_init(&tpm_rpi3_gpio_data); +#endif +} +#endif + void bl1_plat_mboot_init(void) { +#if DISCRETE_TPM + int rc; + + rpi3_bl1_tpm_early_interface_setup(); + rc = tpm_interface_init(&tpm_chip_data, 0); + if (rc != 0) { + ERROR("BL1: TPM interface init failed\n"); + panic(); + } + rc = tpm_startup(&tpm_chip_data, TPM_SU_CLEAR); + if (rc != 0) { + ERROR("BL1: TPM Startup failed\n"); + panic(); + } +#endif + event_log_init(event_log, event_log + sizeof(event_log)); event_log_write_header(); } @@ -49,6 +90,17 @@ void bl1_plat_mboot_finish(void) ep_info = &image_desc->ep_info; ep_info->args.arg2 = (uint64_t) event_log; ep_info->args.arg3 = (uint32_t) event_log_cur_size; + +#if DISCRETE_TPM + int rc; + + /* relinquish control of TPM locality 0 and close interface */ + rc = tpm_interface_close(&tpm_chip_data, 0); + if (rc != 0) { + ERROR("BL1: TPM interface close failed\n"); + panic(); + } +#endif } int plat_mboot_measure_image(unsigned int image_id, image_info_t *image_data) @@ -62,6 +114,14 @@ int plat_mboot_measure_image(unsigned int image_id, image_info_t *image_data) return rc; } +#if DISCRETE_TPM + rc = tpm_pcr_extend(&tpm_chip_data, 0, TPM_ALG_ID, hash_data, TCG_DIGEST_SIZE); + if (rc != 0) { + ERROR("BL1: TPM PCR-0 extend failed\n"); + panic(); + } +#endif + while ((metadata_ptr->id != EVLOG_INVALID_ID) && (metadata_ptr->id != image_id)) { metadata_ptr++; diff --git a/plat/rpi/rpi3/rpi3_bl2_mboot.c b/plat/rpi/rpi3/rpi3_bl2_mboot.c index 3abc5b80d..55c69232e 100644 --- a/plat/rpi/rpi3/rpi3_bl2_mboot.c +++ b/plat/rpi/rpi3/rpi3_bl2_mboot.c @@ -11,8 +11,12 @@ #include "./include/rpi3_measured_boot.h" #include +#include #include #include +#include +#include +#include #include #include #include @@ -27,6 +31,23 @@ const event_log_metadata_t rpi3_event_log_metadata[] = { { EVLOG_INVALID_ID, NULL, (unsigned int)(-1) } /* Terminator */ }; +#if DISCRETE_TPM +extern struct tpm_chip_data tpm_chip_data; +#if (TPM_INTERFACE == FIFO_SPI) +extern struct gpio_spi_data tpm_rpi3_gpio_data; +struct spi_plat *spidev; +#endif + +static void rpi3_bl2_tpm_early_interface_setup(void) +{ +#if (TPM_INTERFACE == FIFO_SPI) + tpm2_slb9670_gpio_init(&tpm_rpi3_gpio_data); + + spidev = gpio_spi_init(&tpm_rpi3_gpio_data); +#endif +} +#endif + static uint8_t *event_log_start; static size_t event_log_size; @@ -35,6 +56,17 @@ void bl2_plat_mboot_init(void) uint8_t *bl2_event_log_start; uint8_t *bl2_event_log_finish; +#if DISCRETE_TPM + int rc; + + rpi3_bl2_tpm_early_interface_setup(); + rc = tpm_interface_init(&tpm_chip_data, 0); + if (rc != 0) { + ERROR("BL2: TPM interface init failed\n"); + panic(); + } +#endif + rpi3_mboot_fetch_eventlog_info(&event_log_start, &event_log_size); bl2_event_log_start = event_log_start + event_log_size; bl2_event_log_finish = event_log_start + PLAT_ARM_EVENT_LOG_MAX_SIZE; @@ -75,6 +107,15 @@ void bl2_plat_mboot_finish(void) /* Dump Event Log for user view */ dump_event_log((uint8_t *)event_log_start, event_log_cur_size); + +#if DISCRETE_TPM + /* relinquish control of TPM locality 0 and close interface */ + rc = tpm_interface_close(&tpm_chip_data, 0); + if (rc != 0) { + ERROR("BL2: TPM interface close failed\n"); + panic(); + } +#endif } int plat_mboot_measure_image(unsigned int image_id, image_info_t *image_data) @@ -90,6 +131,14 @@ int plat_mboot_measure_image(unsigned int image_id, image_info_t *image_data) return rc; } +#if DISCRETE_TPM + rc = tpm_pcr_extend(&tpm_chip_data, 0, TPM_ALG_ID, hash_data, TCG_DIGEST_SIZE); + if (rc != 0) { + ERROR("BL2: TPM PCR-0 extend failed\n"); + panic(); + } +#endif + while ((metadata_ptr->id != EVLOG_INVALID_ID) && (metadata_ptr->id != image_id)) { metadata_ptr++; From 9acaaded3ca9b6966efb72e3a989f8ee753b3a44 Mon Sep 17 00:00:00 2001 From: Abhi Singh Date: Thu, 7 Nov 2024 17:41:58 -0600 Subject: [PATCH 7/9] fix(rpi3): expose BL1_RW to BL2 map for mboot BL2 requires the ability to access the TCG Event Log during Measured Boot. Currently the Platform hangs since the Event Log is not exposed to BL2's mmap. Define a RPI3_BL1_RW region to be added to the BL2 Image, if Measured Boot is enabled. Change-Id: Ic236a80e73ea342b4590cfb65bafbb8ffac17085 Signed-off-by: Abhi Singh --- plat/rpi/common/rpi3_common.c | 3 +++ plat/rpi/rpi3/include/platform_def.h | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/plat/rpi/common/rpi3_common.c b/plat/rpi/common/rpi3_common.c index 4e3c9f233..490835402 100644 --- a/plat/rpi/common/rpi3_common.c +++ b/plat/rpi/common/rpi3_common.c @@ -75,6 +75,9 @@ static const mmap_region_t plat_rpi3_mmap[] = { #endif MAP_DEVICE0, MAP_FIP, +#if MEASURED_BOOT + RPI3_MAP_BL1_RW, +#endif MAP_NS_DRAM0, #ifdef BL32_BASE MAP_BL32_MEM, diff --git a/plat/rpi/rpi3/include/platform_def.h b/plat/rpi/rpi3/include/platform_def.h index 37aceafc7..eb2914ab4 100644 --- a/plat/rpi/rpi3/include/platform_def.h +++ b/plat/rpi/rpi3/include/platform_def.h @@ -171,6 +171,14 @@ #define BL1_RW_BASE (BL1_RW_LIMIT - PLAT_MAX_BL1_RW_SIZE) #define BL1_RW_LIMIT (BL_RAM_BASE + BL_RAM_SIZE) +/* + * In order to access the TCG Event Log in BL2, we need to expose the BL1_RW region + * where the log resides. + */ +#define RPI3_MAP_BL1_RW MAP_REGION_FLAT(BL1_RW_BASE, \ + BL1_RW_LIMIT - BL1_RW_BASE, \ + MT_MEMORY | MT_RW | MT_SECURE) + /* * BL2 specific defines. * From a2dd13cacbb34e3c0bd25dfb6c34a1479763e65c Mon Sep 17 00:00:00 2001 From: Abhi Singh Date: Mon, 21 Oct 2024 13:21:42 -0500 Subject: [PATCH 8/9] docs(tpm): add design documentation for dTPM -documentation for Discrete TPM drivers. -documentation for a proof of concept on rpi3; Measured Boot using Discrete TPM. Signed-off-by: Abhi Singh Change-Id: If8e7c14a1c0b9776af872104aceeff21a13bd821 --- docs/design_documents/dtpm_drivers.rst | 119 +++++ docs/design_documents/index.rst | 2 + docs/design_documents/measured_boot.rst | 10 +- .../measured_boot_dtpm_poc.rst | 458 ++++++++++++++++++ docs/getting_started/build-options.rst | 14 + docs/global_substitutions.txt | 4 + docs/glossary.rst | 12 + docs/resources/diagrams/rpi3_dtpm_driver.png | Bin 0 -> 57302 bytes 8 files changed, 618 insertions(+), 1 deletion(-) create mode 100644 docs/design_documents/dtpm_drivers.rst create mode 100644 docs/design_documents/measured_boot_dtpm_poc.rst create mode 100644 docs/resources/diagrams/rpi3_dtpm_driver.png diff --git a/docs/design_documents/dtpm_drivers.rst b/docs/design_documents/dtpm_drivers.rst new file mode 100644 index 000000000..324af03cf --- /dev/null +++ b/docs/design_documents/dtpm_drivers.rst @@ -0,0 +1,119 @@ +Discrete TPM drivers +==================== + +This section focuses on the design and functionality of Discrete TPM drivers +in |TF-A|. The |TPM| technology is designed to provide +a dedicated, hardware-based solution for storing cryptographic keys and +performing security-related operations. + +Discrete TPMs are separate, standalone hardware components that are physically +isolated from the system's main processor. This isolation helps protect +sensitive information, such as encryption keys and platform credentials, from +being accessed or tampered with by malicious software or unauthorized users. +When a Discrete TPM interface is implemented correctly, the risk of software +based attacks is reduced, further reducing the attack surface. + +TPM measurements establish the security posture of a system and are used for +attestation. Performing measurements using a TPM in TF-A is beneficial from +a security standpoint because it ensures hardware-backed attestation earlier +in the boot flow, reducing the risk of a compromised root of trust. + +The design implemented in TF-A supports multiple types of TPM hardware interfaces +and hardware bus types in order to be compatible with different platforms. +Platforms opt to use a specific messaging interface, such as |CRB| or |FIFO|, +and a specific hardware bus interface, such as |I2C| or |SPI|. + +Driver architecture +------------------- + +The Discrete TPM drivers are split up into four layers, each serving a distinct +purpose in the overall architecture: + + - **Command Layer**: This layer provides various TPM commands based on the + `TCG TPM 2.0 Library Specification`_. It allows a system to initialize the + TPM interface, perform a TPM startup, set up a locality for operations like + PCR extend and read, and release the locality when finished. + - **Interface Layer**: This layer handles sending and receiving TPM commands + via a specific TPM interface like FIFO or CRB. It also includes functions + such as getting information, requesting access, and relinquishing access, + tailored to the specific interface. + - **Link Layer**: Discrete TPMs may appear as a SPI, I2C, or memory mapped + device. The link layer maps the command passed from the interface layer to + the appropriate bus type. It includes hardware link read and write functions + that use the platform bus interface to transfer commands. + - **Platform Layer**: The platform layer implements the details of how to + communicate to the TPM chip for a specific platform. The link layer uses the + platform layer to read and write to the TPM. + + .. note:: + The command, interface, and link layers are implemented in common code in + TF-A. The platform layer is implemented in platform specific code. + +The following diagram illustrates the Discrete TPM driver stack for the Raspberry +Pi 3 platform. + +|rpi3 dtpm driver stack| + +Header files +^^^^^^^^^^^^ +- TPM Drivers: ``include/drivers/tpm`` + + +Source files +^^^^^^^^^^^^ +- TPM Drivers: ``drivers/tpm`` + + +Build time config options +------------------------- + +- ``MBOOT_TPM_HASH_ALG``: The hash algorithm to be used by the TPM, currently + the only supported algorithm is ``sha256``. As additional Discrete TPMs are + tested and integrated in TF-A, support for more algorithms will become + available. +- ``DISCRETE_TPM``: Boolean flag to enable Discrete TPM support. Depending + on the selected TPM interface, the appropriate drivers will be built and + packaged into firmware. +- ``TPM_INTERFACE``: This flag is required when ``DISCRETE_TPM=1``, + currently the only supported interface is ``FIFO_SPI``. + Ideally there should be four options: + + .. code:: shell + + FIFO_I2C + FIFO_SPI + FIFO_MMIO + CRB + + .. note:: + ``MBOOT_TPM_HASH_ALG`` will automatically overwrite ``MBOOT_EL_HASH_ALG``. + This is to ensure the event log and the TPM are using the same hash + algorithm. + + +Discrete TPM Initialization +--------------------------- +The TPM needs to be initialized based on the platform, the hardware interfaces +need to be set up independently, and once they are setup, the TPM commands +``tpm_interface_init()`` and subsequently ``tpm_startup()`` can be called. +``tpm_startup()`` only needs to be called once after startup, or if the system +is reset. + +An example of platform specific TPM hardware initialization for the rpi3 can be +found in ``plat/rpi/rpi3/rpi3_bl1_setup.c`` and ``plat/rpi/rpi3/rpi3_bl1_mboot.c`` + + +Discrete TPM PCR Extend +----------------------- +Once the TPM is setup, the TPM ``pcr_extend`` operation can be used to extend +hashes and store them in PCR 0. + +An example of ``pcr_extend`` that is used during rpi3 measured boot can be found + in ``plat/rpi/rpi3/rpi3_bl1_mboot.c`` and ``plat/rpi/rpi3/rpi3_bl2_mboot.c``. + + +*Copyright (c) 2025, Arm Limited. All rights reserved.* + +.. |rpi3 dtpm driver stack| image:: + ../resources/diagrams/rpi3_dtpm_driver.png +.. _TCG TPM 2.0 Library Specification: https://trustedcomputinggroup.org/resource/tpm-library-specification/ diff --git a/docs/design_documents/index.rst b/docs/design_documents/index.rst index f1d83861f..11c1c5a57 100644 --- a/docs/design_documents/index.rst +++ b/docs/design_documents/index.rst @@ -7,10 +7,12 @@ Design Documents cmake_framework measured_boot_poc + measured_boot_dtpm_poc drtm_poc rse psci_osi_mode measured_boot + dtpm_drivers -------------- diff --git a/docs/design_documents/measured_boot.rst b/docs/design_documents/measured_boot.rst index 005903eda..1f76770ab 100644 --- a/docs/design_documents/measured_boot.rst +++ b/docs/design_documents/measured_boot.rst @@ -91,6 +91,14 @@ The Measured Boot implementation in TF-A supports: and the variable length crypto agile structure called TCG_PCR_EVENT2. Event Log driver implemented in TF-A covers later part. +#. Discrete TPM + + A Discrete TPM (Trusted Platform Module) can be used alongside Event Log to + extend measurements and validate Measured Boot functionality. The use of a + Discrete TPM in TF-A to extend measurements of images and other critical data + allows for an additional layer of security. The TPM can be used to attest the + integrity of the Event Log. + #. |RSE| It is one of the physical backends to extend the measurements. Please refer @@ -229,7 +237,7 @@ Responsibilities of these platform interfaces are - -------------- -*Copyright (c) 2023, Arm Limited. All rights reserved.* +*Copyright (c) 2023-2025, Arm Limited. All rights reserved.* .. _Arm® Server Base Security Guide: https://developer.arm.com/documentation/den0086/latest .. _TCG EFI Protocol Specification: https://trustedcomputinggroup.org/wp-content/uploads/EFI-Protocol-Specification-rev13-160330final.pdf diff --git a/docs/design_documents/measured_boot_dtpm_poc.rst b/docs/design_documents/measured_boot_dtpm_poc.rst new file mode 100644 index 000000000..63a12f263 --- /dev/null +++ b/docs/design_documents/measured_boot_dtpm_poc.rst @@ -0,0 +1,458 @@ +Measured Boot using a Discrete TPM (PoC) +======================================== + +Measured Boot is the process of cryptographically measuring the code and +critical data used at boot time, for example using a TPM, so that the +security state can be attested later. + +The current implementation of the driver included in |TF-A| supports several +backends and each has a different means to store the measurements. +This section focuses on the Discrete TPM backend, which stores measurements +in a PCR within the TPM. This backend can be paired with the `TCG event log`_ +to provide attestation of the measurements stored in the event log. See +details in :ref:`Measured Boot Design`. + +This section provides instructions to setup and build a proof of concept (PoC) +that showcases the use of Measured Boot with a Discrete TPM interface. + +.. note:: + The instructions given in this document are meant to build a PoC to + show how Measured Boot on TF-A can interact with a Discrete TPM interface. + This PoC is platform specific, and uses a SPI based Discrete TPM, the + Raspberry Pi communicates with the TPM via a GPIO pin bit-banged SPI interface. + For other platforms, different may be required to interface with the hardware + (e.g., different hardware communication protocols) and different TPM interfaces + (e.g., |FIFO| vs |CRB|). + +Components +~~~~~~~~~~ + + - **Platform**: The PoC is developed on the Raspberry Pi 3 (rpi3), due to quick + driver development and the availability of GPIO pins to interface with a TPM + expansion module. Measured boot capabilities using the TCG Event Log are + ported to the Raspberry Pi 3 platform inside TF-A. This PoC specifically uses + the Raspberry Pi 3 Model B V1.2, but this PoC is compatible with other + Raspberry Pi 3 models. + + - **Discrete TPM**: The TPM chip selected is a breakout board compatible with + the Raspberry Pi 3 GPIO pins. This PoC uses a |SPI| based LetsTrust TPM + breakout board equipped with a Infineon Optigaâ„¢ SLB 9670 TPM 2.0 chip. Link + to device: https://thepihut.com/products/letstrust-tpm-for-raspberry-pi + + .. note:: + If you have another TPM breakout board that uses the same + Infineon Optigaâ„¢ SLB 9670 TPM 2.0 SPI based chip, it will also work. + Ensure that the correct GPIO pins are utilized on the Raspberry Pi 3 to + avoid communication issues, and possible hardware failures. + + - **TF-A TPM Drivers**: To interface with a physical (Discrete) TPM chip in + TF-A, the PoC uses TF-A drivers that provide the command, interface, link, + and platform layers required to send and receive data to and from the TPM. + The drivers are located in TFA, and not in a |SP|, so that they may be used + in early stages such as BL2, and in some cases, BL1. The design of the TPM + Drivers is documented here: :ref:`Discrete TPM drivers`. + + - **U-boot BL33**: This PoC showcases measured boot up to BL33, and for + simplicity uses a U-boot image for BL33, so that the image is measured and + loaded. Currently U-boot does not have Discrete TPM support for the + Raspberry Pi 3 platform so the boot flow ends here. + + +Building the PoC for the Raspberry Pi 3 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Build instructions for U-Boot.bin for Raspberry Pi 3.** + +First, the build requires a BL33 firmware image that can be packaged and measured +by TF-A. + +U-boot can be built for the Raspberry Pi 3, but there are some changes to be made +to allow the build to succeed. First Clone U-boot and enter the repo. + +.. code:: shell + + git clone https://github.com/u-boot/u-boot.git + cd u-boot + +Now to switch to a specific tag ``v2024.04`` for testing purposes, and then build +the defconfig labelled ``rpi_3_b_plus_defconfig``. + +.. code:: shell + + git checkout tags/v2024.04 -b tfa_dtpm_poc + make CROSS_COMPILE=aarch64-linux-gnu- rpi_3_b_plus_defconfig + +Lastly open the ``.config`` and change ``CONFIG_TEXT_BASE`` and +``CONFIG_SYS_UBOOT_START`` to ``0x11000000`` to match the BL33 starting point. + +.. code:: shell + + vim .config + CONFIG_TEXT_BASE=0x11000000 + CONFIG_SYS_UBOOT_START=0x11000000 + +To build the u-boot binary, use the following command. + +.. code:: shell + + make CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc) + +**Build TF-A for Raspberry Pi 3 with Discrete TPM and Measured Boot.** + +Copy over the ``u-boot.bin`` file over to your TF-A working directory. + +.. code:: shell + + cp /path/to/u-boot/build/u-boot.bin /path/to/tfa/u-boot.bin + +TF-A build command: + +.. code:: shell + + CROSS_COMPILE=aarch64-linux-gnu- \ + make PLAT=rpi3 \ + RPI3_PRELOADED_DTB_BASE=0x200000 \ + BL33=u-boot.bin \ + SUPPORT_VFP=1 \ + DEBUG=0 \ + MEASURED_BOOT=1 \ + DISCRETE_TPM=1 \ + MBOOT_TPM_HASH_ALG=sha256 \ + TPM_INTERFACE=FIFO_SPI \ + MBEDTLS_DIR=/path/to/mbedtls/repo \ + LOG_LEVEL=40 \ + fip all + +This build command is similar to the one provided in the TF-A Raspberry Pi 3 +platform port, To learn more about the platform and its build options, visit +:ref:`Raspberry Pi 3`. + + - ``RPI3_PRELOADED_DTB_BASE`` is given a different address to accommodate the + larger BL1 and BL2 firmware sizes, this is to accommodate the TPM drivers + that are packaged in BL1 and BL2 for this PoC. + - ``BL33`` is the non trusted firmware, in this case the U-Boot binary built + earlier. + - ``SUPPORT_VFP`` is enabled, allows Vector Floating Point operations in EL3. + - ``MEASURED_BOOT`` is enabled to allow the Measured Boot flow. + - ``DISCRETE_TPM=1`` enables the build of Discrete TPM drivers. + - ``MBOOT_TPM_HASH_ALG=sha256`` sets the hash algorithm to sha256, this is + the only algorithm supported by both TF-A Measured Boot and the SLB 9670 + TPM 2.0. + - ``TPM_INTERFACE=FIFO_SPI`` specifies the use of the FIFO SPI interface. + - ``MBEDTLS_DIR`` is the path to your local mbedtls repo. + - ``LOG_LEVEL=40`` ensures that eventlog is printed at the end of BL1 and BL2. + + +**Hardware Setup:** + + - **TPM Connection**: Connect the LetsTrust TPM board to GPIO pins 17 - 26 on + the 40-pin GPIO header on the Raspberry Pi board. The 2x5 header of the TPM + module must be aligned to the pins in a specific orientation, match the 3v3 + and RST pins from the TPM board to pins 17 and 18 respectively on the + Raspberry Pi 3 header. See `rpi3 pinout`_. + + - **Serial Console**: Establish a serial connection to the Raspberry Pi 3 to + view serial output during the boot sequence. The GND, TXD, and RXD pins, + which are labelled 6, 8, and 10 on the Raspberry Pi 3 header respectively, + are the required pins to establish a serial connection. The recommended way + to connect to the board from another system is to use a USB to serial TTL + cable to output the serial console in a easy manner. + + - **SD Card Setup**: Format a SD Card as ``FAT32`` with a default Raspbian + installation that is similar to the default Raspberry Pi 3 boot partition, + this partition will utilize the default files installed in the root + directory with Rasbian such as: + + :: + + bcm2710-rpi3-b.dtb + bootcode.bin + config.txt + fixup.dat + start.elf + + Open ``config.txt`` and overwrite the file with the following lines: + + :: + + arm_64bit=1 + disable_commandline_tags=2 + enable_uart=1 + armstub=armstub8.bin + device_tree_address=0x200000 + device_tree_end=0x210000 + + These configurations are required to enable uart, enable 64bit mode, + use the build TF binary, and the modified rpi3 device tree address + and size. + + Copy ``armstub8.bin`` from the TF-A build path to the root folder of the + SD card. + + The SD Card is now ready to be booted. + +Running the PoC for the Raspberry Pi 3 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Insert the SD Card into the Raspberry Pi 3 SD card port and boot the system. + +To access the serial console output from the Raspberry Pi 3 you can either: + + - Follow `instructions`_ to use PuTTY to connect to Raspberry Pi 3 serial console. + + - Use the linux ``screen`` command: + + .. code:: shell + + screen /dev/ttyUSB0 115200 + +Once booted the output from the serial console will look like this: + +.. code:: shell + + Raspberry Pi Bootcode + + Read File: config.txt, 153 + + Read File: start.elf, 2975040 (bytes) + + Read File: fixup.dat, 7265 (bytes) + + MESS:00:00:01.170422:0: brfs: File read: /mfs/sd/config.txt + MESS:00:00:01.174630:0: brfs: File read: 153 bytes + MESS:00:00:01.211473:0: HDMI0:EDID error reading EDID block 0 attempt 0 + MESS:00:00:01.217639:0: HDMI0:EDID error reading EDID block 0 attempt 1 + MESS:00:00:01.223977:0: HDMI0:EDID error reading EDID block 0 attempt 2 + MESS:00:00:01.230313:0: HDMI0:EDID error reading EDID block 0 attempt 3 + MESS:00:00:01.236650:0: HDMI0:EDID error reading EDID block 0 attempt 4 + MESS:00:00:01.242987:0: HDMI0:EDID error reading EDID block 0 attempt 5 + MESS:00:00:01.249324:0: HDMI0:EDID error reading EDID block 0 attempt 6 + MESS:00:00:01.255660:0: HDMI0:EDID error reading EDID block 0 attempt 7 + MESS:00:00:01.261997:0: HDMI0:EDID error reading EDID block 0 attempt 8 + MESS:00:00:01.268334:0: HDMI0:EDID error reading EDID block 0 attempt 9 + MESS:00:00:01.274429:0: HDMI0:EDID giving up on reading EDID block 0 + MESS:00:00:01.282647:0: brfs: File read: /mfs/sd/config.txt + MESS:00:00:01.286929:0: gpioman: gpioman_get_pin_num: pin LEDS_PWR_OK not defined + MESS:00:00:01.487295:0: gpioman: gpioman_get_pin_num: pin DISPLAY_DSI_PORT not defined + MESS:00:00:01.494853:0: gpioman: gpioman_get_pin_num: pin LEDS_PWR_OK not defined + MESS:00:00:01.500763:0: *** Restart logging + MESS:00:00:01.504638:0: brfs: File read: 153 bytes + MESS:00:00:01.510139:0: hdmi: HDMI0:EDID error reading EDID block 0 attempt 0 + MESS:00:00:01.517254:0: hdmi: HDMI0:EDID error reading EDID block 0 attempt 1 + MESS:00:00:01.524112:0: hdmi: HDMI0:EDID error reading EDID block 0 attempt 2 + MESS:00:00:01.530970:0: hdmi: HDMI0:EDID error reading EDID block 0 attempt 3 + MESS:00:00:01.537826:0: hdmi: HDMI0:EDID error reading EDID block 0 attempt 4 + MESS:00:00:01.544685:0: hdmi: HDMI0:EDID error reading EDID block 0 attempt 5 + MESS:00:00:01.551543:0: hdmi: HDMI0:EDID error reading EDID block 0 attempt 6 + MESS:00:00:01.558399:0: hdmi: HDMI0:EDID error reading EDID block 0 attempt 7 + MESS:00:00:01.565258:0: hdmi: HDMI0:EDID error reading EDID block 0 attempt 8 + MESS:00:00:01.572116:0: hdmi: HDMI0:EDID error reading EDID block 0 attempt 9 + MESS:00:00:01.578730:0: hdmi: HDMI0:EDID giving up on reading EDID block 0 + MESS:00:00:01.584634:0: hdmi: HDMI0:EDID error reading EDID block 0 attempt 0 + MESS:00:00:01.592427:0: hdmi: HDMI0:EDID error reading EDID block 0 attempt 1 + MESS:00:00:01.599286:0: hdmi: HDMI0:EDID error reading EDID block 0 attempt 2 + MESS:00:00:01.606142:0: hdmi: HDMI0:EDID error reading EDID block 0 attempt 3 + MESS:00:00:01.613001:0: hdmi: HDMI0:EDID error reading EDID block 0 attempt 4 + MESS:00:00:01.619858:0: hdmi: HDMI0:EDID error reading EDID block 0 attempt 5 + MESS:00:00:01.626717:0: hdmi: HDMI0:EDID error reading EDID block 0 attempt 6 + MESS:00:00:01.633575:0: hdmi: HDMI0:EDID error reading EDID block 0 attempt 7 + MESS:00:00:01.640431:0: hdmi: HDMI0:EDID error reading EDID block 0 attempt 8 + MESS:00:00:01.647288:0: hdmi: HDMI0:EDID error reading EDID block 0 attempt 9 + MESS:00:00:01.653905:0: hdmi: HDMI0:EDID giving up on reading EDID block 0 + MESS:00:00:01.659769:0: hdmi: HDMI:hdmi_get_state is deprecated, use hdmi_get_display_state instead + MESS:00:00:01.668264:0: HDMI0: hdmi_pixel_encoding: 162000000 + MESS:00:00:01.673988:0: vec: vec_middleware_power_on: vec_base: 0x7e806000 rev-id 0x00002708 @ vec: 0x7e806100 @ 0x00000420 enc: 0x7e806060 @ 0x00000220 cgmsae: 0x7e80605c @ 0x00000000 + MESS:00:00:01.880234:0: dtb_file 'bcm2710-rpi-3-b.dtb' + MESS:00:00:01.889713:0: brfs: File read: /mfs/sd/bcm2710-rpi-3-b.dtb + MESS:00:00:01.894375:0: Loaded 'bcm2710-rpi-3-b.dtb' to 0x200000 size 0x7cb2 + MESS:00:00:01.915761:0: brfs: File read: 31922 bytes + MESS:00:00:02.007202:0: brfs: File read: /mfs/sd/config.txt + MESS:00:00:02.017277:0: brfs: File read: 153 bytes + MESS:00:00:02.020772:0: Failed to open command line file 'cmdline.txt' + MESS:00:00:02.042302:0: gpioman: gpioman_get_pin_num: pin EMMC_ENABLE not defined + MESS:00:00:02.398066:0: kernel= + MESS:00:00:02.455255:0: brfs: File read: /mfs/sd/armstub8.bin + MESS:00:00:02.459284:0: Loaded 'armstub8.bin' to 0x0 size 0xdbe74 + MESS:00:00:02.465109:0: No compatible kernel found + MESS:00:00:02.469610:0: Device tree loaded to 0x200000 (size 0x823f) + MESS:00:00:02.476805:0: uart: Set PL011 baud rate to 103448.300000 Hz + MESS:00:00:02.483381:0: uart: Baud rate change done... + MESS:00:00:02.486793:0: uart: Baud rateNOTICE: Booting Trusted Firmware + NOTICE: BL1: v2.11.0(release):v2.11.0-187-g0cb1ddc9c-dirty + NOTICE: BL1: Built : 10:57:10, Jul 9 2024 + INFO: BL1: RAM 0x100ee000 - 0x100f9000 + INFO: Using crypto library 'mbed TLS' + NOTICE: TPM Chip: vendor-id 0xd1, device-id 0x0, revision-id: 0x16 + NOTICE: rpi3: Detected: Raspberry Pi 3 Model B (1GB, Sony, UK) [0x00a02082] + INFO: BL1: Loading BL2 + INFO: Loading image id=1 at address 0x100b4000 + INFO: Image id=1 loaded: 0x100b4000 - 0x100c0281 + INFO: TCG_EfiSpecIDEvent: + INFO: PCRIndex : 0 + INFO: EventType : 3 + INFO: Digest : 00 + INFO: : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + INFO: : 00 00 00 + INFO: EventSize : 33 + INFO: Signature : Spec ID Event03 + INFO: PlatformClass : 0 + INFO: SpecVersion : 2.0.2 + INFO: UintnSize : 1 + INFO: NumberOfAlgorithms : 1 + INFO: DigestSizes : + INFO: #0 AlgorithmId : SHA256 + INFO: DigestSize : 32 + INFO: VendorInfoSize : 0 + INFO: PCR_Event2: + INFO: PCRIndex : 0 + INFO: EventType : 3 + INFO: Digests Count : 1 + INFO: #0 AlgorithmId : SHA256 + INFO: Digest : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + INFO: : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + INFO: EventSize : 17 + INFO: Signature : StartupLocality + INFO: StartupLocality : 0 + INFO: PCR_Event2: + INFO: PCRIndex : 0 + INFO: EventType : 1 + INFO: Digests Count : 1 + INFO: #0 AlgorithmId : SHA256 + INFO: Digest : 55 11 51 d8 8b 7f 41 d3 18 16 f2 e8 80 bf 80 fa + INFO: : b4 03 6d 96 4c a0 0a 98 45 cf 25 2f 1e a9 09 3e + INFO: EventSize : 5 + INFO: Event : BL_2 + NOTICE: BL1: Booting BL2 + INFO: Entry point address = 0x100b4000 + INFO: SPSR = 0x3c5 + NOTICE: BL2: v2.11.0(release):v2.11.0-187-g0cb1ddc9c-dirty + NOTICE: BL2: Built : 10:56:39, Jul 9 2024 + INFO: Using crypto library 'mbed TLS' + NOTICE: TPM Chip: vendor-id 0xd1, device-id 0x0, revision-id: 0x16 + INFO: BL2: Doing platform setup + INFO: BL2: Loading image id 3 + INFO: Loading image id=3 at address 0x100e0000 + INFO: Image id=3 loaded: 0x100e0000 - 0x100e706b + INFO: BL2: Loading image id 5 + INFO: Loading image id=5 at address 0x11000000 + INFO: Image id=5 loaded: 0x11000000 - 0x110a8ad8 + INFO: TCG_EfiSpecIDEvent: + INFO: PCRIndex : 0 + INFO: EventType : 3 + INFO: Digest : 00 + INFO: : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + INFO: : 00 00 00 + INFO: EventSize : 33 + INFO: Signature : Spec ID Event03 + INFO: PlatformClass : 0 + INFO: SpecVersion : 2.0.2 + INFO: UintnSize : 1 + INFO: NumberOfAlgorithms : 1 + INFO: DigestSizes : + INFO: #0 AlgorithmId : SHA256 + INFO: DigestSize : 32 + INFO: VendorInfoSize : 0 + INFO: PCR_Event2: + INFO: PCRIndex : 0 + INFO: EventType : 3 + INFO: Digests Count : 1 + INFO: #0 AlgorithmId : SHA256 + INFO: Digest : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + INFO: : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + INFO: EventSize : 17 + INFO: Signature : StartupLocality + INFO: StartupLocality : 0 + INFO: PCR_Event2: + INFO: PCRIndex : 0 + INFO: EventType : 1 + INFO: Digests Count : 1 + INFO: #0 AlgorithmId : SHA256 + INFO: Digest : 55 11 51 d8 8b 7f 41 d3 18 16 f2 e8 80 bf 80 fa + INFO: : b4 03 6d 96 4c a0 0a 98 45 cf 25 2f 1e a9 09 3e + INFO: EventSize : 5 + INFO: Event : BL_2 + INFO: PCR_Event2: + INFO: PCRIndex : 0 + INFO: EventType : 1 + INFO: Digests Count : 1 + INFO: #0 AlgorithmId : SHA256 + INFO: Digest : f3 00 5c ed a2 12 8b 76 b7 82 da c5 28 c3 02 52 + INFO: : 19 e4 3a 82 f2 3c ab 1e 0d 78 84 9c b5 fe e2 4f + INFO: EventSize : 14 + INFO: Event : SECURE_RT_EL3 + INFO: PCR_Event2: + INFO: PCRIndex : 0 + INFO: EventType : 1 + INFO: Digests Count : 1 + INFO: #0 AlgorithmId : SHA256 + INFO: Digest : 90 28 81 42 12 b7 9b ca aa 0c 40 76 33 5a 69 71 + INFO: : b6 19 2b 90 f2 d2 69 b8 de 8e 6d 05 4d c2 73 f9 + INFO: EventSize : 6 + INFO: Event : BL_33 + NOTICE: BL1: Booting BL31 + INFO: Entry point address = 0x100e0000 + INFO: SPSR = 0x3cd + NOTICE: BL31: v2.11.0(release):v2.11.0-187-g0cb1ddc9c-dirty + NOTICE: BL31: Built : 10:56:58, Jul 9 2024 + INFO: rpi3: Checking DTB... + INFO: rpi3: Reserved 0x10000000 - 0x10100000 in DTB + INFO: BL31: Initializing runtime services + INFO: BL31: Preparing for EL3 exit to normal world + INFO: Entry point address = 0x11000000 + INFO: SPSR = 0x3c9 + + + U-Boot 2024.04-g84314330-dirty (Apr 23 2024 - 15:41:54 -0500) + + DRAM: 948 MiB + RPI 3 Model B (0xa02082) + Core: 68 devices, 14 uclasses, devicetree: embed + MMC: mmc@7e202000: 0, mmc@7e300000: 1 + Loading Environment from FAT... OK + In: serial,usbkbd + Out: serial,vidconsole + Err: serial,vidconsole + Net: No ethernet found. + starting USB... + Bus usb@7e980000: USB DWC2 + scanning bus usb@7e980000 for devices... + Error: smsc95xx_eth No valid MAC address found. + 2 USB Device(s) found + scanning usb for storage devices... 0 Storage Device(s) found + Hit any key to stop autoboot: 2 1 0 + Card did not respond to voltage select! : -110 + No EFI system partition + No EFI system partition + Failed to persist EFI variables + No EFI system partition + Failed to persist EFI variables + No EFI system partition + Failed to persist EFI variables + Missing TPMv2 device for EFI_TCG_PROTOCOL + ** Booting bootflow '' with efi_mgr + Loading Boot0000 'mmc 0' failed + EFI boot manager: Cannot load any image + Boot failed (err=-14) + Card did not respond to voltage select! : -110 + No ethernet found. + No ethernet found. + U-Boot> + + +Next steps for Discrete TPM and Measured Boot development +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In order to automatically validate the workings of the Discrete TPM, the creation +of test cases that compare the eventlog image hashes with what is stored in PCR0 +are a great way to test the core functionality of the Discrete TPM in Measured Boot. + +Development of Discrete TPM drivers such as a reference FIFO |I2C|, MMIO, and CRB +drivers has not started, these drivers will allow a larger number of platform +to use a Discrete TPM in TF-A. + +*Copyright (c) 2025, Arm Limited. All rights reserved.* + +.. _TCG event log: https://trustedcomputinggroup.org/resource/tcg-efi-platform-specification/ +.. _rpi3 pinout: https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#gpio +.. _instructions: https://www.circuitbasics.com/use-putty-to-access-the-raspberry-pi-terminal-from-a-computer/ +.. _workaround: https://github.com/mhomran/u-boot-rpi3-b-plus diff --git a/docs/getting_started/build-options.rst b/docs/getting_started/build-options.rst index 740f3a633..b5814bba7 100644 --- a/docs/getting_started/build-options.rst +++ b/docs/getting_started/build-options.rst @@ -784,6 +784,20 @@ Common build options This option defaults to 0. +- ``DISCRETE_TPM``: Boolean flag to include support for a Discrete TPM. + + This option defaults to 0. + +- ``TPM_INTERFACE``: When ``DISCRETE_TPM=1``, this is a required flag to + select the TPM interface. Currently only one interface is supported: + + :: + + FIFO_SPI + +- ``MBOOT_TPM_HASH_ALG``: Build flag to select the TPM hash algorithm used during + Measured Boot. Currently only accepts ``sha256`` as a valid algorithm. + - ``MARCH_DIRECTIVE``: used to pass a -march option from the platform build options to the compiler. An example usage: diff --git a/docs/global_substitutions.txt b/docs/global_substitutions.txt index 23a91cdcd..ecf6d6396 100644 --- a/docs/global_substitutions.txt +++ b/docs/global_substitutions.txt @@ -6,6 +6,7 @@ .. |BTI| replace:: :term:`BTI` .. |CoT| replace:: :term:`CoT` .. |COT| replace:: :term:`COT` +.. |CRB| replace:: :term:`CRB` .. |CSS| replace:: :term:`CSS` .. |CVE| replace:: :term:`CVE` .. |DICE| replace:: :term:`DICE` @@ -19,11 +20,13 @@ .. |FCONF| replace:: :term:`FCONF` .. |FDT| replace:: :term:`FDT` .. |FF-A| replace:: :term:`FF-A` +.. |FIFO| replace:: :term:`FIFO` .. |FIP| replace:: :term:`FIP` .. |FVP| replace:: :term:`FVP` .. |FWU| replace:: :term:`FWU` .. |GIC| replace:: :term:`GIC` .. |HES| replace:: :term:`HES` +.. |I2C| replace:: :term:`I2C` .. |ISA| replace:: :term:`ISA` .. |Linaro| replace:: :term:`Linaro` .. |MMU| replace:: :term:`MMU` @@ -55,6 +58,7 @@ .. |SP| replace:: :term:`SP` .. |SPD| replace:: :term:`SPD` .. |SPM| replace:: :term:`SPM` +.. |SPI| replace:: :term:`SPI` .. |SRTM| replace:: :term:`SRTM` .. |SSBS| replace:: :term:`SSBS` .. |SVE| replace:: :term:`SVE` diff --git a/docs/glossary.rst b/docs/glossary.rst index 20ad21c14..8bb35bc32 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -36,6 +36,9 @@ You can find additional definitions in the `Arm Glossary`_. CSS Compute Sub-System + CRB + Command Response Buffer + CVE Common Vulnerabilities and Exposures. A CVE document is commonly used to describe a publicly-known security vulnerability. @@ -88,6 +91,9 @@ You can find additional definitions in the `Arm Glossary`_. FF-A Firmware Framework for Arm A-profile + FIFO + First In, First Out + FIP Firmware Image Package @@ -103,6 +109,9 @@ You can find additional definitions in the `Arm Glossary`_. HES Arm CCA Hardware Enforced Security + I2C + Inter-Integrated Circuit Protocol + ISA Instruction Set Architecture @@ -211,6 +220,9 @@ You can find additional definitions in the `Arm Glossary`_. SPM Secure Partition Manager + SPI + Serial Peripheral Interface + SRTM Static Root of Trust for Measurement diff --git a/docs/resources/diagrams/rpi3_dtpm_driver.png b/docs/resources/diagrams/rpi3_dtpm_driver.png new file mode 100644 index 0000000000000000000000000000000000000000..36b18439c59d26d9e07ee03336864d9053ab11e2 GIT binary patch literal 57302 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�@NajAAASt1%1V0Hn#@L_y-yg2AA@0%D6Bzpm_hwFk za{LvVEbl3+riPi~@I^^U(n0Q^04%oG`>UCQd=yXEOUJGtlfsn8>$wF6ZR9l*5e(+} ztI1RsJ?zsGSyw+WWCDstF&1#O+nC2Ko$n;X@>WymZ+h6Q1_M(+_WH4f({$ur-Ep14 zeI4|uRXU-6d^~xY&o=%X46RS{QUlvcF=-09PVMp zH(pClNDHnZ_i%=n;mnmLuTiCt{M}EROEptniouDRw+O9(cn$1sMPdhVWpX38)z%|E z5yE$N4=JeGMb?~pQgC)NY?~7C)mZ>epZ=y8s{AQ7FrwV_@!YWUm5i;UCm6G`ih;w# zU}~o^UR!&vFy12o(fwQY@*`QfL?{^r6 z%jd-Q%9l6~_|JCpB3rZ*=R%Zs9%}>c`Fk94UW3cXW(PXeW4TE@(ZZ0 zOygooq$Uh=bz{Za9(=%ff%|gvzSU=|8U%@5nGMa^fnjgO3LALDR-5$BNYQHm4Cj{EpOT(s*S?U|IZ09#wmP zAdTYlqDJDWgAl*Y5v5Tf3d2Fa_r&Fu_tON1lmvD2%MV;NV-x{)CiTAaT>5@mu_%}4 z?c^2?_S<=sp>wZ=A!(SvpB`A#8KT&|9bV5rxjRs# z?j;-3ow0=EH|tLHG|uZ^G`YF`BXcKWm5Co!S?8e)?V}-!9=r-kZ1tGljg;J>R|XpygubZfiLd(H%%fc z(?{;{L$v|@e+XX0@%b5g@AAaquMwk~_HF5I;B9aGS+67PQx298a%wGRb1AviAWqN( z-_s=Yt4%k~{Cw#jYjU7kWIN7Pl{enB_;ruV zzucL8ED+rNea<&wPCS=E;*ILk5&8TLf9HmX7F(>}oLpnGmm#l0>nI=ZyQ7&-ys~sb z1z81IHQJJE{Na@ymr~kaCti5#xR%##@4B(9L{ibLf=@OOF{I{KFk2L>F5NCz;wzbW z`2XZ0yb@4e9MSF(DG(a=leX!2h5yptzX4~0LupR&DCk7G;_Q~@3*8Retr*Ue1;Oo;yVH%lh*Wvj`x5jA&_Z_<+lIiYYCwTjv-ElALrfgK z=5(B$ul()ROpbE<#h!20n5WDr1I~p6If4QbI#OovPx` wD}QRvfX?}}+_FVdQ&MBb@07D$EIsgCw literal 0 HcmV?d00001 From b00f6ece560696a89bf2a91289ea9ef51a768c5d Mon Sep 17 00:00:00 2001 From: Abhi Singh Date: Fri, 1 Nov 2024 15:36:18 -0500 Subject: [PATCH 9/9] feat(docs): update mboot threat model with dTPM Add the discrete TPM to the TCG event log section of the measured boot threat model. Include the example of a physical vurnerability that can be used to compromise a dTPM. Signed-off-by: Abhi Singh Change-Id: I2c06edf5e9031adc970c24426a8ae52b06efb614 --- .../firmware_threat_model/threat_model.rst | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/threat_model/firmware_threat_model/threat_model.rst b/docs/threat_model/firmware_threat_model/threat_model.rst index ae0219ee3..c0cc3be50 100644 --- a/docs/threat_model/firmware_threat_model/threat_model.rst +++ b/docs/threat_model/firmware_threat_model/threat_model.rst @@ -928,6 +928,12 @@ nonetheless once execution has reached the runtime EL3 firmware. Measured Boot implementation in |TF-A| is that it does not extend the measurements into a |PCR| of a Discrete |TPM|, where measurements would be securely stored and protected against tampering. + - Discrete |TPM|: Implemented in |TF-A| as a proof of concept, the Discrete + |TPM| is used alongside the existing TCG-compliant Event Log. This + Measured Boot implementation extends measurement hashes to a |PCR| in the + |TPM|, which provides a hardware-backed root of trust. The measurements in + the Event Log can now be hashed and compared to the value of the |PCR| to + determine if tampering of the Event Log has taken place. - `CCA Measured Boot`_: Implemented by |TF-M|. Measurements are stored in |HES| secure on-chip memory. |HES| implements protection against tampering its on-chip memory. |HES| interface is available for BL1 and BL2. @@ -942,6 +948,20 @@ nonetheless once execution has reached the runtime EL3 firmware. to protect or threats to defend against that could compromise |TF-A| execution environment's security. + When considering the implementation of Measured Boot using a TCG-compliant + Event Log backed by a discrete TPM, physical vulnerabilities come to mind. + Platforms have many different ways of integrating a discrete TPM, and these + implementations can be susceptible to man-in-the-middle attacks, where the + attacker intercepts the bus traffic between the discrete TPM and the host + machine. This can lead to PCR extend operations being modified, compromising + Measured Boot. This vulnerability requires physical access to the host machine. + + TF-A does not provide any mitigations against these physical vulnerabilities, + it is the responsibility of the platform owners to address this based on their + specific threat model. Mitigation of this can be achieved through dedicated + hardware solutions, such as an encrypted AP/dTPM bus, or software-based + approaches designed to protect sensitive data such as parameter encryption. + There are general security assets and threats associated with remote/delegated attestation. However, these are outside the |TF-A| security boundary and should be dealt with by the appropriate agent in the platform/system. @@ -1192,7 +1212,7 @@ Threats to be Mitigated by an External Agent Outside of TF-A -------------- -*Copyright (c) 2021-2024, Arm Limited. All rights reserved.* +*Copyright (c) 2021-2025, Arm Limited. All rights reserved.* .. _STRIDE threat analysis technique: https://docs.microsoft.com/en-us/azure/security/develop/threat-modeling-tool-threats#stride-model