Webhook Security
Every webhook request includes an X-Webhook-Signature header containing an HMAC-SHA256 signature. Always verify this signature to ensure the request came from TRST and hasn't been tampered with.
Why Verify Signatures?
Without signature verification, an attacker could:
- Send fake webhook events to your endpoint
- Trigger unauthorized actions in your system
- Cause data integrity issues
Signature verification ensures:
- Authenticity - The request came from TRST
- Integrity - The payload hasn't been modified
- Non-repudiation - TRST sent this specific payload
Signature Format
The signature header contains a prefix followed by the hex-encoded HMAC:
X-Webhook-Signature: sha256=<hex-encoded-signature>Verification Process
1. Extract the Signature
Get the signature from the request header:
const signatureHeader = request.headers["x-webhook-signature"]
// Example: "sha256=a1b2c3d4e5f6..."2. Get the Raw Request Body
Important: You must use the raw request body (before JSON parsing):
// Express.js - use express.raw() middleware
app.post("/webhooks/trst", express.raw({ type: "application/json" }), (req, res) => {
const rawBody = req.body // Buffer containing raw bytes
// ...
})3. Compute the Expected Signature
Use HMAC-SHA256 with your webhook secret:
const crypto = require("crypto")
const hmac = crypto.createHmac("sha256", webhookSecret)
hmac.update(rawBody)
const expectedSignature = hmac.digest("hex")4. Compare Signatures
Use constant-time comparison to prevent timing attacks:
if (
!crypto.timingSafeEqual(
Buffer.from(receivedSignature, "hex"),
Buffer.from(expectedSignature, "hex"),
)
) {
throw new Error("Invalid signature")
}Security Best Practices
1. Use Constant-Time Comparison
Never use === or == for signature comparison:
// ❌ INSECURE - Vulnerable to timing attacks
if (receivedSignature === expectedSignature) {
// ...
}
// ✅ SECURE - Constant-time comparison
if (
crypto.timingSafeEqual(
Buffer.from(receivedSignature, "hex"),
Buffer.from(expectedSignature, "hex"),
)
) {
// ...
}Timing attacks can leak information about the signature by measuring how long comparisons take.
2. Store Secrets Securely
Never hardcode webhook secrets:
// ❌ BAD - Hardcoded secret
const secret = "whsec_a1b2c3d4e5f6"
// ✅ GOOD - Environment variable
const secret = process.env.TRST_WEBHOOK_SECRET
// ✅ BETTER - Secrets manager (AWS, GCP, etc.)
const secret = await secretsManager.getSecret("TRST_WEBHOOK_SECRET")Best practices for secret storage:
- Use environment variables for development
- Use a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.) for production
- Rotate secrets periodically
- Never commit secrets to version control
3. Verify Before Processing
Always verify the signature before parsing or processing the payload:
// ✅ CORRECT ORDER
verifySignature(rawBody, signature, secret) // 1. Verify
const event = JSON.parse(rawBody) // 2. Parse
processEvent(event) // 3. Process
// ❌ WRONG ORDER - Vulnerable to attacks
const event = JSON.parse(rawBody) // Don't parse first!
processEvent(event) // Don't process unverified data!
verifySignature(rawBody, signature, secret) // Too late!4. Use HTTPS for Your Endpoint
Your webhook endpoint must use HTTPS:
- Protects webhook payloads in transit
- Prevents man-in-the-middle attacks
- Required by TRST (HTTP endpoints are rejected)
5. Validate the Payload
After signature verification, validate the payload structure:
function validateWebhookPayload(event) {
if (!event.event_id || typeof event.event_id !== "string") {
throw new Error("Missing or invalid event_id")
}
if (!event.event_type || typeof event.event_type !== "string") {
throw new Error("Missing or invalid event_type")
}
if (!event.timestamp) {
throw new Error("Missing timestamp")
}
if (!event.data || typeof event.data !== "object") {
throw new Error("Missing or invalid event data")
}
return true
}6. Rate Limit Webhook Endpoints
Protect your endpoint from abuse:
const rateLimit = require("express-rate-limit")
const webhookLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 1000, // Limit each IP to 1000 requests per window
message: "Too many webhook requests",
})
app.post("/webhooks/trst", webhookLimiter, webhookHandler)7. Log Security Events
Log all verification failures for security monitoring:
function verifyWebhookSignature(request, secret) {
try {
// Verification logic...
return true
} catch (error) {
// Log security event
logger.security({
event: "webhook_verification_failed",
ip: request.ip,
error: error.message,
timestamp: new Date().toISOString(),
})
throw error
}
}Troubleshooting
Signature Verification Failing
Common causes:
Using parsed body instead of raw body
javascript// ❌ Wrong - body was already parsed app.use(express.json()) app.post("/webhooks", (req, res) => { const body = JSON.stringify(req.body) // This won't match! }) // ✅ Correct - use raw body app.post("/webhooks", express.raw({ type: "application/json" }), (req, res) => { const body = req.body // Raw Buffer })Wrong secret
- Verify you're using the correct webhook secret
- Check for extra whitespace or newlines
- Secrets are case-sensitive
Not removing 'sha256=' prefix
javascript// ❌ Wrong const signature = header // Includes 'sha256=' // ✅ Correct const signature = header.substring(7) // Remove 'sha256='Character encoding issues
javascript// ✅ Ensure consistent encoding const hmac = crypto.createHmac("sha256", secret) hmac.update(rawBody, "utf8") // Explicit encoding if needed
Testing Signature Verification
Generate a test signature locally:
const crypto = require("crypto")
const payload = JSON.stringify({
event_id: "test_123",
event_type: "webhook.test",
timestamp: "2024-11-02T15:00:00Z",
project_id: "proj_test",
data: { test_id: "test_123", message: "Test", triggered_at: "2024-11-02T15:00:00Z" },
})
const secret = "your_webhook_secret"
const hmac = crypto.createHmac("sha256", secret)
hmac.update(payload)
const signature = "sha256=" + hmac.digest("hex")
console.log("Signature:", signature)
// Use this signature in X-Webhook-Signature header when testingNext Steps
- Testing Guide - Test your signature verification
- Best Practices - Production-ready patterns
- Event Reference - Webhook payload schemas