사전 MD5
알고리즘

MD5

gabury1고친 사람 github-actions[bot]

MD5 는 어떤 데이터든 짧은 값 하나로 줄여 그 데이터를 알아보는 지문으로 쓰게 해 줍니다. 같은 데이터를 넣으면 언제나 같은 값이 나옵니다. 줄인 값에서 원래 데이터를 되살리는 길은 없습니다. 지금은 값이 같은 서로 다른 데이터를 일부러 만들 수 있어서 위조를 막아야 하는 곳에서는 물러났습니다.

쉽고 빠른 이해

MD5 는 무엇을 넣든 길이가 같은 짧은 값 하나를 돌려줍니다. 16진수로 적으면 32글자입니다. 내려받은 파일로 이 값을 다시 계산합니다. 그 값을 배포하는 쪽이 적어 둔 값과 견주면 파일이 성한지 압니다.

이게 없으면 받은 파일이 성한지 보려고 원본을 다시 받아 맞대 봐야 합니다. 짧은 값으로 줄여 두면 그 값끼리만 견주면 됩니다. 잠그고 푸는 열쇠가 없으니 암호화와는 다른 계산입니다.

어떻게 도나:

  1. 데이터 끝에 채움 비트와 원래 길이를 붙여 길이를 맞춥니다
  2. 같은 크기 조각으로 나눠 한 조각씩 섞으며 내부 값을 바꿉니다
  3. 마지막에 남은 내부 값이 결과입니다

짧게 줄이는 대가로 서로 다른 데이터가 같은 값을 가질 수 있습니다. MD5 에서는 그런 쌍을 일부러 만드는 방법까지 나왔습니다. 그래서 서명이나 인증서처럼 위조를 막는 곳에는 못 씁니다. 계산이 빨라서 비밀번호를 지키는 데도 맞지 않습니다.

상세

MD5 는 Message-Digest Algorithm 5 의 줄임말입니다. 길이가 제각각인 입력을 받아 언제나 128비트짜리 값 하나를 내놓는 계산 절차입니다. 이 값을 입력의 지문(fingerprint) 또는 메시지 다이제스트(message digest)라고 부릅니다.

입력 길이에는 제한이 없습니다. 빈 입력도 받습니다. 8비트 단위로 떨어지지 않는 비트열도 받습니다.

값 세 개를 보면 감이 옵니다.

입력 MD5 값
빈 문자열 d41d8cd98f00b204e9800998ecf8427e
a 0cc175b9c0f1b6a831c399e269772661
abc 900150983cd24fb0d6963f7d28e17f72

a 와 abc 는 앞 글자가 같습니다. 그런데 두 값에는 닮은 데가 없습니다. 입력이 빈 문자열이어도 값의 길이는 셋 다 같습니다.

32글자 16진수

128비트는 16바이트입니다. 한 바이트를 16진수 두 글자로 적으면 32글자가 됩니다. 위 표의 값이 그렇게 적은 것입니다.

이렇게 적는 것은 관례이지 MD5 가 정한 규칙이 아닙니다. 같은 16바이트를 Base64 로 적는 쓰임도 있습니다. Base64 는 바이트를 영문자·숫자·기호 64가지로 적는 방식입니다.

해시 함수

아무 길이의 입력을 정해진 길이의 값으로 줄이는 계산을 해시 함수라고 부릅니다. 그 결과를 해시값 또는 다이제스트라고 합니다. MD5 는 해시 함수 가운데 위조하려는 상대가 있다고 보고 만든 암호학적 해시 함수입니다.

암호화가 아닌 까닭

「MD5 로 암호화한다」는 말을 흔히 씁니다. 그러나 MD5 는 암호화가 아닙니다. 암호화는 열쇠로 데이터를 잠급니다. 열쇠를 가진 쪽은 잠긴 데이터에서 원래 데이터를 되돌려 받습니다.

MD5 에는 열쇠가 없습니다. 값에서 입력으로 되돌아가는 계산도 정의돼 있지 않습니다.

되돌릴 수 없는 까닭은 길이에 있습니다. 몇 기가바이트짜리 입력도 16바이트로 줄어듭니다. 그 사이에 정보가 버려집니다. 같은 값을 내는 입력도 무수히 많아서 값만 보고는 어느 입력이었는지 가릴 수 없습니다.

MD5 가 처음 맡은 일은 전자 서명의 앞 단계였습니다. 전자 서명은 공개 키 암호의 개인 키로 하는 연산입니다. 암호화와 같은 공개 키 암호를 쓰지만 데이터를 잠가 두는 일은 아닙니다. 큰 파일에 서명하려면 그 개인 키 연산을 해야 합니다. 큰 파일을 통으로 넣는 대신 MD5 로 먼저 짧게 줄입니다. 개인 키 연산은 그 짧은 값에 합니다. 줄이는 일과 서명하는 일은 서로 다른 단계입니다.

