一文读懂+落地常用国密算法(SM2、SM3、SM4)

377 阅读11分钟

嗨,这里是肖恩!一个致力于把企业级的技术方案抽丝剥茧帮助你落地的程序员。

在当今的技术架构中,国产商用密码(简称「国密」)已不再是仅限于政务项目的冷门需求。随着等保2.0三级标准的深度普及,无论是在金融支付、核心系统数据脱敏,还是在移动端安全通信中,SM系列算法已经成为Java工程师必须掌握的核心技能。

掌握国密算法的落地,不仅是应对合规性检查,更是理解现代密码学工程实践的绝佳契机。本文将深入拆解SM2、SM3、SM4三大核心算法,并提供生产级项目的落地建议。


算法图谱:为什么是SM2/3/4?

在国际算法体系中,RSA、SHA256、AES是工程界的老三样。与之对应,国密算法体系提供了一套性能与安全性对标、甚至在特定场景下占优的替代方案。

  • SM2:基于椭圆曲线(ECC)的非对称算法,是RSA算法的最佳替代方案。SM2 算法有 C1C3C2 和 C1C2C3 两种模式,不过一般我们都倾向于使用前者。
  • SM3:消息摘要算法,输出256比特散列值,性能与安全性对标SHA-256。
  • SM4:分组对称加密算法,分组长度和密钥长度均为128比特,是AES算法的强力对手。常见的加密模式有 ECB 模式和 CBC 模式,在后文中我们会详细讲解。

SM2:非对称加密与身份认证的基石

相较于RSA,SM2在相同的安全等级下,密钥长度更短(256位 vs RSA 2048位),这意味着更少的存储空间和更高的运算效率。

1. C1C2C3 与 C1C3C2 模式深度拆解

这是Java开发者最容易踩坑的地方。SM2的密文由三部分组成:

  • C1:椭圆曲线上的随机点计算结果。
  • C2:真正的密文数据。
  • C3:摘要校验值,用于防止密文被篡改。

C1C3C2是当前国标(GB/T 32907)推荐的最优序列,旧版标准(C1C2C3)在早期的硬件加密机中较为多见。在前后端对接或跨语言调用(如Java与Go、C++对接)时,明确密文排序模式是联调成功的第一优先级。

2. 公钥压缩技术的权衡

SM2公钥由X和Y两个分量组成(通常为64字节)。为了节省带宽,可以采用压缩公钥格式(33字节)。在生产环境中,前端JS库和后端Java库对压缩格式的支持程度不一,统一使用非压缩格式(04前缀)是降低系统复杂性的权衡方案。

3. 签名与验签的并发处理

SM2签名操作涉及复杂的点运算,属于CPU密集型任务。在高并发的鉴权场景下,建议使用将相关对象池化或使用ThreadLocal缓存相关算法实例,避免频繁销毁和创建对象带来的GC压力。


SM3:数据完整性校验的防伪标签

SM3不仅是一个简单的哈希函数,它采用了迭代压缩结构,具有极高的碰撞抵御能力。

1. 消息填充与压缩

SM3处理流程是将消息填充至512比特的倍数,通过复杂的步函数运算生成256比特的摘要。它生成的指纹长度固定,不可逆。

2. 加盐哈希的最佳实践

在用户密码存储或敏感信息脱敏场景中,直接使用SM3(明文)极易受到彩虹表攻击。引入随机盐值(Salt)并进行迭代计算是生产环境的必选逻辑。通过SM3(明文 + 盐值),即使两个用户密码相同,存储的哈希值也会完全不同,极大提升了暴力破解的成本。

3. HMAC-SM3的应用

在开放平台的接口签名校验中,HMAC-SM3是确保消息真实性的首选方案。它结合了密钥与SM3算法,防止攻击者篡改请求参数,在政务系统对接中被广泛采用。


SM4:大规模数据加密的高速引擎

SM4算法在生产环境中承担了最繁重的数据加解密任务。其本质是将明文切分为固定长度的块进行迭代变换。在Java工程实践中,理解其工作模式比理解数学原理更重要。

1. ECB模式与CBC模式的抉择

ECB(电子密码本)模式将数据独立分块加密。这类模式的致命缺陷是相同的明文块会生成相同的密文块。在大型生产项目中,这可能导致模式泄露风险。CBC(密码分组链接)模式是这类场景的最佳实践,它通过引入初始向量(IV),使每一块的加密都依赖于前一块的执行结果。

2. 填充方案的标准化

由于SM4要求输入必须是128位的倍数,当末尾数据长度不足时,需要进行填充。PKCS7Padding是目前的通用标准。在Java中集成时,开发者必须严格对齐加解密双方的填充逻辑,否则会频繁的抛出BadPaddingException。


大型生产项目落地建议

在Java生态中实现国密算法,不建议从零编写数学公式,而是基于成熟的底层库进行封装。

