Verify every incoming webhook to confirm it was sent by DMSI Webhooks and the payload was not tampered with in transit.
Signature Algorithm #
The signature is computed as:
string_to_sign = "{X-DMSI-Timestamp}.{X-DMSI-Webhook-ID}.{raw_body}"
X-DMSI-Signature = "sha256=" + HMAC-SHA256(string_to_sign, signing_secret)
Important: Use the raw request body (before any JSON parsing) for signature computation. Parsing and re-serializing JSON can change whitespace and key ordering, causing a verification mismatch.
PHP Example #
<?php
function verify_dmsi_webhook(string $secret): bool
{
$raw_body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_DMSI_TIMESTAMP'] ?? '';
$webhook_id = $_SERVER['HTTP_X_DMSI_WEBHOOK_ID'] ?? '';
$signature = $_SERVER['HTTP_X_DMSI_SIGNATURE'] ?? '';
if (empty($timestamp) || empty($webhook_id) || empty($signature)) {
return false;
}
$string_to_sign = $timestamp . '.' . $webhook_id . '.' . $raw_body;
$expected = 'sha256=' . hash_hmac('sha256', $string_to_sign, $secret);
// Use hash_equals to prevent timing attacks
return hash_equals($expected, $signature);
}
// Usage
if (!verify_dmsi_webhook('your_signing_secret')) {
http_response_code(401);
exit('Unauthorized');
}
$payload = json_decode(file_get_contents('php://input'), true);
// Process $payload...
Node.js Example #
const crypto = require('crypto');
function verifyDmsiWebhook(req, secret) {
const rawBody = req.rawBody; // Must be the raw Buffer or string
const timestamp = req.headers['x-dmsi-timestamp'] || '';
const webhookId = req.headers['x-dmsi-webhook-id'] || '';
const signature = req.headers['x-dmsi-signature'] || '';
if (!timestamp || !webhookId || !signature) return false;
const stringToSign = ${timestamp}.${webhookId}.${rawBody};
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(stringToSign)
.digest('hex');
// Use timingSafeEqual to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
// Express example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifyDmsiWebhook(req, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Unauthorized');
}
const payload = JSON.parse(req.body);
// Process payload...
res.status(200).send('OK');
});
Python Example #
import hashlib
import hmac
import json
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = 'your_signing_secret'
@app.route('/webhook', methods=['POST'])
def receive_webhook():
raw_body = request.get_data()
timestamp = request.headers.get('X-DMSI-Timestamp', '')
webhook_id = request.headers.get('X-DMSI-Webhook-ID', '')
signature = request.headers.get('X-DMSI-Signature', '')
if not timestamp or not webhook_id or not signature:
abort(401)
string_to_sign = f"{timestamp}.{webhook_id}.{raw_body.decode('utf-8')}"
expected = 'sha256=' + hmac.HMAC(
WEBHOOK_SECRET.encode('utf-8'),
string_to_sign.encode('utf-8'),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, signature):
abort(401)
payload = json.loads(raw_body)
# Process payload...
return 'OK', 200
Handling Key Rotation #
During a 24-hour rotation window, X-DMSI-Signature may contain two signatures:
X-DMSI-Signature: sha256=<new>, sha256=<old>Accept either:
$signatures = array_map('trim', explode(',', $signature));
$string_to_sign = $timestamp . '.' . $webhook_id . '.' . $raw_body;
$valid = false;
foreach ($all_secrets as $s) {
$expected = 'sha256=' . hash_hmac('sha256', $string_to_sign, $s);
foreach ($signatures as $sig) {
if (hash_equals($expected, $sig)) {
$valid = true;
break 2;
}
}
}
Security Notes #
- Always use
hash_equals()(PHP) ortimingSafeEqual()(Node.js) — never==or=== - Always use the raw body for signature computation
- Reject requests with missing or empty signature headers
- Consider rejecting requests with timestamps older than 5 minutes to prevent replay attacks
Related Topics #
- Request Headers
- Signing Secret Rotation
- Payload Envelope
