Skip to content

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

HeaderMô tả
X-Zorio-EventTên event, vd pbx.call.hangup
X-Zorio-TimestampEpoch giây UTC lúc Zorio publish event
X-Zorio-DeliveryUUID duy nhất cho mỗi lần delivery (kể cả retry) — dùng cho dedupe
X-Zorio-Signaturesha256=<hex> HMAC-SHA256 của raw body với secret
Content-Typeapplication/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)

js
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ùng request.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.

Tài liệu liên quan

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