Verifying signatures
Every webhook delivery includes a signature header:
X-3AVA-Signature: t=1714000000,v1=5d9f...t is the Unix timestamp the signature was generated at; v1 is HMAC-SHA256 of {t}.{raw_request_body} using your webhook’s signing_secret.
Why verify
Section titled “Why verify”Without verification, anyone who knows your webhook URL can POST to it and impersonate 3AVA Mail. Verification proves the request actually came from us.
Implementation
Section titled “Implementation”import hmacimport hashlibimport time
SIGNING_SECRET = "whsec_XnK8pQrS..."TOLERANCE_SECONDS = 300 # 5 min
@app.post("/webhooks/amdy")def webhook(): sig_header = request.headers.get("X-3AVA-Signature", "") parts = dict(p.split("=", 1) for p in sig_header.split(",")) t = int(parts["t"]) received = parts["v1"]
# Reject old signatures (replay protection) if abs(time.time() - t) > TOLERANCE_SECONDS: return "stale", 400
# Recompute and compare payload = f"{t}.{request.get_data(as_text=True)}" expected = hmac.new( SIGNING_SECRET.encode(), payload.encode(), hashlib.sha256, ).hexdigest()
if not hmac.compare_digest(expected, received): return "bad signature", 400
# Process the verified event event = request.get_json() handle_event(event) return "", 200import crypto from "crypto";import express from "express";
const SIGNING_SECRET = "whsec_XnK8pQrS...";const TOLERANCE_SECONDS = 300;
app.post( "/webhooks/amdy", express.raw({ type: "application/json" }), (req, res) => { const sigHeader = req.header("X-3AVA-Signature") || ""; const parts = Object.fromEntries( sigHeader.split(",").map((p) => p.split("=", 2)) ); const t = parseInt(parts.t, 10); const received = parts.v1;
if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) { return res.status(400).send("stale"); }
const payload = `${t}.${req.body.toString("utf8")}`; const expected = crypto .createHmac("sha256", SIGNING_SECRET) .update(payload) .digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) { return res.status(400).send("bad signature"); }
const event = JSON.parse(req.body.toString("utf8")); handleEvent(event); res.status(200).send(""); });What to do on verification failure
Section titled “What to do on verification failure”Log it, return 400, and do not process the payload. If failures spike, your secret may have leaked — rotate it by deleting the webhook and creating a new one.
Replay protection
Section titled “Replay protection”The t (timestamp) check rejects requests older than 5 minutes. This makes a captured-and-replayed request unusable after that window even if the attacker has the body.