密码学基础:UEFI安全启动的基石

说实话,很多做固件的兄弟一听到「密码学」三个字就头大。我当年也一样,总觉得这是搞应用层的人该操心的事。直到有一次,我在调试一个Secure Boot签名验证失败的问题,翻了三天的代码,最后发现是哈希算法选错了——嗯,从那以后,我再也不敢跳过密码学基础了。

这一章,我们不聊太深的理论。我带你过一遍UEFI安全启动真正用到的几个密码学概念。你理解透了,后面搭建环境时就不会懵。

对称加密 vs 非对称加密

先说说最基础的区别。

对称加密,说白了就是加密和解密用同一把钥匙。你想想看,就像你家大门,锁门和开门用的是同一把钥匙。优点是快,特别快。缺点是——你怎么把钥匙安全地交给对方?

非对称加密就解决了这个问题。它有一对钥匙:公钥和私钥。公钥可以公开给任何人,私钥自己藏好。别人用你的公钥加密数据,只有你能用私钥解密。反过来,你用私钥签名,别人用公钥验证签名。

我在项目中遇到过最典型的场景:固件更新包的分发。更新包本身很大,用对称加密(比如AES)加密,速度快。而AES的密钥,再用非对称加密(比如RSA)保护起来。两者结合,既安全又高效。

特性 对称加密 非对称加密
密钥数量 1个(共享密钥) 2个(公钥+私钥)
典型算法 AES, DES, SM4 RSA, ECC, SM2
速度 快(硬件加速常见) 慢(尤其RSA)
UEFI用途 加密ESP分区内容 签名验证、密钥交换
我的建议:在UEFI环境中,非对称加密主要用于签名和验证,而不是加密大量数据。别拿RSA去加密整个固件镜像,那会慢到你怀疑人生。

哈希函数与数字签名

哈希函数,你可以把它理解成一个「数据指纹生成器」。不管输入多大,输出都是固定长度。而且,哪怕只改了一个bit,整个哈希值都会天翻地覆。

UEFI安全启动里,哈希函数有两个核心用途:

  • 完整性校验:验证固件或驱动有没有被篡改
  • 数字签名的组成部分:先对数据做哈希,再对哈希值签名

为什么会这样?因为非对称签名太慢了。你想想看,一个UEFI驱动可能几MB,直接对它做RSA签名,耗时是秒级的。但先算个SHA-256哈希(32字节),再对这32字节签名,快得多。

数字签名的流程是这样的:

  1. 发送方对原始数据计算哈希值
  2. 用私钥加密这个哈希值,得到签名
  3. 接收方收到数据+签名后,自己算一遍哈希
  4. 用公钥解密签名,对比两个哈希值

我曾经踩过一个坑:在某个平台上,Secure Boot验证总是失败。查了半天,发现是签名时用的哈希算法是SHA-1,但平台只支持SHA-256。嗯,从那以后,我所有新项目都统一用SHA-256。

注意:SHA-1已经被证明不安全。UEFI 2.7之后,官方已经不建议使用SHA-1。如果你的平台还只支持SHA-1,赶紧升级固件。

X.509证书与PKI体系

X.509证书,说白了就是一个「数字身份证」。它把公钥和持有者的身份信息绑定在一起,再由一个可信的第三方(CA)来担保这个绑定关系是真的。

一个典型的X.509证书包含:

  • 证书持有者的公钥
  • 持有者信息(比如公司名、域名)
  • 签发者(CA)信息
  • 证书有效期
  • CA的数字签名

PKI(公钥基础设施)就是管理这些证书的整套体系。它包括CA(证书颁发机构)、RA(注册机构)、证书吊销列表(CRL)等等。

在UEFI安全启动中,PKI的层级关系是这样的:

UEFI安全启动PKI证书链 根CA证书 中间CA证书 签发证书(签UEFI驱动/镜像) UEFI驱动/启动镜像 说明: 平台固件中预置根CA证书(PK/KEK/db),验证时从根到叶子逐级校验签名。 任何一级证书无效或过期,整个链都会断裂,启动就会被拒绝。

你可能会问:为什么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我填错了,导致固件解析签名列表时一直报错。嗯,从那以后,我每次构造这个结构都会反复算一遍偏移量。

避坑指南:EFI_SIGNATURE_LIST的SignatureListSize必须包含所有EFI_SIGNATURE_DATA的总大小。如果你有N个签名条目,总大小 = sizeof(EFI_SIGNATURE_LIST) + N × SignatureSize。少一个字节,SetVariable都会返回EFI_INVALID_PARAMETER。

好了,这一章的内容就到这里。密码学基础是UEFI安全启动的根基,后面的章节我们会反复用到这些概念。你先把哈希、签名、证书链和EFI_SIGNATURE_LIST这几个点吃透,后面搭建环境时就会顺手很多。