Callback verification and signature
Overview
If you want to verify that callbacks come from Sightengine and not from a third-party, Sightengine can sign the callbacks it sends to your endpoint. Signatures are done through a Sightengine-Signature header.
Sightengine generates signatures using an HMAC code with SHA-256.
The Sightengine-Signature header included in each signed callback contains a timestamp and a signature. The timestamp is prefixed by t=, and each signature is prefixed by a scheme. Currently, the only valid live signature scheme is v1. Here is an example:
Sightengine-Signature:t=1492774577,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Note: this header is only present in callbacks sent to your account-level callback URL, as defined here. If you pick a custom callback URL with the callback_url parameter, there will not be any associated signing secret and the callback will not be signed.
Verifying the callback signature
Step 1: Retrieve your endpoint's secret from your dashboard.
Go to the callbacks page on your dashboard to define your callback url (if you haven't already) and retrieve the corresponding signing secret. The signing secret is a string that starts with casec_.
Step 2: Extract the timestamp and signatures from the header
Split the header, using the , character as the separator, to get a list of elements. Then split each element, using the = character as the separator, to get a prefix and value pair.
The value for the prefix t corresponds to the timestamp, and v1 corresponds to the signature (or signatures). You can discard all other elements.
Step 3: Determine the expected signature
Concatenate the following strings into a single string that we will name to_be_signed- The timestamp (as a string)
- The dot character .
- The JSON payload (i.e. the request body)
Compute the HMAC of the to_be_signed string using SHA256 as hash function and using the endpoint's signing secret as the key.
Step 4: Compare the signatures
Compare the signature in the header with the expected signature, and check that they match. We recommend that you use a constant-time string comparison to prevent timing attacks.
You should also check that the difference between the current timestamp and the received timestamp is within your tolerance range.
Code example
Here is a complete example in Python, implementing the 4 steps described above:
Important: the signature is computed on the raw request body, exactly as it was received. Make sure you use the raw payload rather than a JSON object that you parsed and re-serialized, as even a minor difference in formatting would lead to a different signature.
import hashlib
import hmac
import time
# Signing secret of your callback endpoint, as found in your dashboard
CALLBACK_SECRET = 'casec_your_signing_secret'
# Maximum age of a callback that you accept, in seconds
TOLERANCE = 300
def parse_signature_header(header):
"""Extract the timestamp and the v1 signature from the Sightengine-Signature header."""
timestamp, signature = None, None
for element in header.split(','):
prefix, _, value = element.partition('=')
if prefix.strip() == 't':
timestamp = value.strip()
elif prefix.strip() == 'v1':
signature = value.strip()
return timestamp, signature
def compute_signature(payload, timestamp, secret):
"""Compute the HMAC-SHA256 signature of "timestamp.payload"."""
to_be_signed = timestamp.encode('utf-8') + b'.' + payload
return hmac.new(secret.encode('utf-8'), to_be_signed, hashlib.sha256).hexdigest()
def is_signature_valid(payload, header, secret=CALLBACK_SECRET, tolerance=TOLERANCE):
"""payload is the raw request body (as bytes), header is the Sightengine-Signature header."""
timestamp, signature = parse_signature_header(header)
if not timestamp or not signature:
return False
# Reject callbacks that are too old, to protect against replay attacks
try:
if abs(time.time() - int(timestamp)) > tolerance:
return False
except ValueError:
return False
expected_signature = compute_signature(payload, timestamp, secret)
# Constant-time comparison, to protect against timing attacks
return hmac.compare_digest(expected_signature, signature)
In your callback endpoint, pass the raw request body and the Sightengine-Signature header to is_signature_valid. Callbacks that fail verification should be rejected, for instance with a 403 response.