HMAC, AEAD, and Nonce-Misuse Resistance → Webhook Signature Verification TODO
Concept
HMAC is a message authentication code built from a hash function and a secret key: without the key you can't produce a valid tag, so it guarantees both the integrity and the origin of a message together. AEAD is a mode that handles encryption and authentication in one pass, guaranteeing not just plaintext confidentiality but also the integrity of the ciphertext and any additional authenticated data (AAD). Well-known AEAD constructions like AES-GCM and ChaCha20-Poly1305 are catastrophic if a nonce is reused under the same key — the authentication key can leak or the plaintext can be recovered — so nonce-misuse-resistant modes cap the damage of a repeated nonce at merely revealing that the same plaintext produced the same ciphertext. Webhook verification is an authentication problem, not a confidentiality one, so HMAC is the usual tool: include a timestamp in the signed payload, and have the receiver check it against an allowed time window to block replay attacks. Tag comparison must always be constant-time, or a timing side channel lets an attacker match the tag one byte at a time.
Webhook endpoints are open to the internet, so without signature verification and replay defenses, anyone can forge and push in payment or settlement events.
Code & Formula
# HMAC·AEAD와 웹훅 서명 검증 — 타임스탬프 포함 HMAC 서명을 만들고, 상수시간 비교 +
# 허용 시간창 검사로 재전송 공격을 막는 최소 웹훅 검증기를 구현한다. (교육용)
import hashlib
import hmac
import time
WEBHOOK_SECRET = b"whsec_example_only"
TOLERANCE_SECONDS = 300
def sign_webhook(payload: bytes, timestamp: int) -> str:
signed_data = f"{timestamp}.".encode() + payload
return hmac.new(WEBHOOK_SECRET, signed_data, hashlib.sha256).hexdigest()
def verify_webhook(payload: bytes, timestamp: int, signature: str, now: int) -> bool:
if abs(now - timestamp) > TOLERANCE_SECONDS:
return False # 재전송(replay) 공격 방지: 너무 오래된 서명은 거부
expected = sign_webhook(payload, timestamp)
return hmac.compare_digest(expected, signature) # 상수 시간 비교 — 타이밍 사이드채널 방지
# --- 정상 케이스 ---
now = int(time.time())
payload = b'{"event":"payment.succeeded","amount":1000}'
sig = sign_webhook(payload, now)
print("valid webhook accepted:", verify_webhook(payload, now, sig, now))
# --- 재전송 공격: 유효했던 서명을 그대로 재사용하되 시간이 지남 ---
old_timestamp = now - 1000
old_sig = sign_webhook(payload, old_timestamp)
print("replayed (stale) webhook rejected:",
not verify_webhook(payload, old_timestamp, old_sig, now))
# --- 변조 공격: payload만 바꾸고 서명은 그대로 재사용 ---
tampered_payload = b'{"event":"payment.succeeded","amount":999999}'
print("tampered payload rejected:",
not verify_webhook(tampered_payload, now, sig, now))
# --- 위조 공격: 비밀키 없이 서명을 추측 ---
forged_sig = "0" * 64
print("forged signature rejected:", not verify_webhook(payload, now, forged_sig, now))
docs/code/algorithms/algorithms-83.py
Exercise
Build a webhook sender and receiver that sign the request body plus a timestamp with HMAC-SHA256, then verify that a forged signature, a one-byte-tampered body, and a replay outside the time window are each rejected.
Practical Connection
If Verex receives callbacks from an external outcome provider or payment system, that callback is itself a settlement trigger, so signature verification, the timestamp window, and idempotency keys become the first gate on the path to on-chain settlement.
If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.