国密python调java服务

文章目录

背景

使用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)

来时路

  1. 使用 java 编写了生成签名和验签的工具, python 可以使用命令行调用这个java工具. 这个方式也跑通过. 这是个保底策略.
相关推荐
evans在进步11 小时前
LeetCode 17:电话号码的字母组合——Java DFS 回溯法详解
java·leetcode·深度优先
2401_8504811711 小时前
UVA10391
java
UQR11 小时前
一、Java基础高频面试题
java·开发语言
AC赳赳老秦11 小时前
网页公开附件自动采集:OpenClaw 批量下载页面内嵌 Word/Excel 附件,统一格式后结构化入库
java·python·word·php·excel·deepseek·openclaw
北斗落凡尘11 小时前
LangGraph 入门实战(3)
python·langchain
上海云盾商务经理杨杨11 小时前
Tomcat 弱配置漏洞渗透实战!批量 getshell 高频漏洞
java·web安全·tomcat
TAN-90°-11 小时前
Deep Learning for Computer Vision——Image Classification with Linear Classifiers
python·深度学习·算法·计算机视觉·线性回归
AINative软件工程11 小时前
LLM API 成本失控怎么办?工程师的实时异常检测指南
python
练习两年半的攻城狮11 小时前
LlamaIndex ResponseMode 深度解析
python·llamaindex
10mAh11 小时前
【Linux】error while loading shared libraries 怎么解决?——ldd、RPATH 与动态链接排错
java·linux·前端