Signature Verification
Verify HMAC-SHA256 webhook signatures to authenticate Cresora deliveries.
Every webhook delivery is signed with HMAC-SHA256 using your endpoint's signing secret. Verify this signature before processing any event.
Headers
| Header | Value |
|---|---|
X-Cresora-Signature | v1=<hex-encoded-hmac> |
X-Cresora-Timestamp | Unix timestamp of delivery (seconds) |
The v1= prefix is a scheme selector, not part of the digest. It is not
included in the HMAC input, and it exists so a future scheme version can be
introduced without breaking existing receivers.
Verification algorithm
- Build the signing payload:
timestamp.raw_body - Compute
HMAC-SHA256(payload, signing_secret) - Compare to the value in
X-Cresora-Signature(constant-time comparison) - Reject if timestamp is older than 5 minutes
import crypto from "node:crypto";
function verifyWebhook(req, secret) {
const timestamp = req.headers["x-cresora-timestamp"];
const signature = req.headers["x-cresora-signature"];
const body = req.rawBody; // raw bytes, not parsed JSON
// Reject stale deliveries (replay protection)
const ageSeconds = Math.floor(Date.now() / 1000) - Number(timestamp);
if (ageSeconds > 300) throw new Error("Webhook timestamp too old");
// HMAC over the raw body bytes — never over a re-serialized string
const payload = Buffer.concat([Buffer.from(`${timestamp}.`), body]);
const expected = Buffer.from(
"v1=" + crypto.createHmac("sha256", secret).update(payload).digest("hex"),
);
const received = Buffer.from(String(signature ?? ""));
// timingSafeEqual throws on inputs of different length, so a forged or
// missing header must be rejected before it is compared — never crash.
if (received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) {
throw new Error("Webhook signature mismatch");
}
}import hmac, hashlib, time
def verify_webhook(body: bytes, headers: dict, secret: str):
timestamp = headers["X-Cresora-Timestamp"]
signature = headers["X-Cresora-Signature"]
age = int(time.time()) - int(timestamp)
if age > 300:
raise ValueError("Webhook timestamp too old")
# HMAC over the raw body bytes — never decode and re-encode the body
payload = timestamp.encode() + b"." + body
expected = "v1=" + hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected):
raise ValueError("Webhook signature mismatch")Always use constant-time comparison (timingSafeEqual / hmac.compare_digest). String equality (===) is vulnerable to timing attacks. In Node, check the lengths first: timingSafeEqual throws on inputs of different length, and a forged signature must be a clean rejection, not a crash.
Storing your signing secret
Store the signing secret in your secrets manager (not in code or environment files committed to source control). Rotate it in the Partner Portal if compromised.