3. TEE安全启动链:信任根、安全启动与回滚保护
安全启动链,说白了就是TEE的「出生证明」和「成长记录」。我刚开始接触TEE时,总觉得这东西离应用层很远。直到有一次,我在项目中遇到一台设备被刷入了篡改过的固件,整个安全域直接沦陷。嗯,从那以后,我再也不敢轻视启动链的任何一个环节了。
这一章,我们就把TEE安全启动链的四个核心模块拆开来看:信任根怎么建立、启动流程怎么走、镜像怎么签名验证、回滚怎么防。你想想看,如果连启动阶段都不安全,后面做的加密传输、证书配置,那都是空中楼阁。
3.1 信任根(RoT):一切安全的起点
信任根,英文叫Root of Trust,简称RoT。它是整个安全体系的「第一块砖」。这块砖必须是硬件级别的,不可篡改的。我个人习惯把RoT比作「保险箱的物理钥匙」——你软件层做得再花哨,钥匙本身被人复制了,一切白搭。
信任根的核心要求:
- 不可变性:出厂后无法通过软件修改
- 唯一性:每颗芯片的RoT都不同(或至少每批次不同)
- 隔离性:普通软件无法直接读取RoT内容
常见的RoT实现方式有几种:
| 实现方式 | 载体 | 特点 | 我见过的坑 |
|---|---|---|---|
| OTP熔丝 | 芯片内部一次性可编程存储 | 成本低,写入后不可改 | 熔丝烧录时电压不稳,导致部分位未烧透 |
| eFuse | 电熔丝阵列 | 可多次少量编程,密度高 | 某厂商eFuse读接口没加保护,被侧信道攻击 |
| PUF | 芯片物理不可克隆函数 | 每次上电生成唯一指纹,无存储 | PUF在温度变化时误码率飙升,需要纠错 |
| 安全元件 | 独立安全芯片 | 隔离性最强,但成本高 | 通信接口没加密,中间人窃取密钥 |
我个人比较推荐eFuse + PUF的组合方案。eFuse存根密钥,PUF生成会话密钥。这样即使eFuse被物理探针读取,攻击者也拿不到完整的密钥材料。
避坑指南: 我曾经遇到一个项目,RoT密钥直接用明文存在Flash里。理由是「反正量产时再烧录,没人能读到」。结果样机流出后,不到一周就被提取了所有密钥。记住:RoT必须硬件隔离,不能依赖「没人能读到」这种假设。
3.2 安全启动流程:逐级验证的链条
安全启动,本质上是一个「链式信任」的过程。从RoT开始,每一级都验证下一级的签名,验证通过才执行。如果任何一级验证失败,启动立即终止。
为什么会这样设计?你想想看,如果让Boot ROM直接加载整个操作系统,那Boot ROM得有多大?而且一旦OS有漏洞,整个安全体系就崩了。所以,我们把它拆成多个阶段,每个阶段只做一件事,只信任上一级。
典型的TEE安全启动流程如下:
- 阶段0(Boot ROM):芯片上电,CPU从ROM中执行第一条指令。这段代码是掩膜在芯片里的,不可修改。它负责加载并验证下一级Bootloader的签名。
- 阶段1(Bootloader 1):验证通过后,Boot ROM将控制权交给Bootloader 1。Bootloader 1通常负责初始化DDR、时钟等基础硬件,然后验证Bootloader 2。
- 阶段2(Bootloader 2):Bootloader 2加载TEE内核镜像,验证其签名。验证通过后,将TEE内核加载到安全内存中。
- 阶段3(TEE内核):TEE内核启动,初始化安全世界的中断、内存管理、加密服务等。然后加载并验证TEE应用(Trusted Applications)。
- 阶段4(REE侧):TEE启动完成后,REE(普通世界)才开始启动。REE的启动过程同样需要验证,但它的信任链可以追溯到TEE。
下面这张图展示了完整的信任链传递过程:
注意: 安全启动不是「越快越好」。我见过有些厂商为了缩短启动时间,跳过了某些阶段的验证。结果呢?攻击者只需要篡改Bootloader 1,就能绕过后续所有验证。每个阶段都必须验证,这是底线。
3.3 镜像签名与验证:数字签名的实战
镜像签名,说白了就是给固件打个「防伪标签」。这个标签用私钥生成,公钥验证。私钥只有厂商持有,公钥可以公开。这样,任何人都能验证固件是否来自厂商,是否被篡改过。
签名流程通常如下:
- 计算哈希:对镜像文件计算SHA-256(或更安全的SHA-384/512)哈希值。
- 签名:用厂商私钥对哈希值进行RSA或ECDSA签名。
- 打包:将签名、公钥证书(或公钥哈希)、镜像文件打包成一个固件包。
验证流程则是反过来:
- 提取签名和公钥:从固件包中提取签名和公钥信息。
- 验证公钥:用RoT中存储的根公钥验证当前公钥的合法性(如果是证书链,则逐级验证)。
- 计算哈希:对镜像文件计算哈希值。
- 验签:用公钥验证签名是否匹配。
下面是一个简化的签名验证代码示例(伪代码):
// 签名过程(在安全的生产环境中执行)
function sign_image(image_data, private_key):
hash = SHA256(image_data)
signature = RSA_sign(private_key, hash)
return package(image_data, signature, public_key_cert)
// 验证过程(在设备启动时执行)
function verify_image(package_data, root_public_key):
image_data, signature, cert = unpack(package_data)
// 验证证书链
if not verify_cert_chain(cert, root_public_key):
return FAIL
// 提取公钥
image_public_key = extract_public_key(cert)
// 计算哈希并验签
hash = SHA256(image_data)
if not RSA_verify(image_public_key, signature, hash):
return FAIL
return SUCCESS
经验之谈: 我曾经在项目中遇到一个「签名验证通过但镜像被篡改」的诡异问题。查了两天才发现,是签名算法使用了PKCS#1 v1.5填充,攻击者利用填充漏洞伪造了签名。从那以后,我坚持使用PSS填充模式。记住:算法选型很重要,别用已经被攻破的旧标准。
3.4 回滚保护机制:防降级攻击
回滚攻击,也叫降级攻击。攻击者把设备上的固件版本降级到有漏洞的旧版本,然后利用旧版本的漏洞攻入系统。你想想看,如果安全启动只验证签名不验证版本,那攻击者完全可以拿一个有签名的旧版固件刷进去。
回滚保护的常见实现方式:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 版本计数器 | 在RoT中维护一个单调递增的计数器 | 实现简单,硬件开销小 | 计数器耗尽后无法升级 |
| 版本号+签名 | 版本号作为签名内容的一部分 | 灵活,可回退到指定版本 | 需要维护版本号列表 |
| 安全存储+版本表 | 在安全存储中记录已安装的版本列表 | 支持多版本管理 | 安全存储空间有限 |
我个人比较推荐「版本计数器 + 版本号签名」的组合方案。计数器防止无限降级,版本号签名确保每个版本都是厂商授权的。具体做法是:
- 在eFuse中维护一个4位的版本计数器(支持16次升级)
- 每次升级时,新固件的版本号必须大于等于当前计数器的值
- 版本号作为签名内容的一部分,防止篡改
- 计数器只能递增,不能递减
血的教训: 我曾经参与过一个项目,回滚保护只检查了版本号,没检查版本号的签名。结果攻击者把版本号改成「9999」,然后刷入一个旧版固件。系统一看版本号比当前大,直接放行。嗯,那周的加班餐全是泡面味。
3.5 本章小结
安全启动链是TEE安全的基石。信任根提供不可篡改的起点,逐级验证确保每一级都可信,签名验证防止镜像被篡改,回滚保护防止降级攻击。这四个环节缺一不可。
在实际项目中,我建议你从「最坏情况」出发来设计启动链:假设攻击者已经物理接触设备,假设所有外部存储都不可信,假设网络通信被完全监听。只有在这种假设下还能保证安全的方案,才是真正可靠的方案。
核心要点回顾:
- 信任根必须硬件实现,不可软件修改
- 安全启动是链式验证,每一级都不能跳过
- 签名算法选择PSS填充,避免PKCS#1 v1.5
- 回滚保护必须结合版本计数和签名验证
- 所有验证失败必须终止启动,不能降级处理