flowchart TD
    subgraph RED["줄이는 일 · MD5"]
        F0["큰 파일"] --> D0["짧은 값 · 128비트"]
    end
    subgraph SIG["서명하는 일 · 개인 키 연산"]
        D1["짧은 값"] --> S0["전자 서명"]
    end
    D0 --> D1

만들어진 내력

MD5 는 1992년 Ronald Rivest 가 MD4(Message-Digest Algorithm 4)를 고쳐 내놓았습니다. MD4 에는 검토가 충분히 쌓이기 전에 너무 서둘러 퍼지고 있는지도 모른다는 우려가 있었습니다. 유난히 빠르게 만든 탓에 암호 분석 공격에 뚫릴 위험의 가장자리에 있다는 것이었습니다.

MD5 는 속도를 조금 내주고 끝내 안전할 가능성을 크게 높이는 쪽을 골랐습니다. 그래서 MD4 보다 조금 느립니다. 그래도 32비트 기계에서 빠르게 돌도록 설계했습니다. 큰 치환표가 필요 없어서 짧은 코드로 구현할 수 있습니다.

블록과 내부 상태

MD5 는 입력을 한꺼번에 계산하지 않습니다. 길이를 맞춘 입력을 512비트 블록으로 나눠 앞에서부터 하나씩 처리합니다.

처리하는 동안 128비트짜리 내부 상태 하나를 들고 다닙니다. 이 상태는 32비트 워드 네 개로 나뉩니다. 각각을 A·B·C·D 라고 부릅니다.

블록 하나와 지금 상태를 받아 새 상태를 내는 계산을 압축 함수라고 부릅니다. 512비트 블록과 128비트 상태가 들어가 128비트가 나오니 크기가 줄어든다는 뜻의 이름입니다. 마지막 블록까지 처리하고 남은 상태가 MD5 값입니다.

복잡도

MD5 값을 한 번 계산하는 비용은 입력 길이에 비례합니다. 메모리는 입력 크기와 무관하게 작습니다. 반면 값이 같은 두 입력을 찾는 공격 비용은 설계가 기대한 것보다 훨씬 낮다고 밝혀졌습니다.

이 절은 n 비트짜리 입력 하나를 놓고 두 비용을 잽니다. 하나는 값을 계산하는 비용입니다. 다른 하나는 값을 맞춘 입력을 찾아내는 비용입니다.

계산하는 비용

항목 값 까닭
시간 O(n) 블록 하나마다 연산 64번을 합니다. 블록 수는 입력 길이에 비례합니다
추가 공간 O(1) 128비트 상태와 블록 하나, 블록 시작 전에 적어 둔 상태 하나면 됩니다
입력에 따른 갈림 없음 연산의 순서와 횟수가 입력 내용과 무관합니다. 평균과 가장 오래 걸리는 경우가 같습니다

길이를 맞추려고 덧붙이는 양에는 상한이 있습니다. 채움 비트는 많아야 512비트입니다. 원래 길이는 64비트입니다. 입력이 아무리 커져도 덧붙는 양은 576비트를 넘지 않습니다.

블록 하나를 처리하고 나면 다음 블록에 넘길 것은 128비트 상태뿐입니다. 그래서 입력 전체를 메모리에 올려 두지 않고 흘려 넣으며 계산할 수 있습니다. 원래 길이는 맨 끝에 붙이므로 입력이 끝날 때 알면 됩니다.

공격하는 비용

공격 비용은 무엇을 맞추느냐에 따라 둘로 셉니다.

  • 충돌 — 값이 같은 서로 다른 두 입력을 아무거나 한 쌍 찾는 것입니다
  • 역상 — 미리 주어진 값을 내는 입력을 하나 찾는 것입니다

출력이 128비트라 나올 수 있는 값은 2^128 가지입니다. 2^128 은 2를 128번 곱한 수입니다. 역상은 입력을 하나씩 넣어 보는 전수 탐색으로 2^128 번쯤 계산하면 찾습니다.

충돌은 그보다 훨씬 쌉니다. 생일 문제 때문입니다. 한 반에 스물세 명만 있어도 생일이 같은 둘이 있을 확률이 절반을 넘습니다. 값의 가짓수가 N 이면 입력을 N 의 제곱근쯤 모았을 때 겹치는 쌍이 나올 가능성이 커집니다. 2^128 의 제곱근은 2^64 입니다.

