同一次请求里的两道锁:图解搞懂国密 SM2 与 SM3

0 阅读10分钟

在对接政务、金融或各类企业级开放平台时,很多开发者第一次接触到“国密算法”(国家商用密码标准)。

接口文档通常会在开头写下这么一句要求:“为了保障传输安全,本网关所有请求均采用 SM2 加密与 SM3 签名。”

然而在联调阶段,控制台里最频繁弹出的往往是这两句让人抓狂的报错:

  • “验签失败(Signature verification failed)”:正文字段明明一个都没改,为什么对不上?
  • “解密失败(Decryption error)”:公私钥明明是从后台复制出来的,为什么解出来是一堆乱码甚至直接抛出异常?

问题的根源,往往在于很多同学把“签名”和“加密”当成了一回事,导致概念混淆、调用顺序颠倒、公私钥拿错。

其实,同一次请求里包裹着两道性质截然不同的手续:

  • SM2 解决的是 「别人看不懂」(防窃听);
  • SM3 解决的是 「别人改不了」(防篡改)。

本文将循序渐进地拆解这两道锁的工作原理、协作机制与实战排坑要点,带你一次性搞懂国密 SM2 与 SM3。


1. 心智模型:寄一份重要快递

要彻底理清它们的分工,不妨把前端发往网关的一次接口请求,想象成寄送一份装有重要合同的快递:

┌─────────────────────────────────────────────────────────────────┐
│ 📄 业务正文(如:身份证、手机号、银行卡号)                         │
└────────────────────────────────┬────────────────────────────────┘
                                 │
                                 ▼
┌─────────────────────────────────────────────────────────────────┐
│ 🔒 第一道锁:SM2 =「装进密码保险箱」(防窃听,仅特定敏感接口需要)   │
│ • 用平台给的【公钥】把明文锁进箱子,变成一串乱码密文              │
│ • 路上抓包的黑客即使截获快递,没有【私钥】也打不开、看不懂        │
└────────────────────────────────┬────────────────────────────────┘
                                 │
                                 ▼
┌─────────────────────────────────────────────────────────────────┐
│ 🏷️ 第二道锁:SM3 =「贴上防伪封条」(防篡改,所有网关请求必选)     │
│ • 把箱子外面的快递单(含应用ID、时间戳、保险箱密文)排好          │
│ • 用 SM3 算出唯一的 64 位防伪指纹(sign),贴在包裹封口处         │
│ • 哪怕路上有人偷偷改动了一个标点符号,网关重算指纹就会对不上,直接拒收 │
└─────────────────────────────────────────────────────────────────┘

把两道锁的关键维度横向对比:

维度SM3 签名(防伪封条)SM2 加密(密码保险箱)
对标国际标准SHA-256、MD5(哈希摘要 / 散列算法)RSA、ECC(非对称公私钥加密)
能否逆向还原原文绝对不能(单向指纹,不可逆)可以(公钥加密 →\rightarrow 私钥解密)
核心防御目标防篡改:确保数据在传输途中未被微调防窃听:保护敏感个人隐私与商业数据
每次请求都要算吗必须算(只要请求走网关就必须附带)按需开启(仅敏感接口或后台勾选了加密时才开启)
是否需要配置密钥无需密钥(纯数学哈希,公开算法)必须配对密钥(公钥加密,私钥解密)

搞清楚了这个宏观心智模型后,我们逐层深入剖析这两道锁的技术细节。


2. 深入第一道锁:SM3「防伪封条」

2.1 什么是 SM3?

SM3 是中国国家密码管理局发布的密码杂凑算法(商用密码 GM/T 0004)。它的角色就相当于国际密码学体系中的 SHA-256。

SM3 拥有三个核心特性:

  1. 单向不可逆:你可以轻易把一段千万字的长文通过 SM3 压成一段 64 位十六进制字符串,但世界上没有人能根据这 64 位字符串反推出原文。
  2. 固定输出长度:不论输入是空的 ""、几个单词,还是几个 G 的大文件,SM3 计算后的输出长度恒定为 256 bit(即 32 字节,转为十六进制就是 64 个字符)。
  3. 极敏感的雪崩效应(Avalanche Effect):原文只要改变了仅仅 1 个比特(比如一个逗号变句号),算出来的 64 位哈希值就会发生天翻地覆的变化,毫无规律可循。

2.2 网关是如何利用 SM3 验签的?

在开放平台中,客户端与网关之间约定了一套标准的验签流程:

