SM2 公钥压缩格式实战:C1C2C3 密文、签名编码与跨语言互操作完整指南

国密算法 · 2026-10-07 · 1 阅读

前言

SM2 是我国自主研发的椭圆曲线公钥密码算法标准,包含数字签名、密钥交换和公钥加密三大功能。在实际工程中,开发者经常遇到一个令人困惑的问题:同一把密钥,用不同库签名或加密后,结果却无法互相验证。

根本原因往往在于 公钥压缩格式 的处理差异。SM2 标准定义了两种公钥表示方式:非压缩格式(9 字节前缀 + 64 字节坐标)和压缩格式(1 字节前缀 + 32 字节 x 坐标)。不同的密码库在实现 C1C2C3 密文格式、签名编码时,对压缩格式的处理各不相同。

本文从 GM/T 0003.2-2012 和 GM/T 0010-2023 标准出发,通过实际代码演示 SM2 压缩公钥的正确用法,并给出 Python、Java、Go 三语言互操作的完整解决方案。

一、SM2 公钥压缩格式解析

1.1 非压缩格式与压缩格式对比

SM2 曲线参数定义在 GM/T 0003.5-2012 中,公钥点 (x, y) 可以用两种方式表示:

格式字节长度前缀字节内容
非压缩格式65 字节0x040x04 ‖ x ‖ y
压缩格式33 字节0x02/0x03前缀 ‖ x
非压缩格式直接使用 0x04 作为前缀,后面跟完整的 x 和 y 坐标(各 32 字节)。压缩格式则利用椭圆曲线的数学性质:已知 x 坐标和 y 的奇偶性,可以唯一确定 y 值。因此只需存储 x 坐标(32 字节)加上一个标识 y 奇偶性的前缀字节(0x02 表示 y 为偶数,0x03 表示 y 为奇数)。

1.2 压缩格式转换公式

给定压缩公钥 (prefix, x),恢复 y 坐标的公式为:

CODE
y² ≡ x³ + ax + b (mod p)

其中:

  • p = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF
  • a = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC
  • b = 0x28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93
计算出 y² 后,需要求模 p 的平方根。GM/T 0003.5-2012 提供了高效的平方根计算方法:

CODE
y = (y²)^((p+3)/8) mod p  或  y = 2y(y²)^((p-5)/8) mod p

若 y² 是二次剩余(即存在平方根),则上述公式可计算出 y;否则该 x 值不对应曲线上任何点。

二、C1C2C3 密文格式与压缩公钥

2.1 SM2 加密流程回顾

SM2 公钥加密采用混合加密方案,加密过程如下:

CODE
1. 随机生成临时密钥对 (k, kB),其中 k ∈ [1, n-1]
2. 计算 C1 = [k]G = (x1, y1)
3. 计算 [k]PB = (xs, ys),其中 PB 是接收方公钥
4. 计算 t = KDF(xs ‖ ys, klen),klen 为消息长度
5. C2 = M ⊕ t(消息加密)
6. C3 = Hash(C2 ‖ xs ‖ ys)

密文输出格式为 C1 ‖ C2 ‖ C3,其中 C1 的编码方式取决于公钥是否压缩。

2.2 C1 的两种编码方式

非压缩格式(C1C2C3):

CODE
C1 = 0x04 ‖ x1 ‖ y1  (65 字节)

压缩格式(C1C3C2):

CODE
C1 = prefix ‖ x1  (33 字节,prefix 为 0x02 或 0x03)

注意:标准中定义的密文格式是 C1C3C2,但许多实现使用 C1C2C3。这导致了大量的互操作问题。

2.3 实际代码示例

以下 Python 代码演示了 SM2 加密和解密的完整过程:

运行输出:

CODE
压缩公钥: 0232c4ae2c1f1981195f9904466a39c9948fe30bbff2660be1715a4589b9f68d7d
密文长度: 98 字节
解密结果: SM2 compressed key encryption test
验证: ✓ 成功

