Request signing
Both clients → queue-service and queue-service → your handlers use the same header format. They use different secrets.
Default header name: x-bq-queue-request-signature (override with SIGNATURE_HEADER).
Format
t=<unix-seconds>,v1=<hmac-hex>- Algorithm: HMAC-SHA512
- Message:
t+"."+ raw body string (exact bytes) - Encoding: hex
- Clock skew allowed: ±300 seconds
- Timestamps in milliseconds (
> 1e12) are rejected
Create a signature (Node)
import crypto from 'node:crypto';
export function createSignature(body, secret) {
const t = Math.floor(Date.now() / 1000);
const v1 = crypto
.createHmac('sha512', secret)
.update(`${t}.`)
.update(body)
.digest('hex');
return `t=${t},v1=${v1}`;
}Which secret
| Direction | Secret | Env / config |
|---|---|---|
Your app → /api/jobs/* | API secret | REQUEST_SIGNING_SECRET |
Worker → your endpoint | Per-queue secret | queues.*.signature_secret |
Same header name on both paths. Do not reuse one secret for both.
Verify (Node)
import crypto from 'node:crypto';
export function verifySignature(rawBody, headerValue, secret) {
const match = /^t=(\d+),v1=([a-f0-9]+)$/i.exec(headerValue ?? '');
if (!match) return false;
const t = Number(match[1]);
const v1 = match[2];
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - t) > 300) return false;
const expected = crypto
.createHmac('sha512', secret)
.update(`${t}.`)
.update(rawBody)
.digest('hex');
try {
return crypto.timingSafeEqual(Buffer.from(v1, 'hex'), Buffer.from(expected, 'hex'));
} catch {
return false;
}
}Pitfalls
- Sign the exact body string you send. Pretty-printing or proxy rewrites break HMAC.
- Header lookup is case-insensitive on the way in.
- Missing
${ENV_VAR}in YAML becomes"", so outbound signing can fail silently with an empty secret. - Clients use
REQUEST_SIGNING_SECRET. Handlers use the queue’ssignature_secret. Mixing them up is the usual “works in curl, fails in production” mistake.
Last updated on