国密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工具. 这个方式也跑通过. 这是个保底策略.
相关推荐
宠..1 小时前
VS Code SSH 远程连接 Ubuntu 并实现快速运行(C/C++示例)
java·运维·c语言·开发语言·c++·ubuntu·ssh
WL_Aurora1 小时前
Python 算法基础篇之排序算法(二):希尔、快速、归并
python·算法·排序算法
雨落在了我的手上1 小时前
初识java(八):数组的定义与使用
java·开发语言
asdfg12589631 小时前
一文理解“架构思维”
java·软件工程·软件开发·架构思维
RSCompany1 小时前
Frida 17 以后 Python API 跑旧版 JS 报 Java is not defined ?一行 import 直接恢复 Frida 16 体验
开发语言·python·逆向·hook·frida·android逆向·frida17
云烟成雨TD1 小时前
Spring AI Alibaba 1.x 系列【57】SAA Admin 前后端技术栈与分层设计详解
java·人工智能·spring
张道宁1 小时前
从零开始训练YOLO手机检测模型:完整实战教程
python·yolo
快乐的哈士奇1 小时前
对话框打字机效果:Vur + Java/Python 实现
java·开发语言·python
九皇叔叔1 小时前
Spring-Ai-Alibaba [02] chatclient-demo
java·人工智能·spring·ai