SM2 密文和签名对不上的四个原因:C1C2C3、04 前缀、DER 编码、Z 值 userId

· 约 5 分钟 ㊙️ 国密 SM2/SM3/SM4

国密对接花掉的时间,绝大部分不在算法上——SM2、SM3、SM4 的实现都很成熟,各家算出来的数学结果是一致的。耗时间的是四个各家不统一的约定。

先看清 SM2 密文的结构

SM2 密文由三段拼成:

内容长度
C1加密时生成的临时椭圆曲线点64 字节(带 04 前缀则 65)
C2与明文等长的密文= 明文长度
C3SM3 摘要,用于完整性校验恒定 32 字节

关键在于:C1 和 C3 的长度是固定的。这给了我们一条从长度倒推格式的路。

坑一:C1C2C3 还是 C1C3C2

现行规范要求 C1C3C2,但线上大量存在 C1C2C3。

分歧的来源很具体:SM2 算法标准 GM/T 0003-2012 在描述算法时按 C1、C2、C3 的顺序讲,早期实现就照着这个顺序拼接;后来 GM/T 0009-2012《SM2 密码算法使用规范》明确规定密文排列是 C1 || C3 || C2

BouncyCastle 早期版本只有 C1C2C3,到 1.57 才加入 SM2Engine.Mode.C1C3C2。所以两套并存,而且都还在生产环境里跑。

能自动判别吗?不能。 两种排列的 C1 都在最前面,总长度也完全一样,从结构上区分不了。

但试错的代价很低。 SM2 解密会用 C3 做完整性校验——排列选反了会直接校验失败并报错,而不是安静地吐出一段错误明文。对调一下再试就行,不存在「悄悄解错了还不知道」的风险。

坑二:开头那个 04

04 是未压缩椭圆曲线点的标准前缀,来自 SEC 1 的点编码约定:

  • 04 开头 → 未压缩,X 和 Y 都给全(64 字节)
  • 02 / 03 开头 → 压缩,只给 X,用 02/03 区分 Y 的奇偶(32 字节)

C1 本质就是一个曲线点,输出时带不带这个前缀纯属实现选择。带 04 的密文比不带的长 1 字节,Hex 串长 2 个字符。

这个是可以自动判别的,而且判据很强:取开头 64 字节当作 (X, Y),验证它是否落在 SM2 曲线上;再取第 2 到第 65 字节验证一遍。只有正确的那一种能通过——落在曲线上是个极强的约束,随机数据几乎不可能碰巧满足。

本站工具就是这么做的:不靠长度猜,而是拿曲线方程验一遍。

坑三:以 30 开头的 DER 编码

如果密文以 30 开头、而且长度还不固定,那它是 ASN.1 DER 编码,不是裸拼接。

0x30 是 DER 里 SEQUENCE 的标签字节。GM/T 0009-2012 除了裸拼接还定义了一种 ASN.1 表示:

SEQUENCE {
  x           INTEGER
  y           INTEGER
  hash        OCTET STRING (32)
  cipherText  OCTET STRING
}

Java 生态里 DER 很常见,因为 BouncyCastle 的输出默认带 ASN.1 结构;JavaScript 和 Go 的国密库则更多直接给裸拼接。

为什么长度会浮动:DER 的 INTEGER 是有符号的。当 x 或 y 的最高位是 1 时,编码要在前面补一个 0x00 字节,否则会被当成负数。所以同一对坐标可能编码成 32 字节也可能 33 字节。

这解释了一个常见困惑:同一份明文加密多次,DER 密文的总长度会在几个字节之间浮动——这是正常的,不是数据损坏。

坑四:签名的 userId 和编码

SM2 签名验不过,在密钥和数据都确认无误之后,剩下的基本就是这两个。

userId 参与 Z 值计算

SM2 签名不是直接对消息做 SM3。它先算一个 Z 值:

Z = SM3( ENTL || ID || a || b || xG || yG || xA || yA )

然后对 Z || 消息 做 SM3,再对这个摘要签名。

其中 ID 就是用户标识。GB/T 32918.2 给出的默认值是 ASCII 字符串 1234567812345678(16 字节,对应的 ENTL 是 0x0080)。绝大多数库默认用它,但有的实现允许业务自定义,有的用了别的默认值

两端 ID 不一致,Z 值就不同,签名必然验不过。 而报错只会说「验签失败」,不会提示是 ID 的问题——这是最难猜的一个坑。

签名的两种编码

签名本质是 (r, s) 一对数,有两种写法:

编码形态常见于
裸拼接r || s,正好 64 字节 / 128 个 Hex 字符前端国密库
DERSEQUENCE { r INTEGER, s INTEGER },以 30 开头,70–72 字节浮动Java / BouncyCastle

