Search code examples
pythondigital-signatureebay-api

EBay Digital Signature Validation Failed when trying to POST


I have a problem that's quite difficult to find resources for on the internet. I've been trying to send a POST request to E-Bay's refund API. This API has recently switched to using a Digital Signature header for validation and is causing some issues

The following is the python script (found here: https://qubitsandbytes.co.uk/ebay-developer-api/using-ebay-digital-signatures-in-python) I'm using to make the call:

from base64 import b64encode
from urllib.parse import urlparse
import json
import sys
import time
import requests
from Crypto.PublicKey import ECC
from Crypto.Signature import eddsa
from Crypto.Hash import SHA256
from requests.exceptions import HTTPError

class EBayRefund:

    __config: dict = {}

    def __init__(self):
        with open('config.json') as user_file:
            json_file = user_file.read()

        self.__config = json.loads(json_file)

    def __get_access_token(self, ebay_refresh_token: str, oauth_token: str, scope: str):
        """Returns an eBay API access token, required to make further API calls."""

        headers = {
            "Authorization": "Basic " + oauth_token,
            "Content-Type": "application/x-www-form-urlencoded",
        }

        data = {
            "grant_type": "refresh_token",
            "refresh_token": ebay_refresh_token,
            "scope": scope,
        }

        try:
            response = requests.post(
                "https://api.ebay.com/identity/v1/oauth2/token",
                headers=headers,
                data=data,
                timeout=10,
            )

            response.raise_for_status()
            result = response.json()
            
            return result["access_token"]
        except (HTTPError, KeyError) as error:
            sys.exit(f"Unable to fetch access token: {error}")

    def __get_content_digest(self, content: str) -> str:
        """
        Generate a digest of the provided content.

        A content digest is needed when using one of the few POST requests requiring a digital
        signature.
        """

        hasher = SHA256.new()
        hasher.update(bytes(content, encoding="utf-8"))
        digest = b64encode(hasher.digest()).decode()
        return digest

    def __get_digital_signature(self, ebay_private_key: str, ebay_public_key_jwe: str, request_url: str, signature_params: str, digest: str = "") -> str:
        """
        Generate the digital signature using the details provided. The signature is created
        using ED25519.

        To add support for POST requests, pass in a content digest and add a "Content-Digest"
        entry to params, with a value of sha-256:digest:
        """
        url = urlparse(request_url)
        params = (
            f'"x-ebay-signature-key": {ebay_public_key_jwe}\n'
            f'"@method": POST\n'
            f'"@path": {url.path}\n'
            f'"@authority": {url.netloc}\n'
            f'"@signature-params": {signature_params}\n'
            f'"content-digest": sha-256=:{digest}:'
        ).encode()

        print(params)

        try:
            private_key = ECC.import_key(f"""-----BEGIN PRIVATE KEY-----\n{ebay_private_key}\n-----END PRIVATE KEY-----""")
            signer = eddsa.new(private_key, mode="rfc8032")
            signature = signer.sign(params)
            return b64encode(signature).decode()
        except ValueError as error:
            sys.exit(f"Error creating digital signature: {error}")

    def __send_signed_api_request(self, ebay_private_key: str, ebay_public_key_jwe: str, access_token: str) -> None:
        """
        Sends a request to the eBay API with a digital signature attached.

        The API response text is printed before exiting.
        """

        order_id = "XX-XXXXX-XXXXX"

        request_url: str = f"https://api.ebay.com/sell/fulfillment/v1/order/{order_id}/issue_refund"

        signature_input = f'("content-digest" "x-ebay-signature-key" "@method" "@path" "@authority");created={int(time.time())}'

        content = {
            "orderLevelRefundAmount": 
                {
                    "value": "17.52",
                    "currency": "GBP"
                },
                "reasonForRefund": "BUYER_RETURN"
            }
        
        content = json.dumps(content)

        content_digest = self.__get_content_digest(content = content)

        signature = self.__get_digital_signature(
            ebay_private_key=ebay_private_key,
            ebay_public_key_jwe=ebay_public_key_jwe,
            request_url=request_url,
            signature_params=signature_input,
            digest=content_digest
        )

        headers = {
            "Authorization": "Bearer " + access_token,
            "Signature-Input": f'sig1={signature_input}',
            "Signature": f"sig1=:{signature}:",
            "x-ebay-signature-key": ebay_public_key_jwe,
            "x-ebay-enforce-signature": "true",
            "content-digest": f"sha-256=:{content_digest}:"
        }

        print(json.dumps(headers, indent=4))

        try:
            response = requests.post(request_url, headers = headers, data = content, timeout = 10)
            result = response.json()

            print(json.dumps(result, indent = 4))
            sys.exit()
        except HTTPError as error:
            sys.exit(f"Unable to send request: {error}")

    def start(self):
        """Load credentials and read runtime arguments."""

        scope = "https://api.ebay.com/oauth/api_scope/sell.fulfillment"
        
        access_token = self.__get_access_token(
            ebay_refresh_token = self.__config["refreshToken"],
            oauth_token = self.__config["credentials"],
            scope = scope,
        )

        self.__send_signed_api_request(
            ebay_private_key = self.__config["privateKey"],
            ebay_public_key_jwe = self.__config["jweKey"],
            access_token = access_token,
        )


if __name__ == "__main__":
    EBR = EBayRefund()
    EBR.start()

but I'm getting the following error:

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

The original script uses GET as an example and it works fine when I test it with https://apiz.ebay.com/sell/finances/v1/transaction?limit=20&offset=0 but I just can't get it to work with POST

I've tried shuffling things around, playing with the signature inputs etc. but it just won't work


Solution

  • OK, I'll be answering my own question and hope it helps others

    After asking for help here: https://github.com/eBay/digital-signature-verification-ebay-api/issues/20 I finally managed to get it working. Here's how:

    my new signature base looks like this now:

    params = (
       f'"content-digest": sha-256=:{digest}:\n'
       f'"x-ebay-signature-key": {ebay_public_key_jwe}\n'
       f'"@method": POST\n'
       f'"@path": {url.path}\n'
       f'"@authority": {url.netloc}\n'
       f'"@signature-params": {signature_params}'
    ).encode()
    

    note the order in which the properties are arranged

    and the headers:

    headers = {
       "Authorization": "TOKEN " + access_token,
       "Signature-Input": f'sig1={signature_input}',
       "Signature": f"sig1=:{signature}:",
       "Accept": "application/json",
       "Content-Type": "application/json",
       "x-ebay-signature-key": ebay_public_key_jwe,
       "content-digest": f"sha-256=:{content_digest}:"
    }
    

    Note: Because I'm calling the /post-order/v2/return/{return_id}/issue_refund API endpoint I had to replace Bearer with TOKEN inside the Authorization header.

    Big thanks to Ulrich Hergerb from GitHub