설계 때 추정한 비용이 바로 이 두 수였습니다. 아래 표는 그 추정과 밝혀진 공격을 나란히 둡니다. 표에는 충돌의 한 갈래인 선택 접두 충돌도 함께 있습니다. 선택 접두 충돌은 두 입력의 앞부분을 공격자가 각각 마음대로 정해 두는 공격입니다. 그 뒤에 붙일 부분만 계산해서 두 입력의 값을 맞춥니다.

찾는 것 설계 때 추정한 비용 밝혀진 공격
충돌 2^64 번쯤의 연산 2005년에 발표된 공격이 15분에서 한 시간쯤. 뒤이은 공격은 보통 노트북 PC 로 1분쯤. 2.6GHz 프로세서 한 대로 10초 이하라는 보고도 있습니다
선택 접두 충돌 따로 추정하지 않았습니다 압축 함수 호출 2^50 번쯤이 기대 비용입니다
역상 2^128 번쯤의 연산 2^123.4. 전수 탐색보다 싸지만 여전히 꽤 높습니다

예시

MD5 가 제품 안에서 맡는 일은 셋으로 갈립니다. 파일이 성한지 보는 일, 오가는 데이터를 확인하는 일, 이름을 짓는 일입니다. 아래는 제품마다 MD5 값이 어디에 찍히고 무엇과 견주는지를 봅니다.

GNU coreutils 의 md5sum

md5sum 은 파일마다 128비트 체크섬을 계산해 한 줄씩 찍습니다. 한 줄에는 다이제스트, 공백, 입력 모드 표시, 파일 이름이 차례로 놓입니다. 모드 표시는 텍스트 모드면 공백 한 칸, 바이너리 모드면 * 입니다. 빈 파일을 텍스트 모드로 넣으면 이렇게 찍힙니다.

d41d8cd98f00b204e9800998ecf8427e  <파일 이름>

--check 옵션은 이 줄들을 모아 둔 파일을 읽습니다. 적힌 파일을 하나씩 다시 계산해 줄에 적힌 값과 견줍니다. 맞으면 통과, 다르면 실패로 찍습니다. 실패가 하나라도 있으면 다 끝난 뒤 표준 오류에 경고를 냅니다.

flowchart TD
    subgraph SEND["배포하는 쪽"]
        S0["원본 파일"] -->|md5sum| S9["체크섬 줄"]
    end
    subgraph RECV["받는 쪽"]
        R0["받은 파일"] -->|md5sum --check| R9["다시 계산한 값"]
    end
    S9 -->|줄에 적힌 값| CMP["--check 가 두 값을 견준다"]
    R9 --> CMP
    CMP --> OK["같다 · 통과"]
    CMP --> NG["다르다 · 실패"]

우연한 파일 손상을 잡는 데는 MD5 가 단순한 CRC(Cyclic Redundancy Check, 순환 중복 검사)보다 믿을 만합니다. 우연히 MD5 값이 같은 두 파일이 생길 확률이 아주 작기 때문입니다.

악의적인 변조에는 안전하다고 보지 않습니다. 주어진 값을 내는 파일을 찾는 일, 곧 앞에서 본 역상은 지금으로서는 불가능하다고 봅니다. 그런데도 인증서를 비롯한 일부 파일은 고쳐도 MD5 로 한 서명이 맞아 보이게 만드는 방법이 알려져 있습니다. 무너진 성질이 역상이 아니라 충돌이기 때문입니다. 더 안전한 해시가 필요하면 cksum --algorithm 으로 sha2·sha3·blake2b 를 고릅니다. md5sum 자체는 cksum 의 옛 인터페이스로 남아 있습니다.

Amazon S3 의 Content-MD5 와 ETag

Amazon S3(Simple Storage Service)는 객체를 올리는 PutObject 요청에 Content-MD5 헤더를 받습니다. 값은 헤더를 뺀 메시지 본문의 128비트 MD5 다이제스트를 Base64 로 적은 것입니다. S3 는 받은 객체를 이 값과 대조합니다. 맞지 않으면 오류를 돌려줍니다.

목적은 네트워크를 지나는 동안 데이터가 망가지지 않았는지 확인하는 것입니다. 헤더는 선택 사항입니다. 그래도 끝에서 끝까지의 무결성 확인으로 쓰기를 권장합니다.

ETag 는 객체의 해시입니다. 그런데 ETag 가 MD5 다이제스트일 수도 있고 아닐 수도 있습니다. 객체를 어떻게 만들고 어떻게 암호화했는지가 가릅니다.

