SM2密钥协商协议详解:通信流程与密钥派生函数

说实话,密钥协商这个环节,协议设计上可以说是“算得越复杂,坑就越多”。 我之前在一个物联网安全项目里,就是因为密钥派生函数的实现细节没抠死,导致两边算出来的会话密钥不一致,排查了两天才发现是公钥点转换时少了一个坐标。嗯,咱们今天就好好聊聊SM2密钥协商的整个流程,顺便把密钥派生函数这块彻底掰扯清楚。

核心通信流程:两轮交互就够了

SM2密钥协商协议一共只涉及两轮消息交互。说白了,就是发起方先发一个临时公钥,响应方收到后自己也算一个临时公钥,然后双方各自在本地派生出会话密钥。整个过程不需要显式的身份认证证书交换(当然你可以在上层加),但协议本身已经内置了对双方身份的绑定。

我习惯把流程分成三个阶段:准备阶段、交换阶段、推导阶段。下面这张图能帮你一眼看明白。

SM2密钥协商两轮交互流程 发起方 (A) 响应方 (B) ① 生成临时私钥 rA 计算临时公钥 RA = rA·G ① 生成临时私钥 rB 计算临时公钥 RB = rB·G RA, IDA (第一轮) ② 校验RA, 计算点U = h·rB·(RA + tA·PA) K = KDF(xU||yU||ZA||ZB, klen) 生成S = Hash(xU||...) RB, SB (第二轮) ③ 校验RB,计算点V = h·rA·(RB + tB·PB) K = KDF(xV||yV||ZB||ZA, klen) 验证SB, 生成SA SA (可选确认) 会话密钥K可用 会话密钥K可用

参数解读:不仅仅是公钥私钥

你可能注意到了,流程里出现了 tA、tB、ZA、ZB 这些不太眼熟的符号。我来逐个解释一下。说白了,它们就是双方身份信息的加密“指纹”。

符号 含义 计算方式
tA 发起方 A 的临时秘密因子 tA = (dA + rA) mod n,其中 dA 是静态私钥,rA 是临时私钥
PA 发起方的静态公钥 PA = dA · G
ZA 发起方身份哈希值(含椭圆曲线参数) ZA = H(ENTLA || IDA || a || b || xG || yG || xA || yA)
h 余因子(SM2推荐曲线 h=1) 固定值,用于抵抗小群子攻击

这里要注意一个细节:ZA 和 ZB 的拼接顺序在两边是不对称的。 发起方计算 K 时使用的是 xU || yU || ZA || ZB,而响应方则是 xV || yV || ZB || ZA。嗯,这一点我在早期实现时吃过亏:两边把顺序写反了,结果密钥一直对不上。后来翻国标(GM/T 0003.4)才发现,这是协议故意设计的,用来防止镜像攻击。

密钥派生函数 KDF:把点变成密钥

密钥派生函数(KDF)是SM2协商里最容易踩坑的地方之一。 它的输入是一个椭圆曲线点的 x、y 坐标(以及双方身份哈希 ZA、ZB),输出是固定长度的会话密钥。

具体算法是这样的:

输入:字节串 Z(由 x||y||ZA||ZB 拼接而成),目标密钥长度 klen(比特位)
输出:密钥 K(长度为 klen 的比特串)

步骤:
1. 令 n = ceil(klen / v),v = 256(SM3 输出长度)
2. 对于 i = 1 到 n:
   - 计算 Hai = SM3(Z || i 的32位大端编码)
3. 如果 klen 正好是 v 的整数倍:
   - K = Ha1 || Ha2 || ... || Han
   否则:
   - 取前 n-1 个完整哈希,取第 n 个哈希的高 (klen mod v) 位
   - 丢弃第 n 个哈希的剩余位

我建议你在实现时,一定要对 “不满一整块” 的情况做边界测试。之前有个第三方库,在 klen 不是 256 的倍数时,截取位数写错了一位,导致密钥的最高一个字节全部为 0。这种 bug 在单元测试里很难暴露,但放到实际通信中就会造成偶发性的密钥协商失败。

个人习惯: 我通常会在 KDF 输入中显式拼接一个“协议版本号”字节(比如 0x01),这样以后协议升级时,不同版本的密钥天然不同,不会产生交叉干扰。这属于“防御性编程”,国标里没要求,但项目实践中很管用。

校验与确认:你算的不一定是我算的

协议里还有一个容易被忽略的环节——互校验。发起方收到响应方的 RB 和 SB 后,要验证 RB 是否在曲线上、是否是非无穷远点。验证通过后,自己算出 K,再用 SM3 对某些参数做哈希得到期望的 SB',跟收到的 SB 对比。只有完全一致,才认为密钥协商成功。

响应方那边也是类似,在第二轮会收到 SA(如果发起方发的话)。不过第三轮是可选的,有些应用场景为了减少一次交互会不发送 SA,但代价是无法在通信前证明发起方确实算对了密钥。我在一个金融安全项目中,被客户要求加入第三轮确认,因为他们要满足“不可否认性”审计——你想想看,万一出事,有签名就能追溯。

边界权限控制的引入点

好了,讲到这里你可能要问:密钥协商跟边界权限控制有什么关系? 关系大了。SK(会话密钥)只有在协商成功后才会在双方内存中短暂存在。如果攻击者能通过某种方式“劫持”协商过程,让两边各自算出了一个不同的 K,那么后续的加密通信就会彻底乱套。所以边界权限控制的关键就在于:确保只有授权的实体才能完成协商。

具体的控制手段包括但不限于:

  • 在临时公钥 RA、RB 中嵌入访问令牌(例如 JWT),由接收方验证令牌后才进入
  • 在 ZA、ZB 中携带权限标识(比如角色 ID),协商时两侧自行比对权限位
  • 在派生出的 K 基础上再派生一次“权限子密钥”,用于不同安全级别的操作

这些我们后续章节会逐一展开,今天先把底层协议打扎实。

我曾经踩过一个坑: 有一个项目把权限控制做在了 KDF 输出之后,只对 K 本身加密分段。但如果有人做了重放攻击,把之前录制的 RA 重新发给响应方,响应方依然会参与协商。所以边界控制一定要“前移”——在接收到 RA 或 RB 的那一刻就进行身份和权限预校验,而不是等密钥算完了再检查。

小结

SM2密钥协商的两轮流程,本质上就是双方交换临时公钥,各自算出一个共享点,再用 KDF 导出一致的密钥。核心难点在于:正确拼接 ZA、ZB 顺序,正确处理 KDF 的截断,以及在协商开始前引入权限校验。把这三点抓牢了,你的实现就稳了八成。