文章目录
背景
使用python 调用 一个java开发的网络接口, 需要签名, python 的依赖对签名的计算算法与java有些差别,导致频繁的验签不通过, 在此记录下解决验签问题
网络接口给出的文档中,有java示例代码
// 公钥 public_keyaccess_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代码
from gmssl import sm2import base64from asn1crypto.keys import PrivateKeyInfo, PublicKeyInfodef _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 excdef 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 rawif __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工具. 这个方式也跑通过. 这是个保底策略.