嵌入式TPM2.0 SPI驱动:裸机环境下硬件密码学加速实现
1. 项目概述SPITIS_TPM20 是一个严格遵循可信计算组织Trusted Computing Group, TCG制定的《Trusted Platform Module Interface Specification, Version 1.3》TIS 1.3标准的嵌入式 TPM 驱动程序。其核心工程目标并非实现完整的平台信任链启动如 BIOS/UEFI 中的 SRTM 或 DRTM而是将符合 TIS 1.3 规范的物理 TPM 芯片典型为 Infineon SLB9670、Nuvoton NPCT650 等 SPI 接口 TPM 2.0 模块抽象为一个可被上层固件或轻量级操作系统直接调用的硬件密码学加速库。该驱动的设计哲学是“最小化依赖、最大化确定性”它不依赖任何操作系统内核服务如内存管理单元 MMU、虚拟内存、系统调用完全运行在 bare-metal 环境下所有内存分配均采用静态预分配策略避免运行时堆操作带来的不可预测性所有时间等待均基于精确的轮询polling而非中断interrupt确保在无中断控制器或中断被全局屏蔽的严苛场景如安全启动早期阶段、Secure Monitor 运行时下仍能可靠工作。这种设计使其天然适配于 STM32H7、NXP i.MX RT1170、RISC-V GD32VF103 等高性能 MCU 的 TrustZone 或 Secure World 固件开发。TPM 2.0 本身是一个功能完备的可信执行环境TEE其内部集成了符合 FIPS 140-2 Level 2 标准的 SHA-256、RSA-2048、ECC NIST P-256、HMAC-SHA256 等密码算法引擎并提供了密钥生成、存储、封装Seal、解封Unseal、签名Sign、验签Verify等完整密码服务。SPITIS_TPM20 驱动的作用就是将这些原本需要通过复杂 TCG Software StackTSS才能访问的硬件能力以一组精简、确定、可审计的 C 函数接口暴露出来使嵌入式开发者能够像调用HAL_AES_Encrypt()一样直接调用TPM2_Sign()或TPM2_Unseal()。2. 硬件接口与通信协议2.1 SPI 总线时序约束TPM 2.0 的 TIS 1.3 规范对 SPI 通信施加了严格的时序要求这与通用外设如 Flash、ADC有本质区别。SPITIS_TPM20 驱动必须精确满足以下关键时序参数否则将导致命令超时、数据错位或芯片进入错误状态参数符号最小值最大值说明SPI 时钟频率fSPI—20 MHzTPM 芯片规格书明确限定超频将导致内部 FIFO 同步失败片选CS#高电平保持时间tCSH50 ns—CS# 在传输结束后必须维持高电平至少 50ns以完成内部状态机切换命令就绪信号READY建立时间tRDY_SETUP—100 ns主机在拉低 CS# 后必须等待 READY 信号稳定为高电平方可开始发送命令头数据字节间最大间隔tBYTE_GAP—10 μs连续两个字节的 SPI 传输之间SCLK 必须持续且不能有超过 10μs 的空闲期否则 TPM 将中止当前事务驱动通过TPM2_SPI_TransmitReceive()函数封装了底层 SPI 操作并在关键路径插入精确的 NOP 延迟或使用 DWTData Watchpoint and Trace周期计数器进行微秒级校准确保上述时序得到满足。例如在拉低 CS# 后驱动会主动读取 TPM 的STATUS寄存器轮询TPM_STS_READY位直至其置位才开始发送TPM_ST_COMMAND命令头。2.2 TIS 寄存器映射与访问模型TIS 1.3 定义了一组标准化的 8 位寄存器它们通过 SPI 总线以“地址数据”的方式被访问。SPITIS_TPM20 将这些寄存器抽象为一组宏定义其物理地址映射如下以典型的 0x00 地址空间为例// TIS 寄存器基地址偏移SPI 传输时作为地址字节 #define TPM_ACCESS_ADDR 0x00 #define TPM_STS_ADDR 0x01 #define TPM_DATA_FIFO_ADDR 0x05 #define TPM_INT_ENABLE_ADDR 0x08 #define TPM_INT_STATUS_ADDR 0x09 #define TPM_INT_VECTOR_ADDR 0x0A #define TPM_INTF_CAPS_ADDR 0x14 // 寄存器位定义STATUS 寄存器示例 #define TPM_STS_VALID (1 7) // 状态有效位为 0 表示寄存器内容未更新 #define TPM_STS_COMMAND_READY (1 6) // 命令就绪可接收新命令 #define TPM_STS_RESPONSE_RETRY (1 5) // 响应重试需重新读取 #define TPM_STS_GO_BUSY (1 4) // TPM 正忙正在处理命令 #define TPM_STS_DATA_EXPECTED (1 3) // 期望接收数据写入 FIFO #define TPM_STS_DATA_AVAIL (1 2) // 数据可用可从 FIFO 读取 #define TPM_STS_EXPECT (1 1) // 期望下一个字节是命令头的一部分 #define TPM_STS_READY (1 0) // 就绪信号主机可开始通信所有寄存器访问均遵循统一的流程地址阶段SPI 发送 1 字节地址如TPM_STS_ADDR。数据阶段SPI 发送/接收 1 字节数据读操作时第一个数据字节为 dummy byte第二个为有效数据。状态同步每次访问后必须检查STATUS寄存器的TPM_STS_VALID位确认本次操作结果有效。2.3 命令/响应帧结构TPM 2.0 的命令和响应均采用统一的二进制帧格式由一个 10 字节的头部Header和可变长度的主体Body组成。SPITIS_TPM20 提供了TPM2_Packet_t结构体来封装这一帧typedef struct { uint16_t tag; // 命令标签TPM_ST_NO_SESSIONS 或 TPM_ST_SESSIONS uint32_t size; // 整个包的总长度含 header网络字节序Big-Endian uint32_t code; // 命令码TPM_CC_*或响应码TPM_RC_*网络字节序 uint8_t *body; // 指向命令/响应主体的指针 uint16_t body_size; // 主体长度 } TPM2_Packet_t;驱动的核心函数TPM2_Transmit()的工作流程即围绕此帧展开准备阶段调用TPM2_Packet_Prepare()计算并填充size和tag字段将code和body按网络字节序序列化。发送阶段轮询STATUS寄存器等待TPM_STS_COMMAND_READY置位然后分多次将整个Packet写入TPM_DATA_FIFO_ADDR。等待阶段轮询STATUS寄存器等待TPM_STS_GO_BUSY清零且TPM_STS_DATA_AVAIL置位。接收阶段从TPM_DATA_FIFO_ADDR分多次读取响应包调用TPM2_Packet_Parse()解析响应头验证size和code。3. 核心 API 接口详解3.1 初始化与状态管理/** * brief 初始化 TPM 设备执行硬件复位并验证 TIS 兼容性 * param htpm: TPM 句柄包含 SPI 外设句柄、GPIO 引脚等硬件资源 * retval TPM2_RC_SUCCESS 成功 * retval TPM2_RC_FAILURE 失败SPI 通信异常、READY 信号未就绪、INTF_CAPS 不匹配 */ TPM2_RC TPM2_Init(TPM2_HandleTypeDef *htpm); /** * brief 查询 TPM 当前状态用于诊断和调试 * param htpm: TPM 句柄 * param status: 输出参数填充 TPM_STATUS 结构体 * retval TPM2_RC_SUCCESS 成功 */ TPM2_RC TPM2_GetStatus(TPM2_HandleTypeDef *htpm, TPM2_Status_t *status);TPM2_Init()是整个驱动的入口点其内部执行一系列关键的硬件握手拉低RESET#引脚至少 10ms强制 TPM 复位。等待READY信号变为高电平表明 TPM 已完成上电自检POST。读取TPM_INTF_CAPS_ADDR寄存器验证其值是否为0x00000001表示支持 TIS 1.3。读取TPM_ACCESS_ADDR尝试获取TPM_ACCESS_ACTIVE_LOCALITY确认 locality 0 处于活动状态。TPM2_GetStatus()返回的TPM2_Status_t结构体包含了所有STATUS寄存器位的快照以及从TPM_INT_STATUS_ADDR读取的中断状态是现场调试 TPM “卡死”问题的首要工具。3.2 密码学核心服务3.2.1 平台配置寄存器PCR操作PCR 是 TPM 的核心信任锚点其值通过哈希链的方式累积记录平台状态。SPITIS_TPM20 提供了最常用的 PCR 扩展Extend操作/** * brief 将指定数据扩展Hash Extend到指定 PCR 中 * param htpm: TPM 句柄 * param pcrIndex: PCR 索引0-23 * param digest: 指向 SHA256_DIGEST_SIZE (32 bytes) 数据的指针 * retval TPM2_RC_SUCCESS 成功 * retval TPM2_RC_VALUE 无效的 PCR 索引 * retval TPM2_RC_HANDLE 无效的授权会话句柄若启用 */ TPM2_RC TPM2_PCR_Extend(TPM2_HandleTypeDef *htpm, uint32_t pcrIndex, const uint8_t *digest);该函数内部构造一个TPM2_CC_PCR_Extend命令包其主体包含pcrIndex和digest。值得注意的是TPM2_PCR_Extend是一个原子操作TPM 内部会先读取当前 PCR 值将其与输入digest进行 SHA256_Hash(PCR_Value || digest) 运算再将结果写回 PCR。整个过程无需主机参与计算完全由 TPM 硬件完成保证了结果的不可篡改性。3.2.2 非对称密钥操作驱动支持使用 TPM 内部生成的 RSA 或 ECC 密钥进行签名和验签这是实现固件代码签名验证的关键/** * brief 使用 TPM 内部密钥对数据进行签名 * param htpm: TPM 句柄 * param keyHandle: 密钥在 TPM NV 存储中的句柄如 0x81000001 * param digest: 待签名的数据摘要SHA256 * param signature: 输出参数指向存放签名结果的缓冲区 * param sigSize: 输入/输出signature 缓冲区大小 / 实际签名长度 * retval TPM2_RC_SUCCESS 成功 */ TPM2_RC TPM2_Sign(TPM2_HandleTypeDef *htpm, uint32_t keyHandle, const uint8_t *digest, uint8_t *signature, uint16_t *sigSize); /** * brief 验证一个签名的有效性 * param htpm: TPM 句柄 * param keyHandle: 公钥句柄通常为密钥的 publicArea 序列化数据 * param digest: 原始数据摘要 * param signature: 待验证的签名 * param sigSize: 签名长度 * retval TPM2_RC_SUCCESS 签名有效 * retval TPM2_RC_SIGNATURE 签名无效 */ TPM2_RC TPM2_VerifySignature(TPM2_HandleTypeDef *htpm, uint32_t keyHandle, const uint8_t *digest, const uint8_t *signature, uint16_t sigSize);TPM2_Sign()的典型应用场景是在安全启动过程中Bootloader 将应用程序镜像的 SHA256 摘要传给 TPMTPM 使用其内部受保护的私钥进行签名并将签名结果返回。Bootloader 随后将该签名与预存的公钥一起写入外部安全存储如 eMMC RPMB。下次启动时TPM2_VerifySignature()则利用该公钥验证新加载镜像的签名从而实现基于硬件的信任链传递。3.2.3 数据密封与解封Seal/Unseal这是 TPM 最具特色的功能它允许将敏感数据如加密密钥、证书与特定的平台状态PCR 值绑定实现“只有在正确的平台上才能解封”的强访问控制/** * brief 将明文数据密封加密到 TPM 中并与指定 PCR 值绑定 * param htpm: TPM 句柄 * param inData: 指向待密封的明文数据 * param inSize: 明文长度最大 128 bytes * param pcrArray: 指向 PCR 选择数组bitmask如 {0x00000001} 表示只绑定 PCR0 * param outBlob: 输出参数指向存放密封后 blob 的缓冲区 * param outSize: 输入/输出blob 缓冲区大小 / 实际 blob 长度 * retval TPM2_RC_SUCCESS 成功 */ TPM2_RC TPM2_Seal(TPM2_HandleTypeDef *htpm, const uint8_t *inData, uint16_t inSize, const uint32_t *pcrArray, uint8_t *outBlob, uint16_t *outSize); /** * brief 将密封的 blob 解封解密为明文 * param htpm: TPM 句柄 * param inBlob: 密封的 blob 数据 * param inSize: blob 长度 * param outData: 输出参数指向存放解封后明文的缓冲区 * param outSize: 输入/输出明文缓冲区大小 / 实际明文长度 * retval TPM2_RC_SUCCESS 成功且 PCR 值匹配 * retval TPM2_RC_POLICY_FAIL PCR 值不匹配解封失败 */ TPM2_RC TPM2_Unseal(TPM2_HandleTypeDef *htpm, const uint8_t *inBlob, uint16_t inSize, uint8_t *outData, uint16_t *outSize);TPM2_Seal()的工程价值在于它可以将一个 AES 加密密钥密封到 TPM 中并绑定到PCR0CRTM/BIOS 代码哈希和PCR7Secure Boot Policy。这意味着只有当平台的 BIOS 和启动策略与密封时完全一致时TPM2_Unseal()才能成功返回该密钥。这为固件实现了“硬件级条件访问控制”远超软件层面的简单密码保护。4. 集成与工程实践4.1 与 HAL 库的协同在 STM32 平台上SPITIS_TPM20 与 HAL 库的集成是无缝的。驱动的TPM2_HandleTypeDef结构体中Instance字段直接指向SPI_TypeDef*如SPI1而Lock字段则复用 HAL 的HAL_LOCKED/HAL_UNLOCKED状态。一个典型的初始化代码片段如下// 1. 初始化 HAL SPI 外设按 TPM 时序要求配置 hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_4; // 20MHz 80MHz APB2 hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.CLKPhase SPI_PHASE_1EDGE; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; HAL_SPI_Init(hspi1); // 2. 初始化 TPM 句柄 TPM2_HandleTypeDef htpm; htpm.Instance SPI1; htpm.CS_GPIO_Port GPIOA; htpm.CS_Pin GPIO_PIN_4; htpm.RST_GPIO_Port GPIOB; htpm.RST_Pin GPIO_PIN_0; htpm.Ready_GPIO_Port GPIOC; htpm.Ready_Pin GPIO_PIN_5; // 3. 执行 TPM 初始化 if (TPM2_Init(htpm) ! TPM2_RC_SUCCESS) { Error_Handler(); // TPM 初始化失败进入安全降级模式 }4.2 FreeRTOS 下的线程安全封装在多任务环境中必须确保对 TPM 的访问是互斥的。SPITIS_TPM20 本身不提供 RTOS 支持但提供了清晰的钩子hook供用户扩展。一个推荐的 FreeRTOS 封装方案是创建一个专用的 TPM 服务任务并通过队列Queue接收来自其他任务的请求// 定义请求结构体 typedef struct { TPM2_CmdType_t cmd; // 命令类型SEAL, UNSEAL, SIGN... void *inBuf; // 输入缓冲区 void *outBuf; // 输出缓冲区 uint16_t *size; // 长度指针 SemaphoreHandle_t sema; // 用于同步的二值信号量 } TPM2_Request_t; // TPM 服务任务 void TPM2_ServiceTask(void *argument) { TPM2_Request_t req; for(;;) { if (xQueueReceive(xTPM_Queue, req, portMAX_DELAY) pdTRUE) { // 在此执行实际的 TPM2_* 调用 switch(req.cmd) { case TPM2_CMD_SEAL: TPM2_Seal(htpm, req.inBuf, ..., req.outBuf, req.size); break; // ... 其他命令 } xSemaphoreGive(req.sema); // 通知发起任务 } } } // 用户任务调用示例 void AppTask(void *argument) { static uint8_t seal_blob[512]; static uint8_t secret_key[32] {0x01, 0x02, ...}; TPM2_Request_t req { .cmd TPM2_CMD_SEAL, .inBuf secret_key, .outBuf seal_blob, .size blob_len, .sema xSemaphoreCreateBinary() }; xQueueSend(xTPM_Queue, req, portMAX_DELAY); xSemaphoreTake(req.sema, portMAX_DELAY); // 同步等待 vSemaphoreDelete(req.sema); }4.3 安全启动流程中的典型应用在一个基于 STM32H7 的安全启动固件中SPITIS_TPM20 的典型应用流程如下Stage 1 (ROM Bootloader)执行基本硬件初始化验证 Stage 2 的签名使用内置公钥并将 Stage 2 的哈希值扩展Extend到PCR0。Stage 2 (Secure Bootloader)初始化 SPI 和 TPM读取PCR0值并验证其与预期值一致确保 Stage 1 未被篡改然后将自身代码哈希扩展到PCR2最后调用TPM2_Unseal()解封出用于解密 Stage 3 的 AES 密钥。Stage 3 (Application)使用解封出的密钥从外部 Flash 解密并加载应用程序镜像应用程序启动后可继续将自身关键状态扩展到PCR8-PCR15为后续的远程证明Attestation做准备。这一流程将软件逻辑的完整性验证通过签名与硬件状态的完整性验证通过 PCR紧密结合构建了一个纵深防御的信任根Root of Trust其安全性远高于仅依赖软件签名的方案。5. 调试与故障排除5.1 常见错误码解析TPM2_RC 错误码是诊断问题的第一手信息。除标准的TPM2_RC_SUCCESS和TPM2_RC_FAILURE外以下错误码最具诊断价值错误码十六进制值含义典型原因TPM2_RC_INITIALIZE0x00000100TPM 尚未初始化TPM2_Init()未被调用或调用失败后未处理TPM2_RC_DISABLED0x00000120TPM 功能被禁用BIOS/UEFI 设置中关闭了 TPM或TPM_ACCESS寄存器显示TPM_ACCESS_DISABLEDTPM2_RC_EXCLUSIVE0x00000123locality 被占用其他固件如 UEFI已独占使用 locality 0需在TPM_ACCESS中请求TPM_ACCESS_ESTABLISHMENTTPM2_RC_POLICY_FAIL0x0000098D策略评估失败TPM2_Unseal()时当前 PCR 值与密封时的值不匹配表明平台状态已改变5.2 逻辑分析仪调试法当遇到TPM2_RC_FAILURE且错误码无法提供足够信息时最有效的手段是使用逻辑分析仪捕获 SPI 总线波形。关键观察点包括CS# 信号确认其在每次命令前后都有正确的高低电平转换且高电平时间满足tsubCSH/sub。READY 信号确认其在TPM2_Init()后稳定为高电平并在每次命令发送前被正确轮询。SPI 数据流捕获前 16 字节检查其是否符合 TIS 帧格式如第 0-1 字节是否为0x80 0x01即TPM_ST_NO_SESSIONS标签。一个典型的错误波形是CS# 拉低后READY 信号长时间为低这表明 TPM 芯片未上电或RESET#信号异常。此时应立即检查硬件原理图中的电源、复位和 READY 引脚连接。5.3 静态内存配置驱动的所有内部缓冲区如命令包、响应包、临时哈希上下文均在tpm2_conf.h中通过宏定义进行静态配置// 最大命令/响应包长度字节 #define TPM2_MAX_PACKET_SIZE 4096 // 内部哈希计算缓冲区大小SHA256 需要 64 字节块 #define TPM2_HASH_BLOCK_SIZE 64 // 用于存储 PCR 值的数组大小24 个 PCR每个 32 字节 #define TPM2_PCR_COUNT 24 #define TPM2_PCR_SIZE 32用户必须根据实际项目需求调整这些值。例如如果计划使用TPM2_CreatePrimary()创建一个带有大量属性的主密钥TPM2_MAX_PACKET_SIZE可能需要从默认的 1024 扩展到 2048否则会导致TPM2_RC_SIZE错误。所有这些配置都在编译期确定不产生任何运行时开销完美契合嵌入式系统的确定性要求。