Github hmac webhook verify
Skill kjuhwa/skills-hub/skills/cli/github-hmac-webhook-verify
Verify a GitHub webhook's `X-Hub-Signature-256` using HMAC-SHA-256 over the raw body with `timingSafeEqual`, with a same-length guard, prefix-masked logs on mismatch, and a 200-then-async processing pattern.From its SKILL.md
npx -y skills add kjuhwa/skills-hub --skill github-hmac-webhook-verifyAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
5.0 KB, ~1.1k tokens by cl100k_base, as published. Nobody here has run it
Verify GitHub Webhook HMAC-SHA-256 (with Timing-Safe Compare + Async Processing)
When to use
- Your service receives
POST /webhooks/githubevents (issue comments, PR events, etc.). - You have
GITHUB_WEBHOOK_SECRETconfigured in GitHub's repo settings and in your server env. - You want a compact, hardened verify helper that doesn't leak timing information and handles the common failure modes gracefully.
Steps
-
Read the raw body as text, before any JSON parse. HMAC is over the raw bytes; parsing and re-serializing will not reproduce the same digest. In Hono:
const payload = await c.req.text(); const signature = c.req.header('X-Hub-Signature-256') ?? ''; -
Compute
sha256=+ hex HMAC using your secret:import { createHmac, timingSafeEqual } from 'crypto'; const hmac = createHmac('sha256', webhookSecret); const digest = 'sha256=' + hmac.update(payload).digest('hex'); -
Guard against length mismatch before
timingSafeEqual.timingSafeEqualthrows on different lengths. A missing/malformed header shouldn't crash your handler — it should just fail verification.const digestBuffer = Buffer.from(digest); const signatureBuffer = Buffer.from(signature); if (digestBuffer.length !== signatureBuffer.length) { log.error({ receivedLength: signatureBuffer.length, computedLength: digestBuffer.length }, 'github.signature_length_mismatch'); return false; } const isValid = timingSafeEqual(digestBuffer, signatureBuffer); -
Log both prefixes (not full values) on mismatch. Signatures are not secrets, but "compressed" logging gives you a diagnostic hint without bloating the log:
if (!isValid) { log.error({ receivedPrefix: signature.substring(0, 15) + '...', computedPrefix: digest.substring(0, 15) + '...', }, 'github.signature_mismatch'); } -
Wrap the whole verify in a try/catch that returns
falseon any thrown error — a malformed signature header shouldn't crash the request handler. -
Return 200 immediately, process async. GitHub retries on non-2xx and has a 10-second timeout. Return 200 as soon as signature + authorization check pass, then do the heavy work (database writes, AI spawning, git operations) in a fire-and-forget path:
app.post('/webhooks/github', async c => { const payload = await c.req.text(); const signature = c.req.header('X-Hub-Signature-256') ?? ''; if (!adapter.verifySignature(payload, signature)) return c.text('', 401); // fire-and-forget — don't await adapter.handleWebhook(payload, signature).catch(err => log.error({ err }, 'github.handle_webhook_failed')); return c.text('', 200); }); -
Add an authorization layer after signature verification. The HMAC only proves "GitHub sent this with our secret." It doesn't prove the sender is authorized in your application. Archon keeps a whitelist of GitHub usernames and silently rejects unauthorized senders (log with masked username, return success) — no error response (prevents probing).
Counter / Caveats
sha1(X-Hub-Signature) is deprecated. UseX-Hub-Signature-256only.- The GitHub signature format is
sha256=<hex>. Don't forget thesha256=prefix when comparing. - Configure
c.req.text()before any body-consuming middleware — once Hono parses JSON, re-reading raw bytes is not safe. - If you need to re-deliver failed webhooks, persist the raw payload + signature for later reprocessing; do not reserialize from parsed JSON.
- Rotate the webhook secret periodically. Your verify code should accept a list of valid secrets during rotation (match on first success). Archon's current adapter takes a single secret; extend with a list if rotation matters.
Evidence
packages/adapters/src/forge/github/adapter.ts:260-296:verifySignaturewith length guard,timingSafeEqual, prefix-masked logs on mismatch, try/catch returningfalse.handleWebhookorchestrator atadapter.ts:691-725: verify → parse → authorize (whitelist check with masked username log) → close-event handling, etc.adapter.test.ts+context.test.tsexercise the signature path with real HMAC computation.- Root
CLAUDE.mdlines 800-810: "Return 200 immediately, process async. Verify webhook signatures (GitHub: X-Hub-Signature-256). Usec.req.text()for raw webhook body (signature verification)." - Commit SHA: d89bc767d291f52687beea91c9fcf155459be0d9.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.