python-3.xdigital-signatureebay-apivba7

eBay Digital Signatures for APIs VBA okay but Python Signature validation failed to fulfill the request


I can successfully get a response from eBay's REST API with a digital signature using the following VBA code (I have not included all the code as it is quite a lot, but below is the important part):

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

This successfully returns the seller funds summary with either dig_sig set to TRUE or FALSE.

Now, using Python with the same headers and signature (I have compared the signature and it is identical):

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)

With dig_sig = True, the response is:

{
  "errors": [
    {
      "errorId": 215122,
      "domain": "ACCESS",
      "category": "REQUEST",
      "message": "Signature validation failed",
      "longMessage": "Signature validation failed to fulfill the request."
    }
  ]
}

If I set dig_sig = False, then the response is OK:

{"totalFunds":{"value":"1.00","currency":"GBP"},"processingFunds":{"value":"0.00","currency":"GBP"},"availableFunds":{"value":"1.00","currency":"GBP"}}

I tried using the same signature with both VBA and Python code and expected the same OK response. However, the Python code does not accept the signature. The VBA and Python code are both run on the same PC. Software versions:

Microsoft Excel for Microsoft 365 MSO (Versions 2210 Build 16.0.15726.20188) 64-bit

Microsoft Visual Basic for Applications 7.1

Python 3.7.6


Update for Xrender:

Below is a slightly modified getSignature() function which accepts the private key and public key as input parameters. I'm not sure if it will help you or not as it uses a Python library.

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

To get it to work I had to install pycryptodome first:

pip install pycryptodome

The following Python code also produces the same resulting signature if you call the sign(secret, msg) function as sign(private_key, sigbase)

Source: https://www.rfc-editor.org/rfc/rfc8032#section-6

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

## Then follows functions to perform point operations.

# Points are represented as tuples (X, Y, Z, T) of extended
# coordinates, with 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);


# Computes Q = s * Q
def point_mul(s, P):
    Q = (0, 1, 1, 0)  # Neutral element
    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

## Now follows functions for point compression.

# Square root of -1
modp_sqrt_m1 = pow(2, (p-1) // 4, p)

# Compute corresponding x-coordinate, with low bit corresponding to
# sign, or return None on failure
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

    # Compute square root of 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


# Base point
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)

## These are functions for manipulating the private key.

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


## The signature function works as below.

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")

## And finally the verification function.

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

However, as I could not figure out how to obtain the 32 bytes private key from the private key in the PEM file using Python alone, I just opted to use the Crypto library as it was much simpler and shorter code.


Solution

  • Oh my word! I spotted my mistake!

    "x-ebay-signature-key" "@method" "@path "@authority"
    

    was missing a double quote after "@path Replaced with:

    "x-ebay-signature-key" "@method" "@path" "@authority"
    

    and it is now working. Anyhow, I hope this topic helps others trying to get eBay Digital Signatures for APIs working.