You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

VBA调用eBay API签名验证成功,Python同签名验证失败

eBay REST API 签名验证问题:VBA成功但Python失败

问题背景

使用VBA代码通过数字签名调用eBay的REST API获取卖家资金汇总时,无论是否启用签名都能正常返回结果。但用Python实现相同逻辑(已确认签名完全一致)时,启用签名就会返回签名验证失败,禁用签名则响应正常。

VBA工作代码(关键部分)

Set http = New MSXML2.XMLHTTP60
created_time = DateDiff("s", "1/1/1970", now())
dig_sig = TRUE
url = "https://apiz.ebay.com/sell/finances/v1/seller_funds_summary"
With http
    .Open "GET", url
    .setRequestHeader "Content-Type", "application/json"
    .setRequestHeader "Accept", "application/json"
    .setRequestHeader "Authorization", "Bearer " & USER_TOKEN
    If dig_sig Then
        signature_input = "sig1=(""x-ebay-signature-key"" ""@method"" ""@path"" ""@authority"");created=" & created_time
        signature = getSignature("GET", _
            "/sell/finances/v1/seller_funds_summary", _
            "apiz.ebay.com", _
            created_time)
        .setRequestHeader "signature-input", signature_input
        .setRequestHeader "signature", "sig1=:" & signature & ":"
        .setRequestHeader "x-ebay-signature-key", JWE
        .setRequestHeader "x-ebay-enforce-signature", "true"
    End If
    .Send
    Do Until .readyState = 4
        DoEvents
        Application.Wait (Now + 0.00005)
    Loop
    Debug.Print .responseText
End With

Python代码(签名验证失败)

import time, requests

dig_sig = True
request_url = "https://apiz.ebay.com/sell/finances/v1/seller_funds_summary"
if dig_sig:
    created_time = int(time.time())
    sig_input = f'sig1=("x-ebay-signature-key" "@method" "@path" "@authority");created={created_time}'
    signature = getSignature("GET", 
        "/sell/finances/v1/seller_funds_summary",
        "apiz.ebay.com",
        created_time)
    public_key_jwe = *** jwe supplied by eBay ***
    headers = {
        "Content-Type": "application/json",
        "Accept": "application/json",
        "Authorization": "Bearer " + getUserToken(),
        "Signature-Input": sig_input,
        "Signature": f'sig1=:{signature}:',
        "x-ebay-signature-key": public_key_jwe,
        "x-ebay-enforce-signature": "true"
    }
else:
    headers = {
        "Authorization" : "Bearer " + token,
        "Accept" : "application/json",
        "Content-Type" : "application/json"
    }
try:
    response = requests.get(request_url, headers=headers)
    print(response.text)
except Exception as e:
    print(e)

响应情况

  • 当Python代码中dig_sig=True时,返回错误:
{
  "errors": [
    {
      "errorId": 215122,
      "domain": "ACCESS",
      "category": "REQUEST",
      "message": "Signature validation failed",
      "longMessage": "Signature validation failed to fulfill the request."
    }
  ]
}
  • 当dig_sig=False时,返回正常结果:
{"totalFunds":{"value":"1.00","currency":"GBP"},"processingFunds":{"value":"0.00","currency":"GBP"},"availableFunds":{"value":"1.00","currency":"GBP"}}

环境信息

两段代码运行于同一台电脑,软件版本:

  • Microsoft Excel for Microsoft 365 MSO (版本2210 Build 16.0.15726.20188) 64位
  • Microsoft Visual Basic for Applications 7.1
  • Python 3.7.6

补充:Python签名函数实现

使用PyCryptodome的版本

需先安装依赖:

pip install pycryptodome

签名函数代码:

import base64
from Crypto.PublicKey import ECC
from Crypto.Signature import eddsa

def getSignature(private_key, public_key_jwe, method, path, authority, created_time):
    try:
        build_key = f"-----BEGIN PRIVATE KEY-----\n{private_key}\n-----END PRIVATE KEY-----"
        key = ECC.import_key(build_key)
        signer = eddsa.new(key, mode='rfc8032')
        sigbase = (f'"x-ebay-signature-key": {public_key_jwe}\n' +
            f'"@method": {method}\n' +
            f'"@path": {path}\n' +
            f'"@authority": {authority}\n' +
            f'"@signature-params": ("x-ebay-signature-key" "@method" "@path" "@authority");created={created_time}'
            )
        signature = signer.sign(sigbase.encode())
        return base64.b64encode(signature).decode()
    except Exception as e:
        print(e)
    return False

基于RFC8032的原生实现

