文章目录
背景
使用python 调用 一个java开发的网络接口, 需要签名, python 的依赖对签名的计算算法与java有些差别,导致频繁的验签不通过, 在此记录下解决验签问题
网络接口给出的文档中,有java示例代码
java
// 公钥 public_key
access_key = ""
// 私钥 private_key,
secret_key = "MIGTAgE.........xxxxxxxx"
String result = sj.toString();
//生成签名
SM2 sm2 = SmUtil.sm2(secret_key, null);
String signKey2 = sm2.signHex(HexUtil.encodeHexStr(result));
python代码
python
from gmssl import sm2
import base64
from asn1crypto.keys import PrivateKeyInfo, PublicKeyInfo
def _decode_key_material(key: str) -> bytes:
"""
解码密钥材料字符串为字节序列。
自动检测输入是 Base64 编码还是十六进制编码,并进行相应解码。
对于十六进制字符串,支持可选的 '0x' 前缀。
Args:
key (str): 密钥材料字符串,可以是 Base64 编码或十六进制编码。
Returns:
bytes: 解码后的原始字节数据。
Raises:
ValueError: 如果十六进制字符串长度不是偶数。
"""
key = key.strip()
if _looks_like_base64(key):
return base64.b64decode(key)
# 处理十六进制格式:去除可选的 '0x' 前缀并验证长度
raw = key.lower()
if raw.startswith("0x"):
raw = raw[2:]
if len(raw) % 2:
raise ValueError("十六进制密钥长度必须为偶数")
return bytes.fromhex(raw)
def _looks_like_base64(s: str) -> bool:
"""
判断字符串是否看起来像 Base64 编码。
通过检查常见的前缀特征、特殊字符 ('+', '/') 或填充符 ('=') 来启发式判断。
Args:
s (str): 待检查的字符串。
Returns:
bool: 如果字符串疑似 Base64 编码则返回 True,否则返回 False。
"""
return (
s.startswith("MFkw")
or s.startswith("MIG")
or "+" in s
or "/" in s
or (len(s) % 4 == 0 and s.endswith("="))
)
def parse_private_key_hex(private_key: str) -> str:
"""
解析私钥并返回标准化的 64 位十六进制 d 值。
支持多种输入格式:
1. 裸 d 值:64 位十六进制字符串(可选 '0x' 前缀)。
2. PKCS#8 格式:Base64 编码或 DER 十六进制编码的结构化私钥。
Args:
private_key (str): 私钥字符串,支持 PKCS#8 Base64/DER 或裸 d 值十六进制。
Returns:
str: 64 位小写十六进制字符串,表示私钥的 d 值(不含 '0x' 前缀)。
Raises:
ValueError: 如果输入格式无法识别或解析失败。
"""
key = private_key.strip()
raw = key.lower()
if raw.startswith("0x"):
raw = raw[2:]
# 情况1:如果是标准的 64 位十六进制 d 值,直接返回
if len(raw) == 64 and all(c in "0123456789abcdef" for c in raw):
return raw
# 情况2:尝试作为结构化密钥(PKCS#8)解析
der = _decode_key_material(key)
try:
pk = PrivateKeyInfo.load(der)
d = pk["private_key"].parsed["private_key"].native
return format(d, "x").zfill(64)
except Exception as exc:
raise ValueError(
"无法解析 SM2 私钥,请使用 PKCS#8 Base64 或 64 位十六进制 d 值"
) from exc
def parse_public_key_hex(public_key: str) -> str:
"""
从公钥字符串中解析出适用于 gmssl 的坐标数据(x||y)。
返回 128 位十六进制字符串,不包含 '04' 前缀。
支持以下格式:
1. X509 SubjectPublicKeyInfo:Base64 编码。
2. 未压缩点十六进制:'04'||X||Y (130字符) 或 X||Y (128字符)。
Args:
public_key (str): 公钥字符串,支持 Base64 编码的 SPKI 或十六进制编码的点坐标。
Returns:
str: 128 位小写十六进制字符串,表示公钥的 x 和 y 坐标拼接结果。
Raises:
ValueError: 如果公钥格式不支持或解析失败。
"""
key = public_key.strip()
raw = key.lower()
if raw.startswith("0x"):
raw = raw[2:]
# 情况1:Base64 编码的公钥(通常是 SPKI 结构)
if _looks_like_base64(key):
der = base64.b64decode(key)
# 尝试直接从 DER 字节末尾提取未压缩点数据 (优化路径)
if len(der) >= 65 and der[-65] == 0x04:
return der[-64:].hex()
# 使用 asn1crypto 库解析 SPKI 结构获取公钥点
point = PublicKeyInfo.load(der)["public_key"].native
if len(point) == 65 and point[0] == 0x04:
return point[1:].hex()
raise ValueError("无法从 Base64 公钥中解析 SM2 坐标")
# 情况2:十六进制编码的公钥点
# 如果包含 '04' 前缀且总长度为 130 (04 + 64 + 64),则去除前缀
if raw.startswith("04") and len(raw) == 130:
raw = raw[2:]
# 验证最终长度是否为 128 (64 + 64)
if len(raw) != 128:
raise ValueError(f"不支持的公钥格式,期望 128 位十六进制,实际长度 {len(raw)}")
return raw
if __name__ == "__main__":
sj = "this a test word"
secret_key= "MFkwEwYHKoZIzj0C.......................................000O000AAp=="
access_key = "MIGTAgEAMBMGByqG.......................2k"
# 这里有个非常重要的步骤, 就是观察AK, SK, 看是什么编码的. 我的KEY, 都是base64编码的, 所以需要先base64.decode().
priv_hex = parse_private_key_hex(secret_key) # 解析私钥
pub_hex = parse_public_key_hex(access_key) # 解析公钥
crypt = sm2.CryptSM2(private_key=priv_hex, public_key=pub_hex, asn1=True)
signature = crypt.sign_with_sm3(sj.encode("utf-8"))
# 6. 验签,非必须的,可以移除。
crypt = sm2.CryptSM2(private_key="", public_key=pub_hex, asn1=True)
verify_result = bool(crypt.verify_with_sm3(signature, sj.encode("utf-8")))
print("验签结果:", verify_result)
来时路
- 使用 java 编写了生成签名和验签的工具, python 可以使用命令行调用这个java工具. 这个方式也跑通过. 这是个保底策略.