Verifying signatures
HMAC-SHA256 over timestamp and raw body, compared in constant time, inside a five-minute window.
The guiru-signature header is t=<unix seconds>,v1=<hex hmac>. The signed message is the timestamp, a dot, and the raw request body — the exact bytes we sent, before any JSON parsing or re-serialisation by your framework.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret: string, rawBody: string, header: string, toleranceSec = 300): boolean {
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
const ts = Number(parts.t);
if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > toleranceSec || !parts.v1) return false;
const expected = createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}import hmac, hashlib, time
def verify(secret: str, raw_body: bytes, header: str, tolerance_sec: int = 300) -> bool:
parts = dict(kv.split("=", 1) for kv in header.split(","))
try:
ts = int(parts["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - ts) > tolerance_sec:
return False
expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))The five-minute window
Reject a signature whose timestamp is more than five minutes away from now, in either direction. That is what makes a captured delivery useless to replay, and it is why an endpoint with a badly wrong clock fails every verification while looking perfectly healthy.
Rotating the secret
During a rotation window, deliveries carry both guiru-signature (new secret) and guiru-signature-previous (old). Accept either while you deploy, then drop the old one. Nothing breaks if you roll slowly; everything breaks if you roll a secret with no window and one instance still holding the old value.