4、SM2加密与密钥交换:加密解密流程、密钥交换协议、在安全固件升级中的应用
加密和密钥交换,是SM2算法在物联网固件保护中最核心的两个应用场景。说白了,一个负责把固件内容藏起来,一个负责把解密钥匙安全地送到对方手里。这两件事做不好,固件升级就是裸奔。
我个人习惯把SM2的加密和密钥交换放在一起讲,因为它们在底层共享一套椭圆曲线运算,只是上层协议构造不同。你理解了一个,另一个也就通了八成。
4.1 SM2加密解密流程
SM2加密不是直接把明文扔到椭圆曲线上算。它采用了一种混合结构:公钥加密 + 对称密钥派生。我刚开始接触时也觉得绕,后来在项目中踩过坑才明白——这种设计是为了兼顾安全性和效率。
4.1.1 加密流程
假设发送方A要用接收方B的公钥P_B加密一条消息M。步骤如下:
- 生成临时密钥对:A随机选取
k ∈ [1, n-1],计算临时公钥C1 = k * G(G是椭圆曲线基点)。 - 计算共享点:
S = h * k * P_B,其中h是余因子(SM2中h=1)。 - 派生对称密钥:从
S的x、y坐标通过KDF(密钥派生函数)生成t。如果t全零,返回步骤1重新选k。 - 加密明文:
C2 = M ⊕ t,即异或加密。 - 计算杂凑值:
C3 = Hash(x_S || M || y_S),用于完整性校验。 - 输出密文:
C = C1 || C2 || C3。
关键点:SM2密文比明文长很多。C1占65字节(未压缩公钥),C3占32字节(SM3杂凑值)。如果你的固件包只有几百字节,加密后体积膨胀明显。我在做NB-IoT模组固件升级时就遇到过这个问题——传输带宽有限,不得不调整分片策略。
4.1.2 解密流程
接收方B拿到密文C后,用自己的私钥d_B解密:
- 提取C1:检查C1是否在椭圆曲线上。不在?直接返回失败。
- 计算共享点:
S' = h * d_B * C1。如果S'是无穷远点,返回失败。 - 派生对称密钥:同样用KDF从S'生成
t'。 - 解密密文:
M' = C2 ⊕ t'。 - 校验完整性:计算
u = Hash(x_S' || M' || y_S'),与C3比较。不一致则解密失败。
嗯,这里要注意:解密失败不能直接告诉攻击者具体是哪一步出错。我曾经在项目中看到过有人把「C1不在曲线上」和「杂凑值不匹配」返回不同的错误码——这等于给攻击者递刀子,方便他们做选择密文攻击。
4.1.3 代码示例(伪码)
// SM2加密伪码
function sm2_encrypt(M, P_B):
while True:
k = random(1, n-1)
C1 = k * G
S = h * k * P_B
t = KDF(S.x || S.y, len(M))
if t != 0:
break
C2 = M XOR t
C3 = SM3(S.x || M || S.y)
return C1 || C2 || C3
// SM2解密伪码
function sm2_decrypt(C, d_B):
C1 = C[0:65]
if not is_on_curve(C1):
return FAIL
S = h * d_B * C1
if S == O:
return FAIL
t = KDF(S.x || S.y, len(C2))
M = C2 XOR t
u = SM3(S.x || M || S.y)
if u != C3:
return FAIL
return M
4.2 密钥交换协议
密钥交换解决的是另一个问题:双方如何在公开信道上协商出一个共享密钥?SM2的密钥交换协议(SM2 Key Exchange)基于椭圆曲线Diffie-Hellman(ECDH),但加上了身份认证和杂凑校验。
你想想看,如果只是裸的ECDH,中间人攻击怎么办?SM2的协议设计就是为了堵这个漏洞。
4.2.1 协议流程
假设A和B要协商密钥,双方各自持有自己的密钥对(d_A, P_A)和(d_B, P_B),并且知道对方的公钥。
| 步骤 | A(发起方) | B(响应方) |
|---|---|---|
| 1 | 选随机数r_A,算R_A = r_A * G |
— |
| 2 | 发送R_A和辅助信息Z_A |
收到后,选随机数r_B,算R_B = r_B * G |
| 3 | — | 计算共享密钥K_B,发送R_B和S_B(杂凑值) |
| 4 | 计算共享密钥K_A,校验S_B |
— |
| 5 | 发送S_A(杂凑值) |
校验S_A |
共享密钥的计算公式为:K = KDF(x_U || y_U || Z_A || Z_B),其中U = h * r_A * (R_B + [t_B] * P_B),t_B是B的临时值。具体细节在GM/T 0003.4-2012标准中有完整定义。
我的经验:在实际物联网项目中,密钥交换往往不是每次都做。设备第一次注册时协商一次,后续升级用派生密钥。这样可以减少计算量——SM2的点乘运算在低端MCU上可能要几百毫秒,频繁做不现实。
4.3 在安全固件升级中的应用
好了,前面讲了加密和密钥交换,现在把它们串起来——看看在固件升级中到底怎么用。
4.3.1 典型架构
一个安全的固件升级系统,通常包含三个角色:
- 固件发布服务器:持有SM2签名私钥,负责对固件包签名。
- 升级管理平台:持有SM2加密公钥,负责分发加密后的固件。
- 物联网终端设备:持有SM2解密私钥和验签公钥,负责解密和校验。
我个人习惯把签名和加密分开。签名用SM2数字签名,保证固件来源可信;加密用SM2公钥加密,保证固件内容机密。两者缺一不可。
4.3.2 密钥交换在升级中的应用场景
场景一:设备首次激活。设备出厂时只预置了平台公钥,自己的私钥是在首次通信时通过SM2密钥交换协商出来的。这样做的好处是——即使设备被物理破解,泄露的也只是本次会话的密钥,不会影响其他设备。
场景二:批量升级时的会话密钥更新。我记得有个项目,客户要求每台设备每次升级都用不同的加密密钥。如果直接对固件用SM2公钥加密,服务器要为每台设备算一次加密——CPU直接拉满。后来我们改用密钥交换协商一个临时会话密钥,用这个密钥做对称加密(SM4)来加密固件。服务器只需要算一次SM2密钥交换,剩下的对称加密快得多。
避坑指南:我曾经在LoRaWAN设备上做过类似方案,踩过一个坑——SM2密钥交换协议需要双方交换两次消息,但LoRaWAN是纯上行链路,下行窗口极短。最后不得不把密钥交换拆成多个上行周期完成,每次携带一部分数据。如果你在做低功耗广域网设备,一定要评估通信延迟对协议流程的影响。
4.3.3 固件加密传输流程
下面这张图展示了完整的加密固件升级流程:
4.3.4 性能对比
在实际选型时,你需要根据设备算力和固件大小选择加密方式。我整理了一份对比:
| 加密方式 | 计算开销(以STM32F4为例) | 适用场景 | 注意事项 |
|---|---|---|---|
| SM2公钥加密 | 约300-500ms/次 | 小固件(<10KB) | 密文膨胀约97字节 |
| SM2密钥交换+SM4 | 密钥交换约500ms,SM4约5MB/s | 大固件(>100KB) | 需要额外维护会话状态 |
| SM2密钥交换+SM4(预计算) | 预计算后SM4约5MB/s | 批量升级 | 服务器端需要管理密钥表 |
我的建议:如果你的设备CPU主频低于100MHz,尽量走密钥交换+对称加密路线。我曾经在GD32F103上做过测试,一次SM2点乘运算耗时约800ms,而SM4加密1MB数据只要200ms。差距非常明显。
4.4 本章小结
SM2加密和密钥交换,说白了就是两件事:藏数据和传钥匙。加密流程的核心是临时密钥+KDF派生,密钥交换的核心是带身份认证的ECDH。在固件升级中,我建议你根据设备算力和固件大小灵活选择——小固件直接SM2加密,大固件走密钥交换+SM4。别死磕一种方案,硬件资源摆在那里,工程上要懂得取舍。