HMAC 和資料完整性:用共享金鑰偵測篡改
Published: Sat Feb 01 2025 | Modified: Sat Feb 07 2026 , 13 minutes reading.
1. 為什麼要關心這個問題?
你正在建構一個 API。用戶端發送這樣的請求:
{"action": "transfer", "amount": 1000, "to": "attacker"}你怎麼知道這個請求在傳輸過程中沒有被修改?你怎麼知道它來自授權的用戶端?
如果你已經和用戶端共享了一個金鑰(比如 API 金鑰),你可以使用訊息認證碼(MAC)來驗證真實性和完整性——比數位簽章更快。
2. 定義
訊息認證碼 (MAC) 是一小段用於認證訊息並驗證其完整性的資訊。
HMAC(基於雜湊的 MAC) 使用密碼學雜湊函式和金鑰來建構 MAC。
MAC 的特性:
- 任何知道金鑰的人都可以產生
- 任何知道金鑰的人都可以驗證
- 沒有金鑰,無法偽造
數位簽章 vs MAC:
- 簽章:只有簽章者能建立,任何人都能驗證
- MAC:任何知道金鑰的人都能建立和驗證
- 簽章:不可否認性
- MAC:沒有不可否認性(雙方都能建立)3. 為什麼不能只用雜湊?
樸素方法(有漏洞)
# 錯誤:簡單雜湊不是認證
import hashlib
def naive_integrity(message):
return hashlib.sha256(message).hexdigest()
# 攻擊者可以計算任何訊息的雜湊!
# 這提供不了任何認證問題所在
單獨的雜湊證明:
✗ 誰建立了訊息——什麼都沒有
✗ 授權——什麼都沒有
因為:
- 雜湊函式是公開的
- 任何人都可以計算 SHA256(任何訊息)
- 攻擊者可以偽造:message' + SHA256(message')為什麼 HMAC 有效
HMAC 包含金鑰:
HMAC(key, message) = hash(key || hash(key || message))
只有知道金鑰的人才能:
- 計算有效的 MAC
- 驗證 MAC
沒有金鑰,攻擊者無法:
- 為修改過的訊息計算 MAC
- 找到具有相同 MAC 的不同訊息4. HMAC 建構
HMAC 公式
HMAC(K, m) = H((K' ⊕ opad) || H((K' ⊕ ipad) || m))
其中:
K = 金鑰
K' = 填充到區塊大小的金鑰
H = 雜湊函式(SHA-256 等)
⊕ = 互斥或
opad = 外層填充(0x5c 重複)
ipad = 內層填充(0x36 重複)
m = 訊息為什麼是這種結構?
簡單方法有缺陷:
H(key || message): 長度延伸攻擊
H(message || key): 碰撞問題
H(key || message || key):仍然有漏洞
HMAC 的巢狀結構:
- 防止長度延伸
- 有安全性證明
- RFC 2104(1997)以來的標準5. 在 Python 中使用 HMAC
基本 HMAC
import hmac
import hashlib
key = b"super-secret-api-key"
message = b"action=transfer&amount=1000&to=alice"
# 計算 HMAC
mac = hmac.new(key, message, hashlib.sha256).hexdigest()
print(f"HMAC: {mac}")
# 驗證 HMAC(時序安全比較)
def verify_hmac(key, message, received_mac):
expected_mac = hmac.new(key, message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected_mac, received_mac)
# 使用
is_valid = verify_hmac(key, message, mac)
print(f"有效: {is_valid}")常見錯誤:時序攻擊
# 錯誤:容易受到時序攻擊
def insecure_verify(expected, received):
return expected == received # 洩露長度資訊!
# 正確:常數時間比較
import hmac
def secure_verify(expected, received):
return hmac.compare_digest(expected, received)
# 為什麼時序很重要:
# == 運算子在第一個不匹配時提前回傳
# 攻擊者可以透過測量時間逐位元組猜測 MAC
# compare_digest 總是花費相同時間6. 實際應用
API 請求簽章
import hmac
import hashlib
import time
import base64
class APIClient:
def __init__(self, api_key: str, api_secret: str):
self.api_key = api_key
self.api_secret = api_secret.encode()
def sign_request(self, method: str, path: str, body: str = "") -> dict:
timestamp = str(int(time.time()))
# 建立待簽章字串
string_to_sign = f"{method}\n{path}\n{timestamp}\n{body}"
# 計算 HMAC
signature = hmac.new(
self.api_secret,
string_to_sign.encode(),
hashlib.sha256
).hexdigest()
return {
"X-API-Key": self.api_key,
"X-Timestamp": timestamp,
"X-Signature": signature
}
class APIServer:
def __init__(self, secrets: dict):
self.secrets = secrets # api_key -> api_secret
def verify_request(self, method: str, path: str, body: str,
api_key: str, timestamp: str, signature: str) -> bool:
# 檢查時間戳記(防止重放攻擊)
if abs(time.time() - int(timestamp)) > 300: # 5 分鐘視窗
return False
# 取得此金鑰的密鑰
api_secret = self.secrets.get(api_key)
if not api_secret:
return False
# 重新計算簽章
string_to_sign = f"{method}\n{path}\n{timestamp}\n{body}"
expected = hmac.new(
api_secret.encode(),
string_to_sign.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)Cookie 認證
import hmac
import hashlib
import base64
import json
import time
class SecureCookie:
def __init__(self, secret_key: bytes):
self.secret_key = secret_key
def create(self, data: dict, max_age: int = 3600) -> str:
"""建立簽章的 cookie 值"""
payload = {
"data": data,
"exp": int(time.time()) + max_age
}
payload_json = json.dumps(payload, sort_keys=True)
payload_b64 = base64.b64encode(payload_json.encode()).decode()
# 簽章 payload
signature = hmac.new(
self.secret_key,
payload_b64.encode(),
hashlib.sha256
).hexdigest()
return f"{payload_b64}.{signature}"
def verify(self, cookie_value: str) -> dict | None:
"""驗證並解碼簽章的 cookie"""
try:
payload_b64, signature = cookie_value.rsplit(".", 1)
# 驗證簽章
expected = hmac.new(
self.secret_key,
payload_b64.encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, signature):
return None
# 解碼並檢查過期
payload = json.loads(base64.b64decode(payload_b64))
if time.time() > payload["exp"]:
return None
return payload["data"]
except Exception:
return None
# 使用
cookie = SecureCookie(b"my-super-secret-key")
value = cookie.create({"user_id": 123, "role": "admin"})
print(f"Cookie: {value}")
data = cookie.verify(value)
print(f"資料: {data}")Webhook 驗證
import hmac
import hashlib
def verify_github_webhook(payload: bytes, signature: str, secret: str) -> bool:
"""驗證 GitHub webhook 簽章"""
# GitHub 發送:sha256=<hex_digest>
if not signature.startswith("sha256="):
return False
expected = "sha256=" + hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
def verify_stripe_webhook(payload: bytes, signature: str, secret: str) -> bool:
"""驗證 Stripe webhook 簽章"""
# Stripe 格式:t=timestamp,v1=signature
parts = dict(p.split("=") for p in signature.split(","))
# Stripe 簽章:timestamp.payload
signed_payload = f"{parts['t']}.{payload.decode()}"
expected = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, parts["v1"])7. HMAC vs 其他 MAC
對比
HMAC:
- 基於雜湊函式(SHA-256 等)
- 研究充分,保守的選擇
- 比某些替代品稍慢
Poly1305:
- 為速度設計
- 與 ChaCha20 一起使用(ChaCha20-Poly1305)
- 每則訊息使用一次性金鑰
CMAC/OMAC:
- 基於區塊密碼(AES)
- 在某些標準中使用
- 安全性與 HMAC 類似
GMAC:
- GCM 模式的 MAC 部分
- 有 AES-NI 時非常快
- 需要唯一的 nonce何時使用什麼
使用 HMAC-SHA256 當:
- 需要獨立的 MAC
- 最大相容性
- 保守的安全選擇
使用 Poly1305 當:
- 使用 ChaCha20 加密
- 需要最大速度
- 作為認證加密的一部分
使用 GCM/GMAC 當:
- 使用 AES 加密
- 需要認證加密
- 有硬體 AES 支援8. 安全考量
金鑰管理
HMAC 金鑰要求:
- 必須保密(顯然)
- 應該是隨機的,不是從密碼衍生的
- 最少 128 位元,推薦 256 位元
- 不同用途使用不同金鑰
如果需要金鑰衍生:
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
def derive_hmac_key(master_key: bytes, purpose: str) -> bytes:
return HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=None,
info=purpose.encode()
).derive(master_key)HMAC 不提供什麼
HMAC 不提供:
✗ 機密性(訊息是明文)
✗ 不可否認性(雙方都能建立 MAC)
✗ 重放保護(需要時間戳記/nonce)
要獲得機密性 + 完整性:
→ 使用認證加密(AES-GCM、ChaCha20-Poly1305)
要獲得不可否認性:
→ 使用數位簽章
要獲得重放保護:
→ 在訊息中包含時間戳記或序號常見錯誤
# 錯誤:使用密碼作為金鑰
hmac.new(b"password123", message, hashlib.sha256)
# 正確:從密碼衍生金鑰
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
kdf = Scrypt(salt=salt, length=32, n=2**20, r=8, p=1)
key = kdf.derive(b"password123")
hmac.new(key, message, hashlib.sha256)
# 錯誤:加密和 MAC 使用相同金鑰
aes_key = os.urandom(32)
encrypted = aes_encrypt(aes_key, plaintext)
mac = hmac.new(aes_key, encrypted, hashlib.sha256)
# 正確:分開的金鑰
aes_key = os.urandom(32)
mac_key = os.urandom(32)
encrypted = aes_encrypt(aes_key, plaintext)
mac = hmac.new(mac_key, encrypted, hashlib.sha256)
# 最佳:使用認證加密
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
key = AESGCM.generate_key(bit_length=256)
aesgcm = AESGCM(key)
ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data)9. HMAC 在協定中的應用
TLS
TLS 使用 HMAC(或 AEAD)進行記錄認證:
TLS 記錄:
┌────────────────────────────────────────────┐
│ 內容類型 │ 版本 │ 長度 │ 負載 │
├────────────────────────────────────────────┤
│ 加密 + MAC │
└────────────────────────────────────────────┘
TLS 1.2:MAC-then-encrypt 或 AEAD
TLS 1.3:僅 AEAD(GCM 或 ChaCha20-Poly1305)JWT
JWT 結構:
header.payload.signature
對於 HS256(HMAC-SHA256):
signature = HMAC-SHA256(
secret,
base64url(header) + "." + base64url(payload)
)
驗證:
1. 將 token 分成部分
2. 重新計算 HMAC
3. 比較簽章(常數時間!)AWS Signature V4
AWS 請求簽章使用 HMAC 鏈:
DateKey = HMAC-SHA256("AWS4" + SecretKey, Date)
RegionKey = HMAC-SHA256(DateKey, Region)
ServiceKey = HMAC-SHA256(RegionKey, Service)
SigningKey = HMAC-SHA256(ServiceKey, "aws4_request")
Signature = HMAC-SHA256(SigningKey, StringToSign)10. 完整範例:簽章訊息
import hmac
import hashlib
import json
import time
import os
import base64
class SignedMessageProtocol:
"""認證訊息的完整協定"""
def __init__(self, shared_secret: bytes):
self.key = shared_secret
def create_message(self, payload: dict) -> str:
"""建立認證訊息"""
# 新增後設資料
message = {
"payload": payload,
"timestamp": int(time.time()),
"nonce": base64.b64encode(os.urandom(16)).decode()
}
# 序列化
message_json = json.dumps(message, sort_keys=True)
message_b64 = base64.b64encode(message_json.encode()).decode()
# 建立 MAC
mac = hmac.new(
self.key,
message_b64.encode(),
hashlib.sha256
).hexdigest()
return f"{message_b64}.{mac}"
def verify_message(self, signed_message: str,
max_age: int = 300) -> dict | None:
"""驗證並提取訊息"""
try:
# 分割
message_b64, received_mac = signed_message.rsplit(".", 1)
# 驗證 MAC
expected_mac = hmac.new(
self.key,
message_b64.encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected_mac, received_mac):
return None
# 解碼
message_json = base64.b64decode(message_b64)
message = json.loads(message_json)
# 檢查時間戳記
age = time.time() - message["timestamp"]
if age < 0 or age > max_age:
return None
return message["payload"]
except Exception:
return None
# 使用
secret = os.urandom(32)
protocol = SignedMessageProtocol(secret)
# 發送方
msg = protocol.create_message({
"action": "transfer",
"amount": 100,
"to": "[email protected]"
})
print(f"簽章訊息: {msg[:50]}...")
# 接收方
payload = protocol.verify_message(msg)
if payload:
print(f"驗證的負載: {payload}")
else:
print("驗證失敗!")11. 本章小結
三點要記住:
HMAC 提供認證和完整性。 與普通雜湊不同,HMAC 需要知道金鑰。沒有金鑰,攻擊者無法偽造有效的 MAC。
始終使用常數時間比較。 使用
hmac.compare_digest()防止時序攻擊。普通字串比較透過時序洩露資訊。HMAC 不能替代加密。 它驗證完整性但不隱藏內容。要獲得機密性 + 完整性,使用認證加密(AES-GCM)。
12. 下一步
我們現在已經介紹了基本的密碼學原語:對稱加密、非對稱加密、數位簽章、憑證和 MAC。
在下一節中,我們將看到這些部分如何在真實協定中結合在一起:TLS 深入分析——HTTPS 實際上是如何運作的,從握手到安全資料傳輸。