三、SM2 签名编码与压缩公钥

3.1 签名格式对比

SM2 签名的数学本质是两个 256 位整数 r 和 s。常见的编码方式有两种:

格式长度结构适用场景
R+S 原始格式64 字节r ‖ s国密系统内部传输
DER 编码格式70-72 字节SEQUENCE {INTEGER r, INTEGER s}证书、X.509 兼容场景

3.2 压缩公钥对签名的影响

当使用压缩公钥进行签名时,签名验证过程中需要正确还原公钥坐标。错误的压缩格式处理会导致验证失败。

以下是使用 gmssl 库的正确签名验证代码:

3.3 常见错误与解决方案

错误 1:公钥格式不匹配

现象:验签时报 invalid point 或 point not on curve 错误。

原因:签名时使用压缩公钥,但验签时使用了非压缩格式,或反之。

解决:确保签名和验签使用相同格式的公钥。

错误 2:签名长度不一致

现象:一方签名输出 64 字节,另一方期望 70 字节。

原因:R+S 格式与 DER 格式的混淆。

解决:明确双方约定使用哪种格式,并进行格式转换:

四、跨语言互操作实战

4.1 Python ↔ Java 互操作

Python 使用 gmssl 库,Java 使用 Bouncy Castle。两者在公钥压缩格式处理上存在差异。

Python 端代码:

Java 端代码(Bouncy Castle):

4.2 Python ↔ Go 互操作

Go 语言的 crypto/ecdsa 包默认使用压缩格式,需要特别注意。

Python 端:

PYTHON
from gmssl import sm2

# 使用完整公钥(推荐跨语言互操作)
crypt_sm2 = sm2.CryptSM2(
    private_key='3945258278666cebd577773f210053573f52ede6917e5085b719d1702c07bcf1',
    public_key='32c4ae2c1f1981195f9904466a39c9948fe30bbff2660be1715a4589b9f68d7d274010cfedb453b4deb0c2ebf8291b779822fd5dc73f173243b960b21119769'
)

message = b'Test message'
signature = crypt_sm2.sign_with_sm3(message)
print(f"签名 (R+S): {signature}")

Go 端:

4.3 互操作性检查清单

在跨语言系统对接时,请逐项检查:

检查项Python (gmssl)Java (Bouncy Castle)Go (crypto/ecdsa)
公钥格式支持压缩/非压缩支持压缩/非压缩默认压缩
签名格式R+S (64 字节)DER (70-72 字节)DER (70-72 字节)
哈希算法SM3SM3需自定义 SM3
密文格式C1C3C2C1C2C3/C1C3C2需自定义
关键建议:跨语言互操作时,优先使用非压缩公钥格式和 R+S 签名格式,以减少格式转换的复杂度。

五、生产环境最佳实践

5.1 公钥格式选择策略

场景推荐格式理由
内存存储压缩格式节省 50% 空间
网络传输非压缩格式避免解压开销,提高兼容性
证书存储非压缩格式X.509 标准要求
嵌入式设备压缩格式资源受限环境

5.2 错误处理最佳实践

5.3 性能优化建议

  • 批量验签:使用并行处理提高吞吐量
  • 公钥缓存:重复使用的公钥应缓存解析结果
  • 格式预转换:在入口层统一格式转换,避免重复计算

六、总结

SM2 压缩公钥的处理是国密工程实践中的重要环节。本文通过实际代码演示了:

  • 压缩/非压缩公钥的相互转换,包括数学原理和代码实现
  • C1C2C3 密文格式的正确构造与解码
  • 签名编码格式(R+S 与 DER)的转换方法
  • 跨语言互操作的解决方案和注意事项
在生产环境中,建议始终使用标准库(如 gmssl、Bouncy Castle)提供的 API,避免自行实现密码算法。对于跨语言场景,优先选择兼容性最好的格式组合,并在接口文档中明确约定数据格式。

参考