📁 File1. 전송 프로토콜06. Integrity & Checksum — 받은 파일이 보낸 파일과 같다는 증명

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)용도
CRC3232~10⁻⁹~10에러 검출 (Ethernet, ZIP)
CRC32C32~10⁻⁹~20 (HW 가속)iSCSI, S3 새 옵션
Adler-3232~10⁻⁹~5rsync 약한 체크섬
MD5128깨짐 (충돌 가능)~0.5레거시. 보안 ✗
SHA-1160깨짐 (2017 SHAttered)~0.3레거시
SHA-256256안전~0.5 (HW ~2)권장 표준
SHA-3-256256안전~0.4NIST 미래 표준
BLAKE3256안전~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
 
# 일치하면 OK

What — 무결성 검증 함정 표

함정증상실제 원인
같은 파일 두 번 올렸는데 ETag 다름dedup 실패multipart는 청크 경계가 다르면 ETag 다름
ETag로 SHA256 계산했다고 가정보안 검증 실패ETag는 MD5(또는 그 hash) — 암호학적 안전 아님
MD5로 무결성 보장한다고 광고보안 감사 failMD5는 충돌 가능 — 악의적 변조엔 ✗
Content-MD5와 헤더값 인코딩 다름400 BadDigestbase64 + 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, 후자엔 ✗.