Bài hàm băm nói hash dùng để kiểm toàn vẹn. Nhưng có một lỗ hổng: nếu bạn gửi kèm (thông điệp, SHA256(thông điệp)), kẻ ở giữa chỉ cần sửa thông điệp rồi tính lại hash mới — bạn không phát hiện được. Hash trần chống được lỗi ngẫu nhiên (file hỏng khi tải) nhưng không chống được kẻ địch có chủ đích. Để chống giả mạo thật sự, ta cần trộn vào một khóa bí mật — đó là HMAC. Nó là nền của webhook có chữ ký, JWT HS256, cookie ký, API signing. Đo thật bằng openssl 3.0.
Vì sao hash trần không đủ
Toàn vẹn với hash trần chỉ hoạt động khi kẻ tấn công không chạm được vào cả thông điệp lẫn hash. Nhưng trên đường truyền hay trong một token client giữ, kẻ tấn công chạm được cả hai: sửa thông điệp → tính hash mới → gửi cặp mới. Không có gì bí mật để chúng không thể tính lại. HMAC sửa điều này bằng một khóa mà chỉ hai bên hợp lệ biết.
HMAC(key, msg) = H( (key ⊕ opad) || H( (key ⊕ ipad) || msg ) )
Cấu trúc "hai lớp băm lồng nhau với khóa" này không phải trang trí — nó chống lại length-extension attack mà cách tự chế SHA256(key || msg) mắc phải (với các hash kiểu Merkle–Damgård như SHA-256, kẻ tấn công biết H(key||msg) có thể tính H(key||msg||thêm) mà không cần biết key). Vì thế: đừng tự chế, dùng HMAC chuẩn.
openssl dgst -sha256 -hmac "key" file # HMAC-SHA256
python3 -c 'import hmac,hashlib; print(hmac.new(b"key",b"msg",hashlib.sha256).hexdigest())'

Hình 1: Hash trần không chống kẻ sửa thông điệp (họ tính lại hash). HMAC trộn khóa bí mật vào hàm băm nên chỉ người có khóa tạo được mã đúng; cấu trúc lồng nhau chống length-extension — đừng tự chế SHA256(key||msg). Kiểm HMAC phải so hằng thời gian.
Đo thật: khóa và thông điệp đều ảnh hưởng

Hình 2: HMAC-SHA256 với khóa "khoabimat123" cho b6bb171a.... Sai khóa → 4497fa0e... (khác hẳn). Sửa thông điệp (100→900) → 000d7e71.... So với SHA-256 trần (eab1a14c..., không khóa) — hash trần ai cũng tính lại được nên không chống giả mạo, HMAC thì có.
Kết quả xác nhận đủ tính chất:
- HMAC phụ thuộc khóa: khóa đúng cho
b6bb171a..., đổi sang khóa sai cho4497fa0e...— khác hoàn toàn. Kẻ không biết khóa không thể tính ra HMAC đúng cho bất kỳ thông điệp nào. - HMAC phụ thuộc thông điệp: giữ nguyên khóa, sửa
100thành900, HMAC từb6bb171a...thành000d7e71.... Người nhận tính lại HMAC bằng khóa của mình, thấy không khớp → biết thông điệp bị sửa. - Khác biệt cốt lõi với hash trần: SHA-256 trần (
eab1a14c...) không có khóa nên ai cũng tính lại được — sửa thông điệp xong tính hash mới, người nhận không phân biệt được. HMAC gắn khóa nên chỉ hai bên hợp lệ tạo/kiểm được → chống giả mạo thật sự. Đây là lý do GitHub/Stripe ký webhook bằng HMAC: bạn tính lại HMAC của payload bằng secret dùng chung, khớp thì chắc chắn payload thật và chưa bị sửa.
Đánh đổi và lưu ý
So sánh HMAC phải hằng thời gian. Khi kiểm, đừng so bằng == thông thường — nhiều ngôn ngữ so chuỗi dừng sớm ở byte khác đầu tiên, làm thời gian so lệ thuộc số byte khớp, để lộ dần từng byte cho kẻ đo thời gian (timing attack). Dùng hàm so hằng thời gian: hmac.compare_digest(a, b) (Python), MessageDigest.isEqual (Java), crypto.timingSafeEqual (Node).
HMAC là đối xứng — cả hai bên chung một khóa. Nghĩa là ai xác thực được cũng tạo được HMAC. Nếu bạn cần người khác xác thực mà không tự tạo được (ví dụ công khai kiểm chữ ký của bạn), cần chữ ký số bất đối xứng (bài sau), không phải HMAC. Chọn sai là lộ khả năng giả mạo cho mọi bên xác thực.
Khóa HMAC phải đủ dài và ngẫu nhiên. Khóa ngắn/đoán được thì kẻ tấn công brute-force ra khóa rồi giả mạo thoải mái. Khóa nên là chuỗi ngẫu nhiên mật mã ít nhất bằng độ dài output (32 byte cho SHA-256) — bài random sẽ nói cách sinh.
Ba ý mang về
- HMAC = hash trộn khóa bí mật, chống giả mạo có chủ đích mà hash trần không làm được: đo thật, sai khóa (
4497fa0e) hay sửa thông điệp 100→900 (000d7e71) đều làm HMAC khác hẳn; hash trần ai cũng tính lại được nên vô dụng trước kẻ địch. - Đừng tự chế
SHA256(key||msg)— dính length-extension attack; dùng HMAC chuẩn (openssl-hmac,hmaccủa Python) vốn đã có cấu trúc lồng nhau chống sẵn. - Dùng đúng ngữ cảnh: so HMAC bằng hàm hằng thời gian (compare_digest) tránh timing attack; HMAC là đối xứng (bên kiểm cũng tạo được) — cần bên thứ ba chỉ kiểm mà không tạo thì dùng chữ ký số; và khóa phải đủ dài, ngẫu nhiên mật mã.
Nguồn
- RFC 2104 (HMAC) và MDN/OWASP về message authentication: https://www.rfc-editor.org/rfc/rfc2104
- Python docs — hmac (kèm
compare_digest): https://docs.python.org/3/library/hmac.html
Phần sau ta sang mã hóa đối xứng để giấu dữ liệu: AES và các chế độ ECB/CBC/GCM — vì sao ECB lộ mẫu dữ liệu (ảnh "chim cánh cụt" kinh điển), vai trò của IV, đo thật bằng openssl enc.