sequenceDiagram
    autonumber
    participant Client as 客户端 (前端/App)
    participant Hacker as 中间人 (抓包/代理)
    participant Gateway as 业务网关 (Gateway)

    Note over Client: 1. 提取请求字段,按 ASCII 字母升序排序<br/>2. 拼接为 key1=val1&key2=val2...<br/>3. 计算 SM3: sign = sm3Hex(拼接串)
    Client->>Hacker: 发送 POST 请求 (包含所有参数 + sign)
    Note over Hacker: 假设黑客篡改了参数:<br/>比如把 amount=100 改为 amount=99999
    Hacker->>Gateway: 转发被篡改后的请求
    Note over Gateway: 4. 网关按同样规则把收到的字段排序拼接<br/>5. 网关就地重算一次 SM3 哈希
    alt 网关重算出的 SM3 === 客户端传来的 sign
        Gateway-->>Client: ✅ 验签通过,继续执行业务
    else 两者不一致
        Gateway-->>Client: ❌ 拦截请求!报错:签名验证失败
    end

正是因为雪崩效应的存在,黑客中途哪怕只篡改了请求里的某一个参数值,网关收到后重新用 SM3 算出来的指纹就必然和客户端传上来的 sign 不相等,篡改行为立刻暴露无遗。

2.3 SM3 签名的 3 大高频踩坑点

在编写签名代码时,以下几个细节只要稍有疏忽,就会导致网关“验签失败”:

  1. 字典序排序(ASCII 码从小到大): 参与签名的参数名必须按照 ASCII 字典顺序排序。例如:app_id 排在 biz_content 前面,biz_content 排在 charset 前面。
  2. 空值过滤规则: 在绝大多数网关规范中,值为 null、undefined 或空字符串 "" 的参数不参与签名拼接。如果客户端拼接了 version=&...,而网关在计算时剔除了 version,两者拼接出的字符串不同,验签必然失败。
  3. 透传字段与 Header 不要盲目塞进签名串: 许多接口请求会包含 HTTP 请求头(如 Authorization、certify_token)或者外部透传字段(如 header={"userid":"..."})。除非接口文档特别注明,否则这些传输层字段通常不属于签名字段集合,切忌画蛇添足。

3. 深入第二道锁:SM2「密码保险箱」

3.1 什么是 SM2?

SM2 是基于**椭圆曲线密码学(ECC)**的非对称密码算法(商用密码 GM/T 0003),在国际上主要对标 RSA。

相比于传统的 RSA-2048,SM2(256 位椭圆曲线)在提供同等甚至更高安全强度的前提下,密钥长度更短、计算速度更快、网络传输的密文体积也更小。

非对称加密的核心在于成对的密钥:

  • 公钥(Public Key):用于加密(锁门)。公钥是公开的,任何人拿到都可以把明文数据锁进密文保险箱。
  • 私钥(Private Key):用于解密(开门)。私钥必须严格保密,只有掌握私钥的人才能打开保险箱、还原明文。

3.2 疑惑解答:为什么前端配置里既有公钥也有私钥?

刚入行的同学常被一个“安全常识”困扰:“私钥不是绝对不能暴露给前端/客户端吗?为什么网关接入文档要求前端同时配置公钥和私钥?”

这是因为在很多轻量级政务与企业网关场景中,执行的是双向加密通信:

Untitled diagram-2026-10-28-062840.png

  • 发出去时(客户端 →\rightarrow 网关):客户端使用平台下发的公钥把敏感正文锁上。此时密文在网络传输中就算被抓包,中间人没有平台的私钥,什么也看不见;
  • 收回来时(网关 →\rightarrow 客户端):网关用应用的公钥把响应数据锁上,客户端收到后再用本地配置的应用私钥解开。

两对公私钥各司其职,保证了整个传输回路的双向防窃听。

3.3 为什么 SM2 密文大多以 04 开头?

仔细观察 SM2 加密后的十六进制字符串,你会发现几乎所有密文都以 04 开头:

04c852914e34a6bc8910... (后面是一长串十六进制字符串)

这并不是巧合,而是密码学规范中**未压缩椭圆曲线点(Uncompressed Point Format)**的标准标识:

  • 椭圆曲线上的一个公钥点由坐标 (x,y)(x, y) 组成;
  • 国际及国密规范定义:以单字节十六进制 04 作为未压缩点的前缀,表示其后紧跟着 xx 坐标和 yy 坐标。

实战踩坑:

  • 某些前端加解密库(如 sm-crypto)加密输出的十六进制可能默认不带 04,必须根据网关要求手动补充;
  • 解密时,如果密文以 04 开头,需要先将前两个字符(04)剥离后再送给底层解密函数,否则解密器可能会抛出 point not on curve(点不在曲线上)或直接解密失败。

