SM2 签名验证深度解析
一、SM2 算法基础
1.1 椭圆曲线参数
SM2 使用 256 位素数域椭圆曲线,标准参数(GM/T 0003-2012):
p = FFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF 00000000 FFFFFFFF FFFFFFFF
a = FFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF 00000000 FFFFFFFF FFFFFFFC
b = 28E9FA9E 9D9F5E34 4D5A9E4B CF6509A7 F39789F5 15AB8F92 DDBCBD41 4D940E93
n = FFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF 7203DF6B 21C6052B 53BBF409 39D54123
Gx = 32C4AE2C 1F198119 5F990446 6A39C994 8FE30BBF 2660BE17 155F1A4F 0E50C1C2
Gy = 0E54E5B4 8906E88E 3D9DE9DB 58AB5B6B 8026CD29 D6D5D6C0 8B7B29C2 6F984FE4
基点 G = (Gx, Gy),阶为 n。
1.2 密钥对生成
- 私钥 d:随机选取 1 ≤ d ≤ n-2
- 公钥 P:P = d × G
1.3 SM2 与标准 ECDSA 的区别
| 特性 | 标准 ECDSA | SM2 |
|---|---|---|
| 签名前处理 | 直接对 hash 签名 | 需要 Z 值预处理 |
| 哈希算法 | SHA-1/SHA-256 | SM3 |
| 用户标识 | 无 | 有(IDA) |
| 预处理 | 无 | Z = SM3(ENTL ‖ ID ‖ a ‖ b ‖ Gx ‖ Gy ‖ Px ‖ Py) |
关键差异 :SM2 签名不是直接对原始数据或哈希签名,而是先计算 Z 值 (包含用户可区分标识符和公钥),再对 Z ‖ 原始数据 做 SM3 摘要后签名。这是 SM2 与 ECDSA 在工程实现上最大的不同。
二、Z 值计算详解
2.1 Z 值的定义
Z 值是 SM2 签名验证双方的"身份指纹",由以下信息拼接后做 SM3 摘要得到:
Z = SM3(ENTL ‖ ID ‖ a ‖ b ‖ Gx ‖ Gy ‖ Px ‖ Py)
其中:
- ENTL:ID 的比特长度,用 2 字节表示
- ID :用户可区分标识符(默认值
1234567812345678) - a, b:曲线参数
- Gx, Gy:基点坐标
- Px, Py:公钥坐标
2.2 Z 值计算流程
┌─────────────────────────────────────────────┐
│ 1. ENTL = ID长度(字节) × 8,用2字节表示 │
│ 2. 拼接: ENTL ‖ ID ‖ a ‖ b ‖ Gx ‖ Gy ‖ Px ‖ Py │
│ 3. Z = SM3(上述拼接数据) │
└─────────────────────────────────────────────┘
2.3 默认 ID 的重要性
GM/T 0003 标准规定默认用户 ID 为:
"1234567812345678" (16字节ASCII)
对应的 ENTL = 16 × 8 = 128 = 0x0080。
工程陷阱:签名方与验证方必须使用相同的 ID。如果签名时用默认 ID,验证时却用了空 ID 或自定义 ID,Z 值不同,导致验证失败(即使签名本身正确)。
项目经验:
SM2 signature verification requires matching Z value calculation (user ID must be consistent)
三、签名生成流程
3.1 完整流程
SM2 签名生成算法(GM/T 0003 第2部分):
输入:私钥 d、用户 ID、消息 M
输出:签名 (r, s)
1. 计算 Z = SM3(ENTL ‖ ID ‖ a ‖ b ‖ Gx ‖ Gy ‖ Px ‖ Py)
2. 计算 e = SM3(Z ‖ M) ← 关键预处理
3. 生成随机数 k,1 ≤ k ≤ n-1
4. 计算 (x1, y1) = k × G
5. 计算 r = (e + x1) mod n ← 若 r=0 或 r+k=n 则重新生成 k
6. 计算 s = (1+d)^(-1) × (k - r×d) mod n ← 若 s=0 则重新生成
7. 输出 (r, s)
3.2 Z 值在签名中的作用
注意第 2 步:e = SM3(Z ‖ M)。这里的关键是:
- 不是直接对 M 做 SM3
- 而是 对
Z ‖ M做 SM3(Z 在前,M 在后)
这意味着签名者必须知道自己的公钥(P = d × G),才能计算 Z。在 USBKey 中,公钥存储在卡内,签名时由固件完成 Z 计算和签名。
3.3 bInternalHash 参数
VENDOR SKF 接口(SKF_ECCSignData)有 bInternalHash 参数:
bInternalHash = 1:固件内部完成 SM3(Z ‖ rawdata) 摘要再签名bInternalHash = 0:调用方传入已计算好的哈希,固件直接签名
项目经验:
SKF_ECCSignData with bInternalHash=1 requires SM3(Z ∥ raw data) verification
当 bInternalHash=1 时,验证方必须知道固件使用的 Z 值,自行计算 SM3(Z ‖ rawdata) 得到 e,才能验证签名。
四、签名验证流程
3.1 完整流程
SM2 签名验证算法:
输入:公钥 P、用户 ID、消息 M、签名 (r, s)
输出:验证通过/失败
1. 检查 r、s 范围:1 ≤ r,s ≤ n-1
2. 计算 Z = SM3(ENTL ‖ ID ‖ a ‖ b ‖ Gx ‖ Gy ‖ Px ‖ Py)
3. 计算 e = SM3(Z ‖ M) ← 必须与签名方相同
4. 计算 t = (r + s) mod n ← 若 t=0 则验证失败
5. 计算 (x1, y1) = s×G + t×P
6. 计算 R = (e + x1) mod n
7. 比较 R == r,相等则验证通过
4.2 数学等价性证明
签名时有 r = (e + x1) mod n,其中 (x1,y1) = k×G。
验证时计算 (x1',y1') = s×G + t×P,展开:
s×G + t×P = s×G + (r+s)×d×G
= (s + rd + sd)×G / (1+d) × (1+d)
= (s(1+d) + rd) × G / (1+d)
= (s + sd + rd) × G / (1+d)
由签名关系 s = (k - rd)/(1+d),得 s(1+d) = k - rd,即 k = s(1+d) + rd:
s×G + t×P = k×G = (x1, y1)
因此 x1' = x1,R = (e + x1') mod n = (e + x1) mod n = r,验证通过。
4.3 e' 必须等于 e 的关键
验证算法第 7 步比较的是 R = (e + x1) mod n 与 r。而签名时 r = (e + x1) mod n,所以:
验证成功的充要条件是 e' == e(Z 值计算一致)
这是为什么 Z 值一致性如此重要------只要 Z 不同,e 就不同,验证必然失败。
五、verify_with_sm3 与 verify 方法差异
5.1 两种验证方法
在 PolarSSL/mbedTLS 的 SM2 实现中,通常有两个验证函数:
c
// 方法1:标准 SM2 验证(含 Z 预处理)
int sm2_verify_with_sm3(
const unsigned char *ID, size_t ID_len,
const unsigned char *hash, // 原始消息(非哈希)
size_t hash_len,
const unsigned char *sig, size_t sig_len,
const unsigned char *Q, size_t Q_len // 公钥
);
// 方法2:非标准验证(无 Z 预处理)
int sm2_verify(
const unsigned char *hash, // 已计算好的哈希
size_t hash_len,
const unsigned char *sig, size_t sig_len,
const unsigned char *Q, size_t Q_len
);
5.2 关键差异对比
| 特性 | verify_with_sm3 | verify |
|---|---|---|
| Z 值计算 | ✅ 内部计算 Z | ❌ 不计算 Z |
| 输入数据 | 原始消息 M | 已计算的哈希 e |
| 摘要计算 | 内部完成 SM3(Z‖M) | 不做(调用方负责) |
| 适用场景 | 标准 SM2 签名验证 | 非标准/自定义预处理 |
| 一致性保证 | 签名验证双方 Z 必须一致 | 调用方自行保证 e 一致 |
5.3 项目中的选择
项目经验:
测试_副本.sh 使用旧版 dylib 无 Z 预处理(无 'ZZZZZZZZ' 字符串,32-byte SM3 hash)
旧版动态库不含 Z 预处理逻辑(搜索二进制无 ZZZZZZZZ 字符串),直接对 32 字节 SM3 哈希签名。新版动态库增加了 Z 预处理,符合标准。
旧版流程: M → SM3 → e(32字节) → 签名 (非标准)
新版流程: M → Z‖M → SM3 → e(32字节) → 签名 (标准)
对应验证方也要选择匹配的方法:
- 旧版签名 → 用
verify(直接传 32 字节哈希) - 新版签名 → 用
verify_with_sm3(传原始消息,内部计算 Z)
六、PKCS#11 机制选择策略
6.1 三种 SM2 签名机制
PKCS#11 定义了多个 SM2 相关机制:
| 机制 | 值 | 含义 |
|---|---|---|
| CKM_SM2_SIGN | 0x00000001 | SM2 签名(原始数据) |
| CKM_SM2_SIGN_VERIFY | 0x00010002 | SM2 签名验证 |
| CKM_SM2_SM3_SIGN_VERIFY | 0x00010004 | SM2+SM3 签名验证 |
| CKM_SM3 | 0x00000020 | SM3 摘要 |
6.2 项目中的机制选择逻辑
当 ulAlgID=0 时,根据输入数据长度自动选择机制:
c
if (dataLen == 32) {
// 32字节 = 已计算好的SM3哈希
// 使用 CKM_SM2_SIGN_VERIFY,固件内部做 Z‖e 再签名
mechanism = CKM_SM2_SIGN_VERIFY; // 0x10002
} else {
// 原始数据
// 使用 CKM_SM2_SIGN,固件内部完成 SM3(Z‖M) 再签名
mechanism = CKM_SM2_SIGN;
}
6.3 交易签名的特殊路径
交易签名(需要确认,bCfm=1)走特殊路径,涉及 XML 解析(PARTNER_CODE 标准)和用户二次确认。非交易场景(bCfm=0)必须走普通签名路径(APDU P2=0x81),否则 Key 返回 0x6982(安全状态不满足)。
七、Key 类型与 Z 值处理
7.1 CKK_SM2_WIN vs CKK_SM2
CKK_SM2_WIN 作为 SM2 私钥类型标记(非标准 CKK_SM2),查找私钥时必须过滤此类型:
c
CK_ATTRIBUTE keyTemplate[] = {
{ CKA_CLASS, &privKeyClass, sizeof(privKeyClass) },
{ CKA_KEY_TYPE, &sm2WinType, sizeof(sm2WinType) }, // CKK_SM2_WIN
{ CKA_ID, keyId, keyIdLen }
};
CKK_SM2_WIN 类型的密钥签名时,Key 固件期望输入为 Z ‖ raw data 形式(Z 在前),由固件内部完成 SM3 摘要。如果在 session.cpp 中不添加 Z 值前缀,Key 返回 0x6985(使用条件不满足)。
7.2 Z 值预处理库的安装
Z 值预处理逻辑包含在特定版本的动态库中。验证方法:搜索库二进制是否包含 ZZZZZZZZ 字符串标记。
bash
# 检查动态库是否包含 Z 预处理逻辑
strings [VENDOR_LIB_SM2].dylib | grep "ZZZZZZZZ"
八、硬件 Token 类型差异
8.1 CToken3003 vs CTokeni3kPARTNER_CODE
| Token 类 | 适用设备 | 协议特点 |
|---|---|---|
| CToken3003 | 标准 FT DEVICE_MODEL_B | 标准 VENDOR 协议 |
| CTokeni3kPARTNER_CODE | PARTNER_NAME(PARTNER_NAME_CN)系列 | PARTNER_CODE 自定义协议 |
日志中的 ATR 比对(compare Atr)就是用来区分设备类型,从而选择正确的 Token 处理类。
8.2 APDU 命令差异
读取公钥的 APDU 命令:
80 E6 19 1F 40
不同 Token 类型的 APDU 可能不同,这解释了为什么驱动库的公钥命令会变化------硬件版本不同使用不同协议。
九、常见错误码速查
| 错误码 | 含义 | 可能原因 |
|---|---|---|
| 0x6982 | 安全状态不满足 | 未走正确的签名路径(P2值错误) |
| 0x6985 | 使用条件不满足 | Z 值未预处理 / Key 类型不匹配 |
| 0x6A80 | 数据参数错误 | 数据长度/格式不符 |
| 0x6700 | 长度错误 | 输入数据长度不正确 |
十、工程最佳实践
调试技巧
- 打印 Z 值:在签名和验证两端分别打印 Z,确认一致
- 打印 e 值 :确认
SM3(Z‖M)结果相同 - 二进制检查 :
strings libxxx.dylib | grep ZZZZZZZZ确认 Z 处理逻辑 - 抓包对比:对比正常与异常环境的 APDU 报文
参考文献:
- GM/T 0003-2012 《SM2 椭圆曲线公钥密码算法》
- GM/T 0004-2012 《SM3 密码杂凑算法》
- PKCS#11 v2.20 规范