06. Integrity & Checksum — 받은 파일이 보낸 파일과 같다는 증명
5GB 영상을 50개 파트로 올렸다. 마지막 ETag는
9b5c2dc...-50이다. 같은 파일을 다시 올리니 ETag가 다르다. 왜? 멱등 dedup이 깨졌다고 비명을 지르기 전에 multipart의 ETag는 MD5가 아니다라는 사실부터 알아야 한다.
한 줄 답
무결성 검증은 “같은 함수에 같은 입력 → 같은 출력” 을 보장하는 것이다. 단일 PUT은 MD5/SHA256으로 충분하지만, S3 multipart의 ETag는 MD5의 MD5 이라 동일 파일도 청크 크기가 다르면 ETag가 다르다. 이 함정을 모르고 dedup하면 망한다.
Why — 왜 무결성이 필요한가
깨질 수 있는 지점
- 디스크: bit rot, SSD wear-out (장기 보관 시 ~1년에 0.001% 비트 플립)
- 메모리: ECC 없으면 cosmic ray로 비트 플립
- TCP: 16비트 체크섬 → 65,536분의 1 충돌 (대규모 트래픽에선 매일 발생)
- CDN/프록시: 잘못된 트랜스코딩, gzip 재인코딩 사고
- 악의적 변조: MITM, 백도어 주입
무결성 검증은 두 가지 다른 일이다
| 목적 | 함수 | 예시 |
|---|---|---|
| 에러 검출 | CRC32, Adler-32 | 노이즈 검출만 |
| 암호학적 무결성 | MD5(deprecated), SHA-256 | 악의적 변조 검출 |
→ S3는 둘 다 지원. 작은 파일은 CRC32C, 보안 중요하면 SHA-256.
How — 체크섬 함수 비교
알고리즘 표
| 함수 | 출력 비트 | 충돌 저항 | 속도 (GB/s) | 용도 |
|---|---|---|---|---|
| CRC32 | 32 | ~10⁻⁹ | ~10 | 에러 검출 (Ethernet, ZIP) |
| CRC32C | 32 | ~10⁻⁹ | ~20 (HW 가속) | iSCSI, S3 새 옵션 |
| Adler-32 | 32 | ~10⁻⁹ | ~5 | rsync 약한 체크섬 |
| MD5 | 128 | 깨짐 (충돌 가능) | ~0.5 | 레거시. 보안 ✗ |
| SHA-1 | 160 | 깨짐 (2017 SHAttered) | ~0.3 | 레거시 |
| SHA-256 | 256 | 안전 | ~0.5 (HW ~2) | 권장 표준 |
| SHA-3-256 | 256 | 안전 | ~0.4 | NIST 미래 표준 |
| BLAKE3 | 256 | 안전 | ~6 (병렬) | 신흥 표준 |
핵심: MD5는 충돌 가능하지만, 우연한 비트 플립 검출에는 여전히 충분. 그래서 S3는 데이터 무결성에는 MD5를, 보안에는 SHA-256을 쓴다 — 의미가 다르다.
S3의 ETag — 단일 vs Multipart
단일 객체 (PutObject)
ETag: "9b5c2dc8a4c8d3a2..." ← MD5 그대로→ 32자 hex. 같은 파일은 항상 같은 ETag. dedup 키로 OK.
Multipart 객체 (CompleteMultipartUpload)
ETag: "9b5c2dc8a4c8d3a2-50" ← MD5(MD5(part1) + MD5(part2) + ... + MD5(part50)) + "-50"- 각 파트의 MD5를 바이너리로 이어붙여서 다시 MD5 — 그것을 hex로 표현
-50은 파트 수- 같은 파일이라도 청크 크기가 다르면 ETag가 다름
- 같은 청크 크기여도 경계가 어디 잡히느냐에 따라 다름
# 동일 파일, 다른 청크 크기 → 다른 ETag
file = open('video.mp4', 'rb').read() # 5GB
# 100MB 청크 → ETag-A-50
# 50MB 청크 → ETag-B-100
# 200MB 청크 → ETag-C-25→ 이걸 모르고 ETag로 dedup하면: 같은 파일이 5번 업로드되면 5번 모두 다른 객체로 인식, 비용 5배.
Content-MD5 헤더 (업로드 무결성)
PUT /key HTTP/1.1
Content-MD5: nFwtyKTI06KiX4eGfNZGQA== ← base64 인코딩된 MD5
<바이너리>S3가 수신한 바디의 MD5를 직접 계산해서 비교. 불일치면 400 BadDigest 거부.
S3의 새 체크섬 옵션 (2022)
new PutObjectCommand({
Bucket, Key, Body,
ChecksumAlgorithm: 'SHA256', // CRC32 | CRC32C | SHA1 | SHA256
});- 클라이언트가 SHA256 계산
- S3가 검증 + 영구 저장 (ETag와 별개로)
- HeadObject 응답에
x-amz-checksum-sha256헤더로 노출 - multipart에서도 작동 — 각 파트 SHA256 + 전체 합산 SHA256
→ 새 시스템은 ETag 대신 SHA256 체크섬 헤더를 쓰는 게 더 안전.
What — 실전 흐름
단일 업로드 + Content-MD5
# 1) 로컬에서 MD5
md5sum video.mp4
# 9b5c2dc8a4c8d3a2 video.mp4
# 2) base64로
openssl dgst -md5 -binary video.mp4 | base64
# nFwtyKTI06KiX4eG...
# 3) presigned + Content-MD5
curl -X PUT \
-H 'Content-MD5: nFwtyKTI06KiX4eG...' \
--upload-file video.mp4 \
'https://bucket.s3.amazonaws.com/key?X-Amz-...'
# 응답 헤더:
# ETag: "9b5c2dc8a4c8d3a2..."Multipart + 파트별 SHA256
// 클라이언트 (브라우저)
async function sha256Hex(blob: Blob): Promise<string> {
const buf = await blob.arrayBuffer();
const hash = await crypto.subtle.digest('SHA-256', buf);
return [...new Uint8Array(hash)].map(b => b.toString(16).padStart(2, '0')).join('');
}
// 각 파트 업로드 시 헤더에
fetch(presignedUrl, {
method: 'PUT',
body: chunk,
headers: {
'x-amz-checksum-sha256': base64(sha256(chunk)),
},
});Complete 시 전체 검증
new CompleteMultipartUploadCommand({
Bucket, Key, UploadId,
MultipartUpload: {
Parts: parts.map(p => ({
PartNumber: p.n,
ETag: p.etag,
ChecksumSHA256: p.sha256, // ← 검증용
})),
},
});S3는 각 파트의 SHA256을 검증한 뒤, 전체 객체의 합산 SHA256도 저장한다.
다운로드 후 검증
# S3에 저장된 SHA256 가져오기
aws s3api head-object --bucket B --key K --checksum-mode ENABLED
# {
# "ContentLength": 5242880000,
# "ChecksumSHA256": "abc123..."
# }
# 다운로드 후 재계산
sha256sum downloaded.mp4
# abc123... downloaded.mp4
# 일치하면 OKWhat — 무결성 검증 함정 표
| 함정 | 증상 | 실제 원인 |
|---|---|---|
| 같은 파일 두 번 올렸는데 ETag 다름 | dedup 실패 | multipart는 청크 경계가 다르면 ETag 다름 |
| ETag로 SHA256 계산했다고 가정 | 보안 검증 실패 | ETag는 MD5(또는 그 hash) — 암호학적 안전 아님 |
| MD5로 무결성 보장한다고 광고 | 보안 감사 fail | MD5는 충돌 가능 — 악의적 변조엔 ✗ |
| Content-MD5와 헤더값 인코딩 다름 | 400 BadDigest | base64 + binary가 정답, hex 아님 |
BSD md5와 GNU md5sum 출력 형식 다름 | 파싱 깨짐 | BSD는 MD5 (file) = hash, GNU는 hash file |
| Multipart 후 동일성 검증을 ETag로 | 작은 파일은 OK, 큰 파일만 실패 | 단일 PUT vs Multipart의 ETag 알고리즘 불일치 |
| CDN이 gzip으로 재인코딩 | SHA256 다름 | Content-Encoding이 바뀌면 바이트도 바뀜 |
What-if — 깊은 함정 (실전)
시나리오 1 — Lambda가 동일 파일을 두 번 받음
SQS at-least-once 보장 → 같은 메시지가 2회 도착할 수 있음. 처리 멱등성을 S3 ETag로 판단하는 코드:
// ❌ 위험
const etag1 = await getETag(s3Key);
process(s3Key);
const etag2 = await getETag(s3Key);
if (etag1 === etag2) { /* 멱등 OK */ }- 단일 PUT 객체면 OK
- 그런데 사용자가 multipart로 올렸다면 ETag가 청크 경계에 의존 → 같은 내용이라도 다른 ETag 가능
- 더 큰 문제: 사용자가 재시도 하면서 다른 청크 크기로 올림 → ETag 변경 → “내용은 같은데 멱등 깨졌다고 판단”
→ 해결: SHA256 checksum 옵션을 쓴다 (S3 2022). 또는 DB의 fileId를 멱등 키로.
시나리오 2 — CDN이 응답을 변형
CloudFront에 Compress Objects Automatically 설정이 켜져 있으면 원본 mp4를 Brotli/gzip으로 재인코딩해서 보낼 수 있다 (텍스트 컨텐츠에만 적용 — mp4는 기본 제외).
JSON, JS, CSS 같은 컨텐츠는 바이트가 바뀌므로 SHA256 검증이 깨진다.
→ 해결: 무결성 검증은 압축 해제 후 또는 서버가 보낸 압축 그대로에 대해 한다. SRI(Subresource Integrity) 헤더는 원본 바이트에 대한 해시.
시나리오 3 — 큰 파일을 메모리에 다 올리고 SHA256 계산
// ❌ 5GB 파일을 메모리에
const buf = await readFile('video.mp4'); // OOM 가능
const hash = sha256(buf);→ 해결: 스트리밍 해시
import { createHash } from 'crypto';
import { createReadStream } from 'fs';
const hash = createHash('sha256');
createReadStream('video.mp4')
.on('data', chunk => hash.update(chunk))
.on('end', () => console.log(hash.digest('hex')));브라우저: crypto.subtle.digest는 스트리밍 미지원이라 청크 단위 따로 계산 + 합산 또는 WebAssembly의 BLAKE3 사용.
Insight — 흥미로운 이야기
“S3 ETag가 MD5인 이유는 1990년대 캐시 표준에서”
ETag는 RFC 2616(1999)이 정한 캐시 검증용 식별자다. “임의의 짧은 문자열”이면 됐고, S3 초기 (2006) 엔지니어가 MD5를 그대로 박은 게 표준이 됐다. Multipart는 2010년에 추가되면서 같은 MD5의 MD5라는 임시변통 결정을 내렸고, 그 임시 결정이 15년 동안 모든 S3 사용자를 헷갈리게 했다. 2022년 SHA256 옵션 추가는 이 부채를 갚으려는 시도.
“BLAKE3는 SHA-256의 4배 속도, 그런데 왜 표준이 안 됐을까”
2020년 발표된 BLAKE3는 트리 구조 해시로 완벽한 병렬화가 가능하다. SHA-256보다 4~10배 빠르고, 부분 검증도 된다 (메르클 트리). 하지만 NIST 표준화 절차를 따르지 않아 FIPS 140-2 컴플라이언스가 안 잡힌다 → 정부/금융 시스템에서 못 씀 → 클라우드 표준에 못 들어감. “기술이 아니라 인증이 표준을 만든다”의 한 사례.
“git은 SHA-1을 쓴다, 그런데 왜 안 깨졌나”
2017년 Google이 SHA-1 충돌(SHAttered)을 시연했지만 git은 여전히 SHA-1. 이유는 git의 SHA-1은 보안용이 아니라 객체 식별용이기 때문. 같은 내용 헤더 + 데이터에 대해 같은 hash가 나오면 충분 — 충돌은 천문학적 비용이 들어 실용적이지 않다. git은 SHA-256으로 마이그레이션 중이지만, 그 이유는 보안이 아니라 미래 대비.
요약 + Mermaid
무결성 검증은 함수의 결정성에 의존한다. S3 ETag는 단일 PUT엔 MD5, Multipart엔 MD5의 MD5 — 청크 경계에 의존하므로 dedup 키로 부적합. 새 시스템은 S3의 SHA256 ChecksumAlgorithm 옵션을 쓰는 게 표준. Content-MD5는 업로드 시 무결성 증명, SHA256 + 스트리밍 해시는 다운로드 후 검증. “무결성”과 “보안”은 다른 단어다 — MD5는 전자엔 OK, 후자엔 ✗.