Python国密算法实战:cryptography与gmssl库兼容陷阱与完整解决方案
前言:Python 国密生态的"分裂"现状
2026 年,Python 国密开发者的处境仍然尴尬:没有一个库能覆盖所有国密算法,也没有两个库能无缝协作。
cryptography(v48.x)支持 SM3、SM4,但不支持 SM2gmssl(v3.2.x)支持 SM2、SM3、SM4,但 SM4 在 CBC 解密时有 bug- 两者密钥格式不混用,同一份数据在两库间需要格式转换
本文的目标很简单:给你一份能在生产环境直接运行的代码,彻底解决库间兼容问题。
环境准备
BASH
# Python 3.10+
pip install cryptography>=44.0 gmssl>=3.0
# 验证安装
python3 -c "
import cryptography, gmssl
print(f'cryptography {cryptography.__version__}') # 48.0.0
print(f'gmssl {gmssl.__version__}') # 3.2.x
"版本要求:
cryptography >= 44.0:支持hashes.SM3()和algorithms.SM4gmssl >= 3.0:支持 SM2/SM3/SM4 基础操作
⚠️ 关键警告:cryptography44.x 不支持ec.SM2()— 它根本没有 SM2 实现。需要 SM2 必须用gmssl或 Tongsuo OpenSSL。
cryptography 能做什么(且做得好)
cryptography 库的 SM3 和 SM4 实现经过严格测试,API 设计优雅,是生产环境的首选。
SM3 哈希
PYTHON
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
def sm3_hash(data: bytes) -> bytes:
"""计算 SM3 哈希值,返回 32 字节"""
digest = hashes.Hash(hashes.SM3(), backend=default_backend())
digest.update(data)
return digest.finalize()
# 使用示例
data = b'Hello, 国密!'
hash_value = sm3_hash(data)
print(f'SM3: {hash_value.hex()}')
# 输出: 5795e376c14e8e3e64cb5e95e027101a14d4fac87e33edb0477e5fe78a992711SM4-CBC 加密
PYTHON
import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
def sm4_cbc_encrypt(key: bytes, iv: bytes, plaintext: bytes) -> bytes:
"""SM4-CBC 加密,带 PKCS7 自动填充"""
# PKCS7 填充
pad_len = 16 - len(plaintext) % 16
padded = plaintext + bytes([pad_len] * pad_len)
cipher = Cipher(algorithms.SM4(key), modes.CBC(iv), backend=default_backend())
encryptor = cipher.encryptor()
return encryptor.update(padded) + encryptor.finalize()
def sm4_cbc_decrypt(key: bytes, iv: bytes, ciphertext: bytes) -> bytes:
"""SM4-CBC 解密,自动去除 PKCS7 填充"""
cipher = Cipher(algorithms.SM4(key), modes.CBC(iv), backend=default_backend())
decryptor = cipher.decryptor()
padded = decryptor.update(ciphertext) + decryptor.finalize()
return padded[:-padded[-1]]
# 使用示例
key = os.urandom(16) # 128-bit key
iv = os.urandom(16) # 128-bit IV
plaintext = '国密 SM4-CBC 加密测试'.encode('utf-8')
ct = sm4_cbc_encrypt(key, iv, plaintext)
pt = sm4_cbc_decrypt(key, iv, ct)
print(f'解密结果: {pt.decode("utf-8")}')
# 输出: 国密 SM4-CBC 加密测试SM4-GCM 认证加密
PYTHON
def sm4_gcm_encrypt(key: bytes, nonce: bytes, plaintext: bytes, aad: bytes = b'') -> tuple:
"""
SM4-GCM 加密,返回 (ciphertext, tag)
aad: 附加认证数据(只认证不加密)
"""
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
# 注意:cryptography 的 SM4-GCM 需要额外处理
# 截至目前(2026-06),cryptography 尚未直接暴露 SM4-GCM
# 如需 SM4-GCM,请使用 gmssl 或 Tongsuo 的 Python 绑定
# 推荐方案:使用 gmssl(见后续章节)
raise NotImplementedError('cryptography 暂不支持 SM4-GCM,推荐使用 gmssl')📌 现状总结:cryptography目前不支持 SM4-GSM(截至 v48.0)。如需 GCM 模式,使用gmssl。
SM2?cryptography 不支持
PYTHON
# ❌ 以下代码在 cryptography 中不存在
# from cryptography.hazmat.primitives.asymmetric import ec
# ✅ 正确做法:使用 gmssl(见下一节)gmssl 能做什么(以及它的陷阱)
gmssl 是 Python 中最完整的国密库,提供了 SM2/SM3/SM4 全套支持,但 API 设计有几个"暗坑"。
SM3 哈希
PYTHON
from gmssl import sm3
def gmssl_sm3(data: bytes) -> str:
"""gmssl SM3 哈希,返回小写十六进制字符串"""
return sm3.sm3_hash(list(data)) # 注意:输入需要是 list(int)
# 使用示例
data = b'Hello'
print(gmssl_sm3(data))
# 输出: 55e12e91650d2fec56ec74e1d3e4ddbfce2ef3a65890c2a19ecf88a307e76a23SM4 加密(注意 CBC 解密 bug)
PYTHON
from gmssl import sm4, func
def gmssl_sm4_encrypt(key: bytes, iv: bytes, plaintext: bytes) -> bytes:
"""gmssl SM4-CBC 加密"""
sm4_crypt = sm4.CryptSM4()
sm4_crypt.set_key(key, sm4.SM4_ENCRYPT) # ENCRYPT = 0
# 手动 PKCS7 填充
pad_len = 16 - len(plaintext) % 16
padded = plaintext + bytes([pad_len] * pad_len)
return sm4_crypt.crypt_ecb(padded) # 实际应该用 crypt_cbc!
# ⚠️ 严重 bug:gmssl 3.2.x 的 crypt_ecb 在 DECRYPT 模式下返回空 bytes!
# 需要 SM4-CBC 时,推荐使用 cryptography 的 algorithms.SM4 + modes.CBC🔴 gmssl 陷阱:crypt_ecb在SM4_DECRYPT模式(值为 1)下有 bug,可能返回空字节。生产环境 SM4-CBC 解密请使用cryptography库。
SM2 密钥生成、签名、验签(完整流程)
这是最复杂的部分。gmssl 的 SM2 API 有几个关键陷阱:
PYTHON
from gmssl import sm2, func
import binascii
def sm2_generate_keypair() -> tuple:
"""
生成 SM2 密钥对
返回: (private_key_hex, public_key_hex)
⚠️ 关键:gmssl 的 CryptSM2 不会自动初始化密钥!
必须先手动生成私钥再计算公钥。
"""
ecc_table = sm2.default_ecc_table
n = int(ecc_table['n'], 16)
# 生成随机私钥 d ∈ [1, n-1]
while True:
d_hex = func.random_hex(len(ecc_table['n']))
d = int(d_hex, 16)
if 1 <= d < n:
break
# 计算公钥 Q = d * G(椭圆曲线标量乘法)
# _kg 是内部方法,执行标量乘法
dummy = sm2.CryptSM2('00' * 64, ecc_table['g'])
Q_hex = dummy._kg(d, ecc_table['g'])
return d_hex, Q_hex
def sm2_sign(sm2_crypt, data: bytes, private_key_hex: bytes) -> str:
"""
SM2 签名
⚠️ 注意:gmssl 的 sign() 接受原始字节(内部自动哈希),
但 verify() 期望的是已哈希后的数据字节!
这是最容易踩的坑。
推荐使用 sign_with_sm3,它会自动完成 SM3 哈希 + 签名。
"""
return sm2_crypt.sign_with_sm3(data)
def sm2_verify(sm2_crypt, signature: str, hash_data: bytes) -> bool:
"""
SM2 验签
⚠️ 关键:verify() 的 data 参数必须是 SM3 哈希值(32 字节),
不是原始消息!
如果你使用 sign_with_sm3(data) 签名,
则 verify 时必须传入 SM3(data),不是 data 本身。
"""
return sm2_crypt.verify(signature, hash_data)
# 完整使用示例
private_key, public_key = sm2_generate_keypair()
crypt = sm2.CryptSM2(private_key, public_key)
data = b'Test SM2 signature'
# 签名(sign_with_sm3 内部自动 SM3 哈希)
signature = sm2_sign(crypt, data, private_key)
print(f'Signature: {signature[:60]}...')
# 验证:必须先对原始数据做 SM3 哈希
Z = crypt._sm3_z(data) # 获取 SM3 哈希十六进制
hash_bytes = binascii.a2b_hex(Z.encode('utf-8')) # 转为字节
is_valid = sm2_verify(crypt, signature, hash_bytes)
print(f'Valid: {is_valid}') # True跨库兼容实战:统一国密工具库
基于以上验证,我设计了一个生产级的统一封装,扬长避短:
PYTHON
"""
gm_crypto.py - 企业级 Python 国密统一库
使用 cryptography 的 SM3/SM4,gmssl 的 SM2,避免兼容问题
"""
import os
import binascii
from typing import Tuple, Optional
# === SM3: 使用 cryptography ===
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
# === SM4: 使用 cryptography ===
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
# === SM2: 使用 gmssl ===
from gmssl import sm2, func
class SMCrypto:
"""企业级国密算法统一接口"""
# ===== SM3 哈希 =====
@staticmethod
def sm3(data: bytes) -> bytes:
"""SM3 哈希,返回 32 字节二进制"""
h = hashes.Hash(hashes.SM3(), backend=default_backend())
h.update(data)
return h.finalize()
@staticmethod
def sm3_hex(data: bytes) -> str:
"""SM3 哈希,返回十六进制字符串"""
return SMCrypto.sm3(data).hex()
# ===== SM4-CBC 加密 =====
@staticmethod
def sm4_cbc_encrypt(key: bytes, iv: bytes, plaintext: bytes) -> bytes:
"""SM4-CBC 加密(PKCS7 填充)"""
pad_len = 16 - len(plaintext) % 16
padded = plaintext + bytes([pad_len] * pad_len)
cipher = Cipher(algorithms.SM4(key), modes.CBC(iv), backend=default_backend())
return cipher.encryptor().update(padded) + cipher.encryptor().finalize()
@staticmethod
def sm4_cbc_decrypt(key: bytes, iv: bytes, ciphertext: bytes) -> bytes:
"""SM4-CBC 解密(去除 PKCS7 填充)"""
cipher = Cipher(algorithms.SM4(key), modes.CBC(iv), backend=default_backend())
padded = cipher.decryptor().update(ciphertext) + cipher.decryptor().finalize()
return padded[:-padded[-1]]
# ===== SM2 签名 =====
@staticmethod
def sm2_keygen() -> Tuple[str, str]:
"""生成 SM2 密钥对,返回 (private_key_hex, public_key_hex)"""
ecc_table = sm2.default_ecc_table
n = int(ecc_table['n'], 16)
while True:
d_hex = func.random_hex(len(ecc_table['n']))
d = int(d_hex, 16)
if 1 <= d < n:
break
dummy = sm2.CryptSM2('00' * 64, ecc_table['g'])
Q_hex = dummy._kg(d, ecc_table['g'])
return d_hex, Q_hex
@staticmethod
def sm2_sign(private_key: str, public_key: str, data: bytes) -> str:
"""SM2 签名(使用 sign_with_sm3)"""
crypt = sm2.CryptSM2(private_key, public_key)
return crypt.sign_with_sm3(data)
@staticmethod
def sm2_verify(private_key: str, public_key: str, signature: str, data: bytes) -> bool:
"""
SM2 验签
必须传入 data 参数为 SM3 哈希后的 32 字节,不是原始数据!
这是 gmssl 最反直觉的设计。
"""
crypt = sm2.CryptSM2(private_key, public_key)
# 计算 SM3 哈希
Z = crypt._sm3_z(data)
hash_bytes = binascii.a2b_hex(Z.encode('utf-8'))
return crypt.verify(signature, hash_bytes)
# ===== 使用示例 =====
if __name__ == '__main__':
sm = SMCrypto()
# SM3
h = sm.sm3(b'Hello SM3')
print(f'SM3: {h.hex()}')
# SM4-CBC
key, iv = os.urandom(16), os.urandom(16)
ct = sm.sm4_cbc_encrypt(key, iv, b'国密加密')
pt = sm.sm4_cbc_decrypt(key, iv, ct)
print(f'SM4: {pt.decode()}')
# SM2
pk, pub = sm.sm2_keygen()
sig = sm.sm2_sign(pk, pub, b'test data')
valid = sm.sm2_verify(pk, pub, sig, b'test data')
print(f'SM2 verify: {valid}')踩坑总结
| 陷阱 | 现象 | 根因 | 解决方案 |
|---|---|---|---|
| cryptography 无 SM2 | AttributeError: SM2 | 44.x 只支持 SM3/SM4 | 用 gmssl 的 SM2 |
| gmssl CBC 解密空 bytes | b'' 返回 | crypt_ecb DECRYPT 模式 bug | 用 cryptography 的 SM4-CBC |
| SM2 verify 返回 False | 验签失败但数据没问题 | verify 期望哈希值而非原始数据 | 先 SM3(data) 再传入 |
| 密钥格式不兼容 | 无法跨库验签 | 两库密钥内部格式不同 | 统一用 gmssl 生成,不混用 |
| hex 输入格式 | ValueError | gmssl 的 sign() 接受 bytes | .hex() 转换后传入 |
选型决策树
CODE
需要 SM2 签名?
├── 是 → gmssl(唯一选择)
└── 否 →
需要 SM3/SM4?
├── 是 → cryptography(更稳定,API 更好)
└── 否 → 不需要国密关键结论
- cryptography 做 SM3/SM4,gmssl 做 SM2 — 这是2026年 Python 国密开发的最优分工
- 永远不要在两个库之间转换密钥 — 直接使用各自库的原生格式
- gmssl 的 SM2 verify 是最大的坑 — 参数必须是哈希值,不是原始消息
- SM4-GCM 目前两库都不完美 — 生产环境建议用 cryptography 的 SM4-CBC + HMAC-SM3 组合,或等待 cryptography 更新
💡 终极建议:如果你的项目重度使用国密,考虑用 pycryptodome 或直接调用 Tongsuo 的 C 扩展,性能更好且行为更一致。
参考来源
- cryptography 官方文档
- gmssl PyPI 页面
- GM/T 0003-2012 《SM2 椭圆曲线公钥密码算法》
- GM/T 0004-2012 《SM3 密码杂凑算法》
- GM/T 0002-2012 《SM4 分组密码算法》