4、SM2加密与密钥交换:加密解密流程、密钥交换协议、在安全固件升级中的应用

加密和密钥交换,是SM2算法在物联网固件保护中最核心的两个应用场景。说白了,一个负责把固件内容藏起来,一个负责把解密钥匙安全地送到对方手里。这两件事做不好,固件升级就是裸奔。

我个人习惯把SM2的加密和密钥交换放在一起讲,因为它们在底层共享一套椭圆曲线运算,只是上层协议构造不同。你理解了一个,另一个也就通了八成。

4.1 SM2加密解密流程

SM2加密不是直接把明文扔到椭圆曲线上算。它采用了一种混合结构:公钥加密 + 对称密钥派生。我刚开始接触时也觉得绕,后来在项目中踩过坑才明白——这种设计是为了兼顾安全性和效率。

4.1.1 加密流程

假设发送方A要用接收方B的公钥P_B加密一条消息M。步骤如下:

  1. 生成临时密钥对:A随机选取k ∈ [1, n-1],计算临时公钥C1 = k * G(G是椭圆曲线基点)。
  2. 计算共享点:S = h * k * P_B,其中h是余因子(SM2中h=1)。
  3. 派生对称密钥:从S的x、y坐标通过KDF(密钥派生函数)生成t。如果t全零,返回步骤1重新选k。
  4. 加密明文:C2 = M ⊕ t,即异或加密。
  5. 计算杂凑值:C3 = Hash(x_S || M || y_S),用于完整性校验。
  6. 输出密文:C = C1 || C2 || C3。

关键点:SM2密文比明文长很多。C1占65字节(未压缩公钥),C3占32字节(SM3杂凑值)。如果你的固件包只有几百字节,加密后体积膨胀明显。我在做NB-IoT模组固件升级时就遇到过这个问题——传输带宽有限,不得不调整分片策略。

4.1.2 解密流程

接收方B拿到密文C后,用自己的私钥d_B解密:

  1. 提取C1:检查C1是否在椭圆曲线上。不在?直接返回失败。
  2. 计算共享点:S' = h * d_B * C1。如果S'是无穷远点,返回失败。
  3. 派生对称密钥:同样用KDF从S'生成t'。
  4. 解密密文:M' = C2 ⊕ t'。
  5. 校验完整性:计算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 固件加密传输流程

下面这张图展示了完整的加密固件升级流程:

SM2加密固件升级流程 固件发布服务器 (签名私钥) 升级管理平台 (加密公钥) 物联网终端设备 (解密私钥+验签公钥) ① 签名固件 ② 加密分发 阶段一:密钥交换(可选) 设备与平台通过SM2密钥交换协商临时会话密钥 协商成功后,后续固件加密使用SM4对称加密 减少SM2公钥加密的计算开销 阶段二:固件加密传输 方式A:直接使用SM2公钥加密固件包(适用于小固件) 方式B:使用协商的SM4密钥加密固件(适用于大固件) 密文 = C1 || C2 || C3,设备收到后先解密再验签 阶段三:完整性校验与安装 设备使用SM2验签公钥校验固件签名 → 通过后写入Flash

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。别死磕一种方案,硬件资源摆在那里,工程上要懂得取舍。