Ký webhook HMAC-SHA256
Webhook nội dung sâu hơn và sample code đầy đủ ở Webhooks chung → Ký HMAC-SHA256. Trang này tóm tắt nhanh để CRM kỹ thuật onboarding mới hiểu cơ chế ký + verify.
Vì sao cần ký HMAC
CRM nhận webhook từ Zorio gửi đến URL của bạn. Nếu KHÔNG ký:
- Kẻ tấn công có thể giả Zorio → gửi event giả tới URL của bạn → CRM xử lý nhầm.
- Payload có thể bị sửa giữa đường (man-in-the-middle).
- Kẻ tấn công replay lại event hợp lệ đã bắt được.
Ký HMAC-SHA256 giải quyết cả 3.
Cơ chế
Lúc tạo webhook
Khi đăng ký webhook qua Admin Console, hệ thống sinh secret 32 ký tự alphanumeric. Bạn:
- Copy secret ngay (chỉ hiện 1 lần).
- Lưu vào ENV của receiver (
ZORIO_WEBHOOK_SECRET). - KHÔNG hardcode trong code, KHÔNG commit vào git.
Mỗi event Zorio gửi
Zorio gửi POST request kèm headers:
X-Zorio-Event: pbx.call.hangup
X-Zorio-Timestamp: 1782706011
X-Zorio-Delivery: 7c9e6679-7425-40de-944b-e07fc1f90ae7
X-Zorio-Signature: sha256=ab9d7f4...
Content-Type: application/json; charset=utf-8Cách Zorio tính signature
signature = "sha256=" + lowercase(hex(HMAC-SHA256(secret, raw_body)))secret: secret bạn lưu lúc tạo webhook.raw_body: bytes nguyên văn của body request (KHÔNG parse JSON trước).
Receiver verify như nào
4 bước
- Reject timestamp cũ — nếu
X-Zorio-Timestamplệch quá 300 giây với now → reject 400. - Tính lại signature với secret + raw body.
- So sánh constant-time với
X-Zorio-Signature— dùng API timing-safe của ngôn ngữ. - Dedupe theo
X-Zorio-DeliveryUUID — khoá phân tán NX với TTL 24h.
Sample Node.js (tối giản)
const crypto = require('crypto');
function verifyZorioWebhook(req, secret) {
// 1. Timestamp window
const ts = parseInt(req.headers['x-zorio-timestamp'], 10);
if (!ts || Math.abs(Date.now() / 1000 - ts) > 300) {
throw new Error('Stale timestamp');
}
// 2. Recompute
const expected = 'sha256=' +
crypto.createHmac('sha256', secret).update(req.rawBody).digest('hex');
// 3. Timing-safe compare
const sig = req.headers['x-zorio-signature'] || '';
if (sig.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
throw new Error('Invalid signature');
}
}Sample PHP / Python / Go
Xem Mẫu receiver đầy đủ 4 ngôn ngữ.
Pitfall thường gặp
❌ Parse JSON trước khi tính HMAC
// SAI — body đã bị parser modify
app.use(express.json());
app.post('/webhook', (req, res) => {
const expected = createHmac(secret, JSON.stringify(req.body)).digest('hex');
// → KHÔNG match vì JSON.stringify khác raw bytes Zorio gửi
});
// ĐÚNG — dùng raw bytes
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const expected = createHmac(secret, req.body).digest('hex');
});❌ So == thay vì timing-safe
// SAI — leak signature từng byte qua timing attack
if (sig === expected) { ... }
// ĐÚNG
if (crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) { ... }❌ Không check timestamp
Không check timestamp → replay attack. Kẻ tấn công bắt 1 event hợp lệ → gửi lại 1.000 lần cách 1 năm sau vẫn pass signature.
❌ Reverse proxy modify body
Một số reverse proxy (nginx + module compress) hoặc Cloudflare có thể nén lại body → bytes khác. Config skip compression cho route webhook:
location /webhook {
gzip off;
proxy_pass http://app;
}❌ Cấu hình TLS sai
Zorio CHỈ gửi đến URL HTTPS. Nếu cert TLS:
- Self-signed → Zorio reject ngay.
- Expired → Zorio reject.
- Hostname mismatch → Zorio reject.
Test: curl https://your-webhook-url/ từ ngoài internet, không lỗi cert.
Replay protection cấp 2 (optional)
Để bảo mật cao hơn, dedupe theo cả (timestamp + body hash) thay vì chỉ Delivery UUID:
const fingerprint = ts + ':' + sha256(rawBody);
const isNew = await redis.set(`zorio:fp:${fingerprint}`, '1', 'NX', 'EX', 600);
if (!isNew) return res.status(200).send('Duplicate fingerprint');Lý do: nếu attacker bắt được 1 request hợp lệ trong window 300s, vẫn không replay được vì fingerprint đã thấy.