객체를 만든 방식 암호화 ETag 가 MD5 인가
PUT · POST · Copy · 관리 콘솔 없음 · S3 가 관리하는 키 예
PUT · POST · Copy · 관리 콘솔 고객이 준 키 · 키 관리 서비스의 키 아니오
멀티파트 업로드 · 파트 복사 무엇이든 아니오

관리 콘솔은 16MB 가 넘는 객체를 멀티파트 업로드로 올립니다. 그러니 그 객체의 ETag 도 MD5 가 아닙니다. 직접 계산한 MD5 를 ETag 와 견주는 확인은 ETag 가 MD5 다이제스트인 객체에서만 통합니다. S3 의 버킷 종류 가운데 디렉터리 버킷에서는 MD5 를 아예 지원하지 않습니다.

nginx 의 캐시 파일 이름

nginx 는 proxy_cache_path 로 캐시를 둘 경로를 정합니다. 캐시 데이터는 파일로 저장합니다. 캐시 파일 이름은 캐시 키에 MD5 를 적용한 결과입니다.

nginx
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=one:10m;

이 설정이면 캐시 파일이 이런 경로에 놓입니다.

/data/nginx/cache/c/29/b7f54b2df7773722d382f4809d65029c

levels 는 디렉터리를 몇 단으로 나눌지 정합니다. 1단부터 3단까지 둘 수 있습니다. 단마다 이름은 1글자나 2글자입니다. 위 설정의 1:2 는 1단 이름이 1글자, 2단 이름이 2글자라는 뜻입니다. 그래서 위 경로의 디렉터리 이름 c 와 29 는 다이제스트의 끝 글자 c 와 그 앞 두 글자 29 가 됩니다.

이 쓰임에서 MD5 가 하는 일은 무결성 확인이 아니라 이름 짓기입니다. 캐시 키가 아무리 길어도 파일 이름은 32글자로 일정합니다.

Python 의 hashlib

파이썬 표준 라이브러리 hashlib 의 md5() 로 값을 꺼냅니다.

Python
import hashlib

hashlib.md5(b"abc").hexdigest()
# '900150983cd24fb0d6963f7d28e17f72'

hashlib.md5(b"abc", usedforsecurity=False).hexdigest()
# 값은 같다. 보안 용도가 아니라고 밝힌 호출이다

hexdigest() 는 다이제스트를 두 배 길이의 16진수 문자열로 돌려줍니다. 16바이트가 32글자가 되는 것이 여기서 보입니다.

md5() 는 보통 들어 있습니다. 다만 FIPS(Federal Information Processing Standards, 미국 연방 정보 처리 표준)를 따르도록 만든 드문 빌드에서는 없거나 막혀 있을 수 있습니다.

usedforsecurity 인자는 3.9 판부터 모든 해시 생성자에 붙었습니다. False 를 주면 보안 맥락이 아닌 쓰임이라고 밝히는 것입니다. 보안과 무관하게 데이터를 한 방향으로 짧게 줄이는 쓰임이 그 예입니다. 제한된 환경에서 막혀 있던 알고리즘도 False 를 주면 쓸 수 있습니다.

HTTP 의 Content-MD5 헤더

HTTP(HyperText Transfer Protocol)에도 본문의 MD5 를 싣는 Content-MD5 헤더가 있었습니다. HTTP/1.1 의 의미를 다시 정리한 문서에서 이 헤더는 빠졌습니다.

빠진 까닭은 부분 응답에서 구현이 서로 엇갈렸기 때문입니다. 부분 응답은 클라이언트가 고른 바이트 범위만 돌려주는 응답입니다.

동작

이 절은 b 비트짜리 메시지 하나가 128비트 다이제스트가 되기까지의 다섯 단계를 봅니다. 결과부터 말하면 메시지는 512비트 블록의 열로 바뀝니다. 블록마다 연산 64번이 상태 A·B·C·D 를 바꿉니다. 마지막 블록 뒤의 상태가 출력입니다.

flowchart TD
    IV["시작 상태 A·B·C·D · 128비트"] --> C1["압축 함수"]
    M1["블록 1 · 512비트"] --> C1
    C1 --> C2["압축 함수"]
    M2["블록 2 · 512비트"] --> C2
    C2 --> CN["마지막 블록까지 되풀이"]
    CN --> OUT["마지막 상태 · 128비트 다이제스트"]

그림에서 볼 것은 상태 하나가 끝까지 이어진다는 점입니다. 블록마다 새로 시작하지 않습니다. 앞 블록이 남긴 상태 위에 다음 블록을 얹습니다. 이렇게 블록을 차례로 처리하며 상태를 넘기는 얼개를 머클-담고르 구조라고 부릅니다.

단위와 기호

