Signature Verification
Verify webhook authenticity using HMAC SHA256 signatures.
Why Verify Signatures?
Signature verification ensures that:
- Webhooks are actually from YeboLink
- Payloads haven't been tampered with
- Requests aren't replayed by malicious actors
Always Verify
Never process webhook payloads without verifying the signature. This is a critical security measure.
How It Works
- YeboLink creates an HMAC SHA256 hash of the request body using your webhook secret
- The hash is sent in the
X-YeboLink-Signatureheader - Your server recreates the hash and compares it with the received signature
- If they match, the webhook is authentic
Verification Examples
Node.js
javascript
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
// Create expected signature
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Use timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Usage in Express
app.post('/webhooks/yebolink', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-yebolink-signature'];
const secret = process.env.YEBOLINK_WEBHOOK_SECRET;
if (!verifySignature(req.body, signature, secret)) {
return res.status(403).send('Invalid signature');
}
// Process webhook
const payload = JSON.parse(req.body);
handleWebhook(payload);
res.status(200).send('OK');
});Use Raw Body
When using Express, you MUST use express.raw() for webhook routes to preserve the raw body needed for signature verification.
Python
python
import hmac
import hashlib
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
"""Verify webhook signature"""
# Create expected signature
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
# Use timing-safe comparison
return hmac.compare_digest(signature, expected)
# Usage in Flask
from flask import Flask, request, Response
app = Flask(__name__)
@app.route('/webhooks/yebolink', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-YeboLink-Signature')
secret = os.environ.get('YEBOLINK_WEBHOOK_SECRET')
# Get raw body
payload = request.get_data()
if not verify_signature(payload, signature, secret):
return Response('Invalid signature', status=403)
# Process webhook
data = request.get_json()
handle_webhook_data(data)
return Response('OK', status=200)PHP
php
<?php
function verifySignature($payload, $signature, $secret) {
// Create expected signature
$expected = hash_hmac('sha256', $payload, $secret);
// Use timing-safe comparison
return hash_equals($signature, $expected);
}
// Usage
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_YEBOLINK_SIGNATURE'] ?? '';
$secret = getenv('YEBOLINK_WEBHOOK_SECRET');
if (!verifySignature($payload, $signature, $secret)) {
http_response_code(403);
die('Invalid signature');
}
// Process webhook
$data = json_decode($payload, true);
handleWebhook($data);
http_response_code(200);
echo 'OK';
?>Ruby
ruby
require 'openssl'
def verify_signature(payload, signature, secret)
# Create expected signature
expected = OpenSSL::HMAC.hexdigest('SHA256', secret, payload)
# Use timing-safe comparison
Rack::Utils.secure_compare(signature, expected)
end
# Usage in Sinatra
post '/webhooks/yebolink' do
signature = request.env['HTTP_X_YEBOLINK_SIGNATURE']
secret = ENV['YEBOLINK_WEBHOOK_SECRET']
# Get raw body
request.body.rewind
payload = request.body.read
unless verify_signature(payload, signature, secret)
halt 403, 'Invalid signature'
end
# Process webhook
data = JSON.parse(payload)
handle_webhook(data)
status 200
'OK'
endGo
go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
)
func verifySignature(payload []byte, signature, secret string) bool {
// Create expected signature
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
expected := hex.EncodeToString(mac.Sum(nil))
// Use constant-time comparison
return hmac.Equal([]byte(signature), []byte(expected))
}
func handleWebhook(w http.ResponseWriter, r *http.Request) {
signature := r.Header.Get("X-YeboLink-Signature")
secret := os.Getenv("YEBOLINK_WEBHOOK_SECRET")
// Read body
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest)
return
}
defer r.Body.Close()
// Verify signature
if !verifySignature(body, signature, secret) {
http.Error(w, "Invalid signature", http.StatusForbidden)
return
}
// Process webhook
var payload map[string]interface{}
json.Unmarshal(body, &payload)
handleWebhookData(payload)
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}Testing Signature Verification
Generate Test Signature
javascript
const crypto = require('crypto');
const secret = 'whsec_your_webhook_secret';
const payload = JSON.stringify({
event: 'message.delivered',
data: { message_id: 'test-123' }
});
const signature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
console.log('Signature:', signature);
// Send test request
fetch('http://localhost:3000/webhooks/yebolink', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-YeboLink-Signature': signature,
'X-YeboLink-Event': 'message.delivered'
},
body: payload
});Verify Manually
bash
# Create signature manually
echo -n '{"event":"message.delivered","data":{"message_id":"test-123"}}' | \
openssl dgst -sha256 -hmac 'whsec_your_webhook_secret'
# Send test request
curl -X POST http://localhost:3000/webhooks/yebolink \
-H "Content-Type: application/json" \
-H "X-YeboLink-Signature: GENERATED_SIGNATURE" \
-H "X-YeboLink-Event: message.delivered" \
-d '{"event":"message.delivered","data":{"message_id":"test-123"}}'Common Issues
Issue: Signature Always Invalid
Problem: Using JSON-parsed body instead of raw body
javascript
// WRONG - body has been parsed
app.use(express.json());
app.post('/webhooks', (req, res) => {
const signature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(req.body)) // ❌ May not match original
.digest('hex');
});
// CORRECT - use raw body
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const signature = crypto
.createHmac('sha256', secret)
.update(req.body) // ✅ Original raw body
.digest('hex');
});Issue: Signature Valid in Development, Fails in Production
Problem: Proxy or load balancer modifying request body
Solution: Configure proxy to preserve raw body:
nginx
# Nginx
location /webhooks {
proxy_pass http://backend;
proxy_set_header X-Real-IP $remote_addr;
# Don't buffer request body
proxy_request_buffering off;
}Issue: Intermittent Verification Failures
Problem: Using non-constant-time comparison
javascript
// WRONG - vulnerable to timing attacks
if (signature === expected) { }
// CORRECT - use timing-safe comparison
if (crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) { }Security Best Practices
1. Store Secret Securely
bash
# .env file (never commit to git)
YEBOLINK_WEBHOOK_SECRET=whsec_your_webhook_secret
# Add to .gitignore
echo ".env" >> .gitignore2. Use Timing-Safe Comparison
Always use constant-time comparison functions:
- Node.js:
crypto.timingSafeEqual() - Python:
hmac.compare_digest() - PHP:
hash_equals() - Ruby:
Rack::Utils.secure_compare() - Go:
hmac.Equal()
3. Reject Invalid Signatures Immediately
javascript
app.post('/webhooks/yebolink', (req, res) => {
// Verify FIRST, before any processing
if (!verifySignature(req.body, signature, secret)) {
return res.status(403).send('Invalid signature');
}
// Only process if signature is valid
processWebhook(req.body);
res.status(200).send('OK');
});4. Log Verification Failures
javascript
if (!verifySignature(payload, signature, secret)) {
console.error('Webhook signature verification failed', {
timestamp: new Date().toISOString(),
signature_received: signature,
ip: req.ip
});
return res.status(403).send('Invalid signature');
}5. Implement Rate Limiting
Protect against brute force attacks:
javascript
const rateLimit = require('express-rate-limit');
const webhookLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many webhook requests'
});
app.post('/webhooks/yebolink', webhookLimiter, handleWebhook);Debugging
Enable Verbose Logging
javascript
function verifySignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
console.log('Signature Debug:', {
received: signature,
expected: expected,
payload_length: payload.length,
payload_preview: payload.toString().substring(0, 100)
});
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}Test with Known Values
javascript
// Test with known good values
const testPayload = '{"event":"test"}';
const testSecret = 'test_secret';
const testSignature = crypto
.createHmac('sha256', testSecret)
.update(testPayload)
.digest('hex');
console.assert(
verifySignature(testPayload, testSignature, testSecret),
'Verification should pass with known good values'
);Next Steps
- Webhook Setup - Create webhooks
- Webhook Events - Event reference
- API Reference - Complete API docs