Workspace IndexAlgorithms › Day 82

Random Oracles, Length-Extension Attacks, and Domain Separation TODO

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

Concept

The random oracle model is a proof methodology that treats a hash function as an idealized function returning a uniformly random output for every input, and proves security under that assumption; real hashes fall short of that ideal, so such proofs only give heuristic assurance. The classic gap case is the length-extension attack: for Merkle-Damgård hashes like SHA-256, the output is effectively the internal state, so an attacker who knows only H(m) and its length — not m itself — can compute H(m || padding || m'). That means using H(k || m) with a prepended secret as a MAC is forgeable; you need a two-stage construction like HMAC, or a sponge construction like Keccak/SHA-3, instead. Domain separation is the principle of tagging or prefixing hash inputs by purpose so hashes from one context can never collide with another, preventing a signature or commitment from being replayed across contexts. Simply concatenating variable-length fields creates ambiguity where different inputs produce the same byte string, so length prefixes or fixed-width encoding are needed to make parsing uniquely determined.

Code that concatenates arbitrary fields before hashing the message to be signed opens the door to replay or forgery attacks — and that's a vulnerability the application introduces, not the library.

Code & Formula

# 랜덤 오라클·길이 연장 공격·도메인 분리 — 장난감 Merkle-Damgard 해시로 실제 forge 를 수행해
# naive H(key||msg) MAC이 위조 가능함을 검증하고, HMAC은 같은 구조로 위조되지 않음을 대조한다.
# (교육용 토이 해시 — 진짜 SHA-256이 아니며 프로덕션 MAC은 반드시 hmac 모듈을 쓸 것)

import hmac
import hashlib

BLOCK = 16  # bytes

def compress(state, block):  # 토이 압축 함수 — 진짜 암호학적 강도는 없음, 구조 시연용
    x = int.from_bytes(block[:8], "big") ^ int.from_bytes(block[8:], "big")
    state = (state ^ x) & 0xFFFFFFFF
    return ((state * 2654435761 + 0x9E3779B9) ^ (state >> 15)) & 0xFFFFFFFF

def pad(total_len_bytes, tail):
    tail = tail + b"\x80"
    while (total_len_bytes + len(tail)) % BLOCK != 8 % BLOCK:
        tail += b"\x00"
    return tail + (total_len_bytes * 8).to_bytes(8, "big")

def toy_hash(data, state=0x6A09E667):
    padded = data + pad(len(data), b"")
    for i in range(0, len(padded), BLOCK):
        state = compress(state, padded[i : i + BLOCK])
    return state

KEY = b"super-secret-16b"          # 공격자는 값을 모르지만 길이(16)는 안다고 가정 — 흔한 실전 조건
msg = b"amount=100&to=alice"
tag = toy_hash(KEY + msg)          # naive_mac(key, msg) — 서버가 공개하는 값

# --- 공격자: key 없이, tag 와 (key 길이 + msg) 만으로 확장 위조 ---
key_len_guess = 16
orig_len = key_len_guess + len(msg)
glue = pad(orig_len, b"")           # 원본 메시지 뒤에 실제로 붙었을 패딩을 그대로 재구성

def resume(state, processed_len, tail_data):
    tail = pad(processed_len + len(tail_data), tail_data)
    for i in range(0, len(tail), BLOCK):
        state = compress(state, tail[i : i + BLOCK])
    return state

forged_tag = resume(tag, orig_len + len(glue), b"&admin=true")
forged_message = msg + glue + b"&admin=true"   # 공격자가 서버에 제출할, key 없이 만든 메시지

genuine_tag = toy_hash(KEY + forged_message)   # 실제로 key를 아는 쪽이 계산하면 이 값이 나온다
print("forged tag == genuine tag (forged without knowing KEY):", forged_tag == genuine_tag)

# --- HMAC은 key를 안팎으로 감싸는 구조라 같은 방식의 확장이 통하지 않는다 ---
hmac_tag = hmac.new(KEY, msg, hashlib.sha256).digest()
hmac_forged = hmac.new(b"?" * 16, forged_message, hashlib.sha256).digest()  # key 없이는 흉내조차 불가
print("HMAC has no equivalent forge path (unrelated tags):", hmac_tag != hmac_forged)

# --- 도메인 분리: 같은 바이트열도 용도 태그를 접두사로 넣으면 문맥이 섞이지 않는다 ---
h_mac_ctx = hashlib.sha256(b"mac:" + msg).hexdigest()[:12]
h_sig_ctx = hashlib.sha256(b"sig:" + msg).hexdigest()[:12]
print("domain-separated hashes differ even for the same msg:", h_mac_ctx != h_sig_ctx)

Exercise

Implement an H(secret || message) MAC with SHA-256, forge it successfully with a public length-extension tool, then switch to HMAC and confirm the forgery fails.

Practical Connection

EIP-712's domain separator (chainId, contract, version) is exactly domain separation — without it, Verex's order signatures could be replayed on a different chain or a different contract.

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


한국어

랜덤 오라클·길이 연장 공격·도메인 분리 TODO

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

개념

랜덤 오라클 모델은 해시 함수를 모든 입력에 대해 균일 랜덤한 출력을 돌려주는 이상적인 함수로 가정하고 안전성을 증명하는 방법론이며, 실제 해시는 이 이상과 다르기 때문에 증명은 휴리스틱한 보증에 그친다. 그 간극의 대표 사례가 길이 연장 공격으로, Merkle-Damgård 구조 해시(SHA-256 등)의 출력은 사실상 내부 상태이므로 공격자는 원문을 몰라도 H(m)과 길이만 알면 H(m || padding || m')을 계산할 수 있다. 따라서 secret을 앞에 붙인 H(k || m)을 MAC으로 쓰면 위조가 가능하고, 대신 두 단계 구조인 HMAC이나 스펀지 구조인 Keccak/SHA-3을 써야 한다. 도메인 분리는 서로 다른 용도의 해시 입력이 절대 겹치지 않도록 용도 태그나 접두사를 넣어 한 문맥의 서명·커밋먼트가 다른 문맥에서 재사용되지 못하게 하는 원칙이다. 길이 가변 필드를 그냥 이어 붙이면 다른 입력이 같은 바이트열이 되는 모호성이 생기므로, 길이 접두사나 고정 폭 인코딩으로 파싱이 유일하게 결정되도록 해야 한다.

서명 대상 메시지를 임의로 이어 붙여 해시하는 코드는 재사용 공격이나 위조로 이어지며, 이는 라이브러리가 아니라 애플리케이션이 만드는 취약점이다.

코드 · 수식

# 랜덤 오라클·길이 연장 공격·도메인 분리 — 장난감 Merkle-Damgard 해시로 실제 forge 를 수행해
# naive H(key||msg) MAC이 위조 가능함을 검증하고, HMAC은 같은 구조로 위조되지 않음을 대조한다.
# (교육용 토이 해시 — 진짜 SHA-256이 아니며 프로덕션 MAC은 반드시 hmac 모듈을 쓸 것)

import hmac
import hashlib

BLOCK = 16  # bytes

def compress(state, block):  # 토이 압축 함수 — 진짜 암호학적 강도는 없음, 구조 시연용
    x = int.from_bytes(block[:8], "big") ^ int.from_bytes(block[8:], "big")
    state = (state ^ x) & 0xFFFFFFFF
    return ((state * 2654435761 + 0x9E3779B9) ^ (state >> 15)) & 0xFFFFFFFF

def pad(total_len_bytes, tail):
    tail = tail + b"\x80"
    while (total_len_bytes + len(tail)) % BLOCK != 8 % BLOCK:
        tail += b"\x00"
    return tail + (total_len_bytes * 8).to_bytes(8, "big")

def toy_hash(data, state=0x6A09E667):
    padded = data + pad(len(data), b"")
    for i in range(0, len(padded), BLOCK):
        state = compress(state, padded[i : i + BLOCK])
    return state

KEY = b"super-secret-16b"          # 공격자는 값을 모르지만 길이(16)는 안다고 가정 — 흔한 실전 조건
msg = b"amount=100&to=alice"
tag = toy_hash(KEY + msg)          # naive_mac(key, msg) — 서버가 공개하는 값

# --- 공격자: key 없이, tag 와 (key 길이 + msg) 만으로 확장 위조 ---
key_len_guess = 16
orig_len = key_len_guess + len(msg)
glue = pad(orig_len, b"")           # 원본 메시지 뒤에 실제로 붙었을 패딩을 그대로 재구성

def resume(state, processed_len, tail_data):
    tail = pad(processed_len + len(tail_data), tail_data)
    for i in range(0, len(tail), BLOCK):
        state = compress(state, tail[i : i + BLOCK])
    return state

forged_tag = resume(tag, orig_len + len(glue), b"&admin=true")
forged_message = msg + glue + b"&admin=true"   # 공격자가 서버에 제출할, key 없이 만든 메시지

genuine_tag = toy_hash(KEY + forged_message)   # 실제로 key를 아는 쪽이 계산하면 이 값이 나온다
print("forged tag == genuine tag (forged without knowing KEY):", forged_tag == genuine_tag)

# --- HMAC은 key를 안팎으로 감싸는 구조라 같은 방식의 확장이 통하지 않는다 ---
hmac_tag = hmac.new(KEY, msg, hashlib.sha256).digest()
hmac_forged = hmac.new(b"?" * 16, forged_message, hashlib.sha256).digest()  # key 없이는 흉내조차 불가
print("HMAC has no equivalent forge path (unrelated tags):", hmac_tag != hmac_forged)

# --- 도메인 분리: 같은 바이트열도 용도 태그를 접두사로 넣으면 문맥이 섞이지 않는다 ---
h_mac_ctx = hashlib.sha256(b"mac:" + msg).hexdigest()[:12]
h_sig_ctx = hashlib.sha256(b"sig:" + msg).hexdigest()[:12]
print("domain-separated hashes differ even for the same msg:", h_mac_ctx != h_sig_ctx)

연습

SHA-256으로 H(secret || message) MAC을 구현한 뒤 공개된 길이 연장 도구로 위조에 성공시키고, 같은 것을 HMAC으로 바꿔 실패하는지 확인해 볼 것.

실무 · Verex 연결

EIP-712의 도메인 구분자(chainId, contract, version)가 바로 도메인 분리이며, 이게 없으면 Verex의 주문 서명이 다른 체인이나 다른 컨트랙트에서 재생될 수 있다.

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

← 81. [복습] 데이터 모델이 성능을 정한다83. HMAC·AEAD와 nonce 오용 저항 →