Weav Docsv1

Guides / Webhooks

Verifying signatures

Check every webhook before you trust it. The X-Signature header is an HMAC-SHA512 of the raw request body, in lowercase hex, made with your signing key.

Weav sends webhooks from two fixed addresses, which you can allow on your firewall: see IP addresses. Verify the signature anyway: an address alone doesn’t prove a webhook is Weav’s.

Steps

  1. Read the raw body bytes. Never decode and re-encode the JSON first, because that changes the bytes.
  2. Compute HMAC-SHA512 of the raw body with your signing key, as lowercase hex.
  3. Compare it with X-Signature using a constant-time comparison.
  4. Reject the delivery if sent_at is more than 5 minutes from your server’s time.
  5. Reply 2xx right away, then process the event in a queue.

PHP (Laravel)

PHP
// Verify with the raw body, before parsing anything
$raw_payload = $request->getContent();
$signature = (string) $request->header('X-Signature');

$valid = false;
foreach ($signing_keys as $signing_key) {   // one key, or two during a rotation
    $expected = hash_hmac('sha512', $raw_payload, $signing_key);
    if (hash_equals($expected, $signature)) {
        $valid = true;
        break;
    }
}

if (! $valid) {
    abort(401);
}

$payload = json_decode($raw_payload, true);

// Reject old or replayed deliveries
if (abs(now()->diffInSeconds($payload['sent_at'])) > 300) {
    abort(401);
}

// Reply 2xx right away, then do the work in a queue.
// The same reference_id can arrive more than once.
HandleWeavWebhook::dispatch($payload);

return response()->noContent();

Node.js (Express)

Node.js
const crypto = require('crypto');
const express = require('express');
const app = express();

function isValidSignature(rawBody, signature, signingKey) {
  const expected = crypto.createHmac('sha512', signingKey).update(rawBody).digest('hex');
  const a = Buffer.from(expected, 'utf8');
  const b = Buffer.from(String(signature || ''), 'utf8');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// express.raw keeps req.body as the exact bytes Weav sent
app.post('/webhooks/weav', express.raw({ type: 'application/json' }), (req, res) => {
  const keys = [process.env.WEAV_SIGNING_KEY, process.env.WEAV_SIGNING_KEY_NEW].filter(Boolean);
  const signature = req.get('X-Signature');
  if (!keys.some((key) => isValidSignature(req.body, signature, key))) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  if (Math.abs(Date.now() - Date.parse(event.sent_at)) > 5 * 60 * 1000) {
    return res.sendStatus(401);
  }

  res.sendStatus(200);
  queue.add(event); // process later; the same reference_id can arrive twice
});

Rotating your signing key

Weav signs every webhook with one key: your active key. A second key only exists during a rotation, so you can switch keys without failing a single delivery.

  1. Rotate in the dashboard. Weav creates a next key and shows it once. Your active key keeps signing.
  2. Add the next key to your server next to the active one, and deploy. Your server now accepts a signature made with either key.
  3. Retire the old key in the dashboard. The next key becomes active, and every webhook from then on is signed with it.
  4. Remove the old key from your server.

You don’t need to know which key signed a webhook. Compute the HMAC with each key you hold and accept the webhook if either one matches, as the examples above do. Only the key that signed it can produce that signature, so trying both is as safe as checking one.

sequenceDiagram
    autonumber
    participant You as You, in the dashboard
    participant Weav
    participant Server as Your server
    Note over Weav: Active key A signs every webhook
    You->>Weav: Rotate
    Weav-->>You: Next key B, shown once
    You->>Server: Add key B next to key A, deploy
    Weav->>Server: Webhooks still signed with A
    Note over Server: Accepts A or B
    You->>Weav: Retire key A
    Note over Weav: B is now the active key
    Weav->>Server: Webhooks signed with B
    You->>Server: Remove key A