SHA-1
고친 사람 github-actions[bot]
SHA-1 은 길이가 얼마든 상관없는 데이터를 짧은 값 하나로 줄여 줍니다. 같은 데이터를 넣으면 언제나 같은 값이 나옵니다. 그래서 데이터가 바뀌었는지 가려내거나 데이터에 이름을 붙이는 데 오래 쓰였습니다. 지금은 같은 값을 갖는 가짜를 일부러 만들 수 있게 되어 서명처럼 속임을 막아야 하는 곳에서 물러났습니다.
쉽고 빠른 이해
SHA-1 은 어떤 데이터든 받아서 16진수 40글자짜리 값 하나로 바꿔 줍니다. 수 기가바이트 파일을 넣어도, 글자 다섯 개를 넣어도 나오는 값의 길이는 같습니다.
이게 없으면 서명 같은 계산을 큰 원본에 그대로 해야 합니다. 내용이 바뀌었는지 가려내기도 어렵습니다.
- 데이터 끝에 정해진 규칙대로 채움 값을 붙여 길이를 맞춥니다
- 그것을 같은 크기 블록으로 쪼개고, 블록을 하나씩 섞어 내부 값을 바꿔 나갑니다
- 블록을 다 처리하고 남은 내부 값이 그 데이터의 SHA-1 값입니다
대가는 값의 길이가 정해져 있다는 것입니다. 서로 다른 데이터가 같은 값을 갖는 일을 막을 수 없습니다. SHA-1 은 그런 쌍을 일부러 만드는 방법이 알려져 서명에서 물러났습니다. 지금도 쓸 수 있는 곳은 공격자가 없는 손상 확인과 비밀 키를 함께 쓰는 인증입니다.
상세
SHA-1(Secure Hash Algorithm 1)은 데이터를 받아 160비트짜리 값 하나를 내놓는 계산 절차입니다. 넣는 데이터는 2^64 비트보다 짧기만 하면 됩니다. 2^64 비트는 2백만 테라바이트가 넘는 크기라 길이 제한이 없는 것과 다름없습니다.
이런 절차를 해시 함수라고 부릅니다. 해시 함수는 길이가 제각각인 데이터를 정해진 길이의 값 하나로 줄입니다. 넣는 데이터를 메시지라고 합니다. 나오는 값은 해시값 또는 메시지 다이제스트라고 합니다.
160비트는 20바이트입니다. 16진수로 적으면 한 바이트가 두 글자라서 40글자가 됩니다. 수 기가바이트짜리 동영상을 넣어도, abcde 다섯 글자를 넣어도 나오는 값은 똑같이 40글자입니다.
「안전하다」가 약속하는 것
이름의 S 는 secure, 안전하다는 뜻입니다. 표준이 약속하는 안전은 두 가지 일을 계산으로 해내기가 현실적으로 불가능하다는 것입니다.
| 성질 | 해내기 어려워야 하는 일 |
|---|---|
| 역상 저항성 | 해시값 하나가 주어졌을 때 그 값을 내는 메시지를 찾는 일 |
| 충돌 저항성 | 같은 해시값을 내는 서로 다른 메시지 두 개를 찾는 일 |
같은 해시값을 내는 서로 다른 두 메시지를 충돌이라고 부릅니다.
둘 사이에 성질 하나가 더 있습니다. 메시지 하나를 먼저 정해 두고 그것과 충돌하는 다른 메시지를 찾는 일은 제2 역상이라고 부릅니다. 이것을 막는 성질이 제2 역상 저항성입니다. 표준이 든 두 가지에는 안 들어갑니다. 그래도 아래 「실패」 절에서 보듯 Git 은 이 성질까지 요구합니다.
메시지가 조금이라도 바뀌면 해시값은 매우 높은 확률로 달라집니다. 그래서 해시값을 서명과 함께 쓰면 도중에 바뀐 메시지는 검증에서 떨어집니다.
원본 대신 해시값을 쓰는 까닭
SHA-1 은 혼자 쓰이기보다 다른 암호 기법 안에 끼워 쓰이는 일이 흔합니다. 전자 서명, 비밀 키를 섞어 메시지를 인증하는 HMAC(Hash-based Message Authentication Code), 난수 생성이 그런 짝입니다.
서명을 예로 들면 메시지 전체 대신 해시값에 서명합니다. 해시값은 대개 메시지보다 훨씬 작아서 서명 과정이 빨라지는 경우가 많습니다.
이 쓰임은 충돌 저항성에 기댑니다. 같은 해시값을 내는 다른 메시지를 누군가 만들 수 있다면, 한 메시지에 받은 서명이 다른 메시지에도 맞아떨어집니다.
메시지를 블록으로 나눠 처리하는 방식
SHA-1 은 메시지를 한꺼번에 계산하지 않습니다. 먼저 메시지 끝에 채움 비트를 붙여 전체 길이를 512비트의 배수로 맞춥니다. 그리고 512비트 블록으로 나눕니다.
계산은 160비트짜리 내부 값 하나에 블록을 차례로 섞어 넣습니다. 블록 하나를 섞어 내부 값을 새로 만드는 계산을 압축 함수라고 부릅니다. SHA-1 의 압축 함수는 블록 하나에 80단계를 돕니다. 마지막 블록까지 섞고 남은 내부 값이 해시값입니다.
계산은 32비트 묶음 단위로 합니다. 이 묶음을 워드라고 부릅니다. 단계마다 무엇을 하는지는 아래 「동작」 절이 순서대로 보입니다.
같은 표준에 묶인 형제
SHA-1 은 미국 NIST(National Institute of Standards and Technology, 국립표준기술연구소)가 1995년에 표준으로 정한 해시 함수입니다. 1993년에 나온 앞 판을 조금 고친 것입니다. 그 앞 판을 SHA-0 이라고 부릅니다. 지금 표준 문서는 같은 Secure Hash Algorithm 계열의 SHA-224·SHA-256·SHA-384·SHA-512 등을 함께 정합니다.
| 함수 | 메시지 길이 | 블록 크기 | 워드 크기 | 해시값 길이 |
|---|---|---|---|---|
| SHA-1 | 2^64 비트 미만 | 512비트 | 32비트 | 160비트 |
| SHA-256 | 2^64 비트 미만 | 512비트 | 32비트 | 256비트 |
표의 네 칸 가운데 SHA-1 과 SHA-256 이 다른 것은 해시값 길이뿐입니다. 쓰는 상수와 함수는 알고리즘마다 따로 정해져 있습니다. 표가 같다고 계산까지 같은 것은 아닙니다. 다음 절에서 보듯 이 해시값 길이가 충돌을 찾는 비용의 상한을 정합니다.
복잡도
SHA-1 을 한 번 계산하는 비용은 메시지 길이에 비례합니다. 메모리는 길이와 상관없이 일정합니다. 문제는 공격 비용입니다. 충돌을 찾는 데는 원래 약 2^80 번의 계산이 들어야 합니다. 알려진 공격이 이것을 2^61~2^63 수준으로 끌어내렸습니다.
재는 비용은 둘입니다. 하나는 정상적으로 해시값을 한 번 계산하는 비용입니다. 다른 하나는 공격자가 충돌을 찾아내는 비용입니다.
해시값 한 번을 계산하는 비용
메시지 길이를 ℓ 비트라고 하면, 채운 뒤의 블록 수 N 은 (ℓ + 65) 를 512 로 나눠 올림한 값입니다. 채움에 적어도 1비트, 원래 길이를 적는 데 64비트가 붙기 때문입니다. abcde 는 40비트라 블록 하나로 끝납니다.
| 항목 | 값 | 왜 그런가 |
|---|---|---|
| 시간 | 80N 단계. 메시지 길이에 비례 | 블록마다 압축 함수가 80단계를 한 번씩 돈다 |
| 최악과 평균 | 같다 | 메시지 내용과 상관없이 단계 수가 정해져 있다 |
| 공간 | 메시지 길이와 무관한 상수 | 블록 하나를 늘린 80워드, 계산 도중 값을 담는 작업 변수 5워드, 내부 값 5워드만 든다 |
80워드는 블록 하나를 늘려 만든 계산용 목록입니다. 이 목록을 메시지 스케줄이라고 부릅니다. 늘리는 식은 아래 「동작」 절에 있습니다. 뒤 워드가 앞 워드 넷에서 나오는 꼴이라 80칸을 다 들고 있지 않아도 됩니다.
같은 해시값을 내는 계산 방법이 둘입니다. 하나는 80워드를 전부 펼쳐 두고 차례로 꺼내 쓰는 방법입니다. 다른 하나는 16워드짜리 칸만 두고 새로 만든 워드로 가장 오래된 칸을 덮어쓰며 돌려 쓰는 방법입니다. 뒤쪽을 고르면 32비트 워드 64개만큼의 저장 공간을 아낍니다. 대신 단계마다 어느 칸이 지금 필요한 워드인지 셈해야 해서 실행 시간은 늘어나기 쉽습니다.
충돌을 찾는 비용의 기준선
아무 메시지나 해시해 보며 충돌을 찾는 방법을 생일 공격이라고 부릅니다. 23명만 모여도 생일이 같은 두 사람이 나올 확률이 절반을 넘는 것과 같은 원리입니다.
해시값이 n 비트인 함수에서 생일 공격은 평균 √(π/2)·2^(n/2) 번 계산하면 충돌 하나를 찾습니다. SHA-1 은 n 이 160 이라 약 2^80 번입니다. 이보다 적은 계산으로 충돌을 찾는 방법이 나오면 그것을 충돌 공격이라 부르고, 그 함수의 충돌 저항성이 깨졌다고 봅니다.
역상을 찾는 일반 비용은 2^160 번입니다. 역상 저항성은 2020년 기준으로 깨지지 않은 채였습니다.
두 가지 충돌
충돌 공격은 공격자가 얼마나 고를 수 있느냐에 따라 둘로 갈립니다. 아래 표에서 H(…) 는 해시값을 내는 계산입니다. ‖ 는 앞뒤를 이어 붙이는 기호입니다.
| 종류 | 공격자가 만드는 것 | 쓰임새 |
|---|---|---|
| 동일 접두 충돌(identical-prefix collision) | 같은 앞부분 P 뒤에 서로 다른 블록을 붙여 같은 해시값을 만든다. 그 뒤에 같은 뒷부분 S 를 붙이면 계속 같다 | 앞부분이 같아야 해서 쓸 곳이 좁다 |
| 선택 접두 충돌(chosen-prefix collision) | 서로 다른 앞부분 P 와 P' 가 먼저 주어진다. 각각 뒤에 붙일 M 과 M' 를 찾아 H(P‖M) = H(P'‖M') 를 만든다 | 앞부분에 서로 다른 의미 있는 내용을 담을 수 있다 |
두 종류가 메시지를 어떻게 짜는지는 이렇게 생겼습니다.
block-beta columns 4 l1["동일 접두 · 메시지 1"] a["앞부분 P"] b["공격 블록 1"] c["뒷부분 S"] l2["동일 접두 · 메시지 2"] d["앞부분 P"] e["공격 블록 2"] f["뒷부분 S"] l3["선택 접두 · 메시지 1"] g["앞부분 P"] h["붙인 M"]:2 l4["선택 접두 · 메시지 2"] i["앞부분 P'"] j["붙인 M'"]:2
동일 접두 쪽은 가운데 한 칸만 다릅니다. 선택 접두 쪽은 앞부분부터 다릅니다. 그래서 선택 접두 충돌이 훨씬 위협적입니다. 대신 앞부분이 만드는 차이를 다스리기 어려워서 찾기도 훨씬 어렵습니다. 생일 공격으로 찾으면 이것도 약 2^80 번입니다.
알려진 공격의 비용
알려진 공격은 충돌을 찾는 비용을 2^80 에서 2^61~2^63 수준까지 끌어내렸습니다. 아래 표가 그 내림을 공격별로 보입니다.
비용의 단위는 압축 함수를 부른 횟수입니다. 2020년 공격의 두 줄만 단위가 다릅니다. 그 줄의 수치는 Nvidia GTX 970 이라는 GPU(Graphics Processing Unit, 그래픽 처리 장치) 한 종에서 SHA-1 한 번을 계산하는 시간으로 환산한 값입니다.
| 공격 | 종류 | 비용 | 결과 |
|---|---|---|---|
| 생일 공격 | 둘 다 | 약 2^80 | 기준선. 돌릴 수 없다 |
| Wang·Yin·Yu (2005) | 동일 접두 | 2^69 | 생일 공격보다 빠른 첫 충돌 공격. 이론에 머물렀다 |
| Stevens (2013) | 동일 접두 | 2^61. 공격을 설계해 보고 예상한 값 | 이론 |
| SHAttered (2017) | 동일 접두 | 2^63.1. 실제로 돌려서 쓴 계산량 | 첫 실물 충돌 |
| SHA-1 is a Shambles (2020) | 동일 접두 | 2^61.2. GPU 환산 단위다. 같은 환산으로 그 전까지는 2^64.7 이었다 | GPU 를 빌리면 약 1만 1천 달러 |
| SHA-1 is a Shambles (2020) | 선택 접두 | 2^63.4. GPU 환산 단위다. 같은 환산으로 그 전까지는 2^67.1 이었다 | 첫 실물 선택 접두 충돌. GPU 를 빌리면 약 4만 5천 달러 |
숫자가 연도 순으로 곧장 내려가지 않는 까닭은 둘입니다. 하나는 2013년의 2^61 이 예상값이고 2017년의 2^63.1 이 실측값이라는 것입니다. 다른 하나는 2020년의 두 줄이 위에 적은 GPU 환산 단위라 앞 줄들과 자를 맞댈 수 없다는 것입니다.
SHAttered 는 SHA-1 을 모두 약 922경 번(9,223,372,036,854,775,808번) 계산했습니다. 공격은 두 단계로 나뉩니다. 첫 단계에 CPU(Central Processing Unit, 중앙 처리 장치) 계산 6,500년, 둘째 단계에 GPU 계산 100~110년이 들었습니다. 그래도 생일 공격보다 10만 배 넘게 적은 계산입니다.
2020년 공격의 임대 비용 1만 1천 달러와 4만 5천 달러는 대학 연구자가 감당할 수 있는 수준입니다. 그 선택 접두 충돌은 Nvidia GTX 1060 GPU 900개로 두 달을 돌려 만들었습니다. 실제 지출은 7만 5천 달러였습니다. 그때 GPU 값이 더 비쌌습니다. 공격을 준비하느라 쓴 시간도 보탰습니다.
예시
Git 객체 이름
Git 은 파일 내용·디렉터리·커밋에 SHA-1 값으로 이름을 붙입니다. 내용을 해시한 값이 곧 그 내용을 찾는 열쇠가 됩니다. 이런 저장 방식을 내용 주소 지정 파일 시스템이라고 부릅니다.
$ echo -n "what is up, doc?" | git hash-object --stdin
bd9dbf5aae1a3862dd1526723246b20206e5fc37
git hash-object 는 내용을 저장할 때 붙을 이름을 계산해 보여 줍니다. 그런데 이 40글자는 what is up, doc? 만 해시한 값이 아닙니다. Git 은 내용 앞에 머리를 하나 붙여서 해시합니다.
blob 16\0what is up, doc?
머리는 객체 종류, 공백, 내용의 바이트 수, 널 바이트 순서입니다. blob 은 파일 내용을 담는 객체 종류입니다. 커밋과 디렉터리는 이 낱말만 commit · tree 로 바뀝니다. 위 한 줄 전체의 SHA-1 값이 bd9dbf5a… 입니다.
Git 은 내용 하나를 파일 하나로 저장합니다. 그 파일의 경로는 40글자를 둘로 나눠 만듭니다. 첫 두 글자가 하위 디렉터리 이름이 됩니다. 나머지 38자는 그 디렉터리 안의 파일 이름이 됩니다.
Git 이 내용에 해시로 이름을 붙여 얻는 것은 셋입니다.
| 얻는 것 | 어떻게 |
|---|---|
| 무결성 확인 | 비트 하나가 뒤집힌 내용은 해시값이 이름과 안 맞는다 |
| 객체 조회 | 이름이 곧 저장 위치라 바로 찾는다 |
| 서명 | 객체 이름 하나에 서명하면 그 객체와 그것이 가리키는 객체 전부를 믿을 수 있다 |
셋째는 해시 함수가 암호학적으로 안전할 때만 성립합니다. 앞의 둘은 충돌을 일부러 만드는 공격자가 없어도 얻는 것입니다.
TOTP 일회용 비밀번호
TOTP(Time-Based One-Time Password)는 지금 시각을 넣어 한 번 쓰고 버리는 비밀번호를 만드는 절차입니다. 바탕은 HOTP(HMAC-based One-Time Password)입니다. HOTP 는 HMAC-SHA-1 값을 잘라 사람이 읽기 쉬운 숫자로 만듭니다.
HOTP(K, C) = Truncate(HMAC-SHA-1(K, C))
TOTP = HOTP(K, T)
K 는 서버와 사용자만 아는 비밀 키입니다. HOTP 의 C 는 하나씩 올라가는 카운터입니다. TOTP 는 그 대신 시각에서 얻은 T 를 넣습니다. T 는 기준 시각 T0 부터 흐른 초를 시간 간격 X 로 나눈 값입니다. T0 = 0, X = 30 이면 유닉스 시각 59초에는 T = 1, 60초에는 T = 2 입니다.
시험 벡터 한 줄로 보면 이렇습니다. 시각은 UTC(Coordinated Universal Time, 협정 세계시) 기준입니다.
| 비밀 키 | 시각 | T (16진수) | TOTP | 해시 |
|---|---|---|---|---|
12345678901234567890 |
59초 (1970-01-01 00:00:59 UTC) | 0000000000000001 |
94287082 |
SHA-1 |
같은 비밀 키와 같은 시각을 넣으면 누구나 94287082 를 얻습니다. 비밀 키가 없으면 같은 값을 만들 수 없게 하는 것이 이 쓰임이 노리는 것입니다.
이 쓰임에서 SHA-1 은 이름표가 아닙니다. 비밀 키를 가진 쪽만 같은 값을 만들 수 있게 하는 메시지 인증 부품입니다. TOTP 는 SHA-1 대신 HMAC-SHA-256 이나 HMAC-SHA-512 를 써도 됩니다.
동작
SHA-1 은 메시지를 채우고 512비트 블록으로 나눈 뒤, 블록마다 늘리기 → 80단계 섞기 → 내부 값에 더하기를 되풀이합니다. 마지막 블록을 처리하고 남은 내부 값 다섯 워드를 이어 붙이면 해시값입니다. 아래에서는 abcde 다섯 글자를 메시지로 삼아 채우기와 그 블록이 워드로 쪼개지는 데까지를 눈으로 보입니다.
flowchart TD
P["채움 비트와 길이를 붙이고 512비트 블록으로 나눈다"] --> I["내부 값 H0~H4 를 초기값으로 둔다"]
I --> W["블록 하나를 80워드로 늘린다"]
W --> R["80단계 섞기"]
R --> A["섞은 결과를 H0~H4 에 더한다"]
A --> Q{"블록이 남았나"}
Q -->|남았다| W
Q -->|다 처리했다| O["H0~H4 를 이어 붙인 160비트가 해시값"]
그림에서 볼 것은 내부 값 H0~H4 가 끝까지 이어진다는 점입니다. 블록마다 새로 시작하지 않습니다. 앞 블록이 남긴 값 위에 다음 블록을 섞어 넣습니다.
채우기
메시지 끝에 비트 1 을 하나 붙입니다. 이어서 0 을 k 개 붙입니다. k 는 ℓ + 1 + k 를 512 로 나눈 나머지가 448 이 되는 가장 작은 수입니다. 마지막으로 원래 길이 ℓ 을 64비트 이진수로 붙입니다. 그러면 전체가 512비트의 배수가 됩니다.
abcde 는 40비트입니다. 채우고 나면 블록 하나가 이렇게 생깁니다.
block-beta columns 4 a["abcde · 40비트"] b["1 · 1비트"] c["0 · 407비트"] d["길이 40 · 64비트"]
16진수로 적으면 아래와 같습니다. 앞의 61 62 63 64 65 가 abcde 입니다. 바로 뒤 80 은 이진수로 10000000 입니다. 그 맨 앞 1 이 붙인 비트입니다. 뒤따르는 0 일곱 개는 채움 0 의 처음입니다. 맨 끝 28 은 16진수로 40, 원래 길이입니다.
61626364 65800000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000028
초기값
첫 블록을 처리하기 전에 내부 값 다섯 워드를 정해진 상수로 둡니다.
H0 = 67452301 H1 = efcdab89 H2 = 98badcfe
H3 = 10325476 H4 = c3d2e1f0
블록 하나를 80워드로 늘리기
512비트 블록은 32비트 워드 16개입니다. 이것을 W(0)~W(15) 로 둡니다. 나머지 W(16)~W(79) 는 앞의 워드로 만듭니다. 앞 절에서 채운 abcde 의 16진수 네 줄이 바로 그 워드 16개입니다. 왼쪽 맨 앞의 61626364 가 W(0) 입니다.
W(t) = S^1( W(t-3) XOR W(t-8) XOR W(t-14) XOR W(t-16) )
XOR(exclusive OR, 배타적 논리합)은 두 비트가 다르면 1, 같으면 0 을 내는 연산입니다. S^n 은 워드를 왼쪽으로 n 비트 돌리는 연산입니다. 왼쪽 끝으로 밀려난 비트는 오른쪽 끝으로 돌아옵니다.
SHA-1 의 앞 판인 SHA-0 과 다른 곳은 이 식의 1비트 돌리기 하나뿐입니다.
80단계 섞기
작업 변수 다섯 개 A·B·C·D·E 에 내부 값 H0~H4 를 복사합니다. 그리고 t 를 0 부터 79 까지 올리며 같은 계산을 80번 합니다.
TEMP = S^5(A) + f(t; B, C, D) + E + W(t) + K(t)
E = D; D = C; C = S^30(B); B = A; A = TEMP
둘째 줄이 하는 일은 한 칸 밀기입니다. B 는 A 의 값을 받고, C 는 B 를 30비트 돌린 값을 받고, D 는 C 를, E 는 D 를 받습니다. 새로 계산되는 것은 A 하나뿐입니다. 그 값이 TEMP 입니다.
덧셈은 전부 2^32 로 나눈 나머지만 남깁니다. 함수 f 와 상수 K 는 20단계마다 바뀝니다.
| 단계 t | f(t; B, C, D) | 하는 일 | K(t) |
|---|---|---|---|
| 0~19 | (B AND C) OR ((NOT B) AND D) | B 의 비트가 1 이면 C 를, 0 이면 D 를 고른다 | 5a827999 |
| 20~39 | B XOR C XOR D | 세 비트의 배타적 논리합 | 6ed9eba1 |
| 40~59 | (B AND C) OR (B AND D) OR (C AND D) | 세 비트 중 다수를 따른다 | 8f1bbcdc |
| 60~79 | B XOR C XOR D | 세 비트의 배타적 논리합 | ca62c1d6 |
80단계가 끝나면 A~E 를 H0~H4 에 하나씩 더합니다. 이 더한 값이 다음 블록을 섞을 때의 내부 값입니다.
블록을 잇는 얼개
마지막 블록까지 처리하고 나면 H0·H1·H2·H3·H4 를 차례로 이어 붙입니다. 32비트 다섯 개라 160비트, 곧 SHA-1 해시값입니다.
이렇게 고정 크기 블록을 압축 함수로 하나씩 처리하며 내부 값을 갱신해 나가는 얼개를 머클-담고르 구조라고 부릅니다. 「복잡도」 절의 공격 비용을 세는 단위가 이 압축 함수 호출 한 번입니다.
실패
SHA-1 의 충돌 저항성은 실물로 깨졌습니다. 동일 접두 충돌은 2017년에 만들어졌습니다. 선택 접두 충돌은 2019년에 만들어졌습니다. 그 공격을 적은 논문은 2020년에 나왔습니다.
아래에서는 두 공격이 각각 무엇을 뚫었는지, 무엇은 아직 버티는지, 그리고 그 뒤 표준 기관과 Git 이 어떻게 대응했는지를 봅니다.
같은 SHA-1 값을 갖는 두 PDF
2017년 2월 23일 SHAttered 공격이 첫 SHA-1 충돌을 공개했습니다. 내용이 다른 PDF(Portable Document Format) 파일 두 개가 같은 SHA-1 값을 갖습니다. 두 파일은 공격으로 만든 연속 블록 두 개만 다릅니다. 그런데도 화면에는 서로 다른 그림을 보여 줍니다.
공통 앞부분은 아무 값이 아닙니다. 두 파일이 서로 다른 그림을 띄우도록 미리 골라 둔 값입니다. 공격이 만든 블록 두 개는 그 앞부분 뒤에 옵니다. 동일 접두 충돌은 같은 앞부분과 같은 뒷부분 사이에서 공격 블록만 다른 꼴이라, 뒷부분에 무엇을 붙여도 충돌이 유지됩니다.
뚫리는 것은 해시값에 기대는 서명입니다. 첫 PDF 에 받은 SHA-1 서명이 둘째 PDF 에도 유효한 서명이 됩니다. 임대료가 다른 계약서 두 장을 이렇게 만들면, 상대에게 임대료가 낮은 계약서에 서명하게 해서 임대료가 높은 계약서의 서명을 얻을 수 있습니다.
Git 도 같은 위험을 집니다. 원리상 머리 커밋의 해시가 같은데 내용이 다른 저장소 두 개를 만들 수 있습니다. 하나는 멀쩡한 소스 코드, 다른 하나는 뒷문을 심은 코드입니다. 공격자는 노리는 사용자에게만 골라서 둘째 저장소를 내줄 수 있습니다. 다만 공격자가 그 저장소를 위한 충돌을 직접 계산해야 합니다.
PGP 신뢰망의 신원 위조
SHA-1 is a Shambles 공격이 첫 선택 접두 충돌을 만들었습니다. 시연 대상은 PGP(Pretty Good Privacy)의 신뢰망이었습니다. 신뢰망은 중앙 인증 기관 대신 사용자끼리 서로의 신원 인증서에 서명해 주는 신뢰 모델입니다. 당시 GnuPG(GNU Privacy Guard)의 구판 갈래인 1.4 는 신원 인증에 SHA-1 을 기본으로 썼습니다.
공격자 밥은 키 두 개를 만들었습니다. 두 키는 사용자 이름이 다르지만 인증서의 SHA-1 값은 같습니다. 키 B 는 밥의 정상 키입니다. 키 A 는 앨리스 이름을 단 위조 키입니다.
sequenceDiagram
participant 밥
participant 제3자
participant 다른 사용자
밥->>제3자: 키 B 인증서에 서명해 달라
제3자-->>밥: 키 B 인증서에 SHA-1 서명
Note over 밥: 키 A 인증서가 같은 SHA-1 값이라 서명을 옮겨 붙인다
밥->>다른 사용자: 앨리스 이름으로 서명한 문서
Note over 다른 사용자: 제3자가 서명한 앨리스 키라 믿는다
제3자의 서명은 충돌 덕분에 키 A 에서도 유효합니다. 밥은 앨리스 이름의 키를 쥐었습니다. 그 키는 제3자의 서명까지 받은 상태입니다. 그래서 밥은 앨리스 행세를 하며 아무 문서에나 그 이름으로 서명할 수 있습니다.
이 공격을 만든 연구자들은 2019년 10월 1일 그 충돌을 GnuPG 개발자들에게 알렸습니다. 문제는 CVE(Common Vulnerabilities and Exposures) 번호 CVE-2019-14855 로 등록됐습니다. GnuPG 2.2.18(2019년 11월 25일)부터는 2019년 1월 19일 이후 만든 SHA-1 기반 신원 서명을 무효로 봅니다.
MD5(Message-Digest algorithm 5)에서 2009년부터 실용 수준이던 충돌 공격이 이제 SHA-1 에서도 실용 수준이 됐습니다. 선택 접두 충돌은 서명 체계를 깰 수 있습니다. TLS(Transport Layer Security)·SSH(Secure Shell) 같은 보안 채널 프로토콜의 핸드셰이크 보안도 깨질 수 있습니다. 다만 핸드셰이크 쪽은 충돌을 아주 빨리 만들어 낼 수 있을 때의 이야기입니다.
아직 버티는 것
충돌 공격은 SHA-1 의 모든 쓰임을 위협하지는 않습니다. 역상 저항성은 선택 접두 충돌이 발표된 때에도 깨지지 않은 채였습니다. HMAC-SHA-1 은 비교적 안전해 보입니다.
Git 이 해시에 기대는 성질 가운데 객체 조회와 무결성 확인은 SHA-1 에도 남아 있습니다.
그래도 선택 접두 충돌을 만든 암호학자들은 직접 악용할 증거가 없는 곳에서도 SHA-1 을 쓰지 말라고 권합니다. 대신 SHA-256 이나 SHA-3 을 쓰라고 합니다.
표준 기관의 퇴역 결정
NIST 는 2011년에 SHA-1 을 공식으로 사용 중단 대상(deprecated)으로 정했습니다. 또 전자 서명처럼 충돌 공격이 치명적인 곳에서는 연방 기관이 SHA-1 을 쓰지 말라고 밝혀 왔습니다.
2022년 12월 15일에는 SHA-1 을 2030년 12월 31일까지 단계적으로 폐기하고 SHA-2·SHA-3 계열로 옮긴다고 발표했습니다. 그 시한까지 SHA-1 정의를 빼낸 FIPS(Federal Information Processing Standards) 180-5 를 내놓을 계획입니다.
Git 의 대응
Git 2.13.0 부터는 기본으로 충돌 탐지 SHA-1 을 씁니다. 이 구현은 평소처럼 SHA-1 을 계산합니다. 그때 그 파일에 충돌 공격의 흔적이 있는지도 함께 봅니다. SHAttered 공격은 이 구현에 통하지 않습니다. 그래도 Git 은 SHA-1 자체는 여전히 약하다고 봅니다.
탐지가 보는 것은 교란 벡터입니다. 교란 벡터는 충돌 공격이 메시지에 심는 차이의 패턴입니다. 탐지 라이브러리의 성질은 이렇습니다.
| 항목 | 값 |
|---|---|
| 잡아내는 공격 | 상위 32개 교란 벡터를 쓰는 충돌 공격 전부, 확률 1 |
| 오탐 확률 | 2^-90 보다 작다 |
| 계산 시간 | 보통 SHA-1 의 두 배 미만 |
| 공격이 잡혔을 때 | 두 가지 가운데 하나다. 표지를 세워 응용이 알아서 처리하게 하거나, 진짜 SHA-1 대신 예측할 수 없는 안전 해시를 낸다. 안전 해시 쪽에서는 충돌한 두 파일이 서로 다른 값을 얻는다 |
Git 이 두 가지 가운데 어느 쪽으로 쓰는지는 자료에 없습니다(미확인).
확률 1 로 보장하는 범위는 위 상위 32개를 쓰는 공격입니다. 앞으로 나올 공격에 쓸 만한 대응이 있으리라는 보장은 없습니다.
그래서 Git 은 SHA-1 계열에서 아예 벗어나기로 했습니다. Git 이 해시 함수에 요구하는 것은 충돌 저항성과 제2 역상 저항성입니다. Git 은 2018년 말 그 후계로 SHA-256 을 골랐습니다.
쓰임별로 갈리는 위험
지금까지 본 것을 쓰임별로 모으면 이렇습니다. 가르는 기준은 하나입니다. 같은 해시값을 갖는 가짜를 누가 일부러 만들 수 있는 상황인가입니다.
| 쓰임 | 기대는 성질 | 지금 상태 |
|---|---|---|
| 문서 서명 · 인증서 서명 | 충돌 저항성 | 깨졌다. 같은 값을 갖는 두 PDF 가 있다 |
| PGP 신원 인증 | 충돌 저항성 | 깨졌다. 선택 접두 충돌로 위조한 키가 있다 |
| Git 객체 이름 | 충돌 저항성 · 제2 역상 저항성 | 충돌 탐지로 알려진 공격을 막고, SHA-256 으로 옮기기로 했다 |
| 무결성 확인 | 바뀐 내용은 다른 값을 낸다 | 공격자가 없으면 그대로 쓸 수 있다 |
| HMAC-SHA-1 (TOTP 등) | 비밀 키 없이는 값을 못 맞춘다 | 비교적 안전해 보인다 |
| 주어진 해시값에서 메시지 찾기 막기 | 역상 저항성 | 2020년 기준으로 깨지지 않았다 |
관련 항목
SHA-1 이 속하는 상위 분류
해시 함수 · 암호학적 해시 함수 · 해시 · 다이제스트
SHA-1 과 같은 계보의 해시 함수
SHA-0 · MD5 · MD4 · SHA-2
SHA-1 을 대신하는 해시 함수
SHA-256 · SHA-512 · SHA-3 · BLAKE2
SHA-1 이 지켜야 할 성질
충돌 저항성 · 역상 저항성 · 제2 역상 저항성 · 눈사태 효과
SHA-1 을 깨는 공격
충돌 공격 · 생일 공격 · 생일 문제 · 동일 접두 충돌 · 선택 접두 충돌 · SHAttered · 길이 확장 공격
SHA-1 을 이루는 계산 얼개
머클-담고르 구조 · 압축 함수 · 패딩 · 비트 연산 · 순환 이동 · XOR · 메시지 스케줄
SHA-1 로 이름표와 무결성을 다루는 도구
Git · 커밋 · 버전관리 · 머클 트리 · 체크섬 · 무결성 · 내용 주소 지정
SHA-1 이 물러난 보안 용도
전자 서명 · 인증서 · 인증 기관 · TLS · SSH · PGP · GnuPG · 신뢰망
SHA-1 을 안에 넣어 쓰는 인증 방식
HMAC · HOTP · TOTP · 일회용 비밀번호
SHA-1 의 퇴역을 정한 기관과 문서
NIST · FIPS · FIPS 180-4 · CVE
비밀번호 저장에 SHA-1 대신 쓰는 함수
다른 이름: SHA1 · Secure Hash Algorithm 1