密码学基础:UEFI安全启动的基石
说实话,很多做固件的兄弟一听到「密码学」三个字就头大。我当年也一样,总觉得这是搞应用层的人该操心的事。直到有一次,我在调试一个Secure Boot签名验证失败的问题,翻了三天的代码,最后发现是哈希算法选错了——嗯,从那以后,我再也不敢跳过密码学基础了。
这一章,我们不聊太深的理论。我带你过一遍UEFI安全启动真正用到的几个密码学概念。你理解透了,后面搭建环境时就不会懵。
对称加密 vs 非对称加密
先说说最基础的区别。
对称加密,说白了就是加密和解密用同一把钥匙。你想想看,就像你家大门,锁门和开门用的是同一把钥匙。优点是快,特别快。缺点是——你怎么把钥匙安全地交给对方?
非对称加密就解决了这个问题。它有一对钥匙:公钥和私钥。公钥可以公开给任何人,私钥自己藏好。别人用你的公钥加密数据,只有你能用私钥解密。反过来,你用私钥签名,别人用公钥验证签名。
我在项目中遇到过最典型的场景:固件更新包的分发。更新包本身很大,用对称加密(比如AES)加密,速度快。而AES的密钥,再用非对称加密(比如RSA)保护起来。两者结合,既安全又高效。
| 特性 | 对称加密 | 非对称加密 |
|---|---|---|
| 密钥数量 | 1个(共享密钥) | 2个(公钥+私钥) |
| 典型算法 | AES, DES, SM4 | RSA, ECC, SM2 |
| 速度 | 快(硬件加速常见) | 慢(尤其RSA) |
| UEFI用途 | 加密ESP分区内容 | 签名验证、密钥交换 |
哈希函数与数字签名
哈希函数,你可以把它理解成一个「数据指纹生成器」。不管输入多大,输出都是固定长度。而且,哪怕只改了一个bit,整个哈希值都会天翻地覆。
UEFI安全启动里,哈希函数有两个核心用途:
- 完整性校验:验证固件或驱动有没有被篡改
- 数字签名的组成部分:先对数据做哈希,再对哈希值签名
为什么会这样?因为非对称签名太慢了。你想想看,一个UEFI驱动可能几MB,直接对它做RSA签名,耗时是秒级的。但先算个SHA-256哈希(32字节),再对这32字节签名,快得多。
数字签名的流程是这样的:
- 发送方对原始数据计算哈希值
- 用私钥加密这个哈希值,得到签名
- 接收方收到数据+签名后,自己算一遍哈希
- 用公钥解密签名,对比两个哈希值
我曾经踩过一个坑:在某个平台上,Secure Boot验证总是失败。查了半天,发现是签名时用的哈希算法是SHA-1,但平台只支持SHA-256。嗯,从那以后,我所有新项目都统一用SHA-256。
X.509证书与PKI体系
X.509证书,说白了就是一个「数字身份证」。它把公钥和持有者的身份信息绑定在一起,再由一个可信的第三方(CA)来担保这个绑定关系是真的。
一个典型的X.509证书包含:
- 证书持有者的公钥
- 持有者信息(比如公司名、域名)
- 签发者(CA)信息
- 证书有效期
- CA的数字签名
PKI(公钥基础设施)就是管理这些证书的整套体系。它包括CA(证书颁发机构)、RA(注册机构)、证书吊销列表(CRL)等等。
在UEFI安全启动中,PKI的层级关系是这样的:
你可能会问:为什么UEFI要用这么复杂的证书链?直接存一个公钥不行吗?
行,但不够灵活。举个例子:你的根CA私钥万一泄露了,怎么办?如果只有一把公钥,你得更新所有设备的固件。但有了PKI,你只需要吊销被泄露的中间CA证书,重新签发一个就行。设备端只需要更新CRL,不用刷固件。
EFI_SIGNATURE_LIST数据结构
好了,终于到了和代码直接相关的地方。EFI_SIGNATURE_LIST是UEFI规范里定义的一个数据结构,用来存储签名或哈希值列表。Secure Boot的PK、KEK、db、dbx这些变量,底层都是用这个结构来组织的。
它的定义长这样:
typedef struct {
EFI_GUID SignatureType; // 签名类型GUID
EFI_GUID SignatureListSize; // 整个结构的总大小
EFI_GUID SignatureHeaderSize; // 头部大小(通常为0)
EFI_GUID SignatureSize; // 单个签名条目的大小
} EFI_SIGNATURE_LIST;
紧跟在头部后面的,是一个或多个EFI_SIGNATURE_DATA:
typedef struct {
EFI_GUID SignatureOwner; // 签名所有者GUID
UINT8 SignatureData[1]; // 签名数据(变长)
} EFI_SIGNATURE_DATA;
我解释一下这几个关键字段:
- SignatureType:告诉你怎么解释后面的数据。常见的值有:
EFI_CERT_SHA256_GUID— 存的是SHA-256哈希值EFI_CERT_X509_GUID— 存的是X.509证书EFI_CERT_RSA2048_GUID— 存的是RSA-2048公钥
- SignatureSize:每个
EFI_SIGNATURE_DATA的大小。如果是SHA-256,就是16字节GUID + 32字节哈希 = 48字节。 - SignatureOwner:标识这个签名是谁加的。比如微软的GUID是
77fa9abd-0359-4d32-bd60-28f4e78f784b。
重点:EFI_SIGNATURE_LIST是一个「容器」,里面可以装多个签名条目。你在UEFI Shell里用db或dbx变量时,看到的其实就是这个结构的序列化数据。
举个例子,假设你要在db变量里添加一个允许启动的SHA-256哈希值:
// 假设我们有一个32字节的哈希值
UINT8 MyHash[32] = {0xAB, 0xCD, ...};
// 计算单个EFI_SIGNATURE_DATA的大小
UINTN SigDataSize = sizeof(EFI_GUID) + 32; // 16 + 32 = 48
// 分配整个EFI_SIGNATURE_LIST + 一个EFI_SIGNATURE_DATA
UINTN TotalSize = sizeof(EFI_SIGNATURE_LIST) + SigDataSize;
EFI_SIGNATURE_LIST *SigList = AllocatePool(TotalSize);
SigList->SignatureType = EFI_CERT_SHA256_GUID;
SigList->SignatureListSize = TotalSize;
SigList->SignatureHeaderSize = 0;
SigList->SignatureSize = SigDataSize;
// 填充第一个签名条目
EFI_SIGNATURE_DATA *SigData = (EFI_SIGNATURE_DATA *)((UINT8 *)SigList + sizeof(EFI_SIGNATURE_LIST));
SigData->SignatureOwner = gEfiGlobalVariableGuid;
CopyMem(SigData->SignatureData, MyHash, 32);
// 然后就可以用SetVariable()写入db变量了
这段代码我当年调试了整整一个下午。为什么?因为SignatureSize我填错了,导致固件解析签名列表时一直报错。嗯,从那以后,我每次构造这个结构都会反复算一遍偏移量。
SignatureListSize必须包含所有EFI_SIGNATURE_DATA的总大小。如果你有N个签名条目,总大小 = sizeof(EFI_SIGNATURE_LIST) + N × SignatureSize。少一个字节,SetVariable都会返回EFI_INVALID_PARAMETER。
好了,这一章的内容就到这里。密码学基础是UEFI安全启动的根基,后面的章节我们会反复用到这些概念。你先把哈希、签名、证书链和EFI_SIGNATURE_LIST这几个点吃透,后面搭建环境时就会顺手很多。