9장에서 CRC32라는 체크섬을 배웠습니다. 파일 비트가 우연히 깨졌는지 감지하는 빠른 도구였죠. 파트 5에서 우리가 다룰 SHA-256은 같은 "지문"처럼 보이지만, 목적이 완전히 다릅니다. CRC는 실수를 잡는 도구고, 암호학적 해시는 악의도 잡는 도구입니다. 그 차이가 무결성 보장의 기초가 됩니다.
SHA-256은 어떤 입력이든 256비트(32바이트)의 "디지털 지문"으로 바꿉니다 — 같은 입력은 항상 같은 지문, 지문에서 원본을 복원할 수 없고, 단 한 바이트만 달라도 지문이 완전히 달라집니다.
CRC32는 뭘 잡고, 뭘 못 잡나
9장 레코드 프레이밍에서 우리는 프레임 헤더에 crc32(payload)를 넣었습니다. 레코드를 읽을 때 CRC를 다시 계산해서 헤더 값과 비교하면, 디스크가 비트를 뒤집거나 크래시로 파일이 잘린 걸 탐지할 수 있었죠.
그런데 CRC에는 큰 한계가 있습니다. 의도적으로 조작한 경우를 막지 못합니다. 공격자가 레코드를 고쳤다면, 고친 내용에 맞는 CRC도 같이 고치면 그만입니다. CRC는 고작 32비트짜리 숫자라 계산이 단순하고, 원하는 내용에 맞는 CRC를 역산하는 것도 어렵지 않습니다.
| CRC32 | SHA-256 (암호학적 해시) | |
|---|---|---|
| 비트 뒤집힘(디스크 손상) | 탐지 ✅ | 탐지 ✅ |
| torn write(크래시 잔해) | 탐지 ✅ | 탐지 ✅ |
| 의도적 데이터 변조 후 체크섬 조작 | 못 막음 ❌ | 계산상 불가능 ✅ |
| 출력 크기 | 32비트 (4바이트) | 256비트 (32바이트) |
| 설계 목표 | 오류 감지(CRC = error-detection code) | 변조 저항(collision resistance) |
이 차이가 핵심입니다. CRC는 채널 잡음을 잡는 도구이고, 암호학적 해시는 악의적 변조까지 잡는 도구입니다.
CRC는 종이에 손가락 지문을 찍는 것과 같습니다 — 누군가 종이를 찢고 새 종이를 갖다 붙이면, 새 지문도 같이 찍을 수 있으니 속을 수 있죠. SHA-256은 DNA 지문과 같습니다 — 지문 자체를 위조해서 다른 내용에 붙이는 게 현실적으로 불가능합니다.
암호학적 해시 함수의 세 가지 성질
SHA-256이 변조 탐지에 쓸 수 있는 이유는 세 가지 성질 때문입니다.
정리하면:
- 일방향성 — SHA-256(data) 값을 알아도 data를 역산할 수 없습니다. 해시를 공개해도 원본이 노출되지 않습니다.
- 충돌 저항성 — 같은 해시 값을 내는 두 가지 다른 입력을 찾는 게 현실적으로 불가능합니다. "위조 서류에 진짜 지문을 붙이는" 행위가 불가능하다는 뜻입니다.
- 눈사태 효과(avalanche effect) — 입력이 1비트만 달라져도 출력의 절반 이상 비트가 바뀝니다. "조금만 고쳤는데 해시가 약간만 바뀐다"가 아니라, 완전히 다른 해시가 나옵니다.
충돌 저항성과 눈사태 효과가 합쳐지면 이런 보장이 됩니다: "데이터가 바뀌면 해시가 바뀐다. 해시가 같다면 데이터도 같다." 이 보장이 머클 트리(20장)의 기둥입니다 — 루트 해시 32바이트 하나로 수백만 건의 로그가 한 글자도 안 바뀌었음을 보장할 수 있습니다.
Quipu-Log에서 SHA-256을 쓰는 방법
소스를 보면 사용법이 아주 간결합니다. crypto.rs에 딱 두 줄짜리 공용 함수가 있습니다:
crates/quipu-core/src/crypto.rspub fn sha256_hex(data: &[u8]) -> String {
hex(&Sha256::digest(data))
}
// sha2 크레이트의 Sha256::digest 한 번에 32바이트 해시를 계산하고,
// hex()로 소문자 16진수 문자열로 변환합니다.
머클 트리 쪽에서는 [u8; 32] 배열 타입(Hash)을 직접 씁니다. 16진수 문자열보다 저장 효율이 두 배 좋으니까요:
crates/quipu-core/src/merkle.rs/// SHA-256(0x00 || record) — 리프 해시
pub fn leaf_hash(record: &[u8]) -> Hash {
let mut h = Sha256::new();
h.update([LEAF_PREFIX]); // 0x00: 리프임을 표시
h.update(record);
h.finalize().into()
}
여기서 LEAF_PREFIX = 0x00을 앞에 붙이는 이유가 흥미롭습니다. 20장에서 자세히 다루겠지만, 미리 한 마디만 — 리프와 내부 노드를 같은 함수로 해시하면 "두 리프의 연결이 우연히 내부 노드 값과 같아지는" 혼동 공격(second-preimage attack)이 가능합니다. 앞에 다른 접두사를 붙이면 그 혼동이 사라집니다. 이게 RFC 6962의 핵심 규칙이기도 합니다.
DB에서는 페이지 체크섬으로 CRC32(또는 FNV)를 씁니다 — 디스크 오류·torn page 탐지가 목적이고, 공격자가 체크섬을 위조하는 건 고려하지 않습니다. Quipu-Log에서는 감사 로그의 위협이 "악의적 내부자"이므로, 레코드 내용→머클 트리→SHA-256으로 이어지는 체계가 필요합니다. 체크섬 목적 = CRC, 변조 탐지 목적 = 암호학적 해시.
SHA-256의 빠른 성능 특성
혹시 "암호학적이라 느리지 않나요?"라고 걱정할 수 있습니다. 현대 CPU는 SHA-256 하드웨어 가속 명령(SHA-NI, ARMv8 Crypto Extensions)을 내장하고 있어, 초당 수백~수천 MB를 처리합니다. 감사 로그 한 건(수 킬로바이트)을 해시하는 데 마이크로초도 안 걸립니다. Quipu-Log가 실제 성능 병목을 만나는 곳은 해시가 아니라 fsync(11장 내구성)입니다.
정리
- CRC32는 우연한 오류 감지 도구. 의도적 변조는 못 막습니다.
- SHA-256은 암호학적 해시 — 일방향·충돌 저항·눈사태. 의도적 변조도 탐지합니다.
- Quipu-Log에서
sha256_hex()는 필드 보호(24장), 머클 트리(20장)에 두루 씁니다. - 리프 해시에
0x00접두사를 붙이는 건 혼동 공격 방지를 위한 RFC 6962 규칙입니다.
① CRC32와 SHA-256이 둘 다 체크섬이라면, 왜 Quipu-Log는 레코드 프레임(9장)에는 CRC32를, 머클 트리(20장)에는 SHA-256을 따로 씁니까? 역할의 차이를 설명해 보세요.
② 눈사태 효과가 없다면(입력 1비트 변화가 출력 1비트만 바꾼다면) 머클 트리의 "루트 32바이트로 전체 보장" 주장이 무너지는 이유는 무엇인가요?
③ leaf_hash가 데이터 앞에 0x00을 붙이는 이유를 자신의 말로 설명해 보세요.