Workspace IndexAlgorithms › Day 83

HMAC, AEAD, and Nonce-Misuse Resistance → Webhook Signature Verification TODO

Algorithms · Day 83 / 100 · F. Cryptography & ZK (Day 82-96)

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))

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/.


한국어

HMAC·AEAD와 nonce 오용 저항 TODO

Algorithms · Day 83 / 100 · F. 암호학·ZK (Day 82–96)

개념

HMAC은 해시 함수와 비밀키로 만드는 메시지 인증 코드로, 키를 모르면 유효한 태그를 만들 수 없어 메시지의 무결성과 출처를 함께 보장한다. AEAD는 암호화와 인증을 한 번에 처리하는 모드로, 평문의 기밀성에 더해 암호문과 추가 인증 데이터(AAD)의 무결성까지 보장한다. AES-GCM이나 ChaCha20-Poly1305 같은 대표적 AEAD는 같은 키로 nonce를 재사용하면 인증 키가 노출되거나 평문이 복구될 수 있어 치명적이며, 그래서 nonce 오용 저항 모드는 nonce가 반복되더라도 피해를 같은 평문이 같은 암호문으로 보인다는 수준으로 제한한다. 웹훅 검증은 기밀성이 아니라 인증 문제이므로 보통 HMAC을 쓰고, 서명 대상에 타임스탬프를 포함한 뒤 수신 측에서 허용 시간 창을 검사해 재전송 공격을 막는다. 태그 비교는 반드시 상수 시간 비교로 해야 타이밍 사이드채널로 태그를 한 바이트씩 맞춰 나가는 공격을 막을 수 있다.

웹훅 엔드포인트는 인터넷에 열려 있어 서명 검증이나 재전송 방어가 없으면 아무나 결제·정산 이벤트를 위조해 밀어 넣을 수 있기 때문이다.

코드 · 수식

# 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))

연습

요청 본문과 타임스탬프를 함께 HMAC-SHA256으로 서명하는 웹훅 송신기와 수신기를 구현하고, 서명 위조·본문 1바이트 변조·시간 창을 넘긴 재전송이 각각 거부되는지 테스트하기.

실무 · Verex 연결

Verex가 외부 결과 제공자나 결제 시스템의 콜백을 받는다면 그 콜백이 곧 정산 트리거이므로, 서명 검증과 타임스탬프 창, 멱등키가 온체인 정산으로 가는 첫 관문이 된다.

공부한 날 원본 커리큘럼(docs/knowledge/dev-100-curriculum.md)의 이 줄에 노트 링크와 ✅ 를 붙이면, 이 자리는 노트 본문으로 바로 이어집니다. 노트 없이 이 페이지에 바로 적어도 됩니다 — 다만 다시 생성하면 덮어쓰이므로, 남길 글은 docs/algorithms/ 의 마크다운으로 쓰는 편이 안전합니다.

← 82. 랜덤 오라클·길이 연장 공격·도메인 분리84. 난수 생성과 CSPRNG 품질 (TAOCP 2권) →