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.
Where it lands in Jayverse
- Verex: use the exact recipe here for both the Stripe webhook and any oracle-result callback. HMAC-SHA256 over body plus timestamp, a constant-time compare, a bounded replay window, and an idempotency key — two different external signers, the same verification gate before either reaches settlement.
- Bridge: gate relayer messages between the Anvil and Sepolia legs with the same signature-plus-timestamp check. A forged or replayed relayer message does not just corrupt a read — it triggers a mint, so idempotency and the replay window belong on that path before it ships, not after an incident.
Key expressions
| Expression | 뜻 · 쓰이는 자리 |
|---|---|
| catastrophic if | ~하면 치명적인, 회복 불가능한 결과로 이어지는 · 나온스 재사용의 위험성을 경고할 때. "catastrophic if a nonce is reused" |
| cap the damage | 피해를 특정 수준으로 제한하다 · 최악의 상황에서도 피해가 그 이상 커지지 않게 막을 때. "cap the damage of a repeated nonce" |
| constant-time | 입력값과 무관하게 항상 같은 시간이 걸리는 · 타이밍 공격을 막는 비교 방식. "must always be constant-time" |
| side channel | 부채널, 의도치 않게 정보가 새는 경로 · 실행 시간 차이로 비밀값이 노출될 때. "a timing side channel lets an attacker match the tag" |
| one byte at a time | 한 바이트씩 (차례로) · 공격자가 비밀값을 조금씩 알아낼 때. "match the tag one byte at a time" |
| forge and push in | 위조해서 밀어넣다 · 검증 없이 가짜 이벤트를 시스템에 주입할 때. "anyone can forge and push in payment... events" |
| the first gate | 첫 번째 관문 · 다음 단계로 넘어가기 전에 반드시 통과해야 하는 검증. "become the first gate on the path to on-chain settlement" |
| AEAD | 인증 암호화 방식(Authenticated Encryption with Associated Data) · 기밀성과 무결성을 한 번에 보장하는 암호화 모드. "AEAD is a mode that handles encryption and authentication" |
| AAD | 추가 인증 데이터(Additional Authenticated Data) · 암호화하지 않지만 무결성 검증에는 포함되는 데이터. "any additional authenticated data (AAD)" |
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/.