1. 库的选择:Bouncy Castle (BC库)

Bouncy Castle是Java界的事实标准。它从1.57版本开始对国密提供了较好支持。在大型项目中,需要通过Security.addProvider(new BouncyCastleProvider())进行注册。也可以直接使用 hutool 封装好的国密相关工具类(SM2、SM3、SM4)。

2. 兼容性治理:双算法切换架构

在系统从RSA迁移到SM2的过程中,灰度切换策略是保障业务连续性的核心。建议抽象出一层CryptoService接口,通过配置中心动态控制算法开关。对于老用户数据使用RSA解密并SM2重新加密存储,实现无感迁移。

3. 密钥管理:硬件安全模块 (HSM)

在三级等保要求下,软件层面的密钥存储是不合规的。将SM2私钥托管在云厂商的硬件加密机(KMS)或物理服务器的加密卡中,通过调用硬件接口完成加解密,是金融级安全系统的标配。

4. 数据传输:国密双向SSL/TLS

在通信链路层,标准的HTTPS(TLS 1.2/1.3)主要使用国际算法。在特定的合规场景中,需要使用支持国密协议簇的Web服务器(如Tengine国密版)或定制化的Java SDK(如支持国密TLS协议栈的浏览器和客户端),确保全链路国密化。


前文从库选型、兼容性治理及硬件安全管理等维度,为大型项目的国密落地提供了方法论指导。为了进一步将理论转化为生产力,下文将通过一个具体的数据上报案例,拆解 SM2、SM3 与 SM4 算法在真实业务交互中是如何协同工作的。

实战示例

这里给出一个结合 SM2、SM3、SM4 的项目实战流程示例,其中涉及的算法有 CBC 模式的 SM4 对称加密,SM2withSM3 (即先将请求体用SM3做摘要,再用SM2做签名)签名 在这个示例中你可以看到一个涉及到国密加解密、签名验签过程的技术实践:

前期准备流程

sequenceDiagram
    participant Reporter as 上报方
    participant Platform as 平台
    Note over Reporter, Platform: 阶段一:密钥初始化与分发
    Platform->>Reporter: 分发平台 SM2 公钥
    Note right of Platform: 平台持有: 平台 SM2 私钥
    Note left of Reporter: 上报方持有: 平台 SM2 公钥 + 自身 SM2 私钥

    Reporter->>Platform: 提供上报方 SM2 公钥
    Note right of Platform: 平台存储: 应用ID + 上报方 SM2 公钥

    Note over Reporter, Platform: 阶段二:SM4 对称密钥交换
    Note left of Reporter: 生成 SM4 密钥
    Reporter->>Platform: 提供 SM4 密钥
    Note right of Platform: 加密保存上报方 SM4 密钥

    Note over Reporter, Platform: 阶段三:交互准备
Rect rgb(240, 240, 240)
Note over Reporter, Platform: 双方约定签名原文格式与请求头格式
End
  1. 平台提供一个数据上报接口供上报方执行数据上报,所有数据传输必须加密+签名,平台内部持有平台 SM2 私钥,并将平台 SM2 公钥公布出来,所有上报方保存平台 SM2 公钥
  2. 上报方给平台提供上报方 SM2 公钥,并保存好自己的私钥,再给平台提供 SM4 密钥,双方分别对 SM4 密钥做加密保存
  3. 平台保存好上报方 SM2 公钥,并关联对应的应用 id,将上报方 SM2 公钥加密保存
  4. 双方约定好签名原文和请求头格式,准备交互

约定格式示例

签名原文:

应用id\n
请求方式\n
uri(含uri参数)\n // 这里的参数需要按照 a~z 的顺序排好
请求体SM4加密后的摘要\n
时间戳\n
随机数\n

签名原文为什么是这些字段?

因为签名原文的作用是标识这个请求一定是上报方发出的,且每一次请求都不可篡改,其他人不可伪造,基于这个大前提,我们来分析每个字段的作用:

  • 应用id:标识接入方
  • 请求方式+uri(含uri参数):标识请求的接口,uri参数需要按照 a~z 的顺序排好的原因,是服务端验签时也需要构造一样的签名原文来验签
  • 请求体SM4加密后的摘要:上报方构建好请求体后,使用实时生成 SM4 IV,结合密钥对请求体进行加密。
  • 时间戳:平台会校验请求时间戳与服务端的差异,如果相差时间太久就会拒绝本次请求。用于防重放攻击,即使有中间人截获了整个请求,在时间戳的校验下也只有一定时间内可以攻击成功。
  • 随机数:一般形式为 uuid,上报方每次请求都生成不一样的,平台会缓存上报方每次请求的随机数,如果有请求的随机数产生了重复则拒绝本次请求,解决单时间戳校验的短时重放攻击问题。

请求头:

x-signature // 签名
x-app-id // 应用id
x-timestamp // 时间戳
x-nonce // 随机数
x-crypto-iv // 加密后的 SM4 IV

