MD5
고친 사람 github-actions[bot]
MD5 는 데이터를 짧은 값 하나로 줄여 그 데이터의 지문처럼 씁니다. 데이터가 같으면 값도 같습니다. 한 바이트만 달라도 값은 전혀 다르게 나옵니다. 그래서 내려받은 파일이 오는 도중 망가졌는지 가려내는 데 오래 쓰였습니다. 지금은 값이 같은 가짜를 마음대로 만들어 낼 수 있어서 속임을 막아야 하는 곳에서는 물러났습니다.
쉽고 빠른 이해
MD5 는 무엇을 넣어도 32글자짜리 16진수 값 하나를 내놓습니다. 내려받은 설치 파일이 배포처에 적힌 것과 같은 파일인지 볼 때 이 값을 견줍니다.
이게 없으면 두 데이터가 같은지 보려고 처음부터 끝까지 맞대 봐야 합니다. 짧은 값으로 줄여 두면 값끼리만 견주면 됩니다.
어떻게 도나:
- 데이터 끝에 정해진 규칙대로 채움 값을 붙여 길이를 맞춥니다
- 그것을 같은 크기 조각으로 쪼개고, 조각을 하나씩 섞어 내부 값을 계속 바꿉니다
- 조각을 다 먹고 남은 내부 값이 그 데이터의 MD5 값입니다
대가는 값이 같은 다른 데이터를 못 막는다는 것입니다. 나올 수 있는 값의 가짓수가 정해져 있어서입니다. MD5 는 그런 쌍을 일부러 만드는 방법까지 흔해져서, 서명처럼 속임을 막는 곳에서는 후보가 아닙니다.
상세
MD5 는 Message-Digest Algorithm 5 의 줄임말입니다. 우리말로 옮기면 메시지 요약 알고리즘 5 입니다. 무엇을 넣어도 언제나 128비트짜리 값 하나를 내놓습니다.
이런 함수를 해시 함수라고 부릅니다. 해시 함수는 길이가 제각각인 데이터를 짧은 값 하나로 줄입니다. 그 값을 해시값 또는 다이제스트라고 합니다.
그중에서도 속이려는 사람이 있다고 보고 만든 것이 암호학적 해시 함수입니다. MD5 도 그렇게 만들어진 함수입니다.
입력과 출력
넣는 데이터의 크기는 따지지 않습니다. 한 글자짜리 문자열도 받고 몇 기가바이트짜리 파일도 받습니다. 나오는 값은 어느 쪽이든 128비트, 곧 16바이트입니다.
16바이트를 16진수로 적으면 한 바이트가 두 글자로 적히므로 32글자 문자열이 됩니다. 파일 배포 페이지에 파일과 나란히 붙어 있는 32자리 16진수가 이 값입니다.
파이썬 표준 라이브러리 hashlib 의 md5 로 값을 꺼내 봅니다. 같은 입력을 16진수 문자열과 바이트 두 꼴로 뽑고, 큰 파일도 한 번 넣어 봅니다.
md5(b"hello").hexdigest() # 32글자
md5(b"hello").digest() # 16바이트
md5(big_file).hexdigest() # 역시 32글자
세 줄이 보여 주는 것은 하나입니다. 입력이 커져도 출력 길이는 안 변합니다. 되돌아가는 길은 없습니다. 해시값에서 원래 데이터를 계산해 내는 방법이 없습니다.
출력 길이는 함수마다 다릅니다. 이름 뒤에 붙은 숫자가 그 길이를 가리키는 경우가 많습니다.
| 함수 | 출력 비트 | 16진수 자릿수 |
|---|---|---|
| MD5 | 128 | 32 |
| SHA-1(Secure Hash Algorithm 1) | 160 | 40 |
| SHA-256(Secure Hash Algorithm 256) | 256 | 64 |
셋 중 MD5 가 제일 짧습니다. 짧으면 적어 두기도 견주기도 편합니다. 대신 나올 수 있는 값의 가짓수도 그만큼 적습니다. 이 차이가 안전성을 가르는 바탕입니다.
어떻게 도나
MD5 는 데이터를 한꺼번에 계산하지 않습니다. 512비트씩 쪼개서 한 조각씩 먹습니다.
먼저 길이를 맞춥니다. 데이터 끝에 정해진 채움 값과 원래 길이를 붙여 전체가 512비트의 배수가 되게 만듭니다. 이 작업을 패딩이라고 합니다.
block-beta columns 3 a["원본 메시지"] b["채움 값"] c["원래 길이"] d["512비트의 배수"]:3
원래 길이는 맨 뒤에 붙습니다. 이렇게 채우고 나면 전체 길이가 512비트로 딱 떨어집니다.
그다음 내부 값을 준비합니다. 128비트짜리 값 하나를 정해진 상수로 놓고 시작합니다. 이 처음 값을 시작값이라고 합니다.
조각을 하나 읽을 때마다 이 값을 섞어 새 값으로 바꿉니다. 조각을 다 처리한 뒤 남은 내부 값이 그 데이터의 MD5 값입니다.
flowchart TD
IN["원본 + 채움 값"] --> S1["첫 조각 · 512비트"]
IN --> S2["다음 조각 · 512비트"]
IV["시작값 · 128비트"] --> MIX1
S1 --> MIX1["첫 조각으로 내부 값을 바꾼다"]
MIX1 --> MIX2["다음 조각으로 또 바꾼다"]
S2 --> MIX2
MIX2 --> OUT["마지막 내부 값 · MD5 값"]
그림에서 볼 것은 내부 값 하나가 끝까지 이어진다는 점입니다. 조각마다 새로 시작하지 않고 앞 조각이 남긴 값 위에 다음 조각을 얹습니다. 이렇게 조각을 차례로 먹으며 내부 값을 갱신하는 얼개를 머클-담고르 구조라고 부릅니다.
조각 하나를 섞는 계산은 크기와 무관하게 늘 같은 양입니다. 그래서 전체 계산량은 데이터 크기에 정비례합니다.
섞는 계산에 바라는 성질이 눈사태 효과입니다. 입력을 한 비트만 바꿔도 출력 비트가 절반쯤 달라져야 합니다. 비슷한 입력이 비슷한 출력을 내면 출력을 보고 입력을 짐작할 실마리가 생깁니다.
왜 더는 안전하다고 보지 않나
출력 길이가 정해져 있으니 서로 다른 데이터가 같은 값을 갖는 일은 반드시 생깁니다. 이렇게 겹치는 쌍을 해시 충돌이라고 합니다.
암호학적 해시 함수에 바라는 것은 충돌이 없는 것이 아닙니다. 충돌하는 쌍을 계산으로 찾아내기가 현실적으로 불가능한 것입니다. 이 요구를 충돌 저항성이라고 부릅니다.
찾기 어려움에는 출력 길이로 정해지는 상한이 있습니다. 생일 문제가 그 상한을 알려 줍니다. 값의 가짓수가 N 이면 아무 데이터나 N 의 제곱근쯤 모아도 겹치는 쌍이 나옵니다.
출력이 128비트면 값의 가짓수는 2의 128제곱입니다. 그 제곱근은 2의 64제곱입니다. 그만큼 해시해 보면 겹치는 쌍이 나올 가능성이 커집니다.
MD5 의 문제는 그 상한보다 훨씬 적은 계산으로 충돌을 만드는 방법이 밝혀졌다는 것입니다. 계산으로는 못 찾는다는 전제가 깨진 셈입니다.
지금은 앞부분을 원하는 대로 정해 놓을 수도 있습니다. 뒤쪽만 맞춰 같은 값을 내는 두 파일을 보통 컴퓨터로 만들어 냅니다.
flowchart TD
subgraph DA["문서 A"]
A1["원하는 앞부분"] --> A2["맞춘 뒤쪽"]
end
subgraph DB["문서 B"]
B1["원하는 앞부분"] --> B2["맞춘 뒤쪽"]
end
A2 --> SAME["같은 값 · 128비트"]
B2 --> SAME
그래서 전자 서명이나 인증서처럼 속임을 막는 것이 목적인 곳에서는 MD5 를 쓰지 않습니다. 새로 고르는 경우에는 출력이 더 긴 SHA-256 을 씁니다.
MD5 가 아직 남아 있는 쓰임
내려받은 파일이 오는 도중 깨졌는지 보는 체크섬 용도가 첫째입니다. 데이터가 오는 길에 바뀌지 않았는지 확인하는 일을 무결성 확인이라고 합니다.
전송 오류나 디스크 손상은 값을 맞춰 가며 생기는 것이 아니라 아무렇게나 생깁니다. 그런 손상은 MD5 로도 걸러집니다.
flowchart TD
subgraph SEND["보내는 쪽"]
S0["원본 파일"] -->|MD5 계산| S9["적어 둔 값"]
end
subgraph RECV["받는 쪽"]
R0["받은 파일"] -->|MD5 계산| R9["다시 계산한 값"]
end
S9 --> CMP["두 값을 견준다"]
R9 --> CMP
CMP --> OK["같다 · 성한 파일"]
CMP --> NG["다르다 · 깨졌다"]
Amazon S3(Simple Storage Service, 단순 저장 서비스) 같은 객체 저장소도 비슷하게 씁니다. 올리는 쪽이 계산한 MD5 값을 함께 받아 저장소가 다시 계산해 맞춰 봅니다. 그 값을 ETag 같은 메타데이터에 적어 두어 나중에 같은 데이터인지 견주게 합니다.
값이 고르게 흩어진다는 성질만 쓰는 곳도 있습니다. nginx 는 캐시 파일 이름을 캐시 키의 MD5 값으로 짓습니다. 이름은 겹치지 않고 길이도 일정해야 합니다. 이 쓰임에서 해시값이 하는 일은 무결성 확인이 아니라 이름 짓기입니다.
이 쓰임들의 공통점은 값을 맞춘 가짜를 만들어 밀어 넣을 상대가 없다는 것입니다. 그런 상대가 있는 곳이라면 이야기가 달라집니다. 새로 짜는 코드라면 굳이 MD5 를 고를 이유가 없습니다.
비밀번호 저장에 쓰면 안 되는 까닭
비밀번호를 MD5 로 바꿔 저장하는 코드가 아직 남아 있습니다. 충돌 이야기 이전에 한 번 계산하는 데 드는 일이 너무 적다는 것이 문제입니다. 공격자도 후보를 그만큼 많이 대입해 볼 수 있습니다.
MD5 값을 미리 잔뜩 계산해 표로 만들어 두고 거꾸로 찾는 레인보우 테이블도 널리 돌아다닙니다. 해시값을 계산으로 되돌리는 것이 아니라 미리 계산해 둔 답을 조회하는 것입니다. 흔한 비밀번호라면 표를 뒤지는 것만으로 원래 값이 나옵니다.
이런 곳에는 일부러 계산이 오래 걸리게 만든 비밀번호 해싱 함수를 씁니다. bcrypt 가 그런 함수입니다.
그리고 사용자마다 다른 임의 값을 함께 섞습니다. 그 값을 솔트라고 합니다. 솔트가 사용자마다 다르면 미리 만들어 둔 표가 통하지 않습니다.
쓰임별 판단 기준
쓰임별로 모으면 이렇습니다.
| 쓰임 | MD5 로 괜찮나 |
|---|---|
| 전자 서명 · 인증서 | 안 됩니다 |
| 비밀번호 저장 | 안 됩니다. 어느 해시 함수든 그냥 쓰면 안 됩니다 |
| 공격자가 파일을 바꿔치기할 수 있는 곳의 무결성 확인 | 안 됩니다 |
| 전송 중 손상 확인 | 실수로 생긴 손상에는 쓸 수 있습니다 |
| 캐시 키 · 중복 파일 찾기 | 쓸 수 있습니다 |
| 새로 고르는 경우 | SHA-256 이나 그보다 새 함수를 고릅니다 |
가르는 기준은 하나입니다. 값을 맞춘 가짜를 누가 일부러 만들 수 있는 상황인가입니다. 그런 상황이면 MD5 는 후보가 아닙니다.
관련 항목
MD5 가 속하는 상위 분류
해시 함수 · 암호학적 해시 함수 · 해시 · 다이제스트 · 체크섬
MD5 를 대신하는 해시 함수
SHA-256 · SHA-1 · SHA-2 · SHA-3 · BLAKE2 · BLAKE3
MD5 가 지키지 못하게 된 성질
충돌 저항성 · 역상 저항성 · 제2 역상 저항성 · 눈사태 효과 · 생일 문제 · 해시 충돌
MD5 를 이루는 계산 얼개
머클-담고르 구조 · 압축 함수 · 패딩 · 비트 연산 · 초기화 벡터
MD5 값을 무결성 확인에 쓰는 제품과 헤더
무결성 · Amazon S3 · ETag · 메타데이터 · Content-MD5 · nginx · 캐시 키
MD5 가 물러난 보안 용도
전자 서명 · 인증서 · 인증 기관 · TLS · HMAC · 코드 서명
비밀번호 저장에 MD5 대신 쓰는 함수
비밀번호 해싱 · 솔트 · bcrypt · Argon2 · PBKDF2 · 레인보우 테이블
MD5 값을 계산하는 명령과 라이브러리
md5sum · openssl · hashlib · 체크섬 파일
다른 이름: Message-Digest Algorithm 5 · 메시지 다이제스트 알고리즘 5