国密API签名验证工程实践:HMAC-SM3与SM2签名的完整实现
前言
在国密改造过程中,REST API的安全验证是绕不开的一环。传统方案依赖HMAC-SHA256或RSA签名,但在密评合规要求下,必须替换为SM2-SM3或HMAC-SM3方案。
本文解决一个实际问题:如何在Python中实现一套生产可用的国密API签名验证服务?
我们对比两种方案:
- HMAC-SM3:对称密钥,适合内部服务间调用
- SM2签名:非对称密钥,适合对外API,提供不可抵赖性
一、方案选型:HMAC-SM3 vs SM2签名
1.1 核心差异
| 维度 | HMAC-SM3 | SM2签名 |
|---|---|---|
| 密钥类型 | 对称(共享密钥) | 非对称(公私钥对) |
| 密钥管理 | 需安全分发共享密钥 | 私钥本地持有,公钥可公开 |
| 不可抵赖性 | ❌ 双方都能伪造 | ✅ 仅私钥持有者能签名 |
| 性能 | 快(~0.1ms/次) | 较慢(~5-10ms/次) |
| 适用场景 | 内部服务间调用 | 对外API、第三方接入 |
| 标准依据 | GB/T 32905-2016 | GM/T 0003.2-2012 |
1.2 选择建议
内部微服务调用 → HMAC-SM3
- 密钥可通过KMS安全分发
- 性能要求高
- 不需要法律意义上的不可抵赖性
- 第三方开发者需要验证签名来源
- 需要抗抵赖性(司法证据效力)
- 公钥可公开,便于集成
二、HMAC-SM3实现
2.1 签名算法原理
HMAC-SM3基于SM3杂凑算法,公式为:
CODE
HMAC-K(m) = SM3((K' ⊕ opad) || SM3((K' ⊕ ipad) || m))其中:
- K' 是填充后的密钥(SM3块大小64字节)
- opad = 0x5c5c...5c,ipad = 0x3636...36
- m 是消息
2.2 完整实现
PYTHON
"""
国密API签名验证工具库
依赖: pip install gmssl>=3.2.2
"""
import hashlib
import hmac
import time
import os
from typing import Tuple, Dict
from gmssl import sm3, func
class HMACSM3Signer:
"""HMAC-SM3签名验证器"""
BLOCK_SIZE = 64 # SM3块大小
def __init__(self, secret: bytes):
"""
初始化签名器
Args:
secret: 共享密钥(建议至少32字节)
"""
if len(secret) < 32:
raise ValueError("密钥长度不得少于32字节")
self.secret = secret
def _pad_key(self, key: bytes) -> bytes:
"""填充密钥到块大小"""
if len(key) >= self.BLOCK_SIZE:
# 密钥过长,先做SM3哈希再填充
key = sm3.sm3_hash(func.bytes_to_list(key))
key = func.hex_to_bytes(key)
# 填充到块大小
return key.ljust(self.BLOCK_SIZE, b'\x00')
def sign(self, message: str, timestamp: int = None) -> Dict[str, str]:
"""
生成签名
Args:
message: 待签名消息
timestamp: 时间戳(秒),默认当前时间
Returns:
包含signature、timestamp、nonce的字典
"""
if timestamp is None:
timestamp = int(time.time())
# 生成随机nonce
nonce = os.urandom(16).hex()
# 构造签名数据:timestamp || nonce || message
signed_data = f"{timestamp}:{nonce}:{message}"
# 计算HMAC-SM3
padded_key = self._pad_key(self.secret)
ipad = bytes([k ^ 0x36 for k in padded_key])
opad = bytes([k ^ 0x5c for k in padded_key])
inner_hash = sm3.sm3_hash(func.bytes_to_list(ipad + signed_data.encode('utf-8')))
outer_hash = sm3.sm3_hash(func.bytes_to_list(opad + bytes.fromhex(inner_hash)))
return {
'signature': outer_hash,
'timestamp': str(timestamp),
'nonce': nonce
}
def verify(self, message: str, signature_data: Dict[str, str],
max_age_seconds: int = 300) -> Tuple[bool, str]:
"""
验证签名
Args:
message: 原始消息
signature_data: sign()返回的字典
max_age_seconds: 最大允许时间差(秒),默认5分钟
Returns:
(是否有效, 错误信息)
"""
# 1. 验证时间戳(防重放)
try:
timestamp = int(signature_data['timestamp'])
current_time = int(time.time())
if abs(current_time - timestamp) > max_age_seconds:
return False, "签名已过期"
except (ValueError, KeyError):
return False, "无效的时间戳格式"
# 2. 重新计算签名
expected = self.sign(message, timestamp)
expected['nonce'] = signature_data.get('nonce', '')
# 3. 常量时间比较(防时序攻击)
if not hmac.compare_digest(expected['signature'], signature_data['signature']):
return False, "签名验证失败"
return True, "验证通过"2.3 使用示例
PYTHON
# 服务端:生成共享密钥(应通过安全渠道分发)
SECRET_KEY = os.environ.get('HMAC_SM3_SECRET').encode('utf-8')
signer = HMACSM3Signer(SECRET_KEY)
# 客户端:签名请求
message = "GET /api/user/profile"
sig_data = signer.sign(message)
print(f"Signature: {sig_data['signature']}")
print(f"Timestamp: {sig_data['timestamp']}")
print(f"Nonce: {sig_data['nonce']}")
# 服务端:验证签名
is_valid, msg = signer.verify(message, sig_data)
print(f"验证结果: {is_valid} - {msg}")三、SM2签名实现
3.1 签名算法原理
SM2签名基于椭圆曲线数字签名算法(ECDSA的变体),符合GM/T 0003.2-2012标准。
签名过程:
- 计算ZA = SM3(ENTL ‖ ID ‖ a ‖ b ‖ xG ‖ yG ‖ xA ‖ yA)
- 计算e = SM3(ZA ‖ M)
- 随机生成k,计算(x1, y1) = [k]G
- r = (e + x1) mod n
- s = ((1 + dA)^(-1) × (k - r × dA)) mod n
- 签名 = (r, s)
3.2 完整实现
PYTHON
"""
SM2签名验证工具
依赖: pip install gmssl>=3.2.2
"""
from gmssl import sm2, func
from gmssl.sm2 import CryptSM2
import os
class SM2Signer:
"""SM2签名验证器"""
def __init__(self, private_key: str, public_key: str,
user_id: str = "1234567812345678"):
"""
初始化SM2签名器
Args:
private_key: 十六进制私钥(64字符)
public_key: 十六进制公钥(128字符,含04前缀)
user_id: 用户ID,用于计算ZA
"""
self.crypt_sm2 = CryptSM2(
private_key=private_key,
public_key=public_key
)
self.user_id = user_id
def sign(self, message: str) -> Dict[str, str]:
"""
使用SM2签名消息
⚠️ 注意:使用sign_with_sm3()而非sign(),
前者自动处理ZA计算,符合GM/T 0009-2023要求
Args:
message: 待签名消息(UTF-8编码)
Returns:
包含signature_hex的字典
"""
message_bytes = message.encode('utf-8')
# ✅ 正确:使用sign_with_sm3,自动计算ZA
signature_hex = self.crypt_sm2.sign_with_sm3(
message_bytes,
self.user_id.encode('utf-8')
)
return {
'signature_hex': signature_hex,
'algorithm': 'SM2-with-SM3'
}
def verify(self, message: str, signature_hex: str) -> Tuple[bool, str]:
"""
验证SM2签名
⚠️ 注意:使用verify_with_sm3()而非verify()
Args:
message: 原始消息
signature_hex: 十六进制签名值
Returns:
(是否有效, 错误信息)
"""
message_bytes = message.encode('utf-8')
try:
# ✅ 正确:使用verify_with_sm3
is_valid = self.crypt_sm2.verify_with_sm3(
signature_hex,
message_bytes,
self.user_id.encode('utf-8')
)
return (is_valid, "验证通过") if is_valid else (False, "签名验证失败")
except Exception as e:
return False, f"验签异常: {str(e)}"3.3 常见陷阱
陷阱1:手动先哈希再签名
PYTHON
# ❌ 错误:手动SM3哈希后再签名,跳过ZA计算
message_hash = sm3.sm3_hash(func.bytes_to_list(message_bytes))
signature = self.crypt_sm2.sign(message_hash, None) # 非标准签名!
# ✅ 正确:使用sign_with_sm3,自动处理ZA
signature = self.crypt_sm2.sign_with_sm3(message_bytes, user_id_bytes)陷阱2:忽略user_id参数
ZA的计算依赖user_id,如果服务端和客户端的user_id不一致,签名验证必然失败。
PYTHON
# ❌ 错误:未指定user_id,使用默认值可能导致不匹配
crypt_sm2 = CryptSM2(private_key=priv_key, public_key=pub_key)
# ✅ 正确:显式指定user_id
crypt_sm2 = CryptSM2(
private_key=priv_key,
public_key=pub_key,
user_id="YOUR_SCENE_ID" # 应与客户端一致
)四、防重放攻击机制
4.1 时间戳窗口
最简单的防重放机制是时间戳窗口:
PYTHON
class ReplayProtection:
"""防重放攻击保护"""
def __init__(self, window_seconds: int = 300):
self.window = window_seconds
self.used_nonces = {} # nonce -> timestamp
def check(self, timestamp: int, nonce: str) -> Tuple[bool, str]:
"""
检查是否重放
Args:
timestamp: 请求时间戳
nonce: 唯一随机数
Returns:
(是否允许, 错误信息)
"""
current = int(time.time())
# 1. 检查时间窗口
if abs(current - timestamp) > self.window:
return False, "请求已过期"
# 2. 检查nonce是否已使用
nonce_key = f"{timestamp}:{nonce}"
if nonce_key in self.used_nonces:
return False, "重复请求"
# 3. 记录nonce
self.used_nonces[nonce_key] = current
# 4. 清理过期记录
self._cleanup(current)
return True, "允许"
def _cleanup(self, current: int):
"""清理过期记录"""
expired = [k for k, v in self.used_nonces.items() if current - v > self.window]
for k in expired:
del self.used_nonces[k]4.2 完整验证流程
PYTHON
class APISecurity:
"""国密API安全验证器"""
def __init__(self, hmac_secret: bytes, sm2_signer: SM2Signer):
self.hmac_signer = HMACSM3Signer(hmac_secret)
self.sm2_signer = sm2_signer
self.replay_protection = ReplayProtection(window_seconds=300)
def validate_request(self, method: str, path: str,
headers: Dict[str, str]) -> Tuple[bool, str]:
"""
验证API请求
验证顺序:
1. 时间戳窗口检查
2. Nonce防重放检查
3. HMAC-SM3签名验证(传输层安全)
4. SM2签名验证(应用层抗抵赖)
"""
# 1. 提取签名信息
timestamp = headers.get('X-Timestamp')
nonce = headers.get('X-Nonce')
hmac_sig = headers.get('X-HMAC-Signature')
sm2_sig = headers.get('X-SM2-Signature')
if not all([timestamp, nonce, hmac_sig, sm2_sig]):
return False, "缺少签名头"
# 2. 时间戳和nonce检查
try:
ts = int(timestamp)
except ValueError:
return False, "无效时间戳"
allowed, msg = self.replay_protection.check(ts, nonce)
if not allowed:
return False, msg
# 3. 构造签名数据
message = f"{method} {path}"
# 4. HMAC-SM3验证(快速失败)
hmac_valid, msg = self.hmac_signer.verify(message, {
'signature': hmac_sig,
'timestamp': timestamp,
'nonce': nonce
})
if not hmac_valid:
return False, f"HMAC验证失败: {msg}"
# 5. SM2验证(抗抵赖)
sm2_valid, msg = self.sm2_signer.verify(message, sm2_sig)
if not sm2_valid:
return False, f"SM2验证失败: {msg}"
return True, "验证通过"五、性能对比
5.1 测试环境
- CPU: Intel Xeon Gold 6248 @ 3.0GHz
- Python: 3.11.4
- 库: gmssl 3.2.2
- 测试次数: 1000次
5.2 测试结果
| 操作 | HMAC-SM3 | SM2签名 | SM2验签 |
|---|---|---|---|
| 平均耗时 | 0.08ms | 6.2ms | 8.5ms |
| P99耗时 | 0.15ms | 12.0ms | 15.0ms |
| 吞吐量 | 12,500 ops/s | 160 ops/s | 118 ops/s |
5.3 性能优化建议
HMAC-SM3优化:
PYTHON
# 预计算填充密钥,避免重复计算
class OptimizedHMACSM3(HMACSM3Signer):
def __init__(self, secret: bytes):
super().__init__(secret)
self._padded_key = None
self._ipad = None
self._opad = None
def _ensure_padded(self):
if self._padded_key is None:
self._padded_key = self._pad_key(self.secret)
self._ipad = bytes([k ^ 0x36 for k in self._padded_key])
self._opad = bytes([k ^ 0x5c for k in self._padded_key])
def sign(self, message: str, timestamp: int = None) -> Dict[str, str]:
self._ensure_padded()
# 使用预计算的ipad/opad...SM2批量验证优化:
PYTHON
from gmssl.sm2 import elliptic_curve_mult
def batch_verify_sm2(signatures: list, messages: list,
public_keys: list) -> list:
"""
批量验证SM2签名(减少椭圆曲线运算)
原理:使用逆元合并技术,将N次点乘合并为较少次数
"""
# 实际生产环境建议使用Tongsuo或BabaSSL的批量验证API
results = []
for sig, msg, pub_key in zip(signatures, messages, public_keys):
# 单次验证(此处简化,实际应使用gmssl的批量API)
results.append(verify_single(sig, msg, pub_key))
return results六、生产环境部署建议
6.1 密钥管理
YAML
# 密钥存储方案
hmac_secret:
storage: AWS KMS / Azure Key Vault / 阿里云KMS
rotation: 每90天轮换
access: 仅应用服务账户可访问
sm2_private_key:
storage: HSM(硬件安全模块)
usage: 仅用于签名,禁止导出
backup: 分片备份(Shamir秘密共享)6.2 安全最佳实践
- 密钥长度:HMAC-SM3密钥至少32字节,推荐64字节
- 时间窗口:建议120-300秒,根据业务场景调整
- Nonce存储:使用Redis等内存数据库,设置TTL自动过期
- 日志脱敏:签名值、密钥不得写入日志
- 速率限制:单个IP每秒最多100次请求
6.3 HTTP响应头
HTTP
X-Signature-Algorithm: HMAC-SM3,SM2-with-SM3
X-Request-ID: abc123 # 用于问题追踪
X-RateLimit-Remaining: 95七、踩坑记录
坑1:gmssl sign() vs sign_with_sm3()
PYTHON
# ❌ 错误:使用sign()手动传入哈希值
signature = crypt_sm2.sign(message_hash, None)
# 问题:跳过了ZA计算,生成非标准签名,密评不通过
# ✅ 正确:使用sign_with_sm3()
signature = crypt_sm2.sign_with_sm3(message, user_id)
# 自动计算ZA = SM3(ENTL ‖ ID ‖ a ‖ b ‖ xG ‖ yG ‖ xA ‖ yA)坑2:user_id不匹配导致验签失败
PYTHON
# 服务端
crypt_sm2_server = CryptSM2(
public_key=client_pub_key,
user_id="SCENE_A" # 必须与客户端一致
)
# 客户端
crypt_sm2_client = CryptSM2(
private_key=client_priv_key,
public_key=client_pub_key,
user_id="SCENE_A" # 与服务端一致
)
# ❌ 如果user_id不一致,签名验证必然失败坑3:时间戳精度问题
PYTHON
# ❌ 错误:使用时间戳字符串比较
if request.timestamp != expected_timestamp:
return False
# ✅ 正确:使用时间差判断(允许时钟偏差)
if abs(int(time.time()) - int(request.timestamp)) > MAX_AGE:
return False八、总结
国密API签名验证需要同时考虑安全性和性能:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 内部微服务 | HMAC-SM3 | 性能好,密钥管理简单 |
| 对外开放API | SM2签名 | 不可抵赖,公钥可公开 |
| 高安全要求 | HMAC-SM3 + SM2双重签名 | 传输层+应用层双重保障 |
- 使用
sign_with_sm3()而非sign(),确保ZA正确计算 - user_id必须服务端客户端一致
- 实现时间戳窗口+Nonce防重放
- 密钥通过KMS/HSM安全存储
- 定期轮换密钥(HMAC每90天,SM2每1年)
参考标准:
- GM/T 0003.2-2012《SM2椭圆曲线公钥密码算法 第2部分:数字签名算法》
- GM/T 0009-2023《SM2密码算法使用规范》
- GB/T 32905-2016《信息安全技术 SM3密码杂凑算法》