✍️ Header signature

We sign the webhook events by including a signature in each event’s Leeway_Signature header. Each webhook has a secret autogenerated and shared between us and the end user.

Then, to sign the event, we compute an HMAC-SHA256 of the timestamp and the event’s body:

sha256 = HMAC_SHA256(timestamp + “.” + eventBody)

Finally, the Leeway_Signature header has the following format:

Leeway-Signature: t=1492774577, sha256=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

with t the timestamp and sha256 the previously computed hash.

Note that newlines have been added for clarity, but a real Leeway_Signature header is on a single line.

🔍 Signature check

When a client receive a webhook event, he should check that the signature is valid. Here is an example of how to do it:

import {createHmac, timingSageEqual} from 'crypto'; 
function checkSignature(signature, secret, body) {
const timestamp = signature[0].split('=')[1];
const sig = Buffer.from(signature[1].split('=')[1], 'utf8');
const hmac = createHmac('sha256', secret);
const digest = Buffer.from(hmac.update(timestamp + '.' + JSON.stringify(body)).digest('hex'), 'utf8');
if (sig.length !== digest.length || !timingSafeEqual(digest, sig))
{ return false; }
else { return true; }

Note that it’s recommended to use a time constant-time string comparison to avoid timing attack vulnerability, that’s why we are using here timingSafeEqual instead of === (see https://en.wikipedia.org/wiki/Timing_attack)

Moreover, the client should check if the difference between the received timestamp et the current timestamp is within his tolerance (this is to avoid replay attack).

Did this answer your question?