워드는 32비트, 바이트는 8비트입니다. 바이트 넷을 워드 하나로 묶을 때는 하위 바이트를 먼저 놓습니다. 이 순서를 리틀 엔디언이라고 부릅니다.

기호 뜻
+ 워드끼리 더하고 2^32 로 나눈 나머지를 남깁니다
X <<< s X 를 왼쪽으로 s 비트 회전합니다. 왼쪽으로 밀려난 비트는 오른쪽으로 다시 들어옵니다
XY · X v Y · X xor Y · not(X) 비트마다 논리곱 · 논리합 · 배타적 논리합 · 뒤집기

채움 비트

메시지 끝에 비트 1 하나를 붙입니다. 그 뒤로 비트 0 을 붙입니다. 길이를 512 로 나눈 나머지가 448 이 될 때까지 붙입니다. 512의 배수에서 64비트 모자란 길이가 됩니다.

이 단계는 언제나 합니다. 이미 나머지가 448 이어도 붙입니다. 그래서 붙는 비트는 적어도 1비트, 많아야 512비트입니다.

원래 길이

채움 비트를 붙이기 전의 길이 b 를 64비트로 적어 덧붙입니다. b 가 2^64 를 넘는 드문 경우에는 아래쪽 64비트만 씁니다. 64비트는 32비트 워드 둘로 나눠 붙입니다. 하위 워드가 먼저입니다.

block-beta
columns 3
  a["원본 메시지 · b비트"] b["채움 비트 · 1 하나와 0들"] c["원래 길이 · 64비트"]
  d["전체 · 512비트의 배수"]:3

이제 전체 길이가 512비트로 딱 떨어집니다. 워드로 세면 16의 배수입니다.

시작 상태

상태 A·B·C·D 를 정해진 값으로 놓습니다. 아래는 각 워드를 하위 바이트부터 16진수로 적은 것입니다.

워드 시작값
A 01 23 45 67
B 89 ab cd ef
C fe dc ba 98
D 76 54 32 10

블록 처리

블록 하나는 워드 16개입니다. 이 16워드를 X[0] 부터 X[15] 에 옮깁니다. 지금의 A·B·C·D 는 AA·BB·CC·DD 에 따로 적어 둡니다.

그다음 라운드 넷을 돕니다. 라운드마다 연산 16번을 합니다. 라운드마다 쓰는 보조 함수가 다릅니다. 보조 함수는 32비트 워드 셋을 받아 워드 하나를 냅니다.

라운드 보조 함수 비트마다 하는 일 회전량
1 F(X,Y,Z) = XY v not(X) Z X 가 1 이면 Y, 0 이면 Z 를 고릅니다 7 · 12 · 17 · 22
2 G(X,Y,Z) = XZ v Y not(Z) Z 가 1 이면 X, 0 이면 Y 를 고릅니다 5 · 9 · 14 · 20
3 H(X,Y,Z) = X xor Y xor Z 세 비트의 홀짝을 냅니다 4 · 11 · 16 · 23
4 I(X,Y,Z) = Y xor (X v not(Z)) Z 가 0 이면 Y 를 뒤집은 값, 1 이면 X xor Y 를 냅니다 6 · 10 · 15 · 21

네 함수는 비트 자리마다 따로 계산합니다. 입력 비트가 서로 독립이고 치우치지 않으면 출력 비트도 독립이고 치우치지 않습니다.

연산 하나는 이런 꼴입니다. 1라운드의 첫 연산을 적으면 아래와 같습니다.

A = B + ((A + F(B,C,D) + X[0] + T[1]) <<< 7)

위 식을 말로 풀면 이렇습니다. 먼저 워드 A 를 보조 함수 값, 입력 워드 하나, 상수 하나와 더합니다. 그 합을 회전합니다. 거기에 이웃 워드 B 를 더한 값이 A 의 새 값입니다. 다음 연산은 A·B·C·D 의 역할을 한 칸씩 돌립니다. 그래서 둘째 연산은 D 를, 셋째는 C 를, 넷째는 B 를 바꿉니다.

입력 워드를 읽는 순서는 라운드마다 다릅니다. 1라운드는 X[0] 부터 차례로 읽습니다. 2·3·4라운드는 정해진 다른 순서로 읽습니다.

상수 T[i] 는 64개입니다. T[i] 는 4294967296 곱하기 |sin(i)| 의 정수 부분입니다. i 는 라디안입니다. 4294967296 은 2^32 입니다.

64번이 끝나면 블록을 시작하기 전에 적어 둔 값을 더합니다. A 에는 AA 를, B 에는 BB 를, C 에는 CC 를, D 에는 DD 를 더합니다. 그리고 다음 블록으로 넘어갑니다.

