3、度量架构设计:TCG规范简介、PCR寄存器原理、度量事件日志
好,我们进入第三章。这一章是固件度量的地基。
你想想看,不管你是做可信启动、远程证明,还是搞什么机密计算方案,最终都绕不开三个东西:谁定的规矩、度量值存哪、度量过程怎么查。说白了,就是 TCG 规范、PCR 寄存器和事件日志。
我个人习惯把这套东西叫做「信任的铁三角」。缺一个,你的度量体系就立不起来。今天咱们就把这三个角一个一个掰开看。
3.1 TCG 规范:度量世界的通用语言
TCG,Trusted Computing Group,可信计算组织。这帮人定了一整套标准,从硬件到固件到软件,怎么度量、怎么存储、怎么报告,全给你规定好了。
为什么要有这个规范?
因为没有标准,各家各厂各玩各的。你的度量日志格式跟我不一样,我的 PCR 扩展算法跟他不兼容,那整个生态就乱套了。TCG 规范就是让大家说同一种语言。
核心规范族(跟固件度量最相关的):
- TCG PC Client Platform Firmware Profile:PC 客户端平台固件规范,定义了 BIOS/UEFI 怎么度量、PCR 怎么用。
- TCG EFI Protocol Specification:定义了 UEFI 下度量相关的协议接口,比如
EFI_TCG2_PROTOCOL。 - TCG Event Log Format Specification:事件日志的格式标准,二进制怎么排、字段怎么定义。
我在项目中遇到过最头疼的事,就是某厂商的固件号称「兼容 TCG 规范」,结果日志格式自己魔改了一版。嗯,那对接起来简直是噩梦。所以我的建议是:不要自己发明格式,严格按规范走。
3.2 PCR 寄存器:度量值的保险柜
PCR,Platform Configuration Register,平台配置寄存器。这东西是 TPM 里面的一组硬件寄存器,专门用来存度量值的哈希摘要。
为什么叫「寄存器」?因为它真的在硬件里。TPM 芯片内部有一小块 SRAM,上电后自动清零,掉电就丢。这保证了每次启动的度量都是从头开始的。
3.2.1 PCR 的核心操作:Extend
PCR 不能直接写值。你没法说「PCR[0] = 0xABCD」。它只接受一种操作——Extend(扩展)。
PCR_new = SHA256(PCR_old || 度量值)
什么意思?就是把当前 PCR 里的旧值,和新度量值拼在一起,做一次哈希,结果写回 PCR。
这样做的好处是什么?
- 不可逆:你没法从 PCR 新值反推出之前度量过什么。
- 顺序敏感:度量顺序变了,最终 PCR 值就变了。这保证了启动流程的完整性。
- 累积性:PCR 值记录了整个度量链的完整历史。
一个小技巧:调试的时候,如果你想验证某个阶段的度量是否正确,可以自己手动模拟 Extend 过程。把已知的 PCR 初始值和度量值按规范拼起来算一次哈希,跟 TPM 读出来的 PCR 值对比。我曾经靠这个办法抓到一个 BIOS 在度量顺序上搞错的 bug。
3.2.2 PCR 的分配与用途
TCG 规范给每个 PCR 索引分配了明确的用途。PC 客户端平台通常有 24 个 PCR(0-23),但常用的就前面几个。
| PCR 索引 | 用途 | 度量内容 |
|---|---|---|
| PCR[0] | BIOS 固件代码 | BIOS/ UEFI 的 Flash 内容、微码补丁 |
| PCR[1] | BIOS 配置 | CMOS 设置、NVRAM 变量 |
| PCR[2] | Option ROM 代码 | 扩展卡上的固件 |
| PCR[3] | Option ROM 配置 | 扩展卡的配置数据 |
| PCR[4] | MBR / GPT 代码 | 主引导记录或 GPT 头 |
| PCR[5] | MBR / GPT 分区表 | 分区表数据 |
| PCR[6] | 状态变更与唤醒 | S3 恢复、平台状态切换 |
| PCR[7] | 安全处理器 | Intel ME / AMD PSP 等 |
这里要注意一点:PCR[0] 和 PCR[1] 的区别。PCR[0] 度量的是「代码本身」,PCR[1] 度量的是「配置」。代码没变但配置变了,PCR[0] 不变,PCR[1] 会变。这在远程证明里很有用——你可以判断对方是不是只改了配置没改固件。
我曾经踩过的坑:有些平台在 S3 唤醒时不会重新度量 PCR[0]-PCR[5],只更新 PCR[6]。如果你在 S3 流程里依赖 PCR[0] 的值做判断,可能会拿到跟冷启动不一样的结果。嗯,这个要特别小心。
3.3 度量事件日志:度量过程的流水账
PCR 只存了最终的哈希值,但没存「谁度量了谁」「度量了什么内容」。事件日志就是干这个的。
你可以把 PCR 想象成账本的总账余额,事件日志就是明细账。光看余额你只知道钱对不对,看明细你才知道每笔钱花哪了。
3.3.1 事件日志的结构
TCG 定义的事件日志是一个二进制结构体数组。每个事件条目包含以下字段:
typedef struct {
UINT32 PCRIndex; // 度量值扩展到了哪个 PCR
UINT32 EventType; // 事件类型(EV_EFI_BOOT_SERVICES_APPLICATION 等)
UINT8 Digest[20]; // 度量值的哈希摘要(SHA1 或 SHA256)
UINT32 EventSize; // 事件数据的长度
UINT8 Event[]; // 事件数据(变长,比如被度量的文件路径)
} TCG_PCR_EVENT;
日志在内存中的布局是连续的。固件每做一次度量,就往日志末尾追加一条记录。
3.3.2 事件类型
事件类型告诉验证者「这次度量的是什么类型的东西」。常见的类型有:
- EV_EFI_BOOT_SERVICES_APPLICATION:度量了一个 UEFI 应用(比如 bootmgfw.efi)。
- EV_EFI_VARIABLE_DRIVER_CONFIG:度量了一个 UEFI 变量。
- EV_EFI_ACTION:记录了一个动作事件(比如「准备启动到 OS」)。
- EV_SEPARATOR:分隔符事件,标记一个阶段的结束。
个人经验:调试日志的时候,我最常用的就是 EV_EFI_ACTION 类型的事件。它不携带具体的度量数据,只记录一个字符串描述。比如 "Calling ExitBootServices" 这种。你可以用它来验证固件的执行流程是否跟预期一致。
3.3.3 日志的获取方式
在 UEFI 环境下,你可以通过 EFI_TCG2_PROTOCOL 的 GetEventLog 函数拿到日志的起始地址和大小。
EFI_STATUS
EFIAPI
GetEventLog (
IN EFI_TCG2_PROTOCOL *This,
IN EFI_TCG2_EVENT_LOG_BITMAP EventLogLocation,
OUT EFI_PHYSICAL_ADDRESS *EventLogLastEntry,
OUT BOOLEAN *EventLogTruncated,
OUT UINTN *LastLogSize
);
到了 OS 里,Linux 下你可以通过 /sys/kernel/security/tpm0/binary_bios_measurements 读取日志。Windows 下可以用 TBS(TPM Base Services)API 来获取。
3.4 三者的关系:一张图说清楚
好,理论讲完了,咱们用一张图把 TCG 规范、PCR 和事件日志的关系串起来。
这张图你看懂了吗?
固件启动时,按照 TCG 规范的要求,对每个要度量的组件(BIOS 代码、Option ROM、引导加载器等等)做哈希计算。哈希值 Extend 到对应的 PCR 里,同时把这次度量的详细信息(度量了谁、度量结果是什么、扩展到了哪个 PCR)追加到事件日志里。
验证者拿到 PCR 值和事件日志后,可以自己重新走一遍 Extend 流程:从日志里取出每次的度量值,按顺序做 Extend,看最终结果是否跟 PCR 值一致。一致,说明度量过程没被篡改;不一致,说明中间有人动了手脚。
注意:验证者不能只看 PCR。PCR 只告诉你「最终值对不对」,但没告诉你「中间度量了哪些东西」。事件日志才是你判断「度量内容是否合理」的依据。我曾经见过一个攻击案例,攻击者把恶意固件刷进去,PCR 值算出来跟正常的一样——因为他在日志里把恶意组件的度量记录删掉了。所以,PCR 和日志必须一起验证。
3.5 小结
这一章我们讲了三个核心概念:
- TCG 规范:度量世界的通用语言,定义了怎么做、存哪、怎么记。
- PCR 寄存器:硬件保险柜,通过 Extend 操作累积存储度量哈希,不可逆、顺序敏感。
- 事件日志:度量过程的流水账,记录了每次度量的详细信息,是验证 PCR 的依据。
这三者缺一不可。没有规范,大家各搞一套;没有 PCR,度量值没地方存;没有日志,你没法验证度量过程是否可信。