[PonsWarp 개념 교실 6] 무결성 3총사 — CRC32 vs SHA-256 vs Merkle Tree
쉽게 말하면, 무결성 확인에는 "빠르지만 약한 CRC32", "강하지만 무거운 SHA-256", "부분 검증이 가능한 Merkle 트리"라는 세 가지 층위가 있고, PonsWarp는 대부분의 전송 경로에서 CRC 필드를 0으로 비워두는 대신 SCTP와 AES-GCM 인증에 무결성을 맡긴다.

이상하게 들릴 수 있다. 헤더에 CRC 필드가 있는데 왜 0으로 두는 걸까? 이 글에서 그 역설을 푼다.
이게 왜 필요한가 — "무결성"은 한 가지가 아니다
데이터가 네트워크를 건너는 동안 두 가지 일이 일어날 수 있다.
- 우연한 손상: 노이즈, 패킷 드롭, 전송 실수 — 대부분 1~2비트
- 의도적 변조: 공격자가 내용을 바꿔치기
이 둘을 막는 방법은 다르다. 우연한 손상은 감지(detect)만으로 충분하지만, 의도적 변조는 인증(authenticate)이 필요하다. 공격자도 손상된 데이터를 "고쳐서" 보낼 수 있기 때문이다.
무결성 도구들은 이 두 요구에 대해 다른 강도로 답한다.
CRC32 — 빠르지만 "우연한 손상" 전용
CRC32(Cyclic Redundancy Check)는 데이터를 다항식 나눗셈으로 요약한 32비트 값이다.
rust// pons-core-wasm/src/crc32.rs pub fn calculate_crc32(data: &[u8]) -> u32 { // 반사 다항식 0xEDB88320 기반 테이블 연산 // init 0xFFFFFFFF → XOR → 0xFFFFFFFF }
- 장점: 테이블 기반이라 매우 빠르다. 하드웨어/소프트웨어 모두 싸다.
- 한계: 우연한 손상 감지용이다. CRC는 선형적이라, 공격자가 손상 위치를 알고 데이터를 조작하면 같은 CRC 값을 가진 위조 데이터를 만들 수 있다. 의도적 변조를 막지 못한다.
즉 CRC32는 "빠른 위생 점검"이지 "보안"이 아니다.
SHA-256 — 강하지만 "전체 파일" 전용
SHA-256은 입력 전체를 256비트 다이제스트로 압축하는 암호학적 해시다. 역상을 찾는 것이 계산적으로 불가능해서, 의도적 변조에도 강하다.
rust// pons-core-wasm/src/sha256_stream.rs pub struct Sha256Stream { ... } pub fn update(&mut self, data: &[u8]) -> Result<(), JsValue> { ... } // 조각 단위로 먹임 pub fn finalize(&mut self) -> Result<Vec<u8>, JsValue> { ... } // 최종 다이제스트
- 장점: 강하다. 키가 없어도 "변조됐는지"는 알 수 있다.
- 한계: 전체를 다 읽어야 최종 값이 나온다. 스트리밍 중간에 "지금까지 받은 조각이 맞는지"는 확인할 수 없다. 그리고 키가 없으면 공격자도 같은 해시를 다시 만들 수 있으므로, 변조 인증은 여전히 키가 있는 HMAC/AEAD가 담당한다.
Merkle 트리 — 부분 검증이 가능한 해시
Merkle 트리는 데이터 조각들을 리프로 두고, 두 개씩 해시를 결합해 루트까지 올라가는 트리다.
root = H(H(h1,h2) ‖ H(h3,h4))
/ \
H(h1,h2) H(h3,h4)
/ \ / \
h1 h2 h3 h4
[조각1] [조각2] [조각3] [조각4]
핵심 가치: 루트 하나만 알면, 어떤 조각 하나가 정말 그 파일의 일부인지 로그(증명 경로)로 검증할 수 있다. 조각 3이 맞는지 보려면 루트 + H(h1,h2)와 h4만 있으면 된다.
rust// pons-core-wasm/src/merkle_tree.rs pub fn add_chunk(&mut self, data: &[u8]) { ... } // 리프 축적 pub fn finalize(&mut self) { ... } // 홀수 노드 복제 후 상향 결합 pub fn get_proof(&self, index: usize) -> Vec<u8> { ... } // 증명 경로 추출 pub fn verify_proof(root, leaf_data, index, proof) -> bool { ... }
그런데 PonsWarp는 왜 CRC 필드를 0으로 두나
여기서 이 글의 핵심 역설이 나온다. PonsWarp의 평문 패킷 헤더는 22바이트인데, 마지막 4바이트가 CRC32 필드다. 그런데 실제 전송 경로에서는 이 필드를 0으로 채워 보낸다.
typescript// PonsWarp/src/utils/plainPacket.ts view.setUint32(14, payload.byteLength, true); view.setUint32(18, 0, true); // ← CRC 필드: 0 고정
왜일까? 두 가지 이유가 코드에 드러나 있다.
첫째, SCTP가 이미 신뢰성 있는 전송을 보장한다. WebRTC DataChannel은 SCTP 위에서 동작하는데, ordered 채널은 패킷 손실·중복·순서 뒤집힘을 하위 계층에서 처리한다. 우연한 손상의 대부분은 SCTP 체크섬이 이미 잡는다. 애플리케이션 레벨 CRC는 중복이다.
둘째, 진짜 보안 무결성은 AES-GCM 태그가 담당한다. 암호화된 경로에서는 16바이트 인증 태그가 "변조 감지 + 인증"을 완전히 제공한다. 평문 CRC를 계산하는 것은 CPU 낭비일 뿐이고, 암호문을 바꾼 공격자는 태그 검증에서 걸러진다.
즉 "CRC 필드 = 0"은 게으름이 아니라, 무결성 책임을 SCTP(전송)와 AEAD(보안)로 위임한 설계 결정이다. CRC32가 필요한 곳은 레거시 평문 경로(암호화 없는 폴백) 정도다.
그럼 Merkle은 어디에 쓰이나
정직하게 말하면, Merkle 트리와 파일 레벨 SHA-256은 WASM 코어에 구현·export되어 있지만, 실제 전송 경로에서 호출되는 증거는 찾지 못했다. 이건 이 프로젝트의 흥미로운 지점이다. 구현은 했지만(무결성 도구를 갖춰 두고), 전송 설계가 "SCTP + AEAD"로 완결되어서 사용되지 않았다.
이 현상은 다음에 다룰 "구현했지만 쓰지 않은 것들" 편에서 FEC·LZ4와 함께 더 깊게 본다. 지금은 **"무결성 도구가 있다고 해서 반드시 써야 하는 것은 아니다"**는 점만 짚고 넘어간다.
PonsWarp 적용 지점
| 항목 | 값 | 증거 |
|---|---|---|
| CRC32 구현 | 반사 다항식 0xEDB88320, 테이블 연산 | crc32.rs:11, 24-30 |
| CRC32 스트리밍 | Crc32Hasher (조각 단위 update/finalize) | crc32.rs:33-58 |
| 패킷 CRC 필드 | 22B 헤더 [18..22]를 0 고정 | plainPacket.ts:39 |
| 무결성 위임 | SCTP(전송) + AES-GCM 태그(보안) | plainPacket.ts:39, bulk-encrypt.worker.ts:111-117 |
| SHA-256 스트리밍 | Sha256Stream update/finalize | sha256_stream.rs:47-51 |
| Merkle 트리 | add_chunk/finalize/get_proof/verify_proof | merkle_tree.rs:135-233 |
| Merkle 루트 계산 | compute_merkle_root(chunks, chunk_size) | merkle_tree.rs:281-289 |
지금 돌아보는 판단
세 가지를 정리한다.
첫째, 무결성 도구는 "필요한 강도"를 골라 쓰는 것이다. CRC32는 우연한 손상용, SHA-256은 변조 감지용, Merkle은 부분 검증용, AEAD 태그는 변조 인증용. 상황에 맞는 층위가 다르다.
둘째, "겹겹이 쌓을수록 안전하다"는 공식이 아니다. PonsWarp가 CRC 필드를 0으로 둔 결정은, 하위 계층(SCTP)과 상위 계층(AEAD)이 이미 무결성을 담당하므로 중복 계산을 빼는 정당한 판단이다. 모든 곳에 모든 도구를 쓰면 CPU만 낭비한다.
셋째, 구현 ≠ 사용이다. Merkle과 파일 SHA-256이 구현되어 있지만 전송 경로에서 안 쓰이는 것은, 설계가 이미 다른 층위로 완결되었기 때문이다. 재구축할 때 "구현했다면 써야지"라고 생각할 필요가 없다. 검증 요구(위협 모델)가 먼저고, 도구는 그다음이다.
한 문장 정리
CRC32(빠른 손상 감지), SHA-256(강한 변조 감지), Merkle(부분 검증)은 무결성의 세 층위이지만, PonsWarp는 SCTP와 AES-GCM 태그가 그 역할을 이미 하므로 전송 패킷의 CRC 필드를 0으로 비워두고, 구현해 둔 Merkle/SHA-256은 전송 경로에 쓰지 않는다.
관련 개념: [개념 3 — AES-256-GCM] (태그가 무결성 담당) · [개념 5 — HMAC과 상수시간 비교] (인증의 키 의존성)
다음 글: [개념 7 — 22바이트 패킷 헤더: 한 조각이 자신을 설명하는 법]