flowchart TD
    IN["블록 하나 · 워드 16개"] --> SAVE["지금의 A·B·C·D 를 AA·BB·CC·DD 에 적어 둔다"]
    SAVE --> RND["라운드 1·2·3·4 · 라운드마다 연산 16번"]
    RND --> ADD["A 에 AA, B 에 BB, C 에 CC, D 에 DD 를 더한다"]
    ADD --> NEXT["다음 블록으로"]

출력 순서

마지막 블록을 처리한 뒤의 A·B·C·D 가 다이제스트입니다. A 의 하위 바이트부터 시작해 D 의 상위 바이트로 끝나게 16바이트를 이어 붙입니다.

MD4 와의 차이

MD5 는 MD4 의 얼개에서 여섯 가지를 바꿨습니다. 아래 표의 「노린 것」에 나오는 눈사태 효과는 입력의 작은 차이가 출력의 많은 비트로 번지는 성질입니다.

바뀐 것 노린 것
넷째 라운드를 더했습니다 —
연산마다 고유한 상수 T[i] 를 더합니다 —
2라운드 함수를 XY v XZ v YZ 에서 XZ v Y not(Z) 로 바꿨습니다 함수를 덜 대칭으로
연산마다 앞 연산의 결과를 더합니다 눈사태 효과를 더 빨리
2·3라운드에서 입력 워드를 읽는 순서를 바꿨습니다 두 라운드의 읽는 순서가 덜 닮게
라운드마다 회전량을 다시 골랐습니다 눈사태 효과를 더 빨리. 라운드끼리 회전량이 겹치지 않음

표의 「연산마다 앞 연산의 결과를 더합니다」가 위에서 본 그 대목입니다. 이웃 워드 B 를 더해 A 의 새 값으로 삼는 것이 그것입니다.

보장과 가정

이 절은 MD5 가 내건 두 약속과 그 약속이 기대는 가정을 봅니다. 가정이 깨진 뒤 무엇이 무너지고 무엇이 남았는지도 봅니다.

충돌 저항은 무너졌습니다. 역상 저항은 계산 비용이 아직 높게 남아 있습니다. 공격자 없이 우연한 손상을 잡는 쓰임도 남았습니다.

설계가 내건 두 약속

약속 뜻 설계 때 추정한 비용
충돌 저항 값이 같은 두 메시지를 만들어 내기가 계산으로는 불가능합니다 2^64 번쯤의 연산
역상 저항 미리 정한 값을 내는 메시지를 만들어 내기가 계산으로는 불가능합니다 2^128 번쯤의 연산

둘 다 증명이 아니라 추정(conjectured)이었습니다. 약점을 꼼꼼히 살폈지만 새 알고리즘이라 분석이 더 필요하다는 단서도 붙어 있었습니다.

해시 함수에 바라는 성질이 하나 더 있습니다. 주어진 메시지와 값이 같은 다른 메시지를 찾기 어려워야 합니다. 이것을 제2 역상 저항이라고 부릅니다.

두 약속의 비용은 「복잡도」에서 본 일반적인 방법의 비용과 같습니다. 생일 문제로 쌍을 찾는 방법과 하나씩 넣어 보는 방법입니다. 약속이 기대는 가정은 이 두 방법보다 싼 공격이 없다는 것입니다.

약속과 추정 비용은 다릅니다. 약속은 만들어 내기가 계산으로는 불가능하다는 것이고, 두 수는 그 약속을 받치는 추정 비용입니다. 더 싼 공격이 나오면 가정이 먼저 깨지고 추정 비용이 내려갑니다. 약속이 무너지는 것은 그 비용이 해 볼 만한 수준까지 내려갔을 때입니다. 아래 두 절이 그 둘을 갈라 봅니다. 충돌 쪽은 약속까지 무너졌고, 역상 쪽은 가정은 깨졌지만 비용이 아직 높습니다.

무너진 충돌 저항

충돌은 압축 함수부터 무너지기 시작했습니다.

아래 연표에 낱말 둘이 먼저 나옵니다. 유사 충돌은 MD5 전체의 충돌이 아닙니다. 안쪽 압축 함수만 떼어 놓고 조건을 느슨하게 둔 채 찾은 충돌입니다. X.509 는 웹사이트 인증서가 따르는 형식입니다.

해 일어난 일
1993 압축 함수의 유사 충돌이 처음 보고됐습니다
1996 시작 상태를 골라 쓰면 압축 함수에서 충돌 쌍이 나왔습니다
2004 MD5 전체의 충돌 쌍이 처음 발표됐습니다
2005 공격 기법이 자세히 발표됐습니다. 공개 키만 다르고 서명이 같은 X.509 인증서 쌍이 나왔습니다
2007 선택 접두 충돌로 신원까지 다른 X.509 인증서 쌍을 만들었습니다
2008 주요 웹 브라우저가 모두 믿는 가짜 인증 기관 인증서를 만들었습니다

