3. SM2密钥生成与TEE安全存储:在TEE内生成SM2密钥对、安全存储私钥、防止侧信道攻击
这一章,我们聊聊密钥的“出生”和“藏身之处”。
SM2算法本身是公开的,但密钥一旦泄露,整个安全体系就垮了。我见过不少项目,算法选得对,协议设计得也漂亮,最后却栽在密钥管理上——私钥被明文导出、被调试接口偷走、或者被侧信道攻击一点点“嗅”出来。
在TEE里做密钥生成和存储,核心就三件事:生成过程可信、存储位置安全、使用过程抗泄漏。下面我一个个拆开讲。
3.1 为什么必须在TEE内生成密钥?
有人会问:“我在REE(普通世界)生成密钥,然后导入TEE,行不行?”
我的回答是:尽量不要。
为什么?因为密钥在REE内存中出现过,哪怕只有一瞬间,都可能被恶意应用或内核模块读到。我在一次安全审计中就遇到过这种情况——厂商在REE里用OpenSSL生成SM2密钥对,然后通过共享内存传给TEE。结果攻击者只需要一个简单的内存dump工具,就能在传输间隙把私钥抓走。
正确的做法是:密钥的“出生地”必须是TEE。私钥从生成到销毁,永远不离开TEE的安全边界。
核心原则:SM2私钥的生命周期必须完全在TEE内闭环。生成、存储、使用、销毁,任何一个环节离开TEE,安全假设就崩塌了。
3.2 TEE内SM2密钥生成流程
在TEE里生成SM2密钥对,流程和标准SM2一样,但多了几个安全步骤。我习惯把它分成五个阶段:
- 熵源获取:从TEE硬件随机数生成器(TRNG)获取256位随机数。注意,不能用REE的随机数,那可能被预测。
- 私钥生成:用随机数作为SM2私钥 dA(范围 [1, n-2])。
- 公钥计算:计算 PA = [dA]G,G是SM2椭圆曲线基点。
- 密钥对校验:验证 dA 是否在合法范围,验证 PA 是否在曲线上。
- 安全绑定:将私钥与TEE身份(如TA UUID、设备唯一ID)绑定,防止密钥被迁移到其他设备。
嗯,这里要注意第五步。很多方案只做到第四步就结束了,但我在实际项目中吃过亏——攻击者把TEE里的私钥导出到另一个设备的TEE里,照样能用。所以绑定这一步,不能省。
// 伪代码:TEE内SM2密钥生成(简化版)
uint8_t random[32];
tee_trng_get_random(random, 32); // 从TEE硬件TRNG获取随机数
sm2_bn_t dA;
sm2_bn_from_bytes(dA, random); // 转为大数
sm2_bn_mod_n(dA, dA); // 模n,确保在[1, n-2]
sm2_point_t PA;
sm2_point_mul(&PA, &G, dA); // PA = dA * G
// 校验
if (!sm2_bn_is_valid(dA) || !sm2_point_on_curve(&PA)) {
tee_panic("Key generation failed");
}
// 绑定到当前TA
tee_key_bind_to_ta(dA, ta_uuid);
3.3 私钥的安全存储方案
密钥生成后,不能一直放在内存里。TEE虽然比REE安全,但也不是绝对保险——比如TEE内存可能被物理攻击(探针、冷启动)。所以私钥需要加密存储。
我常用的存储方案分三层:
| 层级 | 存储介质 | 保护方式 | 用途 |
|---|---|---|---|
| 第一层 | TEE安全内存(SRAM) | 硬件隔离,CPU访问控制 | 运行时私钥明文(短暂存在) |
| 第二层 | TEE安全存储(RPMB/Replay Protected Memory Block) | 用设备密钥加密后存储 | 持久化存储私钥密文 |
| 第三层 | REE文件系统(可选) | 用TEE主密钥加密后外包 | 大容量备份,但依赖TEE远程认证 |
说白了,第二层是主力。RPMB是eMMC/UFS的一个分区,有硬件防重放机制。TEE把私钥用设备唯一密钥(比如eFuse中烧录的密钥)加密后,写入RPMB。读取时,只有同一个TEE才能解密。
我的经验:加密私钥时,一定要带上额外的认证数据(比如TA ID、密钥用途标签)。这样即使密文被篡改或复制到其他TA,也无法解密。我曾经见过一个方案没加认证数据,结果攻击者把A TA的加密私钥替换到B TA里,绕过了访问控制。
3.4 侧信道攻击与防御
侧信道攻击,是TEE里最容易被忽视的威胁。你想想看,攻击者可能拿不到你的私钥明文,但他能测量功耗、电磁辐射、执行时间、甚至缓存命中率,然后反推出私钥的每一位。
SM2的核心运算是椭圆曲线标量乘法 [k]P。如果k的每一位处理时间不同,或者分支路径不同,攻击者就能通过时序分析还原k。我在一次攻防演练中,就亲眼看到团队用简单的时序分析,在20分钟内还原了一个没有防护的SM2私钥。
防御手段主要有三种:
- 常数时间实现:所有路径的执行时间、功耗曲线必须一致。比如标量乘法用Montgomery ladder,不依赖k的比特位做条件分支。
- 随机化盲化:在计算前,给私钥或基点加上随机数盲化因子。比如计算 [k]P 时,先算 [k + r*n]P,再减去 [r*n]P,让攻击者无法关联到真实的k。
- 硬件隔离:利用TEE的硬件安全特性,比如ARM TrustZone的L1缓存隔离、Intel SGX的MEE(内存加密引擎),防止缓存侧信道。
// 常数时间标量乘法(Montgomery ladder 伪代码)
// 确保循环次数固定,不依赖k的比特值
sm2_point_t montgomery_ladder(sm2_bn_t k, sm2_point_t P) {
sm2_point_t R0 = O; // 无穷远点
sm2_point_t R1 = P;
for (int i = 255; i >= 0; i--) {
if (sm2_bn_get_bit(k, i) == 0) {
R1 = point_add(R0, R1); // 无论比特位,都执行相同操作
R0 = point_double(R0);
} else {
R0 = point_add(R0, R1);
R1 = point_double(R1);
}
}
return R0;
}
注意:常数时间实现不是万能的。即使代码是常数时间,CPU的微架构特性(如分支预测、缓存行填充)仍可能引入侧信道。所以最好结合硬件隔离和随机化盲化,做纵深防御。
3.5 整体架构图
下面这张图,把密钥生成、存储、使用、防侧信道的流程串起来了。你可以看到,私钥从出生到退休,始终在TEE的“保护罩”里。
3.6 避坑指南
最后,我把自己踩过的坑整理一下,希望能帮你少走弯路。
- 不要复用随机数:SM2签名需要随机数k,如果k重复或可预测,私钥会被直接算出。我在一个IoT项目里见过,厂商为了省时间,把随机数写死在代码里——结果签名算法变成了确定性的,私钥被秒破。
- 注意密钥版本管理:密钥更新时,旧密钥要彻底擦除。TEE安全存储的擦除操作,要确认物理介质真的覆写了,而不是只删了文件索引。
- 警惕调试接口:TEE的调试通道(比如JTAG、串口)在量产时必须关闭。我见过一个方案,调试口没熔断,攻击者直接通过JTAG读出了TEE内存里的私钥。
- 不要迷信“TEE内就安全”:TEE只是缩小了攻击面,不是万能盾牌。侧信道、物理攻击、软件漏洞(如TA内存越界)仍然可能突破防线。纵深防御才是正道。
我的习惯:每次做完密钥生成和存储的代码,我都会做两件事:一是用形式化工具验证常数时间属性,二是做实际的侧信道测试(比如用示波器采功耗曲线)。纸上谈兵没用,跑一遍数据才放心。
好了,这一章的内容就到这里。密钥生成和存储是TEE安全方案的基石,基础打牢了,后面的协议设计、应用开发才能站得住脚。