使用方法
- 選擇演算法。預設是 HMAC-SHA256,也是大多數 Webhook 和 API 使用的演算法。
- 輸入金鑰,並把「金鑰格式」設為文字(按 UTF-8 讀取)、十六進位或 Base64。如果金鑰是原始位元組,而不是類似口令的字串,請選擇十六進位或 Base64。
- 輸入或貼上訊息,也可以把輸入格式切換為十六進位,或者開啟檔案。HMAC 會隨輸入即時更新。
- 選擇小寫十六進位、大寫十六進位或 Base64 輸出並複製結果;也可以把收到的簽名貼上到「對比」框中,檢視是否一致。
HMAC 的工作原理
HMAC(基於雜湊的訊息認證碼,RFC 2104)把密碼學雜湊函式與金鑰結合在一起。只有知道金鑰的人才能為某條訊息算出正確的值,所以 HMAC 一致同時說明兩件事:訊息沒有被改動,並且來自持有金鑰的一方。
它的結構是 H((K ⊕ opad) ‖ H((K ⊕ ipad) ‖ message))。金鑰如果比雜湊函式的分組更長(MD5、SHA-1 和 SHA-256 為 64 位元組,SHA-384 和 SHA-512 為 128 位元組),會先被雜湊一次,再用零填充到分組長度;然後與 ipad(重複的 0x36)異或,和訊息一起計算內層雜湊,再與 opad(重複的 0x5c)異或,和內層結果一起計算外層雜湊。簡單拼接的 SHA-256(key ‖ message) 沒有外層雜湊,任何人看到一個標籤後,都能在訊息末尾追加資料,並在不知道金鑰的情況下算出新的有效標籤,這就是長度擴充套件攻擊。
HMAC 的輸出長度與底層雜湊相同,HMAC-SHA256 為 32 位元組,即 64 個十六進位字元。RFC 2104 建議金鑰不短於這個長度,可以用隨機 Base64 生成器生成合適的金鑰。
HMAC 簽名的常見用途
- GitHub Webhook。
X-Hub-Signature-256請求頭的值是sha256=加上原始請求體的 HMAC-SHA256(十六進位),金鑰為 Webhook 金鑰。可以把整個請求頭的值貼上到「對比」框中,sha256=字首會被自動忽略。 - Stripe Webhook。
Stripe-Signature請求頭包含時間戳t和簽名v1。簽名是對「時間戳、點號和原始請求體」計算的十六進位 HMAC-SHA256,金鑰是按文字使用的端點金鑰(whsec_...)。 - Shopify Webhook。
X-Shopify-Hmac-Sha256中是原始請求體 HMAC-SHA256 的 Base64 形式,金鑰為應用的客戶端金鑰(client secret)。可以選擇 Base64 輸出,或直接把請求頭的值貼上到「對比」框中。 - AWS 簽名版本 4。簽名金鑰通過鏈式 HMAC-SHA256 派生:先以
AWS4加秘密訪問金鑰為金鑰對日期計算 HMAC,再依次對區域、服務和aws4_request計算,每一步都以上一步的結果為金鑰。把金鑰格式設為十六進位,就能把上一步的輸出直接用作下一步的金鑰。 - JSON Web Token。HS256、HS384 和 HS512 就是對「編碼後的頭部、點號和編碼後的載荷」計算的 HMAC-SHA256、-384 和 -512。權杖的第三段是這個 HMAC 的 URL 安全 Base64 形式,可以原樣貼上到「對比」框中。完整的權杖可以用 JWT 解碼工具驗證。
務必基於收到的原始請求體計算 HMAC,不要做任何改動。把 JSON 解析後再重新序列化,會改變空格或鍵的順序;哪怕只多了一個換行符,HMAC 也會完全不同。
在程式碼中計算 HMAC
# Python
import hmac, hashlib
sig = hmac.new(b'key', b'message', hashlib.sha256).hexdigest()
hmac.compare_digest(sig, received) # 恆定時間比較
// Node.js
const crypto = require('crypto');
const sig = crypto.createHmac('sha256', 'key').update('message').digest('hex');
// 長度不同時 timingSafeEqual 會丟擲異常,請先檢查長度
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(received));
// PHP:訊息在前,金鑰在後
$sig = hash_hmac('sha256', 'message', 'key');
hash_equals($sig, $received);
# OpenSSL:文字金鑰,以及以十六進位給出的同一金鑰
printf '%s' 'message' | openssl dgst -sha256 -hmac 'key'
printf '%s' 'message' | openssl dgst -sha256 -mac HMAC -macopt hexkey:6b6579
# 測試向量:金鑰 "key",訊息 "The quick brown fox jumps over the lazy dog"
# HMAC-SHA256 = f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8
在自己的程式碼中校驗簽名時,請使用上面這類恆定時間比較函式。普通的 == 遇到第一個不同的字元就會返回,攻擊者可以利用這種時間差逐位元組試出有效簽名。對於 Stripe 這類帶時間戳的方案,還應拒絕時間戳距今已超過幾分鐘的請求,以防重放攻擊。
常見問題
HMAC-MD5 和 HMAC-SHA1 還安全嗎?
碰撞攻擊使 MD5 和 SHA-1 作為普通雜湊不再安全,但這些攻擊並不能實際用來偽造 HMAC,因為 HMAC 依賴的是雜湊函式的其他特性以及金鑰(RFC 6151 專門討論了 HMAC-MD5)。必須使用它們的現有整合,例如基於 HMAC-SHA1 的 TOTP 動態驗證碼,可以繼續使用;新的設計請選擇 HMAC-SHA256。
為什麼我算出的 HMAC 與 Webhook 簽名不一致?
常見原因有:對解析後重新序列化的 JSON 而不是原始請求體進行計算;訊息末尾多了換行符;金鑰錯誤或金鑰格式不對(例如把 Base64 或十六進位金鑰當作文字輸入,或者反過來);演算法選錯。十六進位與 Base64 的差別則不會造成問題:「對比」框同時接受大小寫十六進位、標準 Base64 和 URL 安全 Base64。
HMAC 和普通雜湊有什麼區別?
HMAC 就是數字簽名嗎?
不是。HMAC 的雙方共享同一個金鑰,能驗證標籤的人也能生成標籤。RSA、ECDSA 等數字簽名(例如 JWT 中的 RS256、ES256)用私鑰簽名、用公鑰驗證,驗證方無法偽造簽名。
我的金鑰會被髮送到伺服器嗎?
不會。HMAC-SHA1 和 SHA-2 系列由瀏覽器的 Web Crypto API 計算,HMAC-MD5 由頁面中的 JavaScript 計算,金鑰和訊息都不會離開你的裝置。儘管如此,生產環境的金鑰仍需謹慎對待,如果可能已經洩露,請及時更換。