4. 密钥派生场景:密钥协商中的派生、加密中的派生、签名中的派生
密钥派生,说白了就是「一把钥匙开多把锁」。SM2 的私钥只有一个,但实际应用中我们需要多个密钥——加密密钥、签名密钥、会话密钥。总不能每次都拿主私钥去干活吧?那太危险了。
我个人习惯把派生场景分成三类:密钥协商中的派生、加密中的派生、签名中的派生。这三者的技术逻辑完全不同,但都依赖同一个核心工具——KDF(密钥派生函数)。
核心原则:主私钥永远不直接用于业务加密或签名。派生出来的子密钥用完即弃,或者按生命周期管理。
4.1 密钥协商中的派生
密钥协商,就是双方在公开信道上「聊」出一个共享密钥。SM2 的密钥协商协议基于 ECDH,但比标准 ECDH 多了一步——身份认证。
我记得第一次实现 SM2 密钥协商时,踩过一个坑:派生出来的密钥长度不对。后来才发现,KDF 的输入参数里漏掉了双方的 ID。嗯,这里要注意。
4.1.1 协商流程中的派生点
- 步骤一:双方交换临时公钥 RA、RB
- 步骤二:各自计算共享点 U = dA * RB(或 dB * RA)
- 步骤三:将共享点的 x 坐标、双方 ID、公钥等信息拼接
- 步骤四:调用 KDF 派生出会话密钥
你想想看,如果派生时不带身份信息,中间人就能伪造协商。SM2 协议强制要求把 IDA 和 IDB 塞进 KDF 输入,这就是防伪的关键。
实战建议:协商派生出的会话密钥通常只存活一次会话。我曾经在项目中看到有人把会话密钥存到数据库里重复用——这等于把临时房卡当永久钥匙用,风险极大。
4.1.2 派生参数对比
| 参数 | 说明 | 是否必选 |
|---|---|---|
| 共享点 x 坐标 | ECDH 计算出的共享密钥材料 | 是 |
| 双方 ID | 身份标识,防中间人 | 是 |
| 临时公钥 | RA、RB | 是 |
| 协商长度 | 期望输出的密钥比特数 | 是 |
4.2 加密中的派生
SM2 加密用的是「混合加密」——椭圆曲线加密 + 对称加密。公钥加密的只是对称密钥,真正的数据用 SM4 或 AES 加密。那对称密钥从哪来?派生来的。
加密场景的派生,我个人觉得是最容易出 bug 的地方。为什么?因为加密时派生一次,解密时还要派生一次,两次的输入参数必须完全一致。差一个字节,解密就炸了。
4.2.1 加密派生的标准流程
- 生成临时密钥对 (r, R)
- 计算共享点 P = r * QB(QB 是接收方公钥)
- 取 P 的 x 坐标作为 KDF 输入
- KDF 输出 t 字节的对称密钥
- 用对称密钥加密明文
避坑指南:我曾经遇到一个案例,加密方用了 128 位密钥,解密方却默认派生 256 位。结果前 128 位对得上,后 128 位全是乱码。KDF 的输出长度必须在两端显式约定,不能靠「默认」。
4.2.2 派生长度与安全等级
| 对称算法 | 推荐派生长度 | 说明 |
|---|---|---|
| SM4 | 128 位 | 国密标准对称算法 |
| AES-128 | 128 位 | 兼容国际标准 |
| AES-256 | 256 位 | 更高安全等级 |
4.3 签名中的派生
签名场景的派生,和前面两个不太一样。签名不需要共享密钥,也不需要对称加密。那派生用在哪?
答案是:派生随机数 k。SM2 签名的安全性高度依赖 k 的随机性和不可预测性。如果 k 被预测或重复使用,私钥就能被算出来。嗯,这就是著名的「k 重用攻击」。
4.3.1 确定性派生 vs 随机派生
- 随机派生:每次签名用真随机数生成器产生 k。优点是安全,缺点是依赖硬件熵源。
- 确定性派生:用 HMAC_DRBG 或类似算法,从私钥和消息哈希派生出 k。优点是无需熵源,缺点是实现复杂。
我个人更倾向确定性派生。为什么?因为嵌入式设备上真随机数生成器经常「罢工」,而确定性派生只要种子不变,结果就是确定的——便于调试和复现。
关键点:签名中的派生不是为了生成「密钥」,而是为了生成「随机数」。但底层用的还是 KDF 或 DRBG 机制。
4.4 三种派生的核心差异
我把三种场景的差异整理成一张表,方便你对照。
| 场景 | 派生目标 | 输入材料 | 输出用途 | 安全风险 |
|---|---|---|---|---|
| 密钥协商 | 会话密钥 | 共享点 + 身份信息 | 对称加密通信 | 身份伪造、重放攻击 |
| 加密 | 对称密钥 | 共享点 x 坐标 | 加密明文数据 | 派生参数不一致 |
| 签名 | 随机数 k | 私钥 + 消息哈希 | 生成签名值 | k 重用导致私钥泄露 |
4.5 派生场景全景图
下面这张图展示了三种派生场景在 SM2 协议栈中的位置和关系。我建议你把它当作「路线图」——每次实现派生逻辑时,先看看自己站在哪个节点上。
4.6 实战中的派生注意事项
最后,分享几个我在项目中积累的经验。这些坑,我基本都踩过一遍。
- 参数一致性:加密和解密、协商双方,KDF 的输入参数必须完全一致。我曾经因为字符串编码不一致(UTF-8 vs GBK)导致派生结果不同,排查了两天。
- 长度约定:派生长度必须在协议层显式约定。不要依赖「默认值」,不同库的默认值可能不同。
- 随机数质量:签名场景的 k 值,如果使用随机派生,务必确认随机数生成器已经初始化。嵌入式设备上,熵池耗尽是个真实存在的风险。
- 密钥生命周期:派生出的子密钥要有明确的过期时间。会话密钥用完即焚,对称密钥按数据生命周期管理。
我的习惯:每次实现派生逻辑时,先写一个「参数清单」,把 KDF 的所有输入参数列出来,两端逐一核对。这个习惯帮我避免了很多线上事故。