RISC-V端侧安全启动实战:验签链、可信度量与安全世界切换开销
1. 信任根与信任链
安全启动要解决的问题是:设备如何确认即将执行的代码来自可信来源且未被篡改。这个问题无法通过“再运行一段代码去检查”来回答——检查代码本身也可能是伪造的,因此信任必须有一个不可再追问的起点,即信任根(Root of Trust)。
端侧设备的信任根由两部分构成:一是固化在只读存储区、出厂后不可修改的引导代码;二是存放在一次性可编程存储(OTP 或熔丝区)中的公钥摘要或密钥哈希。前者保证引导逻辑本身不可被替换,后者保证验证所用的公钥不可被替换。二者的共同属性是不可写——这是信任根得以成立的前提。
信任链的传递方式是逐环验证:每一级在把控制权交给下一级之前,先验证下一级镜像的完整性与来源。链条从不可修改的引导代码开始,依次覆盖安全固件、引导加载程序、内核、根文件系统,直至模型权重等数据文件。链条中任何一环被跳过,其后所有验证都不再有意义。
2. 验签链的三个维度
镜像验证同时回答三个独立的问题,缺一不可:
- 完整性:镜像内容在传输或存储过程中是否被改动。由摘要算法保证,验证方重新计算摘要并与签名覆盖的摘要比对。
- 真实性:镜像是否由持有私钥的一方签发。由非对称签名保证,镜像头中携带签名与证书链,验证方依信任根中的公钥逐步校验证书链。
- 版本有效性:镜像版本是否低于设备已接受的最低版本。前两项无法阻止“把设备回滚到存在已知漏洞的旧版本”这一攻击,版本校验需要单调计数器的配合。
前两个维度是验签的常规内容,第三个维度最常被忽略。单调计数器存放在一次性可编程区域,每次接受更高版本固件时递增且不可回退,固件镜像头的版本号必须不低于计数器当前值。这一机制的直接代价是:一旦计数器推进,设备将永久无法回退到更早版本,因此生产环境中的计数器更新必须在确认新版本稳定之后进行。
3. 安全启动与度量启动的分工
两者常被混为一谈,但机制与用途完全不同:
-
安全启动在验证失败时阻断启动。它保证设备只运行可信代码,但验证结论是二值的——系统无法向外部证明自己运行的是哪个版本。
-
度量启动在启动过程中记录每一级镜像的摘要,不做阻断。度量值经过哈希扩展写入受保护的寄存器:
新值 = H(旧值 || 本次度量)扩展而非覆盖是关键——度量序列的任何重排都会改变最终值,且写入方无法通过选择性覆盖来伪造历史。
度量日志的用途有两个方向:本地侧可用于密封密钥,即让密钥仅在特定启动度量值下才可解封,从而把密钥与固件版本绑定;远程侧可用于向服务方证明设备的运行状态。端侧设备若需要接入云端服务并证明自身完整性,度量启动是不可省略的一环。
工程上通常两者并用:安全启动保证底线,度量启动提供可证明性。
4. 安全世界的实现路径
可信执行环境在各架构上的实现方式不同。RISC-V 并未像部分架构那样在特权级中预留固定的安全状态位,而是通过多域隔离的方式构造安全执行环境,其技术基础是几项相互配套的隔离机制:
- 物理内存保护:PMP 与其增强扩展 ePMP 从 M 模式一侧限定各执行域可访问的物理地址范围,是域隔离的硬件基础。
- 设备访问隔离:处理器侧的内存保护不覆盖外设发起的访问,因此还需要设备侧的内存保护机制限制 DMA 可达的地址范围,否则设备可绕过处理器写入受保护内存。
- 运行环境:在 S 模式下承载安全域与普通域的运行时,把内存区域、中断源、外设按域划分,由固件在启动时完成配置。
这一路径的特点是隔离粒度由固件配置决定,灵活但需要固件正确实现,任何一处配置遗漏都会使隔离失效。验证隔离是否生效的方法是直接尝试访问而非阅读配置——例如在普通域中读取安全域内存,确认触发访问异常而非成功。
5. 世界切换的开销来自哪里
安全域与普通域之间的切换成本容易被低估。开销集中在三处:
- 上下文保存与恢复:寄存器的保存集与域切换的寄存器约定相关,切换路径本身不承载业务逻辑,其耗时直接计入敏感操作的端到端延迟。
- 地址空间切换的连带代价:域切换通常伴随页表基址变化,随之而来的是地址转换缓存的失效或标记切换,以及分支预测器状态的污染。这部分开销在切换后的若干次访存中持续体现,不是一次性的。
- 参数编组与拷贝:跨域调用不能直接传递指针——安全域不能信任普通域提供的地址。参数与返回数据需要经共享缓冲拷贝并校验,拷贝量随调用接口的数据规模增长。
对端侧推理场景,由此得到一条明确的取舍原则:把整个推理流程放进安全域并不经济。推理过程涉及大量参数读取与中间张量搬运,切换与拷贝开销会显著抬高延迟;更合理的划分是只把密钥运算、许可证校验、隐私数据的解密等少量敏感操作放入安全域,把算力密集的矩阵运算保留在普通域,两者之间以最小化的接口交换数据。
判断划分是否合理的方法是比较“跨域调用次数 × 单次切换成本”与“敏感操作本身耗时”。前者远小于后者时,划分是划算的;若接近,说明切换过于频繁,应合并调用或调整边界。
6. 实战:验证安全启动链
以下操作序列用于确认一条安全启动链是否真正生效(命令为通用示意):
$ dmesg | grep -iE 'verify|signature|secure boot'
# 2) 确认信任根公钥摘要与设备内记录一致
$ firmware_tool read-otp --field pk-hash
$ firmware_tool hash --file /boot/pubkey.pem --expect "$PK_HASH"
# 3) 读取度量日志,确认度量序列完整覆盖各级镜像
$ cat /sys/kernel/security/tpm0/ascii_bios_measurements 2>/dev/null \
|| firmware_tool read-measurements
# 4) 篡改镜像后重启,确认系统拒绝启动(而非静默继续)
$ printf '\x00' | dd of=/boot/kernel.img bs=1 seek=1024 conv=notrunc
$ reboot # 预期:启动中止并给出验证失败提示
# 5) 关闭调试口与开发模式,确认信任根不可被旁路
$ firmware_tool get-debug-state # 预期:closed / production
第 4 步是整条链的判定性验证。若篡改后系统仍能启动,说明验签环节缺失或验证结果未被强制执行——后者在工程中更常见,表现为“验签失败仅打印日志”。
7. 常见问题与成因
- 验签通过但启动失败:镜像格式或加载地址不匹配,与验签环节无关。应核对镜像头声明的加载基址与引导程序的加载约定。
- 篡改镜像后仍可启动:验证结果未被强制执行,或校验范围未覆盖被篡改的字段(如仅校验头部而未覆盖正文)。检查校验覆盖范围比检查算法更重要。
- 度量日志不完整:启动路径存在未被度量的分支,例如从恢复模式或从外部存储加载的镜像。度量应覆盖所有可执行代码的加载路径,包括异常分支。
- 防回滚计数器耗尽:一次性可编程区域的位宽有限,更新次数存在上限。产品规划时需评估全生命周期内的固件更新次数。
- 切换开销导致推理抖动:跨域调用过于频繁。用切换次数与单次成本做乘积估算,据此合并接口或调整划分边界。
- 调试口未关闭导致信任根被旁路:量产设备应关闭调试接口并锁定启动模式,开发阶段保留的开关若未在量产配置中关闭,会使整条链失效。
- 密钥轮换后旧设备无法启动:新镜像由新密钥签发,而设备信任根中的旧公钥无法验证。轮换策略需要保留可验证旧镜像的过渡密钥,或提供带签名的过渡固件。
浙公网安备 33010602011771号