3. 安全启动的密码学基础:PKI、数字签名与哈希算法
说实话,很多做固件的兄弟一听到「密码学」三个字就头大。我当年也一样,觉得那是搞安全的人的事。直到我在一个项目里,因为没搞懂哈希链的校验顺序,导致安全启动在量产阶段大面积失败……嗯,从那以后我老老实实把这块啃透了。
安全启动说白了,就是一套「验身」流程。UEFI 固件要确保自己加载的每一个模块、每一个驱动、甚至操作系统内核,都是经过授权的、没有被篡改过的。那怎么证明「没被篡改」?靠的就是密码学里的三个核心工具:哈希算法、数字签名、以及公钥基础设施(PKI)。
一句话总结:哈希保证完整性,签名保证来源可信,PKI 保证签名本身可被验证。三者缺一不可。
3.1 哈希算法:安全启动的「指纹」
哈希算法,你可以把它理解成一个「数字指纹」。不管输入是一行代码还是一个 4MB 的 EFI 驱动,哈希算法都会给你一个固定长度的输出——比如 SHA-256 就是 32 字节。而且这个输出是唯一的:哪怕你只改了一个 bit,哈希值就会变得面目全非。
在安全启动里,哈希主要干两件事:
- 完整性校验:固件在加载一个镜像之前,先算它的哈希,然后跟签名里记录的哈希比对。对不上?说明被改过了,直接拒绝加载。
- 白名单匹配:UEFI 的 db(允许数据库)里可以存哈希值。如果某个镜像的哈希匹配 db 里的记录,即使没有签名也能被信任。这个机制常用于一些老旧或特殊的驱动。
我个人习惯把哈希比作「防篡改封条」。你想想看,如果快递箱子的封条是完好的,你基本能确认箱子没被打开过。哈希就是那个封条。
避坑指南:我曾经在一个项目里,发现安全启动偶尔会失败。查了两天才找到原因——有人把 SHA-1 和 SHA-256 的哈希长度搞混了,导致比对时数据截断。记住:UEFI 安全启动现在强制要求 SHA-256,SHA-1 已经被标记为不安全,别再用。
3.2 数字签名:谁来证明「你是谁」
哈希只能告诉你「东西没变」,但不能告诉你「这东西是谁写的」。数字签名就是来解决这个问题的。
数字签名的原理其实不复杂:
- 发布者用私钥对镜像的哈希值进行加密,生成签名。
- 验证者用发布者的公钥解密签名,得到哈希值 A。
- 验证者自己算一遍镜像的哈希值 B。
- 如果 A == B,说明镜像确实是私钥持有者签发的,而且没被篡改。
在 UEFI 安全启动里,签名通常采用 PKCS#7 格式。你可以在一个 .efi 文件的末尾看到签名数据——它被附加在 PE 镜像的尾部,UEFI 固件在加载时会自动解析。
// 一个典型的签名验证流程(伪代码)
EFI_STATUS VerifyImageSignature(EFI_PE_IMAGE *Image) {
// 1. 从镜像中提取签名数据(PKCS#7)
PKCS7_SIGNATURE *Sig = ExtractPKCS7(Image);
// 2. 从数据库中查找对应的公钥
EFI_SIGNATURE_LIST *KeyDB = GetDB("db");
PUBLIC_KEY *PubKey = FindMatchingKey(KeyDB, Sig->Issuer);
// 3. 用公钥验证签名
if (!RsaVerifyPkcs1(PubKey, Sig->Digest, Image->Hash)) {
return EFI_SECURITY_VIOLATION;
}
return EFI_SUCCESS;
}
注意:签名验证的成败,完全取决于公钥是否可信。如果攻击者把自己的公钥写进了 db,那他签名的恶意驱动也能通过验证。所以,db 的写入权限必须被严格保护——这就是 PKI 要管的事。
3.3 PKI:信任的「金字塔」
PKI(公钥基础设施)是一套管理公钥的体系。它解决了一个核心问题:你怎么知道手里的公钥是真的?
在 UEFI 安全启动里,PKI 的层级结构非常清晰:
- PK(平台密钥):最顶层的根密钥。它由设备制造商持有,用来签署 KEK。PK 的私钥一旦丢失,整个信任链就崩塌了。
- KEK(密钥交换密钥):用来签署 db 和 dbx 的更新。操作系统厂商(比如 Microsoft)会持有 KEK。
- db(允许数据库):存储受信任的公钥、签名或哈希。只有 db 里的实体才能被加载。
- dbx(禁止数据库):存储被吊销的签名或哈希。一旦某个签名进了 dbx,对应的镜像就永远无法启动了。
这个结构,说白了就是一个「信任链」。从 PK 到 KEK 到 db,层层签名,环环相扣。只要根是可信的,整条链就是可信的。
3.4 三者在安全启动中的协作
你可能会问:哈希、签名、PKI,它们在实际启动过程中是怎么配合的?我画个流程你就明白了。
当 UEFI 固件加载一个 .efi 驱动时,大致会走这么几步:
- 提取签名:从 PE 镜像的尾部读取 PKCS#7 签名数据。
- 查找公钥:在 db 里搜索签名者的证书。如果找不到,直接拒绝。
- 哈希比对:用公钥解密签名,得到原始哈希值。同时自己算一遍镜像的 SHA-256 哈希。
- 结果判定:两个哈希一致?通过。不一致?拒绝。另外还要检查 dbx——如果签名或哈希在 dbx 里,即使哈希一致也拒绝。
关键点:安全启动的验证是「先查 dbx,再查 db」。也就是说,先确认这个镜像没有被吊销,再确认它是否被信任。顺序搞反了会有安全漏洞——我曾经见过一个实现,先查 db 再查 dbx,结果一个被吊销的驱动因为还在 db 里,反而被放行了。
3.5 实际项目中的常见坑
做安全启动开发,有几个地方特别容易出问题。我列出来,你以后遇到了可以少走弯路:
| 问题场景 | 原因 | 解决方案 |
|---|---|---|
| 镜像签名后无法启动 | 签名时使用了错误的哈希算法(如 SHA-1) | 统一使用 SHA-256,并在签名工具中明确指定 |
| 更新 db 后新驱动仍被拒 | KEK 签名验证失败,db 更新未被采纳 | 检查 KEK 证书是否在 PK 的信任链中 |
| dbx 更新后旧驱动仍能启动 | 固件未在启动时重新加载 dbx | 确保 dbx 更新后触发一次安全启动重置 |
| 跨平台签名不兼容 | 不同厂商的签名格式有细微差异 | 使用标准的 PKCS#7 格式,避免厂商扩展 |
我的习惯:每次做安全启动的签名验证测试,我都会准备三个镜像:一个正常签名的、一个没签名的、一个签名但哈希被改过的。三个都跑一遍,能覆盖 90% 的边界情况。
3.6 小结
哈希、签名、PKI,这三样东西是安全启动的基石。你不需要成为密码学专家,但必须理解它们是怎么配合的。哈希保证「没被改」,签名保证「是谁签的」,PKI 保证「这个谁可信」。三者形成一个闭环,缺一个,安全启动就是纸糊的。
嗯,这一章的内容就到这里。记住:在 UEFI 的世界里,信任不是天生的,是算出来的。