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