압축 함수의 유사 충돌에서 MD5 전체의 충돌까지는 열한 해가 걸렸습니다.

인증 기관은 인증서 내용의 해시값에 서명합니다. 그러니 MD5 값이 같은 인증서 두 장을 만들면 한 장에 받은 서명이 다른 한 장에도 맞습니다.

2007년의 선택 접두 충돌은 두 인증서의 앞부분을 공격자가 따로 고릅니다. 그 덕에 소유자 신원을 적는 이름 칸까지 다르게 할 수 있었습니다. 2005년 쌍은 이름 칸이 같아야 했습니다.

flowchart TD
    subgraph DA["인증서 A"]
        A1["공격자가 고른 앞부분"] --> A2["계산해 붙인 뒷부분"]
    end
    subgraph DB["인증서 B"]
        B1["공격자가 고른 앞부분"] --> B2["계산해 붙인 뒷부분"]
    end
    A2 --> SAME["같은 MD5 값 · 같은 서명"]
    B2 --> SAME

이 공격의 실제 위력은 제한적이라고 평가됐습니다. 그래도 아무 쌍이나 나오는 충돌보다는 더 걱정스러운 공격입니다.

2008년 연구진은 그 걱정을 실물로 보였습니다. 몇몇 루트 인증 기관이 몇 년 동안 경고를 받고도 여전히 MD5 로 서명하고 있었습니다. 연구진은 그 틈으로 가짜 인증 기관 인증서를 만들었습니다. 그 인증서로 서명한 웹사이트 인증서는 어느 사이트든 흉내 낼 수 있습니다.

충돌 공격은 비밀번호로 묻고 답하는 인증 방식에도 적용할 수 있습니다. 우편함 접속에 쓰는 APOP(Authenticated Post Office Protocol) 옵션에 그렇게 적용해 보인 연구가 있습니다.

그래서 전자 서명처럼 충돌 저항이 필요한 곳에서 MD5 는 더 이상 받아들일 수 없습니다. 전자 서명에는 MD5 를 써서는 안 됩니다(must not).

남은 성질

역상 저항은 아직 계산 비용이 높습니다. 알려진 가장 싼 공격도 2^123.4 이 듭니다. 주어진 값을 내는 파일을 새로 찾는 일은 지금으로서는 불가능하다고 봅니다.

오류만 막으려고 프로토콜 안에서 쓰는 MD5 체크섬은 아직 받아들일 수 있는 쓰임입니다. 대신 그 체크섬에 어떤 보안을 기대하는지를 응용과 프로토콜이 밝혀야 합니다.

HMAC(Hash-based Message Authentication Code)은 해시 함수에 비밀 키를 섞어 메시지 인증 코드를 만드는 방식입니다. HMAC-MD5 에 대한 공격은 메시지 인증 코드로 쓸 때 실용적인 취약점을 보이지는 않는 듯합니다. 그래서 기존 프로토콜에서 HMAC-MD5 를 당장 빼낼 만큼 급하지는 않을 수 있습니다.

그래도 새로 설계하는 프로토콜에는 HMAC-MD5 를 넣지 않기를 권합니다(should not). HMAC 쪽에서 뚫린 데가 보여서가 아닙니다. 걸린 것은 MD5 라는 함수 자체입니다. 전자 서명에 써서는 안 되는 함수가 됐으니, 새로 짜는 설계에는 그 함수를 쓰는 묶음을 아예 넣지 말자는 것입니다. 대안으로 HMAC-SHA256(Secure Hash Algorithm 256 을 쓰는 HMAC)이 있습니다.

비밀번호 저장

비밀번호 저장은 충돌과 다른 축의 문제입니다. 비밀번호 저장용 해시는 일부러 느려야 합니다. 얼마나 오래 걸릴지는 작업 계수(work factor)로 조절합니다.

MD5 와 SHA-1(Secure Hash Algorithm 1)은 거꾸로 빠르게 만들어진 함수입니다. 계산이 빠르면 공격자가 비밀번호 후보를 대입하는 속도도 빠릅니다. 그런 옛 해시로 저장한 비밀번호는 비밀번호 전용 해시 알고리즘으로 옮길 수 있습니다.

PostgreSQL 은 비밀번호 인증 방식에 md5 를 두고 있습니다. 그러나 MD5 해시 알고리즘은 이제 작정한 공격에 안전하다고 보지 않습니다. MD5 로 저장한 비밀번호 지원은 폐기 예정입니다. 앞으로의 판에서 없어집니다.

