sha256(비밀키 + 메시지)가 MAC이 아닌 이유
비밀키를 몰라도 메시지를 늘리고 서명을 다시 만들 수 있습니다. 길이 확장 공격을 실제로 성공시켜 보고 HMAC과 SHA-3가 이를 어떻게 막는지 정리했습니다.
메시지에 서명을 붙이는 가장 단순한 방법은 비밀키와 메시지를 이어서 해싱하는 것입니다.
mac = sha256(secret + message)
동작은 합니다. 비밀키를 모르면 값을 만들 수 없어 보입니다. 그런데 만들 수 있습니다.
실제로 해 보면
서버가 이렇게 만들었다고 하겠습니다.
secret : sk_live_7f3a9c21 (16바이트, 공격자는 모름)
message : user=alice&role=viewer
MAC : 9eefd4164c1c21cfe861494e8c8ad441e69acfa8f4d558700bc965475f417d44
공격자가 아는 것은 메시지와 MAC, 그리고 비밀키의 길이뿐입니다. 값은 모릅니다. 이 상태에서 &role=admin을 붙인 메시지의 MAC을 계산할 수 있습니다.
붙인 값 : &role=admin
글루 패딩 : 26바이트 (8000000000000000...0130)
위조 MAC : b8cd1bfab0b404bfa763aec54a04dfa8bee367af7d4f32a8b72336fdcc448789
서버가 그 메시지를 받아 실제로 계산하면 같은 값이 나옵니다.
서버 계산 : b8cd1bfab0b404bfa763aec54a04dfa8bee367af7d4f32a8b72336fdcc448789
일치 : true
비밀키를 끝까지 몰랐는데 검증을 통과합니다.
왜 되는가
SHA-256은 입력을 64바이트 블록으로 잘라 하나씩 처리하고, 매 블록마다 8개의 32비트 값으로 된 내부 상태를 갱신합니다. 마지막 블록까지 처리하고 나면 그 내부 상태가 그대로 출력됩니다.
즉 다이제스트는 계산이 끝난 시점의 상태 전체입니다. 감춰 두는 부분이 없습니다.
다이제스트 9eefd416 4c1c21cf e861494e 8c8ad441 e69acfa8 f4d55870 0bc96547 5f417d44
└─ 이 여덟 개가 곧 내부 상태 ─┘
공격자는 이 값을 상태로 되돌려 넣고, 원래 입력이 거기서 끝난 것처럼 이어서 해싱하면 됩니다. 원래 입력이 무엇이었는지는 알 필요가 없습니다. 길이만 알면 됩니다.
길이가 필요한 이유는 패딩 때문입니다. 해시는 입력 끝에 0x80, 0들, 그리고 마지막 8바이트에 비트 길이를 붙입니다. 공격자는 원래 입력이 받았을 패딩을 그대로 재현해 메시지에 끼워 넣어야 하고, 그러려면 원래 길이를 알아야 합니다. 위 예에서 글루 패딩 26바이트가 그것입니다.
비밀키 길이를 모르면? 16, 17, 18…을 차례로 시도하면 됩니다. 몇십 번 안에 맞습니다.
같은 문제를 가진 해시
Merkle–Damgård 구조를 쓰고 내부 상태를 전부 출력하는 해시가 전부 해당합니다.
| 해시 | 길이 확장 |
|---|---|
| MD5 | 가능 |
| SHA-1 | 가능 |
| SHA-256 | 가능 |
| SHA-512 | 가능 |
| SHA-224, SHA-512/256 | 불가 — 상태의 일부만 출력한다 |
| SHA-3 계열 | 불가 — 스펀지 구조 |
| BLAKE2, BLAKE3 | 불가 |
SHA-224가 안 되는 이유가 구조를 잘 보여 줍니다. 내부 상태는 256비트인데 출력은 224비트입니다. 잘려 나간 32비트를 공격자가 모르므로 상태를 복원할 수 없습니다.
SHA-3는 스펀지 구조라 매 단계에서 상태의 일부(capacity)를 출력에 내보내지 않습니다. 그래서 sha3_256(비밀키 + 메시지)는 그 자체로 온전한 MAC이 됩니다. 물론 그 목적으로 설계된 것은 KMAC입니다.
HMAC이 막는 방식
HMAC은 해시를 두 번 겹칩니다.
HMAC(K, m) = H( (K ^ opad) || H( (K ^ ipad) || m ) )
안쪽 해시의 결과는 32바이트 고정입니다. 바깥 해시의 입력은 키 블록 + 32바이트로 길이가 정해져 있어서, 공격자가 뒤에 무언가를 이어 붙일 자리가 없습니다. 바깥 해시의 다이제스트를 상태로 복원해도 그 뒤에 올 것이 없습니다.
그래서 SHA-256 위에 얹은 HMAC은 SHA-256의 길이 확장과 무관합니다. 해시 자체를 바꿀 필요가 없습니다.
무엇을 쓰면 되나
- HMAC-SHA256 — 가장 널리 쓰이고 모든 언어에 표준 라이브러리가 있습니다
- KMAC128 / KMAC256 — SHA-3 기반. 새 설계에서 SHA-3를 쓰기로 했다면 이쪽입니다
- BLAKE3의 키드 모드 — 해시 자체가 키를 받습니다
- Ed25519 서명 — 검증자가 비밀키를 갖지 말아야 한다면 MAC이 아니라 서명입니다
직접 만든 hash(key + msg)는 어느 경우에도 답이 아닙니다.
확인 순서
- 코드에서
sha256(secret + ...),md5(key . data)같은 이어 붙이기를 검색합니다 hash(메시지 + 비밀키)순서도 안전하지 않습니다. 길이 확장은 막히지만 해시 충돌이 그대로 MAC 충돌이 되므로, 충돌 저항성이 깨진 해시에서는 무너집니다- 검증할 때 타이밍 안전 비교를 쓰는지 봅니다 —
hmac.compare_digest,crypto.timingSafeEqual - 서명 대상 문자열이 모호하지 않은지 봅니다.
a=1&b=23과a=1&b=2+3이 같은 바이트가 되면 필드 경계가 무의미해집니다
tools.onuel.dev의 HMAC 생성기에서 같은 키와 메시지로 값을 만들어 서버 구현과 대조할 수 있습니다. SHA-256과 SHA3-256에 같은 입력을 넣어 보면 두 계열이 전혀 다른 값을 내는 것을 확인할 수 있고, Keccak-256은 SHA3-256과 또 다릅니다.
- #해시
- #MAC
- #보안