数据交互流程

这个流程,会有一点绕,结合着时序图和文字会更好理解哈~

sequenceDiagram
    autonumber
    participant Reporter as 上报方
    participant Platform as 平台

    Note over Reporter, Platform: 预置条件:双方交换公钥并存储己方私钥,预存基于 app-id 的 SM4 密钥

    rect rgb(240, 248, 255)
        Note over Reporter: 【数据准备与加密】
        Reporter->>Reporter: 1. 生成实时 SM4 IV
        Reporter->>Reporter: 2. 使用 SM4 密钥 + IV 加密原始数据 -> 密文
        Reporter->>Reporter: 3. 生成 x-timestamp (时间戳) 和 x-nonce (随机数)
        Reporter->>Reporter: 4. 准备 x-app-id
    end

    rect rgb(255, 245, 238)
        Note over Reporter: 【加签与 IV 加密】
        Reporter->>Reporter: 5. 组装签名原文 -> SM3 摘要 -> 上报方 SM2 私钥签名 -> x-signature
        Reporter->>Reporter: 6. 使用 平台 SM2 公钥 加密 SM4 IV -> x-crypto-iv
    end

    Reporter->>Platform: 7. 发起 HTTP 请求 (携带密文 Body 及所有扩展 Header)

    rect rgb(245, 255, 245)
        Note over Platform: 【验签流程】
        Platform->>Platform: 8. 接收请求,按约定构建签名原文并计算 SM3 摘要
        Platform->>Platform: 9. 使用 上报方 SM2 公钥 验证 x-signature
    end

    alt 验签失败
        Platform-->>Reporter: 10a. 返回对应报错提示
    else 验签成功
        rect rgb(255, 250, 205)
            Note over Platform: 【解密流程】
            Platform->>Platform: 10b. 使用 平台 SM2 私钥 解密 x-crypto-iv -> SM4 IV
            Platform->>Platform: 11. 根据 x-app-id 检索预存的 SM4 密钥
            Platform->>Platform: 12. 使用 SM4 密钥 + SM4 IV 解密密文 -> 原始数据
        end
        Platform->>Platform: 13. 数据处理并返回响应结果
    end
  1. 上报方用密钥加密原始数据,得到密文
  2. 获取当前时间戳,生成随机数,写入 x-timestampx-nonce 请求头
  3. 将应用id写入 x-app-id 请求头
  4. 上报方按照约定构成签名原文,再对签名原文进行摘要后得到签名,写入 x-signature 请求头
  5. 实时生成 SM4 IV,再用平台公钥加密 SM4 IV,写入 x-crypto-iv 请求头
  6. 上报方构建好上述 http 请求后,发起请求,请求体
  7. 平台接收到请求,构建好签名原文后获取摘要,进行验签,验签有问题,就给出对应的报错提示
  8. 验签没问题,就用平台私钥解密 x-crypto-iv 请求头中的数据拿到 SM4 IV,结合应用 id 拿到 SM4 密钥,即可对数据进行解密
  9. 解密完成后做数据处理,再将响应结果

一些 Q&A

1. 为什么要实时生成 IV?

可以想象下,如果没有 IV,或 IV 是固定的,在加密过程被中间人拿到了 SM4 密钥和 IV,那么中间人就可以解密历史和未来的所有数据。而动态的 IV,就是为了解决固定密钥一旦泄露,会导致所有数据都有安全风险的问题。

2. 签名原文为什么是这些字段?

签名原文的作用是保证这一次请求,没有其他任何的方法可以伪造,因此就要考虑以下几个点:

  • 是不是某个上报方调用的?

  • 调用的接口和参数是不是这一个?会不会被抓包后请求别的接口?

  • 会不会被抓包后重放?

而签名原文中的每个字段,则分别解决了这些问题:

  • 应用id:解决「我」是「我」的问题

  • 请求方式 + uri + uri参数 + 请求体摘要:解决了调用的接口和参数的唯一性问题

  • 时间戳 + 随机数:解决了抓包后,一定时间内被重放的问题。随机数在平台服务端,一般是使用 Redis 等缓存来存储,存储的有效期刚好就是请求时间戳与服务器时间戳允许的时间差,这样就可以保证,一个请求即使被抓包,也无法被重新调用

3. 为什么 SM2 加密是用公钥,而签名是用私钥?

因为私钥是属于系统中的「最高机密」,绝对不允许外泄,而公钥是可以公布在一定的网络范围甚至是公网的。

加密后的数据要保证只有特定的人可以解密,而签名要保证只有特定的人可以签名,因此 SM2 是公钥加密、私钥解密,私钥签名、公钥验签。

小尾巴

嗨,这里是肖恩!

其实上面的流程,还有一个可以补充的步骤,能够让上报方也能够验证平台的可信,知道的小伙伴也可以在评论区补充!