Skip to main content

Signature verification

Every webhook Osigu delivers is signed with HMAC-SHA256 using a secret Osigu generates during onboarding. Your receiver must verify the signature and reject any request where it doesn't match — or where the request is stale — otherwise anyone who guesses your webhook URL can inject fake events.

This page is the formal spec of the signature format. For an end-to-end walkthrough including framework examples, see Implement a webhook receiver.

The signature header

Every request carries:

X-Osigu-Signature: t=<unix_seconds>,v1=<hex_hmac_sha256>
  • t — the Unix-seconds timestamp of when Osigu signed the request.
  • v1 — the hex-encoded HMAC-SHA256 of the signed string.

Multiple v1= values may appear (comma-separated) during key rotation. Accept the request if any of them match.

What Osigu signs

The signed string is the concatenation:

<t>.<raw_request_body>

where:

  • <t> is the exact ASCII decimal string from the header.
  • <raw_request_body> is the exact bytes of the HTTP body — no re-encoding, no whitespace normalisation, no JSON parsing beforehand.

The HMAC key is the webhook signing secret Osigu gave you.

Verification algorithm

1. Extract t and v1 values from X-Osigu-Signature.
2. Reject if |now - t| > 300 seconds. ← replay protection
3. signed_string = t + "." + raw_body
4. expected = hex(HMAC-SHA256(secret, signed_string))
5. Constant-time compare expected against v1.
6. If no match → reject with 401.
7. Otherwise → proceed to processing.

Constant-time compare — why it matters

A naive expected === v1 compare with a short-circuit !== is vulnerable to timing attacks. An attacker who can measure your response time can, byte by byte, guess the correct signature. Use your language's constant-time comparison primitive:

  • Node.js: crypto.timingSafeEqual
  • Python: hmac.compare_digest
  • Go: hmac.Equal
  • Java: MessageDigest.isEqual
  • Ruby: Rack::Utils.secure_compare

Replay protection — the 5-minute window

Even a valid signature is rejected if the t timestamp is more than 5 minutes off from your server's now(). This blocks replay attacks where an attacker who intercepts a legitimate delivery attempts to replay it hours later.

If your servers' clocks drift more than a few seconds, sync them with NTP. Otherwise valid deliveries will bounce off your receiver.

Key rotation

If your webhook secret leaks or you want to rotate proactively:

  1. Ask Osigu (email webhooks@osigu.com) to issue a new secret.
  2. Osigu enables dual-signing: deliveries carry v1=<new>,v1=<old> for a 48-hour overlap.
  3. You deploy code that accepts either — trivial if you already iterate over v1= values.
  4. After 48h Osigu switches to signing with only the new secret.
  5. You delete the old secret from your config.

Never do a hard cut-over. The dual-signing window prevents in-flight retries from failing during the rotation.

Example: Node.js verification

import crypto from 'node:crypto';

function verify(header, rawBody, secret) {
const parts = header.split(',').reduce((acc, kv) => {
const [k, v] = kv.split('=');
if (k === 'v1') (acc.v1 ||= []).push(v);
else acc[k] = v;
return acc;
}, { v1: [] });

const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;

const signed = `${t}.${rawBody}`;
const expected = crypto.createHmac('sha256', secret).update(signed).digest('hex');

return parts.v1.some((sig) =>
sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))
);
}

Debug checklist

If your verification is always failing:

  1. Are you using the raw body bytes, not the JSON-parsed object? Middleware that parses JSON before your handler destroys the exact bytes that were signed.
  2. Are you concatenating with a literal dot between t and body, no spaces?
  3. Are you comparing hex to hex? Not base64.
  4. Is your secret exactly what Osigu gave you (no leading/trailing whitespace, no URL-decoded copy)?
  5. Is your server clock within 5 minutes of true time?

Sandbox has a test endpoint that echoes back the signed string Osigu computed — email support@osigu.com if you're stuck and we'll enable it for your account.