Call Signature
The call’s ID, the timestamp of the call attempt, and the call’s payload are aggregated into a dot separated string to produce the signature. The ID can be used to determine whether you've seen the message before, and the call's timestamp can be used to impose a window of validity. Both of these together help protect the receiving application from replay attacks.
Example of the call’s signed data:
# [x-webhook-id].[x-webhook-timestamp].[payload]
0009728d-e612-4434-93bf-48e47b2f0fd3.1715616466.{"type":"currencyStatus.updated","timestamp":"2024-05-13T16:07:43.79968Z","data":{"currency":"Bitcoin Cash","status":"enabled"}}
We use this to produce an HMAC-256 signature and encode it to base64 using the webhook’s secret in the plaintext format that it was communicated in. The v1, prefix in our signature header indicates that this is a symmetric signature.
Tip
Additions can later be made to this field, be sure to handle it as a space delimited list. Asymmetric key signatures could later be added with a
v1a
, prefix to produce a header value such asv1,[symmetric signature] v1a,[asymmetric signature]
Example of signature verification
Given an example webhook call:
# Headers:
x-webhook-id: 485a79b0-13f6-43ab-a9b8-ce5b31cdade1
x-webhook-timestamp: 1717490117
x-webhook-signature: v1,+y43Ke7KJyZT2jopc6C6KLRn09Zp01ZqLjiJo8qFYCo=
# Payload:
{
"type": "currencyStatus.updated",
"createdAt": "2024-06-04T08:35:15.442268Z",
"data": {
"currencyId": "abc123",
"currency": "Bitcoin",
"status": "enabled"
}
}
You should verify that:
- The
x-webhook-signature
value matches the signature that you produce using the dot separated notation shown above. - The
x-webhook-timestamp
value is within a window of validity such as 30 seconds. - The
x-webhook-id
value has not been seen before, at least within that window of validity.
Once you have received a webhook call and extracted the headers listed above, you can produce your own signature and compare it to what you received. Here’s a trivial Python example:
def sign(id, timestamp, payload):
# Produce the signable dot separated byte string
signable = '.'.join((id, timestamp, payload)).encode()
# Produce the HMAC-SHA256 signature using our webhook secret as a key
b_sig = hmac.new(_webhook_secret.encode(), signable, hashlib.sha256).digest()
# Encode the byte string signature into a base64 string
return base64.b64encode(b_sig).decode('utf-8')
Remember that the x-webhook-signature
value contains a list. We must extract the signature in the specific format that we’re interested in. Currently we only support symmetric key signatures that are indicated with v1
,.
def extract_signature_v1(signature_header):
# Split the signature list
sigs = signature_header.split(' ')
for sig in sigs:
# Identify the signature containing the 'v1' prefix that we want
(version, signature) = sig.split(',')
if version == 'v1':
return signature
return ''
We can now compare the desired signature that we produced with the signature that we received in the header:
def handle_webhook_call(request):
headers = request.headers
payload = request.data.decode('utf-8')
id_header = headers['x-webhook-id']
timestamp_header = headers['x-webhook-timestamp']
signature_header = headers['x-webhook-signature']
if '' in (id_header, timestamp_header, signature_header):
return 'missing call values', 400
receivedSignature = extract_signature_v1(signature_header)
expectedSignature = sign(id_header, timestamp_header, payload)
if receivedSignature == expectedSignature:
return 'call OK', 200
else:
return 'invalid webhook call signature', 401
return '', 500
Updated 22 days ago