How-to guide

Verify a delivery on your receiver

When a queue signs its outbound deliveries, every request to your endpoint carries the same X-Queuey-* signature headers. Verify them so you only act on requests that genuinely came from Queuey — and reject replays.

The checks

  1. Read the signature headers; reject if any are missing.
  2. Reject stale timestamps (a few minutes of clock skew).
  3. Recompute the body hash from the raw bytes — never from a re-serialized JSON object.
  4. Rebuild the canonical string and HMAC-SHA256 it with your secret.
  5. Compare to X-Queuey-Signature in constant time.

Verify (TypeScript / Node)

verify.ts
import crypto from "crypto";

// Express-style handler. IMPORTANT: verify against the RAW body bytes,
// before any JSON parsing/re-serialization.
export function verifyQueueyDelivery(req, secret) {
  const keyId = req.get("X-Queuey-Key-Id");
  const timestamp = req.get("X-Queuey-Timestamp");
  const nonce = req.get("X-Queuey-Nonce");
  const signature = req.get("X-Queuey-Signature");
  if (!keyId || !timestamp || !nonce || !signature) return false;

  // 1. Reject stale deliveries (±5 minutes).
  const skew = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (!Number.isFinite(skew) || skew > 300) return false;

  // 2. Recompute the body hash from the raw bytes.
  const bodyHash = crypto.createHash("sha256")
    .update(req.rawBody)              // Buffer of the raw request body
    .digest("hex").toLowerCase();

  // 3. Rebuild the canonical string and HMAC it with your shared secret.
  const canonical = [
    req.method,                       // METHOD
    req.path,                         // PATH
    req.originalUrl.split("?")[1] ?? "", // QUERY
    String(timestamp),                // TIMESTAMP
    nonce,                            // NONCE
    bodyHash,                         // CONTENT_SHA256
  ].join("\n");

  const expected = crypto
    .createHmac("sha256", Buffer.from(secret, "utf8"))
    .update(Buffer.from(canonical, "utf8"))
    .digest("hex").toLowerCase();

  // 4. Constant-time compare.
  const a = Buffer.from(signature, "utf8");
  const b = Buffer.from(expected, "utf8");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Verify the raw body
Frameworks that parse JSON before your handler runs will break verification — the bytes you hash must be exactly what Queuey sent. Capture the raw body (e.g. express.raw() or a rawBody hook) and hash that.

Related