开门见山:一秒钟的选型结论
非对称用 SM2(曲线固定为 sm2p256v1,绝不能换其他椭圆曲线),签名前置哈希必须按 GM/T 0003 规范计算 Z 值(不是简单 SM3(消息)),加密密文顺序统一用 C1C3C2(即 04 || X || Y || SM3摘要 || 密文);哈希用 SM3(256 位输出),HMAC 用 HMAC-SM3;对称用 SM4,新系统一律 SM4-GCM 或 SM4-CBC + HMAC-SM3(绝不用裸 ECB);协议层国产系统走 TLCP 双证书(签名证书+加密证书),跨网络与 TLS 互通走 RFC 8998 定义的 TLS 1.3 套件(TLS_SM4_GCM_SM3);工具链选 GmSSL 或 Tongsuo(铜锁)做底层,Java 用 BouncyCastle 1.68+,Node 用 sm-crypto,绝不用网上流传的"纯 JavaScript 国密实现"。
如果你赶时间,把上面这段话贴到方案评审文档里就够了。如果你接着往下读,这篇文章会回答一个更本质的问题:为什么 2026 年了,国密改造项目里"签名验证失败"和"加密结果无法互通"仍然是最常见的两大翻车现场?为什么同一份"国密实现",在不同库之间互通的成功率不到 50%? 答案几乎从来不在算法本身------SM2/SM3/SM4 的算法设计都是经过严格评审的------而在规范细节的工程实现上:Z 值怎么算、密文顺序怎么拼、曲线 OID 怎么标、双证书怎么配、密码套件怎么协商,每一处"看起来无所谓"的偏差,都可能让两套"都符合国密"的系统无法互通。
一、先看清地图:国密算法家族与"三件套"的定位
1.1 完整的国密算法谱系
"国密"是国家密码管理局发布的**商用密码(SM,即"商密")**算法系列。核心是五个算法家族,覆盖现代密码学的三大类目:
| 算法 | 类别 | 国家标准 | 对标的国际算法 | 典型用途 |
|---|---|---|---|---|
| SM1 | 分组密码(不公开,仅芯片实现) | - | AES-128 | 智能卡、轨道交通门禁 |
| SM2 | 椭圆曲线公钥密码 | GB/T 32918-2016(5 部分) | RSA / ECDSA / ECDH | 数字签名、密钥交换、公钥加密 |
| SM3 | 密码杂凑 | GB/T 32905-2016 | SHA-256 | 摘要、HMAC、签名前置哈希 |
| SM4 | 分组密码 | GB/T 32907-2016 | AES-128 | 传输与存储加密 |
| SM9 | 标识密码(IBC) | GB/T 38635-2020 | 无直接对标 | 以身份为公钥的签名与加密 |
| ZUC(祖冲之) | 序列密码 | GB/T 33133 系列 | Snow 3G / AES-CTR | 5G/4G 通信加密 |
一句话记忆 :SM2 管非对称、SM3 管摘要、SM4 管对称、SM9 管身份即公钥、ZUC 管流密码 。日常开发中 90% 的国密改造只涉及 SM2/SM3/SM4 三件套 ,所以这三件我们展开讲,SM9 和 ZUC 讲清定位即可。
SM1 的特殊性 :SM1 算法本身不公开 ,只以加密芯片 的形式提供(如 HSM、加密卡、智能卡)。这意味着软件层面无法直接调用 SM1------如果项目要求 SM1,意味着必须采购密码卡/密码机等硬件。这是密评现场最常被搞错的点之一:方案里写"用 SM1 加密",落地时必须先理解 SM1 不是软件算法。
1.2 为什么是"三件套"?工程定位的清晰映射
把 SM2/SM3/SM4 放到国际算法的坐标系里看,定位非常清晰:
| 能力 | 国际方案 | 国密方案 | 性能对比 |
|---|---|---|---|
| 数字签名 | RSA-2048 / ECDSA P-256 | SM2(256 位曲线) | SM2 签名速度 > RSA-2048,密钥短一半 |
| 密钥交换 | ECDH P-256 | SM2 密钥交换 | 等价 |
| 公钥加密 | RSA-OAEP | SM2 公钥加密 | 密文比 RSA 短一半 |
| 摘要 | SHA-256 | SM3(256 位输出) | 性能接近 |
| HMAC | HMAC-SHA256 | HMAC-SM3 | 等价 |
| 对称加密 | AES-128-GCM | SM4-GCM | 软实现 SM4 慢于 AES(无 AES-NI 等价指令) |
一个重要的性能背景 :AES 在 Intel/ARM 上有 SIMD 指令集加速(AES-NI、Crypto Extensions),SM4 在通用 CPU 上长期只有纯软件实现。近年来 ARM 的 SM4 Crypto Extensions 和 Intel 的 AVX2 优化版本开始出现(如 Linux 内核的 crypto-sm4-arm64-ce-gcm 模块),但软实现 SM4 比硬加速 AES 慢一个数量级 是常态。这意味着:如果你改造的是高吞吐场景(如 CDN、网关),必须考虑硬件密码卡或 GPU 加速。
1.3 三大标准与合规强制
国密算法的落地不是"技术选型",而是合规强制。三条硬性法规把国密变成必选项:
- 《密码法》(2020 年施行):明确要求关键信息基础设施使用商用密码开展安全防护;
- 《商用密码管理条例》:规范商用密码检测认证体系;
- GB/T 39786-2021《信息系统密码应用基本要求》:密评(商用密码应用安全性评估)的核心依据,对等保三级及以上系统强制要求。
密评 vs 等保的关系 :等保是"整体网络安全评估"(公安主导),密评是"密码应用专项评估"(密码管理部门主导)。对于使用商用密码的等保三级及以上系统,两项评估都要做 ,密评会重点检查:传输通道是否用 SM 算法、存储是否用 SM4、摘要是否用 SM3、签名是否用 SM2。
一个高频整改单场景 :密评机构出单------"系统传输通道未采用商用密码算法保护,数据存储加密未使用 SM4,摘要算法应替换为 SM3"。真到动手时,大多数工程师的第一个困惑是:"国密算法到底有哪几个?SM2 和 RSA 什么关系?SM4 用什么模式?TLCP 双证书又是什么?"------这正是本文要逐一拆解的。
二、SM2:国产椭圆曲线的"一鱼三吃"
2.1 数学直觉与曲线参数
SM2 是基于椭圆曲线密码(ECC)的公钥算法,使用国密局指定的 256 位素域曲线 sm2p256v1 。它的安全性来自椭圆曲线离散对数问题(ECDLP)------已知私钥 d 和基点 G 算公钥 P = dG 是一次乘法,而已知 P、G 反推 d 在计算上不可行。
关键工程事实 :SM2 的曲线参数是固定的,由 GB/T 32918.5-2017 严格规定,RFC 8998 给出了完整参数:
text
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 F2660BE1 715A4589 334C74C7
Gy = BC3736A2 F4F6779C 59BDCEE3 6B692153 D0A9877C C62A4740 02DF32E5 2139F0A0
RFC 8998 的明文规定 :"与其他基于 ECC 的公钥算法不同,SM2 不得选择其他椭圆曲线 "。这是一个非常关键的合规红线------你不能用 P-256 曲线跑"SM2 算法",那不叫 SM2,那是普通 ECDSA,密评时会被判不合规。
国密 OID :所有国密算法在 ASN.1 体系下的 OID 都挂在 1.2.156.10197(iso(1) member-body(2) cn(156) gm(10197))这个中国注册根下:
id-sm2-sign = 1.2.156.10197.1.301(SM2 签名)sm3WithSM2 = 1.2.156.10197.1.501(SM2 with SM3 签名)id-sm2-keyAgreement = 1.2.156.10197.1.303(SM2 密钥协商)
常见翻车现场 :国密证书验签失败的第一原因就是签名算法 OID 错配 ------证书里 SignatureAlgorithm 字段必须为 1.2.156.10197.1.501(SM2 with SM3),如果写成 sha256WithRSAEncryption 或 ECDSA 的 OID,标准密码库会直接判"未知算法"或"签名不匹配"。
2.2 三种用途之一:SM2 数字签名与 Z 值的"陷阱"
SM2 签名看起来像 ECDSA,但预处理完全不同 。这是国密改造中最大的一个翻车点 。
很多人以为 SM2 签名 = SM3(消息) → 签名 ,这是错的 。GM/T 0003.2 规定的完整流程是:
Step 1:计算 Z 值(身份绑定)
text
Z = SM3( ENTL || IDA || a || b || Gx || Gy || xA || yA )
其中:
- ENTL:用户 IDA 的比特长度(2 字节大端)
- IDA:用户身份标识字符串(默认值 "1234567812345678",即 ASCII 0x31323334353637383132333435363738)
- a, b:SM2 曲线参数
- Gx, Gy:基点 G 坐标
- xA, yA:签名方公钥坐标
Step 2:计算真正的摘要
text
e = SM3( Z || M )
Step 3:标准 ECDSA-like 签名运算
text
随机选 k ∈ [1, n-1]
(x1, y1) = [k]·G
r = (e + x1) mod n 若 r=0 或 r+k=n,重选 k
s = ((1 + dA)^(-1) · (k − r·dA)) mod n 若 s=0,重选 k
签名 = (r, s)
Z 值的存在意义 :它把签名者身份 绑进了签名。没有 Z 值,攻击者可以构造一张"替换公钥"的攻击------保留签名,把证书里的公钥换成自己的,验签仍然通过(这是 ECDSA 的经典 key substitution attack)。Z 值通过把公钥坐标混入哈希 ,让签名"自证"公钥身份。
工程陷阱清单:
- ID 不匹配 ------签名端用 "1234567812345678",验签端用空字符串或另一个 ID,必然验签失败。IDA 必须作为协议的一部分显式传递,或者双方硬编码同一默认值;
- Z 值没算 ------某些简化实现直接对 SM3(M) 签名,这类签名与标准 SM2 不兼容 。GmSSL、BouncyCastle、tjfoc/gmsm 等标准库都会自动算 Z 值,自己实现时最容易漏;
- ENTL 字节序 ------必须是大端 2 字节,错写成小端必然失败;
- 签名格式 ------SM2 签名有**纯 R||S 拼接(64 字节)**和 **DER 编码(变长,通常 70-72 字节)**两种格式,两端必须约定同一种。Java BouncyCastle 默认输出 DER,某些前端 JS 库输出裸 64 字节,互通时报"签名不是 ASN.1 编码"是常见错误。
2.3 三种用途之二:SM2 公钥加密与 C1C2C3 / C1C3C2
SM2 加密结构(GB/T 32918.4):
text
密文 = C1 || C2 || C3 (老顺序)或 C1 || C3 || C2 (新顺序,标准推荐)
C1:椭圆曲线随机点(04 || x || y,共 65 字节未压缩)
C3:SM3 摘要(32 字节,用于完整性校验)
C2:与明文等长的密文
C1C2C3 vs C1C3C2 是另一个经典翻车点 。GB/T 32918.4-2016 标准顺序是 C1||C3||C2 ,但早期很多实现(包括 BouncyCastle 老版本)默认 C1||C2||C3 。两端约定不一致时,解密会"成功但拿到垃圾明文",或直接报摘要校验失败 。
密文格式还有第三层坑 :BouncyCastle 输出的密文会在 C1 前加一个 04 头(表示未压缩点),而 sm-crypto 等 JS 库的输出不带 04 头 。跨语言互通时必须先做长度判断补头或去头 。
GM/T 0003 的 ASN.1 描述:
text
SM2Cipher ::= SEQUENCE {
XCoordinate INTEGER, -- C1 的 x
YCoordinate INTEGER, -- C1 的 y
HASH OCTET STRING SIZE(32), -- C3
CipherText OCTET STRING -- C2
}
工程建议:
- 新系统统一用 C1C3C2 + 04 头(这是标准推荐,也是 GmSSL/Tongsuo 等主流库的默认);
- 跨语言交互时显式声明密文顺序------比如在 API 文档里写明"密文格式为 04||X(32bytes)||Y(32bytes)||SM3(32bytes)||Ciphertext";
- 不要试图"试错"------错误的顺序解出来的是错位字节,可能偶然通过摘要校验(极小概率),但本质上是错误实现。
2.4 三种用途之三:SM2 密钥交换
SM2 密钥交换(GB/T 32918.3)类似 ECDH,但加入了双方 ID 和临时密钥的复杂运算,比普通 ECDH 多了双方身份验证 ------可以看作"带认证的 ECDH"。在 TLCP 协议中,这是 ECDHE-SM2-WITH-SM4-SM3 套件的基础。
实际工程中 ,SM2 密钥交换很少直接在应用层使用,主要由 TLCP/国密 TLS 协议栈在握手阶段自动处理。应用开发者不需要手写密钥交换,只需要正确配置证书。
2.5 SM2 Python 实战代码
Python 生态的国密支持有三个主流库:gmssl(纯 Python 实现) 、tjfoc/gmsm(Go) 、pyca/cryptography + 第三方扩展。下面用 gmssl 演示:
python
# pip install gmssl
from gmssl import sm2, func
# ============ 1. 密钥生成 ============
# 生产环境:私钥应来自 HSM 或 CSPRNG,不要用 func.random_hex 这种简化方式
private_key = func.random_hex(64) # 32 字节 = 64 个十六进制字符
# 公钥是 04 || X(64) || Y(64) 共 130 个十六进制字符
public_key = "04" + "9EF573019D9A03B16B0BE44FC8A5B4E8E098F56034C97B312282DD0B4810AFC3" \
+ "CC759673ED0FC9B9DC7E6FA38F0E2B121E02654BF37EA6B63FAF2A0D6013EADF"
# ============ 2. SM2 加密(C1C3C2 模式)============
crypt_sm2 = sm2.CryptSM2(public_key=public_key, private_key=private_key)
plaintext = b"Transfer 100 to Alice"
ciphertext = crypt_sm2.encrypt(plaintext) # 默认为 C1C3C2 模式
decrypted = crypt_sm2.decrypt(ciphertext)
assert decrypted == plaintext
# ============ 3. SM2 签名(自动计算 Z 值)============
random_hex = func.random_hex(32) # 签名所需的随机数 k
sig = crypt_sm2.sign(plaintext, random_hex)
# sig 是 64 字节裸格式(r || s),如需 DER 需手动封装
verified = crypt_sm2.verify(sig, plaintext)
assert verified
# ============ 4. 指定 ID 的签名(与 GmSSL 命令行互通)============
# gmssl 命令行的默认 ID 是 "1234567812345678"
custom_id = "1234567812345678"
# gmssl 3.x 的 CryptSM2 支持 asn1 验签(用于国密证书场景)
GmSSL 命令行实战:
bash
# ============ SM2 密钥生成 ============
gmssl sm2keygen -pass 1234 -out sm2.pem -pubout sm2pub.pem
# ============ SM2 签名(自动算 Z 值,默认 ID = 1234567812345678)============
echo hello | gmssl sm2sign -key sm2.pem -pass 1234 -out sm2.sig -id 1234567812345678
# ============ SM2 验签 ============
echo hello | gmssl sm2verify -pubkey sm2pub.pem -sig sm2.sig -id 1234567812345678
# ============ SM2 加密解密 ============
echo hello | gmssl sm2encrypt -pubkey sm2pub.pem -out sm2.der
gmssl sm2decrypt -key sm2.pem -pass 1234 -in sm2.der
关键工程点:
- GmSSL 命令行的
-id参数对应 GM/T 0003 的用户 IDA,必须与对端约定一致; - GmSSL 输出的签名是裸 R||S 格式,如果对接国密证书的 X.509 验签,需要 DER 封装 (gmssl 有
sm2-verify-cert类命令); - 生产环境的 SM2 私钥应放密码卡/密码钥匙 (通过 SKF/SDF 接口访问),不要让私钥落盘。
三、SM3:国产摘要算法的"直接替换型"
3.1 SM3 的算法特点
SM3(GB/T 32905-2016)输出 256 位哈希,功能上对标 SHA-256 ,但内部压缩函数、消息扩展逻辑完全独立自研 ------这意味着 SM3 哈希和 SHA-256 哈希输出不同,不能互相校验 。
SM3 的工程要点:
- 输出 256 位(32 字节),与 SHA-256 一致;
- 没有长度扩展攻击 ------SM3 的 Merkle--Damgård 结构与 SHA-256 类似,但不建议 直接用"SM3(key || message)"这种 naive MAC(仍然存在长度扩展风险),必须用 HMAC-SM3;
- HMAC-SM3 是标准用法,输出 32 字节;
- PBKDF2-SM3 可以用于密码哈希,但密码存储更推荐 Argon2id(国密场景下 SM3 加盐 + 多次迭代是合规最低线)。
3.2 SM3 在签名和 Hmac 中的实战
python
from gmssl import sm3, func
# ============ SM3 摘要 ============
data = b"Hello, 国密"
sm3_hash = sm3.sm3_hash(func.bytes_to_list(data))
print(f"SM3: {sm3_hash}") # 64 个十六进制字符 = 256 位
# ============ HMAC-SM3 ============
from gmssl.sm3 import sm3_hash
import hmac
def hmac_sm3(key: bytes, msg: bytes) -> str:
"""手动实现 HMAC-SM3(gmssl 库未直接提供)"""
block_size = 64 # SM3 block size
if len(key) > block_size:
key = bytes.fromhex(sm3.sm3_hash(func.bytes_to_list(key)))
key = key.ljust(block_size, b'\x00')
ipad = bytes(b ^ 0x36 for b in key)
opad = bytes(b ^ 0x5c for b in key)
inner = sm3.sm3_hash(func.bytes_to_list(ipad + msg))
return sm3.sm3_hash(func.bytes_to_list(opad + bytes.fromhex(inner)))
GmSSL 命令行:
bash
# SM3 摘要
echo -n abc | gmssl sm3
# HMAC-SM3
echo -n abc | gmssl sm3hmac -key 11223344556677881122334455667788
3.3 SM3 的"特殊用法":配合 SM2 签名的 Z 值哈希
如第二节所述,SM2 签名的预处理会用 SM3 计算 Z 值 ------这是 SM3 在国密生态里最特殊的用法。如果你的系统自己做 SM2 签名验签,必须实现完整的 Z 值计算 ,不能用简单的 SM3(消息) 替代。
OpenHarmony 的 HVB(Hypervisor Verified Boot)源码给出了一个完整的 Z 值计算实现示例:
c
// Z 值计算(OpenHarmony hvb_sm2.c 简化)
void sm2_compute_z(uint8_t *z_out,
const uint8_t *user_id, size_t id_len,
const sm2_pubkey_t *pubkey) {
hvb_sm3_init(&ctx);
// 1. ID 长度(2 字节大端)
uint16_t entl = id_len * 8;
hvb_sm3_update(&ctx, (uint8_t[]){(entl >> 8) & 0xff, entl & 0xff}, 2);
// 2. ID
hvb_sm3_update(&ctx, user_id, id_len);
// 3. 曲线参数 a, b
hvb_sm3_update(&ctx, curve_a, 32);
hvb_sm3_update(&ctx, curve_b, 32);
// 4. 基点 G 坐标
hvb_sm3_update(&ctx, curve_gx, 32);
hvb_sm3_update(&ctx, curve_gy, 32);
// 5. 公钥坐标
hvb_sm3_update(&ctx, pubkey->x, 32);
hvb_sm3_update(&ctx, pubkey->y, 32);
hvb_sm3_final(&ctx, z_out); // 32 字节 Z 值
}
// 真正的签名摘要
void sm2_compute_digest(uint8_t *e_out,
const uint8_t *z_value,
const uint8_t *msg, size_t msg_len) {
hvb_sm3_init(&ctx);
hvb_sm3_update(&ctx, z_value, 32); // Z 值在前
hvb_sm3_update(&ctx, msg, msg_len);
hvb_sm3_final(&ctx, e_out); // e = SM3(Z || M)
}
四、SM4:国产对称算法与"模式选择"
4.1 SM4 的结构
SM4(GB/T 32907-2016)是 128 位分组、128 位密钥的分组密码,采用 32 轮 Feistel 变体结构 。密钥长度和分组长度都固定为 128 位 ------这一点与 AES 不同(AES 支持 128/192/256 位密钥),SM4 只有 128 位密钥 。
安全强度评估 :SM4 的 128 位安全强度在 2026 年仍被认为是充分的(与 AES-128 等价),但低于 AES-256。对于"需要对抗未来量子计算"的场景,SM4 与 AES 一样都不抗量子。
4.2 模式选择:绝不要裸用 ECB
与 AES 一样,SM4 的安全性几乎完全取决于工作模式的选择 。这是国密改造中第二个大坑------很多密评整改方案只说"用 SM4 加密",不指定模式,最后代码写成了 ECB。
| 模式 | 安全性 | 推荐度 | 备注 |
|---|---|---|---|
| SM4-ECB | ❌ 极差(块模式泄露) | 禁止 | 仅 16 字节内密钥包装可勉强用 |
| SM4-CBC | ⚠️ 中(需配合 HMAC) | 可用 | 必须外挂 HMAC-SM3(EtM) |
| SM4-CFB | ⚠️ 中 | 少用 | 流模式 |
| SM4-OFB | ⚠️ 中 | 少用 | 流模式 |
| SM4-CTR | ✅ 好(需配合 HMAC) | 推荐 | 并行、无 padding |
| SM4-GCM | ✅ 最佳(AEAD) | 强烈推荐 | 内置认证,国密 TLS 主推 |
GCM 模式 是 SM4 的现代推荐------RFC 8998 定义的 TLS 1.3 国密套件就用 SM4-GCM ,Linux 内核也已加入 CRYPTO_SM4_ARM64_CE_GCM 加速模块。新系统一律 SM4-GCM 。
GCM 的 Nonce 规则与 AES-GCM 完全一致:
- Nonce 12 字节,同一密钥下绝不可复用;
- Nonce 重用会导致机密性泄露(密钥流相同)+ 完整性被伪造(认证密钥 H 被恢复);
- 分布式系统多副本场景用 Nonce 前缀(实例 ID + 本地计数器)或换用 SM4-CTR + HMAC-SM3。
4.3 SM4 实战代码
GmSSL 命令行:
bash
# SM4-CBC 加密
KEY=11223344556677881122334455667788
IV=11223344556677881122334455667788
echo hello | gmssl sm4 -cbc -encrypt -key $KEY -iv $IV -out sm4.cbc
gmssl sm4 -cbc -decrypt -key $KEY -iv $IV -in sm4.cbc
# SM4-CTR(推荐,无 padding)
echo hello | gmssl sm4 -ctr -encrypt -key $KEY -iv $IV -out sm4.ctr
Python(gmssl 库):
python
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
key = bytes.fromhex("0123456789abcdef0123456789abcdef") # 16 字节
iv = bytes.fromhex("11223344556677881122334455667788")
# ============ SM4-CBC ============
crypt = CryptSM4()
crypt.set_key(key, SM4_ENCRYPT)
plaintext = b"Hello, 国密 SM4"
ciphertext = crypt.crypt_cbc(iv, plaintext) # 自动 PKCS7 padding
crypt_dec = CryptSM4()
crypt_dec.set_key(key, SM4_DECRYPT)
decrypted = crypt_dec.crypt_cbc(iv, ciphertext)
assert decrypted == plaintext
# ============ SM4-ECB(仅用于密钥包装,禁止用于业务数据)============
ecb_crypt = CryptSM4()
ecb_crypt.set_key(key, SM4_ENCRYPT)
ecb_ct = ecb_crypt.crypt_ecb(plaintext)
Java(BouncyCastle):
java
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.security.Security;
// 一次性注册 BC Provider
static {
Security.addProvider(new BouncyCastleProvider());
}
// ============ SM4-GCM(推荐)============
public static byte[] sm4GcmEncrypt(byte[] key, byte[] plaintext, byte[] aad) {
try {
byte[] nonce = new byte[12];
new SecureRandom().nextBytes(nonce); // 每次加密生成新 Nonce
Cipher cipher = Cipher.getInstance("SM4/GCM/NoPadding", "BC");
SecretKeySpec keySpec = new SecretKeySpec(key, "SM4");
GCMParameterSpec spec = new GCMParameterSpec(128, nonce); // 128 位 tag
cipher.init(Cipher.ENCRYPT_MODE, keySpec, spec);
if (aad != null) cipher.updateAAD(aad);
byte[] ciphertext = cipher.doFinal(plaintext);
// 输出格式:nonce || ciphertext+tag
byte[] out = new byte[12 + ciphertext.length];
System.arraycopy(nonce, 0, out, 0, 12);
System.arraycopy(ciphertext, 0, out, 12, ciphertext.length);
return out;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
BouncyCastle 版本注意事项:
- 必须用 1.60+ 版本,1.57 及更早版本存在 SM2 密文格式问题和 C1C2C3 固定 bug;
- 版本冲突 是 Java 国密改造最常见的报错来源------Hutool 的 SmUtil 依赖 BouncyCastle,如果项目里其他依赖也带 BC,会报
NoSuchMethodError: org.bouncycastle.crypto.engines.SM2Engine。用mvn dependency:tree排查,统一版本。
五、TLCP 与 RFC 8998:国密 TLS 的两条路线
5.1 国密 SSL 的演变:GM/T 0024 → TLCP
国密传输层安全协议经历了两个阶段:
阶段 1:GM/T 0024-2014《SSL VPN 技术规范》 ------2014 年发布的行业标准,定义了国密 SSL 协议的基本框架(协议版本号 0x0101),双证书体系,密码套件如 ECC_SM4_CBC_SM3。这个标准已在 2023 年被新版 GM/T 0024-2023 替代。
阶段 2:GB/T 38636-2020《传输层密码协议 TLCP》 ------2020 年发布的国家标准,是国密 SSL 的标准化升级版 ,协议版本号仍为 0x0101 ,与 GM/T 0024 兼容。
工程上不用纠结两者的区别,都统称为"国密 SSL"或"TLCP"即可。
5.2 双证书体系:TLCP 与 TLS 的根本区别
TLCP 与 TLS 最核心的差异不是算法,而是证书模型:
| 维度 | 标准 TLS | TLCP(国密) |
|---|---|---|
| 证书数量 | 1 张(签名+加密共用) | 2 张(签名证书 + 加密证书) |
| 密钥用途 | 一张密钥对承担身份认证和密钥交换 | 严格隔离:签名密钥不加密,加密密钥不签名 |
| 协议版本 | TLS 1.2 (0x0303) / TLS 1.3 (0x0304) | TLCP v1.1 (0x0101) |
| 前向安全 | TLS 1.3 原生支持 | 静态 ECC-SM2 不支持,ECDHE-SM2 支持 |
| 浏览器支持 | 全部主流浏览器 | 仅国产浏览器(奇安信、360、密信等) |
| 算法 | RSA/ECDSA/AES/SHA | SM2/SM4/SM3 |
为什么拆双证书? 主要是安全隔离 + 合规要求:
- 签名证书 :证明"我是谁",其私钥用于生成签名(如 SM2-with-SM3 签名),私钥必须不出卡(HSM/UKey 中保护);
- 加密证书 :用于密钥交换,其私钥用于解密临时密钥,可以在服务器上保护级别稍低;
- 即使加密私钥因长期使用泄露,攻击者也无法冒充服务器(因为签名私钥独立且保护更严格);
- 这是金融、政务强制要求------GM/T 0006、GB/T 35275 都要求签名/加密密钥用途隔离。
5.3 双证书的 Nginx 配置实战
Nginx 不原生支持 TLCP,需要用**国密版 OpenSSL(Tongsuo/铜锁、GmSSL、openHiTLS)**重新编译,或使用国密网关。
nginx
# Tongsuo/BabaSSL 编译启用 NTLS
./config enable-ntls --prefix=/path/to/tongsuo
make -j && make install
# Nginx 配置(国密双证书 + 国际证书双栈)
server {
listen 443 ssl;
server_name example.com;
# ============ 国际证书(TLS 1.2/1.3 用)============
ssl_certificate /path/to/rsa.fullchain.crt;
ssl_certificate_key /path/to/rsa.key;
# ============ 国密签名证书(TLCP 身份认证用)============
ssl_sign_certificate /path/to/server_sign.crt;
ssl_sign_certificate_key /path/to/server_sign.key;
# ============ 国密加密证书(TLCP 密钥交换用)============
ssl_enc_certificate /path/to/server_enc.crt;
ssl_enc_certificate_key /path/to/server_enc.key;
# ============ 协议与套件 ============
ssl_protocols TLSv1.2 TLSv1.3 TLCPv1.1;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:TLS_AES_128_GCM_SHA256:\
ECDHE-SM2-WITH-SM4-SM3:ECC-SM2-WITH-SM4-SM3:\
TLCP_ECC_SM4_GCM_SM3:TLCP_ECC_SM4_CBC_SM3;
}
双栈部署的价值 :同一台服务器同时服务国际浏览器(走 TLS+RSA)和国密客户端(走 TLCP+SM2),这是 2026 年大多数政企系统的标准做法。
5.4 RFC 8998:把国密塞进 TLS 1.3
RFC 8998 (2021 年 3 月发布)定义了如何在标准 TLS 1.3 框架下使用国密算法,密码套件为:
text
TLS_SM4_GCM_SM3 = { 0x13, 0xC6 } ← IANA 分配的 TLS 1.3 套件编号
TLS_SM4_CCM_SM3 = { 0x13, 0xC7 }
RFC 8998 与 TLCP 的本质区别:
- RFC 8998 是 TLS 1.3 的国密扩展,单证书体系,客户端可以用任何支持 TLS 1.3 + SM 套件的实现(如 BoringSSL with SM patch);
- TLCP 是国密自有协议,双证书体系,需要国密专用栈(Tongsuo/GmSSL);
- 从合规角度 :密评只认 TLCP 双证书------"TLS 套国密算法"不等于 TLCP,无法满足密评要求;
- 从兼容角度:RFC 8998 让国密可以进入国际生态,蚂蚁集团等大厂已在 BabaSSL(Tongsuo 前身)里支持。
一个值得关注的进展 :IETF 还有 SM2-MLKEM 混合后量子密钥交换草案(draft-yang-tls-hybrid-sm2-mlkem),把 SM2 和 ML-KEM768 组合作为 TLS 1.3 的 named group,为后量子时代的国密迁移做准备。
5.5 s_server / s_client 实战验证
bash
# ============ 服务端(TLCP 双证书)============
openssl s_server -accept 127.0.0.1:4433 \
-enc_cert test/certs/sm2/server_enc.crt \
-enc_key test/certs/sm2/server_enc.key \
-sign_cert test/certs/sm2/server_sign.crt \
-sign_key test/certs/sm2/server_sign.key \
-enable_ntls
# ============ 客户端(ECC-SM2 静态套件)============
openssl s_client -connect 127.0.0.1:4433 \
-cipher ECC-SM2-WITH-SM4-SM3 \
-enable_ntls -ntls
# ============ 客户端(ECDHE-SM2 前向安全套件,双向认证)============
openssl s_client -connect 127.0.0.1:4433 \
-cipher ECDHE-SM2-WITH-SM4-SM3 \
-sign_cert test/certs/sm2/client_sign.crt \
-sign_key test/certs/sm2/client_sign.key \
-enc_cert test/certs/sm2/client_enc.crt \
-enc_key test/certs/sm2/client_enc.key \
-enable_ntls -ntls
Wireshark 抓包识别:TLCP 的版本号是 0x0101,与国际 TLS 的 0x0303/0x0304 完全不同,抓包时一眼可分。
六、工具链对比:GmSSL vs Tongsuo vs BouncyCastle vs sm-crypto
6.1 五大主流工具链
| 工具链 | 语言 | 维护方 | 适用场景 | 关键特性 |
|---|---|---|---|---|
| GmSSL 3.x | C(多语言绑定) | 北京大学关志团队 | 嵌入式、C/C++、服务器 | 完整支持 SM2/3/4/9/ZUC、TLCP、SKF/SDF 硬件接口 |
| Tongsuo(铜锁) | C | OpenAtom 基金会 | OpenSSL 替换、Nginx、TLS | OpenSSL 兼容、TLCP 原生支持、RFC 8998 |
| BouncyCastle | Java/.NET | BC 社区 | Java 后端、Android | JCE Provider,需 1.60+ |
| sm-crypto | JavaScript | 国内社区 | 浏览器、Node.js | 前端加密签名 |
| tjfoc/gmsm | Go | 江苏天融 | Go 服务、云原生 | 与 Go crypto 集成良好 |
GmSSL 3.0 的关键改进:
- 支持 X86/ARM/RISC-V 汇编级优化(
ENABLE_SM3_AVX2、ENABLE_SM4_AESNI); - 移除不安全算法,仅保留国密和主流国际算法;
- 支持 SKF/SDF 接口的硬件密码模块(PCI-E 密码卡、USB 密码钥匙);
- 提供国密 Nginx、国密浏览器、自助 CA 服务等子项目。
Tongsuo(铜锁)的关键定位: - OpenSSL 的国内分支,由开放原子开源基金会孵化;
- 原生支持 TLCP 协议(NTLS),是国密改造 Nginx/Apache 的首选;
- 同时支持 RFC 8998(TLS 1.3 国密套件);
- PostgreSQL 国密改造的官方推荐库。
6.2 OpenSSL 主线与国密支持
OpenSSL 主线从 1.1.1 开始加入了基础的 SM2/SM3/SM4 算法支持 (openssl sm2 命令、SM4-GCM 模式等),但不支持 TLCP 协议 。工程上的选择:
- 只需算法,不需协议 → OpenSSL 主线足够;
- 需要 TLCP/双证书 → 必须用 Tongsuo 或 GmSSL;
- 需要 RFC 8998(TLS 1.3 国密套件) → Tongsuo 或 patched BoringSSL。
6.3 工具链互通性"惨案"
同一份"国密实现"在不同库之间互通的成功率不到 50% 是工程现实。常见的互通性问题:
| 互通场景 | 常见问题 | 原因 |
|---|---|---|
| sm-crypto(JS)↔ BouncyCastle(Java) | 密文开头 04 头差异 | JS 库不带 04 头,BC 默认带 |
| sm-crypto ↔ GmSSL | 签名格式(裸 vs DER) | JS 输出裸 64 字节,GmSSL 可 DER |
| 老版 BC ↔ 新版 BC | C1C2C3 vs C1C3C2 | 1.60 前默认 C1C2C3 |
| 国密证书在不同 CA 间 | OID 不一致 | 部分证书用错误的 SM2 OID |
| SM2 签名 ↔ 标准库 | Z 值计算差异 | 自实现漏算 Z 值或 ID 不一致 |
| GmSSL ↔ Tongsuo TLCP | 套件编号差异 | 0xE0 开头的国密套件编号约定 |
一个真实案例 :某政务系统用 Node.js 的 sm-crypto 做签名,Java 后端用 BouncyCastle 验签,签名一直失败。排查发现是 sm-crypto 默认 ID 是空字符串 ,而 Java 端默认 ID 是 "1234567812345678"------两端 ID 不一致导致 Z 值不同,签名永远对不上 。修正方案:在 API 文档中明确约定 ID 必须为 "1234567812345678",并强制两端显式传入。
另一案例 :国密 HTTPS 网关访问失败,抓包发现服务器只发了签名证书、没发加密证书。TLCP 双证书必须两张一起发,单发一张客户端会报 "no certificate"------这是初学者最常见的坑。
6.4 接口加密的典型场景(政务/金融 API)
国密接口加密的标准组合是 SM2 + SM4 + HMAC-SM3 三件套:
text
客户端请求流程:
1. 从服务端获取 SM2 公钥
2. 客户端生成一次性 SM4 对称密钥(128 位)
3. 用 SM4 加密业务 JSON body → encryptedBody
4. 用 SM2 公钥加密 SM4 密钥 → ciphertextBlob
5. 生成 HMAC-SM3 密钥(与 SM4 密钥独立)
6. 分别计算 ciphertextBlob 和 encryptedBody 的 HMAC-SM3
7. 用 SM2 公钥加密 HMAC 密钥 → encryptedHashKey
8. 发送 { ciphertextBlob, encryptedBody, encryptedHashKey,
ciphertextBlobHash, encryptedBodyHash }
服务端响应流程:
1. 用 SM2 私钥解密 SM4 密钥和 HMAC 密钥
2. 验证 HMAC → 防篡改
3. 用 SM4 解密 body
4. 响应也用 SM4 加密 + HMAC-SM3 校验
这个组合的安全意义:
- SM2 解决"对称密钥怎么安全传给对方"(非对称封装);
- SM4 解决"业务数据怎么高效加密"(对称加密);
- HMAC-SM3 解决"报文是否被篡改"(完整性);
- 这是典型的"混合加密"模式,与国际场景的 RSA+AES-GCM 思路完全一致。
七、密评合规:从整改单到落地
7.1 密评的核心检查项
依据 GB/T 39786-2021,密评从物理和环境安全、网络和通信安全、设备和计算安全、应用和数据安全 四个层面检查密码应用。对大多数业务系统,重点在后面三层:
| 层面 | 检查项 | 常见不合规 |
|---|---|---|
| 网络和通信 | 传输通道是否用商用密码(TLCP/TLS+SM) | 用 HTTP 或标准 TLS 被判不合规 |
| 设备和计算 | 系统登录、远程管理是否用国密 | SSH 密码登录、RDP 明文 |
| 应用和数据 | 应用层加密、签名、存储加密是否用国密 | 用 MD5/SHA-1/AES 而非 SM 算法 |
| 密钥管理 | 密钥生成/存储/使用是否符合规范 | 密钥硬编码、密钥无 KMS 管理 |
| "高风险不合规项" (如使用已破解算法、密钥硬编码)会导致直接不通过,需立即整改;**"中风险"**允许整改期内完成。 |
7.2 密评整改的"三步走"实战
Step 1:差距分析
用密评机构的差距分析工具,对现有系统逐项打分。典型差距项:
- 传输层用 HTTP/标准 TLS(无国密);
- 数据库字段加密用 AES 而非 SM4;
- 摘要用 MD5/SHA-256 而非 SM3;
- 签名用 RSA 而非 SM2;
- 密钥存于配置文件(未用 KMS/HSM)。
Step 2:算法替换与协议升级
按"传输 → 存储 → 摘要 → 签名 "的顺序逐步替换。传输层改造是工作量最大的: - 公网 HTTPS → 部署国密 Nginx(Tongsuo)+ 双证书;
- 内部服务间通信 → 改造为 mTLS + SM2 双向认证;
- 数据库 TLS → PostgreSQL/MySQL 用国密 TLS 插件(如 Tongsuo 编译版)。
Step 3:密钥管理升级
密钥不能硬编码,必须用密码机(HSM)+ KMS: - SM2 私钥(签名)→ 不出卡,在 HSM/密码卡中生成和使用;
- SM4 密钥(数据加密)→ KMS 统一管理,按信封加密模式分发;
- 国密专用密码机(三未信安、江南天安、卫士通等厂商)提供 SKF/SDF/PKCS#11 接口。
7.3 与硬件密码机的集成
密码卡/密码机(HSM)的接口是国密落地的最后一公里,三种主流接口:
| 接口 | 全称 | 典型设备 |
|---|---|---|
| SDF | 设备管理接口 | PCI-E 密码卡、服务器密码机 |
| SKF | 智能密码钥匙接口 | USB-Key、TF 卡 |
| PKCS#11 | 通用密码令牌接口 | 跨平台通用 |
| GmSSL 集成示例: |
bash
# 查询密码卡信息
gmssl sdfinfo -lib /path/to/sdf/library.so
# 用密码卡里的密钥做签名(私钥不离开 HSM)
gmssl sdfsign -lib /path/to/sdf/library.so -key 1 -pass password \
-in message.txt -out signature.bin
# 用密码卡做 SM3 摘要
gmssl sdfdigest -lib /path/to/sdf/library.so -key 1 -in data.txt
# 用密码卡做混合加密
gmssl sdfencrypt -lib /path/to/sdf/library.so ...
工程上的关键原则:
- 签名私钥必须不出卡------HSM 内部生成,外部只能拿到公钥和签名结果;
- 加密私钥可以放服务器软件层(TLCP 双证书的设计本意);
- 对称密钥通过 KMS 的信封加密模式分发,每次业务加密生成 DEK、KMS 加密 DEK 后立即从内存擦除。
八、FAQ:最常见的 5 个问题
Q1:SM2 和 RSA 到底选哪个?如果项目没有强制国密要求,还要用 SM2 吗?
合规强制是第一决定因素 。如果项目涉及政务、金融、关键信息基础设施、等保三级及以上系统,必须用 SM2 (密评硬性要求)。如果没有合规强制,新系统用 ECDSA P-256 或 Ed25519 更合适 ------生态成熟、库支持广泛、性能与 SM2 相当。SM2 的安全性与 ECDSA P-256 等价 ,技术上并不更"安全"。
Q2:SM4-GCM 和 SM4-CBC 该怎么选?CBC 是不是必须外挂 HMAC?
新系统一律 SM4-GCM 。GCM 是 AEAD 模式,加密+认证一体,与 AES-GCM 的安全模型完全一致。如果必须用 CBC (如某些老协议强制),必须外挂 HMAC-SM3(Encrypt-then-MAC 模式),否则就是 CBC Padding Oracle 攻击的活靶子------国密和 AES 在模式选择上的原理完全一样。
Q3:为什么我做的 SM2 签名,和另一端验签总是失败?
三个最可能的原因(按概率排序):
- ID 不一致------签名端用 "1234567812345678",验签端用空字符串或另一个值,Z 值不同,签名永远对不上;
- 签名格式不一致------一方输出裸 R||S(64 字节),另一方期待 DER 编码(70-72 字节),格式解析失败;
- Z 值实现不完整 ------自实现漏算 Z 值,直接 SM3(M) 签名,与标准 SM2 不兼容。
排查方法 :用 GmSSL 命令行做基准------gmssl sm2sign -id 1234567812345678生成签名,再用你的实现去验这个签名;如果 GmSSL 签名你能验、你签的 GmSSL 不能验,说明你的签名端有问题;反之亦然。
Q4:SM2 和国密证书能进系统信任库吗?
国密证书需要国密信任锚 。Chrome/Firefox/Edge 不支持国密证书 (会报"未知 CA"),只有国产浏览器 (奇安信、360 安全浏览器、密信、GmSSL 浏览器)内置国密根证书。工程方案:
- 内部系统 → 把国密根证书导入企业内部 trust store;
- 对公网服务 → 部署国密网关 + 双栈(国密浏览器走国密,国际浏览器走 TLS+RSA);
- 证书透明度 → 国密证书也建议加入 CT 日志,便于监控。
Q5:怎么检测我的系统是否符合密评要求?
工具化检查:
- 协议扫描:testssl.sh 加国密支持版本,检查 TLCP/国密套件协商;
- 代码审计 :全局搜索
MD5、SHA1、AES/ECB、RSA等关键词,定位所有国际算法使用点; - 密钥审计:检查所有硬编码密钥、密钥存储位置、是否用了 KMS/HSM;
- 工具库审计 :
mvn dependency:tree(Java)排查 BouncyCastle 版本; - 对接官方密评机构做差距分析(如中国信息安全测评中心授权的密评机构)。
九、结语:国密的本质,是自主可控的密码工程
写到这里,你可能已经看到本文反复出现的主题------
国密算法本身不是"更安全"的算法,而是"自主可控"的算法 。SM2 的安全强度与 ECDSA P-256 等价,SM4 的安全强度与 AES-128 等价,SM3 的安全强度与 SHA-256 等价------从纯密码学角度,国密并没有超越国际算法。它的真正价值在于:
- 合规强制------《密码法》、密评、等保 2.0 把国密变成政务、金融、关键信息基础设施的硬性要求;
- 供应链自主------不依赖美国 NIST 算法标准,不受国外技术规则制约;
- 生态完整------从算法、协议、硬件、CA 到密评体系形成完整闭环。
但这套生态的工程成熟度与国际生态仍有差距 :软实现 SM4 比硬加速 AES 慢一个数量级、不同工具链互通性问题频发、TLCP 的浏览器支持仅限国产浏览器、文档和示例远不及 OpenSSL/RFC 体系丰富。国密改造的本质是"在合规约束下,把一套相对年轻的生态跑通" ------这需要的是扎实的工程能力,而不是对"国密更安全"的迷思。
密码学工程的第一铁律 :不要相信"用了国密就合规" 。算法选对了,还要看密文顺序对不对、Z 值算没算、曲线参数对不对、双证书发全没发、密钥管没管好------每一处细节的偏差,都可能让密评不通过,也可能让两套"都符合国密"的系统无法互通。
国密的工程难度,不在算法,在细节。