옮겨 가기 쉽도록 장치가 하나 있습니다. pg_hba.conf 는 어느 접속에 어느 인증 방식을 쓸지 적어 두는 PostgreSQL 설정 파일입니다. 거기에 md5 라고 적었어도 서버의 비밀번호가 SCRAM(Salted Challenge Response Authentication Mechanism) 용으로 저장돼 있으면 SCRAM 인증을 자동으로 고릅니다. SCRAM 은 이름대로 비밀번호를 그대로 보내지 않고 묻고 답해 확인하는 방식입니다.

쓰임마다 기대는 성질

같은 MD5 라도 쓰임마다 기대는 성질이 다릅니다. 그 성질이 무너졌는지가 그 쓰임이 남는지를 가릅니다.

쓰임 기대는 성질 MD5 로 되나
전자 서명 · 인증서 충돌 저항 안 됩니다. 써서는 안 됩니다(must not)
공격자가 파일을 바꿀 수 있는 곳의 무결성 확인 충돌 · 제2 역상 저항 악의적인 변조에는 안전하다고 보지 않습니다
오류만 막는 체크섬 우연한 손상 감지 받아들일 수 있습니다. 기대하는 보안을 밝혀야 합니다
기존 프로토콜의 HMAC-MD5 키를 섞은 메시지 인증 당장 빼낼 만큼 급하지는 않을 수 있습니다
새 프로토콜의 HMAC 키를 섞은 메시지 인증 넣지 않기를 권합니다(should not)
비밀번호 저장 일부러 오래 걸리게 만든 계산 맞지 않습니다. 빠르게 만들어진 함수입니다

표가 갈리는 축은 하나가 아닙니다. 앞 세 줄은 값을 맞춘 가짜를 누군가 일부러 만들 수 있는 쓰임인지를 봅니다. HMAC 두 줄은 키를 섞은 꼴이라 그 축에 안 걸립니다. 기존 프로토콜은 남기고 새 설계에만 빼라는 권고가 그 두 줄을 나눴습니다. 비밀번호 저장은 계산 속도라는 다른 축에서 걸립니다. 대입이 싸지기 때문입니다.

더 엄격한 판단도 있습니다. 2008년 가짜 인증 기관 사건 직후 CERT/CC(CERT Coordination Center)는 어떤 용도로든 MD5 를 피하라고 권고했습니다. 암호학적으로 깨졌고 더 쓰기에 맞지 않는다고 보았습니다.

관련 항목

MD5 가 속하는 상위 분류

해시 함수 · 암호학적 해시 함수 · 해시 · 다이제스트 · 체크섬

MD5 와 한 계열인 해시 함수

MD4 · MD2 · RIPEMD · HAVAL · SHA-1

MD5 를 대신하는 해시 함수

SHA-256 · SHA-2 · SHA-3 · BLAKE2 · BLAKE3

MD5 가 지키려던 성질

충돌 저항성 · 역상 저항성 · 제2 역상 저항성 · 눈사태 효과 · 생일 문제 · 해시 충돌

MD5 를 이루는 계산 얼개

머클-담고르 구조 · 압축 함수 · 패딩 · 비트 연산 · 초기화 벡터 · 리틀 엔디언 · 비트 회전

MD5 를 깬 공격 기법

선택 접두 충돌 · 차분 공격

값을 맞추는 일반적인 방법

생일 공격 · 역상 공격 · 전수 탐색

MD5 값을 무결성 확인에 쓰는 제품과 헤더

Amazon S3 · ETag · Content-MD5 · 멀티파트 업로드

무결성 확인을 둘러싼 개념과 표기

무결성 · Base64 · CRC

MD5 로 캐시 파일 이름을 짓는 제품

nginx · proxy_cache_path · 캐시 키 · 프록시 캐시

MD5 가 물러난 보안 용도

전자 서명 · 인증서 · X.509 · 인증 기관 · 공개 키 기반 구조 · TLS · 코드 서명 · APOP

MD5 를 키와 섞어 쓰는 메시지 인증

HMAC · 메시지 인증 코드 · HMAC-SHA256 · AES-CMAC

비밀번호 저장에 MD5 대신 쓰는 함수

비밀번호 해싱 · 솔트 · bcrypt · Argon2 · PBKDF2 · 레인보우 테이블 · 작업 계수 · SCRAM

MD5 값을 계산하는 명령과 라이브러리

md5sum · cksum · openssl · hashlib · 체크섬 파일

다른 이름: Message-Digest Algorithm 5 · 메시지 다이제스트 알고리즘 5