4. SM2密钥对生成:私钥生成与公钥计算
好,咱们进入实战环节。密钥派生第一步,就是先生成一对SM2密钥。这一步看似基础,但在TEE环境里做,坑其实不少。我最早做TEE应用时,就因为在随机数获取上偷了个懒,结果被安全审计打了个回票……嗯,从那以后我再也不敢轻视这个环节了。
4.1 私钥生成:随机数与域校验
SM2的私钥,说白了就是一个满足特定范围的大整数。这个范围由SM2的椭圆曲线参数决定。咱们先看看私钥长什么样:
// SM2推荐曲线参数(部分)
// 素数域阶 n = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123
// 私钥 d 的取值范围:[1, n-2]
私钥生成分两步走:
- 获取安全随机数:从TEE内部的真随机数发生器(TRNG)读取字节序列
- 域校验:检查随机数是否在合法范围内,不在就重试
核心要点:私钥必须满足 1 ≤ d ≤ n-2。为什么是n-2而不是n-1?因为公钥计算时如果d=n-1会导致点乘结果异常,SM2标准特意留了这个安全边界。
4.2 TEE内安全随机数获取
这是整个密钥派生的基石。普通Linux下用/dev/urandom,但在TEE里不行——你得调用TEE内部API。我个人习惯用GP TEE Internal API的TEE_GenerateRandom接口:
// TEE内获取安全随机数示例
TEE_Result get_secure_random(uint8_t *buf, size_t len) {
// 调用TEE内部TRNG
TEE_GenerateRandom(buf, len);
// 注意:有些TEE实现需要额外做熵源校验
// 我在某款SoC上遇到过TRNG输出全0的硬件bug
// 所以建议加一道简单的非零检查
for (size_t i = 0; i < len; i++) {
if (buf[i] != 0) break;
if (i == len - 1) return TEE_ERROR_BAD_STATE;
}
return TEE_SUCCESS;
}
我曾经踩过的坑:某次在ARM TrustZone上开发,TEE_GenerateRandom返回的数据竟然有周期性模式。后来发现是厂商的TRNG驱动没做在线自检。从那以后,我每次拿到新平台都会先跑一遍NIST SP 800-22随机性测试,确保TRNG靠谱。
4.3 公钥计算:点乘运算
私钥有了,公钥就是私钥乘以基点G。SM2的基点G坐标是固定的:
// SM2基点G(十六进制)
Gx = 0x32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7
Gy = 0xBC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0
点乘运算 P = d * G,说白了就是重复做椭圆曲线上的加法。但直接循环加效率太低,实际用双倍-加算法:
// 双倍-加点乘算法(简化版)
ECC_Point scalar_mult(const uint8_t *d, const ECC_Point *G) {
ECC_Point Q = {0}; // 无穷远点
ECC_Point R = *G;
// 从最高位开始扫描
for (int i = 255; i >= 0; i--) {
Q = point_double(&Q); // 倍点
if (d[i/8] & (1 << (i%8))) {
Q = point_add(&Q, &R); // 加点
}
}
return Q;
}
性能提示:在TEE里做点乘,建议用固定时间实现。为什么?因为算法执行时间如果和私钥比特位相关,攻击者能通过时序分析反推私钥。我见过一个产品就因为没注意这个,被侧信道攻击破解了。
4.4 完整密钥对生成流程
把上面几步串起来,就是完整的密钥对生成流程。我习惯用这张图来跟团队对齐:
4.5 密钥对验证
生成完别急着用,先验证。SM2标准要求密钥对必须通过以下检查:
| 检查项 | 验证方法 | 我在项目中遇到的坑 |
|---|---|---|
| 私钥范围 | 1 ≤ d ≤ n-2 | 某次d=0没拦住,点乘直接崩了 |
| 公钥非无穷远点 | P ≠ O(无穷远点) | 概率极低,但遇到一次就够呛 |
| 公钥在曲线上 | 代入曲线方程验证 | 点乘库有bug时能提前发现 |
| 公钥阶验证 | n * P = O | 计算量大,生产环境可选 |
我的习惯:在TEE里生成密钥对后,我会立即用公钥加密一段已知数据,再用私钥解密。如果加解密能正确还原,说明密钥对没问题。这一步虽然多花几毫秒,但能避免很多诡异的线上故障。
4.6 安全存储与导出策略
密钥对生成后,私钥必须留在TEE内,公钥可以导出到REE侧。我见过有些方案把私钥用TEE内部密钥加密后存到REE文件系统——嗯,这其实违背了TEE的安全模型。正确的做法是:
- 私钥:存储在TEE安全存储区,用TEE内部密钥加密
- 公钥:可以明文导出,但建议加签名防篡改
- 密钥句柄:REE侧只持有句柄,不接触私钥明文
我曾经犯过的错:早期一个项目里,我把私钥用TEE_GenerateRandom生成的密钥加密后存到REE侧。后来发现TEE_GenerateRandom在某些场景下会返回弱随机数,导致加密密钥可预测。教训是:TEE内的密钥派生必须用专门的密钥派生函数,不能直接用随机数当密钥。
好了,密钥对生成这块就这些。记住:随机数质量决定安全下限,域校验决定正确性,点乘实现决定性能。三者缺一不可。