判别很简单:长度正好 128 个 Hex 字符就是裸拼接;以 30 开头且长度浮动就是 DER。

一条完整的排查路径

1. 密文以 30 开头?
   ├─ 是 → 先做 DER 解码,得到裸的三段,回到第 2 步
   └─ 否 → 继续

2. 算长度:Hex 字符数 ÷ 2 = 总字节数
   总字节数 − 96 = ?
   ├─ 等于明文字节数     → 没有 04 前缀
   └─ 比明文字节数多 1   → 带 04 前缀
   (明文按 UTF-8 算,一个汉字 3 字节)

3. 用曲线校验复核:
   取出认定为 C1 的 64 字节,验证 (X, Y) 是否在 SM2 曲线上

4. C1C3C2 和 C1C2C3 各试一次
   → 能通过 C3 完整性校验的那个就是对的

5. 签名问题:先对 userId,再看长度判编码

第 2 步里「明文是字节不是字符」这一条容易翻车——一个汉字按 UTF-8 是 3 字节,算错会让整条推导跑偏。

SM4 那边的情况

SM4 的坑比 SM2 少得多。它的结构和 AES 几乎一样:128 位分组、128 位密钥,支持 ECB / CBC / GCM,填充也是 PKCS7。所以 AES 的那套对接问题原样适用——密钥按 UTF-8 还是 Hex 解释、CBC 的 IV 从哪来、填充用哪种、输出编码是什么。

唯一值得单说的是密钥长度:SM4 只有 128 位一种,没有 192 和 256 的变体。看到「SM4-256」这种说法,基本可以判定对方搞错了。

SM4-GCM 需要额外传 nonce 和 16 字节认证标签,漏传标签是常见的失败原因,和 AES-GCM 一模一样。

用工具把变量固定下来

国密 SM2/SM3/SM4把上面几个变量都做成了显式选项:

  • 密文排列可选 C1C3C2 或 C1C2C3
  • ASN.1 DER 编码可开可关
  • 签名编码可选裸拼接或 DER
  • userId 可以自定义,默认是国标的 1234567812345678
  • 密文会按段拆开展示 C1 / C3 / C2,直接看出各段长度对不对
  • 04 前缀靠曲线校验判别,不靠长度猜

SM3 用 GM/T 0004 的官方测试向量验证,SM4 用 GB/T 32907 的官方向量验证,全部在浏览器本地计算,密钥不上传。

排查时的建议顺序是:先用工具把对方给的密文解出来——这一步成功就意味着四个变量全部对齐了;然后用同一组设置加密自己的数据发回去。中间任何一步失败,对照上面那条路径逐项排除。

如果你还在判断这个项目到底要不要上国密、三个算法各自对应哪份标准,见 国密 SM2/SM3/SM4 到底什么时候必须用

❓ 常见问题

C1C2C3 和 C1C3C2 到底哪个是对的?

现行国密规范要求 C1C3C2,但你会大量遇到 C1C2C3,两个都得支持三段分别是什么:C1 是加密时生成的临时椭圆曲线点(64 字节,或带 04 前缀 65 字节),C2 是与明文等长的密文,C3 是 32 字节的 SM3 摘要,用来做完整性校验。分歧从哪来:SM2 算法标准 GM/T 0003-2012 在描述算法时按 C1、C2、C3 的顺序讲,早期实现就照着拼;后来 GM/T 0009-2012《SM2 密码算法使用规范》明确规定密文的排列是 C1 || C3 || C2。BouncyCastle 早期版本只有 C1C2C3,1.57 版才加入 SM2Engine.Mode.C1C3C2。所以线上同时存在两套,且都还在跑。能不能自动判别:不能。两种排列的 C1 都在最前面,长度也完全一样,从结构上分不出来。但试错的代价很低——SM2 解密会用 C3 做完整性校验,排列选反了会直接校验失败并报错,不会安静地吐出一段错误明文。对调一下再试即可。

密文开头的 04 是什么?有的有有的没有

04 是未压缩椭圆曲线点的标准前缀,表示后面跟着完整的 X 和 Y 坐标。这是 SEC 1 里定义的点编码约定:04 开头表示未压缩(X 和 Y 都给全),0203 开头表示压缩(只给 X,用 02/03 区分 Y 的奇偶)。为什么各家不一样:C1 本质就是一个曲线点,输出时带不带这个前缀纯属实现选择。带 04 的密文比不带的长 1 字节,也就是 Hex 串长 2 个字符。怎么判断:这个是可以自动判别的——取开头 64 字节当作 (X, Y) 验证它是否落在 SM2 曲线上,再取第 2 到第 65 字节验证一遍,只有正确的那一种能通过。落在曲线上是个极强的约束,随机数据几乎不可能碰巧满足。如果知道明文长度还能更快:C1 加 C3 固定占 96 字节,所以「密文总字节数减 96」正好等于明文字节数时就没有 04 前缀,比明文多 1 字节时就带 04。注意 Hex 字符数永远是偶数,靠奇偶判断不出前缀,必须换算成字节再比。

