Content:
From February 2023, eBay will require certain API calls made on behalf of UK and EU domiciled sellers to include a digital signature.
This article will go through the steps required to create the required digital signature, and send a valid request including the signature using Python.
If you haven’t already, we recommend reading our article explaining the process required to create a digital signature, before progressing here. This guide gives more of an overview of the process, while this guide is focused specifically on a Python solution.
We’ve created a gist over on Github, covering the main functions required, as well as a functional example in this Github repo. The code snippets in this guide are derived from this code, so it might be useful to reference the code as you go along.
Prerequisites
For the purpose of this guide, it’s assumed your application is already capable of obtaining refresh tokens and generating oauth tokens as required by the eBay API.
If not, be sure to check out our other guides, where we outline these steps in detail.
The Python code outlined in this guide will make use of the following libraries:
pycryptodome
requests
Ensure you have these installed before continuing. You may wish to use urllib.request
in place of requests
, just be aware that the code used in this guide will need tweaking to suit. You can find more info on using urllib in our guide.
Getting a Signing Key
A signing key can be created using the createSigningKey
endpoint of the eBay Key Management API. The eBay documentation for this endpoint can be found here.
The request to the Key Management API requires two headers, one of which uses an API access token. The access token needs to be created with the https://api.ebay.com/oauth/api_scope
scope.
headers = {
'Authorization': f'Bearer {access_token}'
'Content-Type': 'application/json'
}
A payload describing the signing algorithm chosen needs to be send in the request body.
data = '{"signingKeyCipher": "ED25519"}'
If you want to use an RSA key, set this value to RSA
.
That’s all that’s required to send the request. The code below shows how to send the request using the requests
module.
try:
response = requests.post(
'https://apiz.ebay.com/developer/key_management/v1/signing_key',
headers=headers,
data='{"signingKeyCipher": "ED25519"}',
timeout=10
)
response.raise_for_status()
except HTTPError as e:
print(e)
The response is a JSON string, which can be automatically processed using the requests .json()
function.
key = response.json()
The response values can be accessed using [] notation.
The key values you’ll want to note down are the privateKey
, and jwe
.
key['privateKey']
key['jwe']
Creating the Signature-Input Header
This part is pretty straightforward. The Signature-Input header contains a list of the elements which make up the request header.
For a GET request, your header will contain the following.
"x-ebay-signature-key" "@method" "@path" "@authority"
For a POST request, you’ll also have a content digest, which needs to be included as well.
"content-digest" "x-ebay-signature-key" "@method" "@path" "@authority"
This string should be surrounded by brackets.
("x-ebay-signature-key" "@method" "@path" "@authority")
This needs to be followed by a created
attribute, containing a Unix timestamp of the current time. You can create this using the time module, which is part of the Python standard library.
creation_time = int(time.time())
Add this to the end of the signature input string created above, preceded by ;created=
.
f'("x-ebay-signature-key" "@method" "@path" "@authority");created={creation_time}'
The API requires an up-to-date creation time, so ensure this is created dynamically at the time the API request is being prepared.
For the Signature-Input header, this value needs to be prefixed with a name. The standard name to use here is sig1
.
f'sig1=("x-ebay-signature-key" "@method" "@path" "@authority");created={creation_time}'
However, the digital signature must NOT have the name prefix. As a result, it’s suggested to omit this from the signature input string, and append it later on. This allows the same string to be used for both the Signature-Input header, and to create the digital signature.
creation_time = int(time.time())
signature_params = f'"x-ebay-signature-key" "@method" "@path" "@authority");created={creation_time}'
signature_input_header = f'sig1={signature_params}'
signature = get_signature(signature_params, ...)
Creating a Content Digest
In Python, you can create the required digest using the pycryptodome
Crypto.Hash.SHA256
module.
from Crypto.Hash import SHA256
hasher = SHA256.new()
hasher.update(bytes(content, encoding='utf-8'))
digest = b64encode(hasher.digest()).decode()
You’ll first need to create a new hasher object, before updating it to include the content to hash. This content needs to be passed to update()
as a bytes object, rather than a string. A string value can be converted to a bytes object using bytes()
.
The digest can then be calculated using digest()
. This result needs to be base64 encoded, which can be done using b64encode()
.
b64encode()
returns a bytes object, so it’s suggested to convert this back to a UTF-8 string at this point using decode()
. All base64 values are valid UTF-8, so the value itself won’t be altered by this conversion.
When sending the calculated value in the request header, it should be formatted as sha-256=:digest:
.
sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
Creating the Signature
This is the most important part of the request, and it’s the one that’s easiest to get wrong. The formatting of the data being signed must be exactly correct, as outlined below, to generate the correct signature.
There are a few steps required to create the signature successfully, which are broken down below.
Signature Parameters
Your signature input string defines the parameters which need to be included in your signature. Don’t forget, though, that the signature input parameters themselves also need to be present.
For example, with a signature input containing
"content-digest" "x-ebay-signature-key" "@method" "@path" "@authority"
your signature needs to be made up of the following:
x-ebay-signature-key
: Public Key JWE returned by eBay Key Management API@method
: HTTP request method e.g. GET@path
: URL path (section of the URL after the hostname, excluding parameters)@authority
: Hostname@signature-params
: Signature input parameters, minus the signature input name (optional)
If you’re not using a content digest, be sure to leave this out.
Each entry needs to be separated using a line break (\n) character.
The basic format required (including a content digest) is demonstrated below.
signature_params = (
f'"x-ebay-signature-key": {ebay_public_key_jwe}\n'
f'"@method": {method}\n'
f'"@path": {path}\n'
f'"@authority": {authority}\n'
f'"@signature-params": f'{signature_params}'
f'"content-digest": f'sha-256=:{digest}:'
)
And here is an example including example values for each entry.
signature_params = (
'"x-ebay-signature-key": eyJ6aXAiOiJERU...\n'
'"@method": GET\n'
'"@path": /sell/finances/v1/transaction\n'
'"@authority": apiz.ebay.com\n'
'"@signature-params": ("x-ebay-signature-key" "@method" "@path" "@authority");created=1670882848\n'
'"content-digest": sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:'
)
For signing, this string needs to be converted to a bytes object. To do this, use encode()
.
signature_params = signature_params.encode()
The parameters are now ready to be signed.
Loading Your Private Key
The corresponding private key from the eBay Key Management API is used to create the signature.
However, the API only returns the raw bytes used for the key itself. Depending on the library you’re using to create the signature, this might be enough. For cryptodome
, however, it’s not.
The key needs to include the ‘BEGIN’ and ‘END’ sections usually seen in a private key.
For example, if your private key returned by the API is
X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=
the key required by cryptodome
is
-----BEGIN PRIVATE KEY-----\n
X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=\n
-----END PRIVATE KEY-----
Line break characters have been added here, to highlight the fact that the header and footer are on their own lines.
When storing your private key, you might want to store it in the format above and loading it directly. Alternatively, you can add the ‘BEGIN’ and ‘END’ strings in code.
f'-----BEGIN PRIVATE KEY-----\n{ebay_private_key}\n-----END PRIVATE KEY-----'
The key can be imported ready for signing using the Crypto.PublicKey.ECC
pycryptodome
module.
from Crypto.PublicKey import ECC
private_key = ECC.import_key(f'-----BEGIN PRIVATE KEY-----\n{ebay_private_key}\n-----END PRIVATE KEY-----')
Creating the Signature
Once the private key has been loaded, and parameters are ready, all that’s left is to create the signature.
The private key can be used to create a signer object using the required algorithm. This example uses Crypto.Signature.eddsa
, for use with an Ed25519 key.
from Crypto.Signature import eddsa
signer = eddsa.new(private_key, mode='rfc8032')
The signature can be created using sign()
, with the signature parameters created earlier as input.
signed = signer.sign({signature_params})
The signature result needs to be base64 encoded (which returns a bytes object), then UTF-8 encoded to convert it back to a string (which will make it easier to handle later on).
signature = b64encode({signed}).decode()
It takes a few steps, but with that, the signature is complete.
Sending the Signed API Request
You’re now ready to send an API request, using the created signature.
It’s pretty much a case of piecing the parts together to define the request headers.
headers = {
"Signature-Input": f'sig1={signature_params}',
"Signature": f'sig1=:{signature}:',
"x-ebay-signature-key": {ebay_public_key_jwe},
"x-ebay-enforce-signature": 'true',
"content-digest": f'sha-256=:{digest}:'
}
Ensure the "Signature"
and "Signature-Input"
values are prefixed with sig1=
, with the signature also being surrounded by colons (:
). The "content-digest"
value, if present, should be prefixed with sha-256=
, and again, surrounded by colons.
An example with dummy values can be found below.
headers = {
"Signature-Input": sig1=("x-ebay-signature-key" "@method" "@path" "@authority");created=1670882848,
"Signature": sig1=:u1jEapg9bm0...:,
"x-ebay-signature-key": eyJ6aXAiOiJERU...,
"x-ebay-enforce-signature": 'true',
"content-digest": sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
}
Using the requests library, sending the request is simple.
try:
response = requests.request(
{method},
url={url},
headers={headers},
timeout=10,
data={content}
)
response.raise_for_status()
print(response.text)
except HTTPError as e:
print(e)
The url
should be set to the correct value for the API you’re trying to use.
If you’re sending a POST request, be sure to include the request body content as the data
value. This is the content you used to create the content digest.
If everything has been done correctly, you should get a valid response from the API.
Github Code
The Github repo here provides a full working example of a mini app using eBay API with a digital signature. To use it, simply fill in your developer API details in credentials.json
.
If you don’t already have a key from the eBay Key Management API to use to create a digital signature, run the demo.py
file with the --create-key
argument to generate one.
The code snippets used in this guide are derived from this example, so you should be able to reference the code when reading the guide.
The gist is a more stripped-down version focused only on the code used for the digital signature. This should hopefully provide a useful starting point to add digital signatures to your own code.