How webhook signing works
- Sender (e.g. GitHub) computes
HMAC(secret, raw_body)and sends the result in a header (X-Hub-Signature-256,Stripe-Signature, etc.). - Your server receives the request, reads the raw body and the same shared secret, recomputes
HMAC(secret, raw_body), and compares. - Match → trust the request. Mismatch → reject. Always use constant-time comparison to avoid timing-side-channel attacks (PHP
hash_equals, Nodecrypto.timingSafeEqual).
Why size matters
The secret should be at least as long as the HMAC output:
- HMAC-SHA256 → 32-byte secret
- HMAC-SHA1 → 20-byte secret (avoid for new integrations)
- HMAC-SHA512 → 64-byte secret
Shorter secrets weaken the HMAC and don't add usability. Longer secrets work but offer no extra security past the hash size.
Verification snippets
// Node.js (Express) const crypto = require('crypto'); const sig = req.headers['x-hub-signature-256']; // "sha256=…" const expected = 'sha256=' + crypto .createHmac('sha256', SECRET) .update(req.rawBody) .digest('hex'); if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) { return res.status(401).end(); } # PHP $sig = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? ''; $expected = 'sha256=' . hash_hmac('sha256', $rawBody, $SECRET); if (!hash_equals($expected, $sig)) { http_response_code(401); exit; }