다운로드 페이지 옆에 붙은 SHA256: 2cf24dba… 같은 긴 문자열, DB에 저장된 비밀번호 칸의 알 수 없는 글자, 웹훅 요청 헤더의 X-Hub-Signature-256. 전부 해시인데, 막상 "MD5랑 SHA-256은 뭐가 다르고, 해시는 암호화랑 뭐가 다르냐"고 물으면 설명이 꼬이더라고요. 결론부터 말하면 해시는 어떤 데이터든 고정 길이 지문으로 바꾸는 단방향 함수라서 원래 값으로 되돌릴 수 없고, 암호화는 키만 있으면 되돌릴 수 있는 양방향 변환이에요. 그리고 MD5와 SHA-1은 서로 다른 두 입력이 같은 지문을 갖는 "충돌"이 실제로 만들어져서 보안 용도로는 끝났고, 지금 기본값은 SHA-256입니다.
이 글에서는 해시·암호화·인코딩의 차이, MD5·SHA-1·SHA-256의 길이와 안전성 비교, 다운로드한 파일의 체크섬을 운영체제별로 확인하는 명령어, MD5와 SHA-1이 깨진 실제 사례, 비밀번호 저장에 SHA-256이 아니라 bcrypt·Argon2를 써야 하는 이유, 그리고 API 서명에 쓰는 HMAC까지 순서대로 정리할게요. 문자열을 바로 해시해 보고 싶다면 해시 생성기에서 알고리즘별 결과를 한 번에 볼 수 있어요.
해시 함수란? 암호화·인코딩과의 차이
해시 함수는 길이가 제각각인 입력을 받아 항상 같은 길이의 값을 내놓는 함수예요. "hello" 다섯 글자를 넣어도, 4GB짜리 ISO 파일을 넣어도 SHA-256 결과는 똑같이 256비트(16진수 64자)예요. 실제로 계산해 보면 이렇습니다.
| 입력 | SHA-256 결과 (16진수 64자) |
|---|---|
hello | 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 |
Hello | 185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969 |
hello. | 1589999b0ca6ef8814283026a9f166d51c70a910671c3d44049755f07f2eb910 |
| (빈 문자열) | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
hello 와 Hello 는 첫 글자 대소문자 하나 차이인데, 결과 256비트 중 125비트가 달라요. 거의 절반이 뒤집힌 거죠. 이걸 눈사태 효과(avalanche effect)라고 하고, 좋은 해시의 기본 조건이에요. 결과만 보고 "입력이 비슷했겠구나"를 짐작할 수 없어야 하니까요. 빈 문자열의 해시 e3b0c442… 는 로그에서 자주 보이니 기억해 두면 좋아요.
암호용 해시 함수는 세 가지 성질을 가져야 해요.
- 역상 저항성: 해시값만 보고 원래 입력을 찾을 수 없어야 해요.
- 제2 역상 저항성: 특정 입력과 같은 해시를 갖는 다른 입력을 찾을 수 없어야 해요.
- 충돌 저항성: 해시가 같은 아무 두 입력 쌍이라도 찾을 수 없어야 해요. MD5와 SHA-1이 무너진 게 바로 이 성질이에요.
해시 vs 암호화 vs 인코딩
세 가지가 자주 섞여 쓰이는데 목적이 완전히 달라요.
| 구분 | 되돌리기 | 키 필요 | 목적 | 예 |
|---|---|---|---|---|
| 인코딩 | 누구나 가능 | 없음 | 형식 변환 | Base64, URL 인코딩 |
| 암호화 | 키 있으면 가능 | 있음 | 비밀 유지 | AES, RSA |
| 해시 | 불가능 | 없음(HMAC은 있음) | 무결성·식별 | SHA-256, MD5 |
"비밀번호를 MD5로 암호화했다"는 말은 엄밀히 틀린 표현이에요. 해시는 복호화가 없어요. 대신 같은 입력이면 같은 값이 나오니까, 사람들이 자주 쓰는 비밀번호를 미리 해시해 둔 표(레인보우 테이블)와 대조하면 원문이 드러나요. 되돌리는 게 아니라 맞혀 보는 거죠. Base64를 암호화로 착각하는 실수는 Base64는 암호화가 아니다 글에서 따로 다뤘어요.
MD5 SHA-1 SHA-256 차이 한눈에 보기
세 알고리즘의 가장 큰 차이는 출력 길이와 지금도 안전한가예요. 출력이 길수록 우연히 겹칠 확률이 낮아지는데, 생일 문제 때문에 n비트 해시의 충돌은 대략 2^(n/2)번 시도하면 찾을 수 있어요. 그래서 설계상 충돌 저항 강도는 출력 길이의 절반이에요.
| 알고리즘 | 발표 | 출력 | 16진수 길이 | 설계상 충돌 강도 | 현재 상태 |
|---|---|---|---|---|---|
| MD5 | 1992 (RFC 1321) | 128비트 | 32자 | 2⁶⁴ | 충돌 생성이 일반 PC로 수 초. 보안 용도 금지 |
| SHA-1 | 1995 (FIPS 180-1) | 160비트 | 40자 | 2⁸⁰ | 2017년 실제 충돌 공개. NIST는 2030년까지 퇴출 예정 |
| SHA-256 | 2001 (SHA-2) | 256비트 | 64자 | 2¹²⁸ | 알려진 실용 공격 없음. 현재 표준 |
| SHA-512 | 2001 (SHA-2) | 512비트 | 128자 | 2²⁵⁶ | 안전. 64비트 CPU에서 SHA-256보다 빠를 때도 있음 |
| SHA3-256 | 2015 (FIPS 202) | 256비트 | 64자 | 2¹²⁸ | 안전. SHA-2와 내부 구조가 완전히 다름 |
길이로 알고리즘을 짐작하는 건 디버깅할 때 꽤 유용해요. 40자는 Git 커밋 해시 길이이기도 하고, SHA3-256도 64자라 길이만으로 확정할 순 없어요. 참고로 SHA-2는 SHA-224·256·384·512를 묶은 가족 이름이고 숫자는 출력 비트 수예요.
파일 체크섬 확인하는 법 (Windows·macOS·리눅스)
리눅스 ISO나 개발 도구를 받을 때 배포 페이지에 함께 적힌 SHA-256 값을 체크섬이라고 해요. 내가 받은 파일을 직접 해시해서 그 값과 똑같으면, 다운로드 도중 깨지지 않았고 중간에 바뀌지 않았다는 뜻이에요.
# Windows (명령 프롬프트)
certutil -hashfile ubuntu.iso SHA256
# Windows (PowerShell) — 기본 알고리즘이 SHA256
Get-FileHash .\ubuntu.iso
# macOS
shasum -a 256 ubuntu.iso
# 리눅스
sha256sum ubuntu.iso
# 배포처가 준 SHA256SUMS 파일로 한 번에 대조 (리눅스)
sha256sum -c SHA256SUMS --ignore-missing
확인할 때 몇 가지를 조심하세요.
- 알고리즘을 맞추세요. 배포처가 SHA-256을 줬는데 MD5로 계산하면 당연히 달라요. 길이부터 비교해 보면 바로 알 수 있어요.
- 대소문자는 상관없어요.
certutil은 소문자,Get-FileHash는 대문자로 출력하지만 같은 값이에요. - 눈으로 앞 몇 자만 보지 마세요. 비교는 전체 64자를 해야 해요. PowerShell에서는
(Get-FileHash .\파일).Hash -eq "배포처값"처럼 비교식을 쓰면 True/False로 바로 나와요. - 같은 서버의 체크섬은 한계가 있어요. 서버가 해킹당해 파일과 체크섬을 함께 바꿔 놓으면 체크섬은 일치해요. 그래서 우분투 같은 배포처는
SHA256SUMS파일에 GPG 서명(SHA256SUMS.gpg)을 따로 붙여요. 체크섬은 "전송 중 손상"을, 서명은 "배포자가 맞는지"를 확인하는 거예요.
왜 MD5와 SHA-1은 깨졌다고 할까
"깨졌다"는 말은 해시값에서 원래 비밀번호를 복원할 수 있게 됐다는 뜻이 아니에요. 같은 해시값을 갖는 서로 다른 두 파일을 일부러 만들 수 있게 됐다는 뜻이에요. 그게 왜 문제냐면, 전자 서명과 인증서는 문서 전체가 아니라 문서의 해시에 서명하거든요. 해시가 같은 두 문서를 만들 수 있으면, 무해한 문서에 받은 서명을 악성 문서에 그대로 옮겨 붙일 수 있어요.
MD5: 2004년 충돌, 2008년 가짜 인증서
- 2004년 중국 왕샤오윈(Wang Xiaoyun) 연구팀이 MD5의 실제 충돌 쌍을 발표했어요. 이후 기법이 발전해 지금은 일반 노트북으로 몇 초 만에 충돌 쌍을 만들 수 있어요.
- 2008년 연구팀이 PlayStation 3 약 200대로 MD5 충돌을 계산해, 실제 인증기관이 서명한 것처럼 보이는 가짜 CA 인증서를 만드는 데 성공했어요. 이 발표 이후 인증서에서 MD5가 빠르게 퇴출됐어요.
- 2012년 플레임(Flame) 악성코드는 MD5 충돌을 이용해 마이크로소프트 코드 서명 인증서를 위조했어요. 학계의 이론이 실제 공격에 쓰인 대표 사례예요.
SHA-1: 2017년 SHAttered
2017년 2월 구글과 네덜란드 CWI 연구소가 SHAttered 공격을 발표했어요. 내용은 다르지만 SHA-1 해시가 완전히 같은 PDF 파일 두 개를 실제로 만들어 공개한 거예요. 계산량은 SHA-1 연산 약 9.2×10¹⁸번(약 2⁶³)으로, 연구팀 설명으로는 CPU 6,500년치와 GPU 110년치에 해당했어요. 설계상 2⁸⁰번이 필요하다던 것보다 10만 배 이상 적은 양이었죠.
2020년에는 "SHA-1 is a Shambles" 연구가 선택 접두 충돌로 PGP 키 위조까지 시연했어요. 브라우저들은 2017년 무렵 SHA-1 인증서 신뢰를 끊었고, NIST는 2022년에 SHA-1을 2030년 말까지 퇴출하겠다고 발표했어요.
그럼 Git은 왜 아직 SHA-1을 쓸까
Git 커밋 ID는 40자 SHA-1이에요. Git은 SHAttered 직후 충돌 공격 패턴을 감지하는 "hardened SHA-1"으로 바꿨고, 저장소를 SHA-256으로 만드는 옵션(git init --object-format=sha256)도 실험 기능으로 들어가 있어요. 다만 기존 생태계와의 호환 문제로 기본값은 아직 SHA-1이에요.
비밀번호 저장에 SHA-256을 쓰면 안 되는 이유
여기서 많이들 헷갈려요. SHA-256은 안전한 해시인데 왜 비밀번호에는 쓰면 안 될까요? 이유는 너무 빨라서예요. 해시는 원래 빠를수록 좋은 함수인데, 비밀번호에서는 그게 공격자에게 유리해요. DB가 유출되면 공격자는 후보 비밀번호를 하나씩 해시해서 대조하는데, 1초에 몇 번을 시도할 수 있느냐가 곧 뚫리는 속도거든요.
비밀번호 전용 해시는 일부러 느리고, 메모리를 많이 쓰게 만들어서 이 속도를 떨어뜨려요.
| 방식 | 특징 | 권장 설정 예 (OWASP 기준) |
|---|---|---|
| Argon2id | 2015년 비밀번호 해싱 대회 우승. 메모리를 많이 써서 GPU 병렬 공격에 강함 | 메모리 19MiB, 반복 2, 병렬 1 |
| bcrypt | 1999년 발표. 가장 널리 지원됨. 입력 72바이트까지만 사용 | cost 10 이상 |
| scrypt | 메모리 비용을 조절 가능 | N=2¹⁷, r=8, p=1 |
| PBKDF2 | FIPS 인증이 필요한 환경용 | HMAC-SHA256으로 60만 회 반복 |
이 함수들은 솔트(salt)도 자동으로 붙여 줘요. 솔트는 사용자마다 다른 랜덤 값이라, 같은 비밀번호 1234 를 쓴 두 사람도 저장되는 해시가 달라져요. 그래서 레인보우 테이블이 통하지 않고, 공격자는 사용자 한 명씩 따로 계산해야 해요. bcrypt 결과 $2b$12$… 에는 알고리즘, cost(12), 솔트, 해시가 한 문자열에 다 들어 있어서 따로 솔트 칸을 만들 필요도 없어요.
비밀번호 길이가 왜 중요한지, 몇 자리면 무차별 대입에 몇 년이 걸리는지는 비밀번호 엔트로피와 무차별 대입 시간 글에서 계산해 두었어요.
HMAC, 키가 들어간 해시로 위변조 막기
웹훅이나 API 요청이 정말 상대방이 보낸 건지 확인하려면 해시만으로는 부족해요. 요청을 가로챈 사람도 본문을 고친 뒤 SHA-256을 다시 계산해서 붙이면 그만이니까요. 그래서 서로만 아는 비밀 키를 섞어 해시하는 HMAC을 써요.
import hmac, hashlib
secret = b"secret"
body = b"hello"
sig = hmac.new(secret, body, hashlib.sha256).hexdigest()
# 88aab3ede8d3adf94d26ab90d3bafd4a2083070c3bcce9c014ee04a443847c0b
# 검증할 땐 == 대신 상수 시간 비교
hmac.compare_digest(sig, received_sig)
GitHub 웹훅의 X-Hub-Signature-256 헤더, 결제 대행사의 콜백 서명, JWT의 HS256 서명이 모두 HMAC-SHA256이에요. JWT 서명이 어떻게 만들어지는지는 JWT 구조와 서명 원리 글에 자세히 있어요.
HMAC을 직접 흉내 내서 SHA256(키 + 메시지) 로 만들면 안 돼요. SHA-256 같은 구조의 해시는 길이 확장 공격에 약해서, 키를 몰라도 원래 해시값과 메시지 길이만 알면 뒤에 내용을 덧붙인 메시지의 올바른 해시를 계산할 수 있거든요. HMAC은 키를 안쪽·바깥쪽으로 두 번 섞는 구조(RFC 2104)라서 이 문제가 없어요. 그리고 서명을 비교할 땐 일반 == 대신 hmac.compare_digest 같은 상수 시간 비교를 써야 앞 글자부터 맞혀 나가는 타이밍 공격을 피할 수 있어요.
상황별로 어떤 해시를 쓸까
| 상황 | 추천 | 피할 것 |
|---|---|---|
| 다운로드 파일 무결성 | SHA-256 (+ GPG 서명) | MD5, SHA-1 |
| 비밀번호 저장 | Argon2id, bcrypt | SHA-256, MD5 (솔트를 붙여도) |
| API·웹훅 서명 | HMAC-SHA256 | SHA256(키+메시지) |
| 캐시 키·중복 제거 (공격자 없음) | MD5, SHA-1, xxHash도 가능 | — |
| 전자 서명·인증서 | SHA-256 이상 | SHA-1 |
| 랜덤 토큰 생성 | 암호학적 난수(crypto) | 시각이나 ID를 해시한 값 |
마지막 줄도 자주 하는 실수예요. md5(user_id + 현재시각) 으로 만든 재설정 토큰은 입력을 추측해 똑같이 계산할 수 있어요. 토큰은 secrets.token_urlsafe() 같은 난수로 만드세요.
자주 묻는 질문
해시값으로 원래 문자열을 복호화할 수 있나요?
수학적으로 되돌리는 방법은 없어요. 다만 "1234", "password"처럼 흔한 입력은 미리 계산해 둔 표와 대조하면 금방 찾아져요. 온라인 "MD5 복호화" 사이트가 하는 일이 바로 이 대조예요. 그래서 짧거나 흔한 값을 해시한 결과는 사실상 공개된 것과 같아요.
MD5와 SHA-256 중 무엇이 더 빠른가요?
일반적으로 MD5가 더 빨라요. GPU 벤치마크에서도 MD5가 SHA-256보다 7배쯤 빠르게 나와요. 하지만 파일 체크섬 정도면 둘 다 디스크 읽기 속도가 병목이라 체감 차이가 거의 없으니, 보안이 걸린 곳에선 SHA-256을 쓰세요.
같은 파일인데 체크섬이 다르게 나와요.
먼저 알고리즘이 같은지(길이로 확인), 다운로드가 끝까지 됐는지(파일 크기 비교) 보세요. 텍스트 파일이라면 줄바꿈(CRLF와 LF)이나 BOM 차이로 바이트가 달라졌을 수 있어요. 그래도 다르면 다른 미러에서 다시 받고, 계속 다르면 그 파일은 쓰지 않는 게 안전해요.
비밀번호를 SHA-256으로 두 번, 천 번 해시하면 안전해지나요?
조금 느려지긴 하지만 직접 만든 방식은 솔트·반복 횟수·비교에서 실수가 나기 쉬워요. 이미 검증된 Argon2id나 bcrypt 라이브러리를 쓰는 게 정답이에요. 반복 방식이 꼭 필요하면 표준인 PBKDF2를 쓰세요.
한글 문자열을 해시하면 왜 사이트마다 결과가 다른가요?
해시는 글자가 아니라 바이트를 계산하기 때문이에요. 같은 "안녕"도 UTF-8과 EUC-KR은 바이트가 달라서 해시가 달라져요. UTF-8 기준 "안녕"의 SHA-256은 e8f817f3…으로 시작해요. 앞뒤 공백이나 줄바꿈이 섞여도 결과가 바뀌니 입력부터 확인하세요.
정리
해시는 되돌릴 수 없는 고정 길이 지문이고, 암호화는 키로 되돌리는 잠금, 인코딩은 누구나 되돌리는 형식 변환이에요. MD5(32자)와 SHA-1(40자)은 각각 2004년과 2017년 SHAttered로 실제 충돌이 만들어져 서명·인증서·위변조 검사에는 쓰면 안 되고, 지금 기본은 SHA-256(64자)이에요. 다운로드 파일은 certutil·shasum -a 256·sha256sum 으로 전체 값을 대조하세요.
비밀번호는 빠른 해시 대신 솔트가 자동으로 붙는 Argon2id·bcrypt로, API 서명은 HMAC-SHA256으로 만들면 돼요. 문자열의 MD5·SHA-1·SHA-256 값을 바로 비교해 보려면 해시 생성기를, 강한 비밀번호가 필요하면 비밀번호 생성기를 써 보세요. 해시값을 2진수·16진수로 뜯어보고 싶다면 2진수 16진수 변환 원리 글도 같이 보면 좋아요.