调用sign(secret, msg)函数并传入sign(private_key, sigbase),也能生成相同签名:

import hashlib

def sha512(s):
    return hashlib.sha512(s).digest()

# Base field Z_p
p = 2**255 - 19

def modp_inv(x):
    return pow(x, p-2, p)

# Curve constant
d = -121665 * modp_inv(121666) % p

# Group order
q = 2**252 + 27742317777372353535851937790883648493

def sha512_modq(s):
    return int.from_bytes(sha512(s), "little") % q

## 点运算相关函数

# 点用扩展坐标(X, Y, Z, T)表示,x = X/Z, y = Y/Z, x*y = T/Z
def point_add(P, Q):
    A, B = (P[1]-P[0]) * (Q[1]-Q[0]) % p, (P[1]+P[0]) * (Q[1]+Q[0]) % p;
    C, D = 2 * P[3] * Q[3] * d % p, 2 * P[2] * Q[2] % p;
    E, F, G, H = B-A, D-C, D+C, B+A;
    return (E*F, G*H, F*G, E*H);

# 计算Q = s * Q
def point_mul(s, P):
    Q = (0, 1, 1, 0)  # 单位元
    while s > 0:
        if s & 1:
            Q = point_add(Q, P)
        P = point_add(P, P)
        s >>= 1
    return Q

def point_equal(P, Q):
    # x1/z1 == x2/z2 等价于 x1*z2 == x2*z1
    if (P[0] * Q[2] - Q[0] * P[2]) % p != 0:
        return False
    if (P[1] * Q[2] - Q[1] * P[2]) % p != 0:
        return False
    return True

## 点压缩相关函数

# -1的平方根
modp_sqrt_m1 = pow(2, (p-1) // 4, p)

# 根据y坐标和符号恢复x坐标,失败则返回None
def recover_x(y, sign):
    if y >= p:
        return None
    x2 = (y*y-1) * modp_inv(d*y*y+1)
    if x2 == 0:
        if sign:
            return None
        else:
            return 0

    # 计算x2的平方根
    x = pow(x2, (p+3) // 8, p)
    if (x*x - x2) % p != 0:
        x = x * modp_sqrt_m1 % p
    if (x*x - x2) % p != 0:
        return None

    if (x & 1) != sign:
        x = p - x
    return x

# 基点
g_y = 4 * modp_inv(5) % p
g_x = recover_x(g_y, 0)
G = (g_x, g_y, 1, g_x * g_y % p)

def point_compress(P):
    zinv = modp_inv(P[2])
    x = P[0] * zinv % p
    y = P[1] * zinv % p
    return int.to_bytes(y | ((x & 1) << 255), 32, "little")

def point_decompress(s):
    if len(s) != 32:
        raise Exception("Invalid input length for decompression")
    y = int.from_bytes(s, "little")
    sign = y >> 255
    y &= (1 << 255) - 1

    x = recover_x(y, sign)
    if x is None:
        return None
    else:
        return (x, y, 1, x*y % p)

## 私钥处理函数

def secret_expand(secret):
    if len(secret) != 32:
        raise Exception("Bad size of private key")
    h = sha512(secret)
    a = int.from_bytes(h[:32], "little")
    a &= (1 << 254) - 8
    a |= (1 << 254)
    return (a, h[32:])

def secret_to_public(secret):
    (a, dummy) = secret_expand(secret)
    return point_compress(point_mul(a, G))

## 签名函数

def sign(secret, msg):
    a, prefix = secret_expand(secret)
    A = point_compress(point_mul(a, G))
    r = sha512_modq(prefix + msg)
    R = point_mul(r, G)
    Rs = point_compress(R)
    h = sha512_modq(Rs + A + msg)
    s = (r + h * a) % q
    return Rs + int.to_bytes(s, 32, "little")

## 验证函数

def verify(public, msg, signature):
    if len(public) != 32:
        raise Exception("Bad public key length")
    if len(signature) != 64:
        Exception("Bad signature length")
    A = point_decompress(public)
    if not A:
        return False
    Rs = signature[:32]
    R = point_decompress(Rs)
    if not R:
        return False
    s = int.from_bytes(signature[32:], "little")
    if s >= q: return False
    h = sha512_modq(Rs + public + msg)
    sB = point_mul(s, G)
    hA = point_mul(h, A)
    return point_equal(sB, point_add(R, hA))

注:由于无法直接从PEM文件提取32字节私钥,最终选择使用PyCryptodome库实现,代码更简洁易用。


内容的提问来源于stack exchange,提问作者ecom-tech

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 09:35:39