Skip to content

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:

  1. Copy secret ngay (chỉ hiện 1 lần).
  2. Lưu vào ENV của receiver (ZORIO_WEBHOOK_SECRET).
  3. 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-8

Cá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

  1. Reject timestamp cũ — nếu X-Zorio-Timestamp lệch quá 300 giây với now → reject 400.
  2. Tính lại signature với secret + raw body.
  3. So sánh constant-time với X-Zorio-Signature — dùng API timing-safe của ngôn ngữ.
  4. Dedupe theo X-Zorio-Delivery UUID — khoá phân tán NX với TTL 24h.

Sample Node.js (tối giản)

js
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

js
// 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

js
// 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:

nginx
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:

js
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.

Tài liệu liên quan

Cấp phép theo điều khoản sử dụng của Zorio.