3.4 模式之坑:C1C3C2 还是 C1C2C3?

SM2 密文由三部分构成:

  • C1:随机产生的椭圆曲线点;
  • C2:真正的密文数据;
  • C3:针对明文和曲线点计算出的摘要校验码(防篡改)。

历史上出现过两种排列模式:

  1. 老国标排列:C1C2C3
  2. 新国标与主流规范:C1C3C2

目前主流政务和金融网关几乎统一采用 C1C3C2(例如在 sm-crypto 中传入 cipherMode 为 1)。如果网关是 C1C3C2,而你的客户端配置成了 C1C2C3,哪怕两边的公私钥完全配对,也会直接导致解密失败!


4. 终极合体:两道锁的「穿衣顺序」

当一个请求既要开启 SM2 加密,又要走 SM3 签名时,顺序只有一个,绝不能颠倒!

铁律:先锁保险箱,再贴封条!

sequenceDiagram
    autonumber
    participant App as 业务代码
    participant GatewaySDK as 请求封装层
    participant Network as 网关 (gateway.do)

    App->>GatewaySDK: 传入明文业务对象 { &#34;phone&#34;: &#34;138...&#34; }
    Note over GatewaySDK: 步骤 1:SM2 加密<br/>将明文转为密文:&#34;04a1b2...&#34;
    Note over GatewaySDK: 步骤 2:字典序拼接待签字符串<br/>把密文放入 biz_content!<br/>app_id=...&biz_content=04a1b2...&timestamp=...
    Note over GatewaySDK: 步骤 3:计算 SM3 封条<br/>sign = sm3Hex(待签字符串)
    GatewaySDK->>Network: 发送 POST 请求 (含密文 biz_content 和 sign)
    Note over Network: 步骤 4:网关首先验签 (SM3)<br/>按密文重算哈希,比对 sign<br/>(验签通过后,才启动 SM2 私钥解密)

为什么反过来做必死无疑?

假设你犯了逻辑错误:先用明文算好签名,再去加密正文。

  1. 客户端计算签名时,用的是明文字符串 biz_content={"phone":"138..."};
  2. 算完签名后,你把 biz_content 加密成了 04a1b2...;
  3. 网关收到请求时,只能看到密文 04a1b2...,网关拿密文重新计算 SM3;
  4. 结果:密文算出来的哈希必然不可能等于明文算出来的签名,网关在第一步验签时就会直接将其当成“非法篡改”予以拦截,根本不会走到解密步骤!

5. 联调排错速查手册(Checklist)

下次联调若遇到报错,请对照这份检查清单逐项排查:

🛠️ 当遇到「验签失败(SM3)」

  • 顺序对了吗? 参与签名的 biz_content 是否是 SM2 加密后的密文,而不是明文?
  • 参数排序对了吗? 是否严格按照 ASCII 字典升序(从小到大)排列?
  • 空值是否已过滤? null、undefined、"" 是否已被排除在待签名字符串之外?
  • 是否误加入了额外字段? 检查 HTTP Headers、Cookie、透传 Token 是否被错误塞进了签名列表?
  • 字符串拼接格式对不对? 是否存在多余的空格、换行符或多余的 & 符号?编码是否统一为 UTF-8?

🔓 当遇到「解密失败(SM2)」

  • 04 前缀处理对了吗? 检查解密前是否需要剥离 04,或者加密后是否漏掉了 04?
  • 排列模式是否一致? 确认双方是否都统一采用了 C1C3C2(新国标推荐)?
  • 公私钥是否配对? 确认是否将“平台公钥”和“应用自建公钥”混淆?私钥是否写错?
  • 返回数据真是密文吗? 某些网关在报错(如 404/500/Token 过期)时,返回的其实是明文错误 JSON(例如 {"code":500,"msg":"系统异常"}),若直接将其当作密文送入 SM2 解密函数,必然报语法错误。

6. 总结

搞懂国密 SM2 与 SM3 的本质,其实就这一张图与一段十六字口诀:

                  ┌──────────────────────┐
                  │      业务请求        │
                  └──────────┬───────────┘
                             │
            【第一道锁】      ▼
         SM2 加密(可逆)   [ 04... 密文保险箱 ]  ---> 解决「别人看不懂」
                             │
            【第二道锁】      ▼
         SM3 签名(不可逆) [ 64 位防伪封条 sign ] ---> 解决「别人改不了」

联调十六字口诀:
公钥锁箱,密文贴标;先密后签,收回私开。

分清职责、理顺先后,国密网关对接其实一点也不神秘。