Ký HMAC-SHA256
Mọi webhook Zorio gửi đến receiver của bạn đều được ký HMAC-SHA256 với secret bạn lưu lúc tạo webhook. Mục đích:
- Xác thực event đến từ Zorio (không phải kẻ giả mạo).
- Toàn vẹn payload (không bị sửa giữa đường).
- Chống replay attack qua timestamp window.
Headers Zorio gửi
| Header | Mô tả |
|---|---|
X-Zorio-Event | Tên event, vd pbx.call.hangup |
X-Zorio-Timestamp | Epoch giây UTC lúc Zorio publish event |
X-Zorio-Delivery | UUID duy nhất cho mỗi lần delivery (kể cả retry) — dùng cho dedupe |
X-Zorio-Signature | sha256=<hex> HMAC-SHA256 của raw body với secret |
Content-Type | application/json; charset=utf-8 |
Cách Zorio tính signature
signature = "sha256=" + lowercase(hex(HMAC-SHA256(secret, raw_body)))secret: 32 ký tự alphanumeric do hệ thống sinh khi tạo webhook.raw_body: bytes nguyên văn body request — KHÔNG parse JSON trước khi tính.
Cách receiver verify
Bước 1 — Reject timestamp quá cũ
So sánh X-Zorio-Timestamp với thời gian hiện tại. Nếu lệch quá 300 giây (5 phút) → reject 400.
Lý do: chống replay attack (kẻ tấn công bắt được 1 request hợp lệ rồi gửi lại nhiều lần).
Bước 2 — Tính lại signature
expected = "sha256=" + lowercase(hex(HMAC-SHA256(secret, raw_body)))Bước 3 — So sánh constant-time
So sánh expected với X-Zorio-Signature bằng hàm constant-time (crypto.timingSafeEqual ở Node.js, hmac.compare_digest ở Python, hash_equals ở PHP).
KHÔNG dùng == để so chuỗi
So == thông thường có thể leak signature từng byte qua timing attack. Luôn dùng API timing-safe của ngôn ngữ.
Bước 4 — Dedupe theo X-Zorio-Delivery
Lưu X-Zorio-Delivery UUID đã xử lý vào Redis/DB với TTL 24h. Nếu UUID đã thấy → trả 200 ngay (không xử lý lại).
Mẫu Node.js (Express)
const crypto = require('crypto');
const express = require('express');
const app = express();
const SECRET = process.env.ZORIO_WEBHOOK_SECRET;
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const ts = parseInt(req.headers['x-zorio-timestamp'], 10);
if (!ts || Math.abs(Date.now() / 1000 - ts) > 300) {
return res.status(400).send('Stale timestamp');
}
const sig = req.headers['x-zorio-signature'] || '';
const expected = 'sha256=' + crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');
if (sig.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body.toString('utf8'));
console.log('Verified event:', event.event, event.data);
res.status(200).send('OK');
});
app.listen(3000);Xem thêm mẫu Python, PHP, Go ở Mẫu receiver.
Lỗi thường gặp
Lỗi: signature không khớp dù secret đúng
Nguyên nhân phổ biến:
- Parse JSON trước khi tính HMAC — bạn phải dùng RAW body, không phải object đã parse. Ở Express dùng
express.raw(). Ở hệ thống dùng$request->getContent(). Ở Flask dùngrequest.get_data(). - Charset encoding — phải UTF-8 bytes thuần, không encode lại.
- Body bị middleware modify — vd nginx/CDN nén lại gzip rồi giải nén → bytes khác. Cần config skip body modification cho route webhook.
- Trim whitespace — KHÔNG trim, byte cuối có thể là
}không có newline.
Lỗi: timestamp luôn lệch
- Đồng hồ server của bạn lệch — chạy
ntpd/chronydđồng bộ NTP. - Receiver chạy trong container không sync clock với host — đảm bảo container dùng đúng timezone.
