Every webhook delivery carries an X-Kodori-Signature header in the form sha256=<hex>. To trust the delivery is from Kodori (and not from an attacker who learned your URL), recompute the signature on receive and reject mismatches.
The signing recipe:
1. Read X-Kodori-Timestamp from the request headers. This is an ISO-8601 timestamp Kodori set when the delivery left. 2. Concatenate timestamp + "." + raw_request_body. The body MUST be the exact bytes Kodori sent — read it before any framework parses + re-serializes the JSON, or signatures won't match. 3. Compute HMAC-SHA256 over that concatenation using your subscription's signing secret (the whsec_... string shown at creation) as the key. 4. Hex-encode the digest. 5. Compare to the hex value after "sha256=" in X-Kodori-Signature. Use a constant-time comparison to avoid timing attacks. 6. Reject if the timestamp drifts more than 5 minutes from now — that's the replay-protection window.
Node.js example (raw-body via express + 'crypto'):
``` import crypto from 'node:crypto'; import express from 'express';
const app = express(); app.post('/webhooks/kodori', express.raw({ type: 'application/json' }), (req, res) => { const ts = req.header('X-Kodori-Timestamp'); const sig = req.header('X-Kodori-Signature') ?? ''; if (!ts || !sig.startsWith('sha256=')) return res.sendStatus(400);
// Replay protection. const skewMs = Math.abs(Date.now() - new Date(ts).getTime()); if (skewMs > 5 * 60_000) return res.sendStatus(401);
const expected = crypto .createHmac('sha256', process.env.KODORI_WEBHOOK_SECRET) .update(`${ts}.${req.body}`) .digest('hex'); const provided = sig.slice('sha256='.length); const ok = expected.length === provided.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(provided)); if (!ok) return res.sendStatus(401);
const event = JSON.parse(req.body.toString('utf8')); // …handle event… res.sendStatus(200); }); ```
Common pitfalls:
- Re-serializing the body. If your framework parses JSON before you read it, the bytes you sign over differ from what Kodori sent. Use a raw-body middleware in the order shown above. - Not checking the timestamp. Without the drift check, an attacker who captured a single delivery can replay it forever. - Logging the signing secret. The plaintext was shown once for a reason — don't ship it to a log aggregator.
If verification keeps failing on otherwise-correct setups, check the /webhooks delivery log: it shows the response status code and the first 500 characters of the body your endpoint returned. Most "signature mismatch" reports we see are actually a body-encoding mismatch upstream.