密文以 30 开头、长度还不固定,是什么格式?

那是 ASN.1 DER 编码的 SM2 密文,不是裸拼接0x30 是 DER 里 SEQUENCE 的标签字节,所有 DER 结构都以它开头。GM/T 0009-2012 除了裸拼接之外还定义了一种 ASN.1 表示:一个 SEQUENCE 里依次装 x(INTEGER)、y(INTEGER)、hash(32 字节 OCTET STRING)、cipherText(OCTET STRING)。Java 生态里 DER 格式很常见,因为 BouncyCastle 的输出默认就带 ASN.1 结构;而 JavaScript、Go 的国密库更多直接给裸拼接。为什么长度不固定:DER 的 INTEGER 是有符号的,当 x 或 y 的最高位是 1 时,编码要在前面补一个 0x00 字节以免被当成负数——所以同样一对坐标,编码出来可能是 32 字节也可能是 33 字节。这也解释了一个常见困惑:同一份明文加密多次,DER 密文的总长度会在几个字节之间浮动,这是正常的。

SM2 签名验不过,密钥和数据都确认对了,还能是什么问题?

先查 userId,再查签名编码userId 是什么:SM2 签名不是直接对消息做 SM3,而是先算一个 Z 值——Z = SM3(ENTL || ID || a || b || xG || yG || xA || yA),其中 ID 就是用户标识,然后对 Z || 消息 做 SM3 再签名。关键在于 ID 是可变的:GB/T 32918.2 给出的默认值是 ASCII 字符串 1234567812345678(16 字节),绝大多数库默认用它,但有的实现允许业务自定义,有的干脆用了别的默认值。两端 ID 不一致,Z 值就不同,签名必然验不过——而且报错只会说验签失败,不会提示是 ID 的问题。第二个坑是签名编码:签名本质是 (r, s) 一对数,可以编码成 64 字节的裸拼接 r || s(Hex 128 个字符),也可以编码成 DER 的 SEQUENCE { r INTEGER, s INTEGER }(以 30 开头,长度 70 到 72 字节浮动)。Java 侧多用 DER,前端库多用裸拼接。判别很简单:长度正好 128 个 Hex 字符就是裸拼接,以 30 开头且长度浮动就是 DER。

有没有一条能倒推格式的排查路径?

有,从长度入手,因为 SM2 密文的三段里有两段长度是固定的已知量:C1 是 64 字节(带 04 前缀则 65),C3 恒定 32 字节,C2 与明文等长。所以:密文总字节数减去 96 就应该等于明文字节数。推导步骤:(1) 密文 Hex 字符数除以 2 得总字节数;(2) 如果是 30 开头,先停——那是 DER,得先解码;(3) 总字节数减 96,如果结果和你预期的明文长度一致,说明没有 04 前缀;如果比预期多 1,说明带 04 前缀;(4) 前缀确定后,用曲线校验复核一遍:正确的那 64 字节一定落在 SM2 曲线上;(5) 最后才试 C1C3C2 和 C1C2C3,两种都试一遍,能通过 C3 校验的那个就是对的。注意明文是字节不是字符——一个汉字按 UTF-8 是 3 字节,这一步算错会让整条推导跑偏。

SM4 那边有类似的坑吗?

比 SM2 少得多,但 IV 和填充仍然要对齐SM4 的结构和 AES 几乎一样:128 位分组、128 位密钥、支持 ECB / CBC / GCM 等模式,填充也是 PKCS7。所以 AES 那一套对接问题原样适用——密钥按 UTF-8 还是 Hex 解释、CBC 的 IV 从哪来、填充用哪种、输出编码是 Base64 还是 Hex。唯一值得单说的是密钥长度:SM4 只有 128 位一种,没有 192 和 256 的变体。看到「SM4-256」这种说法基本可以判定对方搞错了,或者用的是某种非标准扩展。另外 GCM 模式:SM4-GCM 需要额外传 nonce 和 16 字节认证标签,漏传标签是常见的对接失败原因,和 AES-GCM 完全一样。

㊙️ 打开 国密 SM2/SM3/SM4 SM4 对称加解密·SM2 公钥加解密与签名验签·SM3 摘要/HMAC·国标 GM/T·本地运行不上传

📖 同一工具的其他教程

🔗 相关阅读

全部教程 →