VNPay, một trong những cổng thanh toán được dùng rộng rãi nhất tại Việt Nam, ký mỗi thông báo webhook bằng HMAC-SHA256 dưới tên tham số SecureHash: đơn vị tích hợp nhận một khóa bí mật khi thiết lập, và mỗi thông báo gửi đến kèm theo một chữ ký cần được tính lại cục bộ rồi so sánh với giá trị nhận được. Nếu chữ ký không khớp, thông báo đó thực ra không đến từ cổng thanh toán — và không nên được xử lý như một xác nhận thanh toán thật.
Vì sao cần khóa bí mật, không chỉ là hash
Một hash thông thường như SHA-256 chỉ chứng minh dữ liệu không bị hỏng khi truyền đi — không chứng minh ai đã gửi nó. Nếu máy chủ chỉ băm nội dung webhook rồi so sánh với một hash đính kèm trong yêu cầu, kẻ tấn công có thể tự tạo payload giả và đính kèm một hash khớp. HMAC giải quyết điều này bằng cách trộn thêm một khóa bí mật mà chỉ người gửi và người nhận biết — nếu không có khóa đó, việc tạo ra chữ ký hợp lệ gần như bất khả thi, ngay cả khi kẻ tấn công biết chính xác hàm hash nào đang được dùng.
Vì sao đây không chỉ là "hash của khóa cộng tin nhắn"
Việc nối khóa và tin nhắn một cách đơn giản trước khi băm (ví dụ hash(khóa + tin nhắn)) dễ bị tấn công mở rộng độ dài với một số hàm băm — kẻ tấn công đôi khi có thể thêm dữ liệu và tính ra một hash hợp lệ mới mà không cần biết toàn bộ khóa. HMAC dùng một cấu trúc riêng với hai lần băm và đệm trong/ngoài, đóng hoàn toàn loại tấn công này.
HMAC còn xuất hiện ở đâu
Ngoài việc ký webhook, HMAC còn xác thực các yêu cầu API, và là thành phần cốt lõi bên trong thuật toán TOTP — thuật toán tạo ra các mã sáu chữ số trong các ứng dụng xác thực.
Vì sao cần điều này
- Xác minh tính xác thực của webhook từ cổng thanh toán hoặc các dịch vụ bên thứ ba khác.
- Ký các yêu cầu API mà không bao giờ gửi khóa bí mật qua mạng.
- Hiểu cơ chế bên trong của mã TOTP và token phiên làm việc.
So sánh trong thời gian không đổi
So sánh chữ ký nhận được với chữ ký mong đợi bằng toán tử === thông thường là không an toàn: toán tử này thường dừng lại ở byte khác biệt đầu tiên, và thời gian thực hiện so sánh tiết lộ cho kẻ tấn công biết họ đã đoán đúng bao nhiêu byte đầu tiên. Một trình xử lý webhook được xây dựng đúng cách luôn dùng hàm so sánh trong thời gian không đổi — như hash_equals trong PHP hoặc crypto.timingSafeEqual trong Node.js — luôn kiểm tra mọi byte bất kể sự khác biệt đầu tiên nằm ở đâu.