Webhook Signature Verification
Verify webhook signatures using HMAC-SHA256 to ensure events are genuinely from TruthVouch.
Overview
Each webhook includes an HMAC-SHA256 signature in the X-TruthVouch-Signature header. Verify this signature to confirm:
- Event authenticity (from TruthVouch)
- Event integrity (not modified in transit)
- Prevent replay attacks
Signature Header Format
X-TruthVouch-Signature: sha256=abcdef1234567890...The header contains the HMAC-SHA256 signature of the request body in hex-encoded format.
Verification Algorithm
1. Extract Signature
signature_header = "sha256=abcdef1234567890..."signature = signature_header.split('=', 1)[1]2. Compute HMAC
Use webhook secret key and sha256 algorithm on the raw request body:
import hmacimport hashlib
webhook_secret = "whsec_abc123..."body = request.get_data() # Raw bytes, not parsed JSON
computed_signature = hmac.new( webhook_secret.encode(), body, hashlib.sha256).hexdigest()3. Compare Signatures
Use constant-time comparison to prevent timing attacks:
if hmac.compare_digest(signature, computed_signature): # Signature valid!else: # Invalid signature - reject event return 401 UnauthorizedImplementation Examples
Python
import hmacimport hashlibimport jsonimport timefrom flask import Flask, request
app = Flask(__name__)WEBHOOK_SECRET = "whsec_abc123..."MAX_AGE = 300 # 5 minutes
def verify_signature(signature_header, body): """Verify webhook signature.""" if not signature_header: return False
# Extract signature from header try: _, signature = signature_header.split('=', 1) except ValueError: return False
# Compute expected signature expected_signature = hmac.new( WEBHOOK_SECRET.encode(), body, # Use raw bytes hashlib.sha256 ).hexdigest()
# Compare signatures (constant-time comparison) return hmac.compare_digest(signature, expected_signature)
@app.route('/webhook', methods=['POST'])def handle_webhook(): body = request.get_data() # Get raw bytes signature_header = request.headers.get('X-TruthVouch-Signature')
# Verify signature if not verify_signature(signature_header, body): return {'error': 'Invalid signature'}, 401
# Process event event = json.loads(body) process_event(event) return {'status': 'success'}, 200Node.js
const crypto = require('crypto');const express = require('express');
const app = express();const WEBHOOK_SECRET = 'whsec_abc123...';const MAX_AGE = 300; // 5 minutes
function verifySignature(signatureHeader, body) { if (!signatureHeader) { return false; }
// Extract signature from header const [, signature] = signatureHeader.split('='); if (!signature) { return false; }
// Compute expected signature const expectedSignature = crypto .createHmac('sha256', WEBHOOK_SECRET) .update(body) // Use raw body bytes .digest('hex');
// Compare (constant-time) return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expectedSignature) );}
app.post('/webhook', (req, res) => { const signatureHeader = req.get('X-TruthVouch-Signature'); const body = req.rawBody; // Need raw body, not parsed JSON
if (!verifySignature(signatureHeader, body)) { return res.status(401).json({ error: 'Invalid signature' }); }
const event = JSON.parse(body); processEvent(event); res.json({ status: 'success' });});
// Middleware to store raw bodyapp.use(express.raw({ type: 'application/json' }));.NET
using System;using System.Linq;using System.Security.Cryptography;using System.Text;using Microsoft.AspNetCore.Mvc;
[ApiController][Route("webhook")]public class WebhookController : ControllerBase{ private const string WEBHOOK_SECRET = "whsec_abc123..."; private const int MAX_AGE = 300; // 5 minutes
private bool VerifySignature(string signatureHeader, byte[] body) { if (string.IsNullOrEmpty(signatureHeader)) return false;
// Extract signature from header var parts = signatureHeader.Split('='); if (parts.Length != 2) return false;
var signature = parts[1];
// Compute expected signature using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(WEBHOOK_SECRET))) { var hash = hmac.ComputeHash(body); var expectedSignature = BitConverter.ToString(hash) .Replace("-", "") .ToLower();
return signature.Equals(expectedSignature); } }
[HttpPost] public IActionResult HandleWebhook() { var signatureHeader = Request.Headers["X-TruthVouch-Signature"].ToString(); Request.Body.Position = 0; using (var reader = new BinaryReader(Request.Body)) { var body = reader.ReadBytes((int)Request.Body.Length);
if (!VerifySignature(signatureHeader, body)) return Unauthorized(new { error = "Invalid signature" });
var @event = JsonConvert.DeserializeObject<WebhookEvent>(Encoding.UTF8.GetString(body)); ProcessEvent(@event); return Ok(new { status = "success" }); } }}Go
package main
import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" "io" "net/http" "strconv" "strings" "time")
const WEBHOOK_SECRET = "whsec_abc123..."const MAX_AGE = 300
func verifySignature(signatureHeader string, body []byte) bool { if signatureHeader == "" { return false }
// Extract signature from header parts := strings.Split(signatureHeader, "=") if len(parts) != 2 { return false } signature := parts[1]
// Compute expected signature h := hmac.New(sha256.New, []byte(WEBHOOK_SECRET)) h.Write(body) expectedSignature := hex.EncodeToString(h.Sum(nil))
// Compare return hmac.Equal([]byte(signature), []byte(expectedSignature))}
func handleWebhook(w http.ResponseWriter, r *http.Request) { signatureHeader := r.Header.Get("X-TruthVouch-Signature") body, _ := io.ReadAll(r.Body)
if !verifySignature(signatureHeader, body) { http.Error(w, "Invalid signature", http.StatusUnauthorized) return }
// Process event processEvent(body) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"status":"success"}`))}
func main() { http.HandleFunc("/webhook", handleWebhook) http.ListenAndServe(":8080", nil)}Getting Your Webhook Secret
- Go to Settings → Webhooks
- Create or edit webhook
- Copy “Signing Secret” (starts with
whsec_) - Store securely (use environment variable, secrets manager)
Never commit webhook secret to version control!
Testing Signatures
Generate Test Signature
import hmacimport hashlib
webhook_secret = "whsec_abc123..."body = b'{"event_id":"evt-test"}'
signature = hmac.new( webhook_secret.encode(), body, hashlib.sha256).hexdigest()
header = f"sha256={signature}"print(f"X-TruthVouch-Signature: {header}")Test with curl
WEBHOOK_SECRET="whsec_abc123..."BODY='{"event_id":"evt-test","event_type":"alert.detected"}'
SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha256 -mac HMAC -macopt "key:$WEBHOOK_SECRET" -hex | cut -d' ' -f2)
curl -X POST https://yourserver.com/webhook \ -H "Content-Type: application/json" \ -H "X-TruthVouch-Signature: sha256=$SIGNATURE" \ -d "$BODY"Best Practices
-
Use constant-time comparison — Prevents timing attacks
- Python:
hmac.compare_digest() - Node.js:
crypto.timingSafeEqual() - .NET: Implement yourself or use BouncyCastle
- Python:
-
Verify timestamp — Reject events older than 5 minutes
-
Store secret securely — Use environment variable or secrets manager
-
Rotate secrets periodically — Every 90 days or if compromised
-
Log signature failures — Monitor for attacks
-
Use HTTPS only — Encrypt in transit
See Webhook Events and Testing for related topics.