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:
- Message: JSON stringified webhook payload
- Secret: Your webhook secret (from dashboard)
- Algorithm: HMAC-SHA256
- 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 '', 200go
// 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
