Skip to content

Signature Verification

Learn how to verify webhook signatures using HMAC-SHA256 to ensure authenticity.

Why Verify Signatures?

Webhook signature verification ensures:

  • ✅ Webhooks are from Rach (not attackers)
  • ✅ Payload hasn't been tampered with
  • ✅ Replay attacks are prevented

Security Critical

Always verify signatures! Skipping verification allows attackers to send fake webhooks.


How It Works

Rach signs every webhook using HMAC-SHA256:

  1. Message: JSON stringified webhook payload
  2. Secret: Your webhook secret (from dashboard)
  3. Algorithm: HMAC-SHA256
  4. Header: X-Webhook-Signature

Verification Examples

javascript
// Node.js
const crypto = require('crypto');

function verifySignature(payload, signature, secret) {
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(JSON.stringify(payload));
  const computed = hmac.digest('hex');
  
  // Use timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(computed)
  );
}

// Usage
app.post('/webhooks/payment', (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const secret = process.env.RACH_WEBHOOK_SECRET;
  
  if (!verifySignature(req.body, signature, secret)) {
    return res.status(401).send('Invalid signature');
  }
  
  // Process webhook
  handleWebhook(req.body);
  res.sendStatus(200);
});
python
# Python
import hmac
import hashlib
import json

def verify_signature(payload, signature, secret):
    """Verify webhook signature"""
    message = json.dumps(payload, separators=(',', ':')).encode()
    computed = hmac.new(
        secret.encode(),
        message,
        hashlib.sha256
    ).hexdigest()
    
    return hmac.compare_digest(signature, computed)

# Usage in Flask
@app.route('/webhooks/payment', methods=['POST'])
def webhook():
    signature = request.headers.get('X-Webhook-Signature')
    secret = os.getenv('RACH_WEBHOOK_SECRET')
    
    if not verify_signature(request.json, signature, secret):
        return 'Invalid signature', 401
    
    handle_webhook(request.json)
    return '', 200
go
// Go
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
)

func verifySignature(payload []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(payload)
    computed := hex.EncodeToString(mac.Sum(nil))
    
    return hmac.Equal([]byte(signature), []byte(computed))
}

// Usage
func webhookHandler(w http.ResponseWriter, r *http.Request) {
    signature := r.Header.Get("X-Webhook-Signature")
    secret := os.Getenv("RACH_WEBHOOK_SECRET")
    
    body, _ := ioutil.ReadAll(r.Body)
    
    if !verifySignature(body, signature, secret) {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }
    
    handleWebhook(body)
    w.WriteHeader(http.StatusOK)
}
php
// PHP
<?php

function verifySignature($payload, $signature, $secret) {
    $computed = hash_hmac('sha256', $payload, $secret);
    return hash_equals($signature, $computed);
}

// Usage
$signature = $_SERVER['HTTP_X_RACH_SIGNATURE'] ?? '';
$secret = getenv('RACH_WEBHOOK_SECRET');
$payload = file_get_contents('php://input');

if (!verifySignature($payload, $signature, $secret)) {
    http_response_code(401);
    die('Invalid signature');
}

$event = json_decode($payload, true);
handleWebhook($event);

http_response_code(200);
?>

Common Mistakes

❌ Wrong: Verifying parsed JSON

javascript
// DON'T DO THIS
const signature = computeHMAC(req.body); // req.body is already parsed

✅ Right: Verify raw body

javascript
// DO THIS
const signature = computeHMAC(JSON.stringify(req.body));

❌ Wrong: String comparison

javascript
// DON'T DO THIS (timing attack vulnerability)
if (signature === computed) { ... }

✅ Right: Timing-safe comparison

javascript
// DO THIS
if (crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(computed))) { ... }

Testing Verification

Generate a test signature:

javascript
const crypto = require('crypto');

const payload = {
  event: 'payment.confirmed',
  checkout_id: 'test_123',
  amount: '100.00'
};

const secret = 'your_webhook_secret';

const hmac = crypto.createHmac('sha256', secret);
hmac.update(JSON.stringify(payload));
const signature = hmac.digest('hex');

console.log('Signature:', signature);

// Send test webhook
fetch('http://localhost:3000/webhooks/payment', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Webhook-Signature': signature
  },
  body: JSON.stringify(payload)
});

Security Best Practices

Security Checklist

  • ✅ Store webhook secret in environment variables
  • ✅ Use timing-safe comparison
  • ✅ Verify signatures before processing
  • ✅ Use HTTPS in production
  • ✅ Rotate secrets periodically
  • ✅ Log failed verification attempts
  • ✅ Rate limit webhook endpoint

Next Steps

Built with ❤️ by Rach Finance