암호학적 해시 함수란 무엇인가: 일방향성의 원리와 쓰임새

암호학적 해시 함수란 무엇인가: 일방향성의 원리와 쓰임새

비밀번호를 다루거나, 파일을 다운로드한 뒤 무결성을 검사하거나, JWT 서명을 살펴볼 때 어김없이 등장하는 친구가 있습니다. 바로 암호학적 해시 함수입니다. 해시의 주된 목적은 데이터를 숨겼다가 되찾는 것이 아니라, 짧은 지문을 만들어 같은 데이터인지 또는 중간에 바뀌었는지 확인하는 데 있습니다. 해시 함수는 흔히 “단방향 암호화”라고 불리지만, 암호학에서는 일방향 해시 함수나 일방향성이라고 표현하는 편이 더 정확합니다. 정확히 말하면 해시는 암호화와도 다른 변환인데요. 암호화는 키를 사용해 평문과 암호문을 오갈 수 있지만, 해시는 원본으로 되돌리는 절차가 없습니다.

SHA-256이나 bcrypt 같은 이름은 익숙한데, 막상 “해시는 정확히 무엇이고 어디까지 보장하나요”라고 물으면 답이 잘 안 나오는 경우가 많죠.

이번 글에서는 보안을 목적으로 설계된 암호학적 해시 함수가 어떤 약속을 지키는지, 흔히 쓰이는 알고리즘들이 어떻게 다른지, 그리고 실무에서 자주 만나는 쓰임새를 한 번에 정리해 보겠습니다.

암호학적 해시 함수가 지키는 약속

암호학적 해시 함수는 임의의 길이를 가진 데이터를 입력으로 받아, 정해진 길이의 짧은 출력으로 변환하는 함수입니다. 이 출력을 해시값(hash value), 다이제스트(digest), 또는 줄여서 해시라고 부릅니다. 얼핏 보면 그냥 압축처럼 들리지만, 암호학적 해시 함수는 몇 가지 약속을 더 지켜야 합니다.

첫째는 결정성(determinism)입니다. 같은 입력에는 항상 같은 출력을 돌려준다는 뜻이라, 한 시간 뒤에 같은 파일을 해시해도 결과가 동일하죠. 둘째는 일방향성(one-wayness)입니다. 출력값만 보고 원래 입력을 역으로 알아낼 수 없도록 설계되어 있습니다. 셋째는 충돌 저항성(collision resistance)입니다. 서로 다른 두 입력이 같은 출력을 내는 일을 의도적으로 만들기가 매우 어려워야 합니다.

충돌 저항성이 충돌 자체를 완전히 없앤다는 뜻은 아닙니다. 입력의 길이에는 제한이 없지만 출력은 고정된 길이이므로, 서로 다른 입력이 같은 해시값에 도착하는 경우는 수학적으로 반드시 존재합니다. 현대적인 암호학적 해시 함수의 목표는 충돌을 없애는 게 아니라 찾아내는 데 현실적으로 감당할 수 없는 계산량이 들게 만드는 것입니다.

이상적인 n비트 해시 함수에서 충돌을 찾는 데 필요한 계산량은 생일 역설(birthday paradox) 때문에 대략 2n/2번입니다. SHA-256이라면 약 2128번의 시도가 필요하다고 보는 셈이죠. 새로운 공격법이 이 계산량을 크게 낮춘다면 출력 길이가 그대로여도 더는 안전한 충돌 저항성을 제공한다고 볼 수 없습니다.

일방향 변환
"hello world"

       ▼ SHA-256
b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9

이 셋이 깨지면 암호학적 해시 함수가 약속하는 모든 보안적 쓸모도 같이 무너집니다. 그래서 새 암호학적 해시 알고리즘이 등장할 때마다 학계는 이 세 성질이 충분히 강한지를 검증하고, 약점이 발견되면 알고리즘이 빠르게 권장 목록에서 빠지게 됩니다.

또 하나 자주 언급되는 성질이 눈사태 효과(avalanche effect)입니다. 입력의 한 비트만 바뀌어도 출력이 거의 절반 가까이 바뀌도록 만드는 성질인데, 이 효과가 충돌 저항성과 일방향성을 떠받치는 토대가 됩니다. 원본 메시지의 작은 변화가 해시에 즉각 반영되니, 해시값을 비교하는 것만으로도 위변조를 잡아낼 수 있는 셈이죠.

암호학적 해시와 비암호학적 해시는 다르다

비슷해 보여서 자주 헷갈리는 것이 자료구조에서 쓰는 비암호학적 해시 함수입니다. HashMap이나 Set 같은 자료구조에도 해시 함수가 들어가지만, 이쪽은 빠른 분포가 목적이라 일방향성이나 충돌 저항성이 강하지 않습니다. 오히려 빠르고 분포만 적당히 고르면 충분하다는 다른 기준이 적용되죠.

해시 테이블에서는 MurmurHash, xxHash, FNV-1a처럼 속도와 분산 성능에 초점을 맞춘 비암호학적 해시 함수가 주로 사용됩니다. 외부 입력을 키로 받는 언어 런타임은 의도적인 해시 충돌 공격을 막기 위해 SipHash처럼 비밀 키를 사용하는 알고리즘을 쓰기도 합니다. 이런 해시는 같은 키를 같은 슬롯에 넣고 다른 키는 여러 슬롯에 고르게 흩뿌리는 데에 초점이 있어 계산이 매우 빠릅니다. 보안에 쓸 수는 없지만 본래 목적인 빠른 검색에서는 훌륭한 도구이니, “해시”라는 단어가 같다고 해서 같은 자리에 쓸 수 있다고 오해하지 않는 것이 첫걸음입니다.

현재 많이 쓰이는 알고리즘

새로운 시스템에서 가장 자주 선택되는 것은 SHA-2 계열입니다. 그중에서도 SHA-256은 출력 길이가 256비트(16진수 64자)이고 거의 모든 언어와 플랫폼에서 지원해 기본값으로 삼기 좋습니다. 파일 무결성 검사부터 PKCE의 code challenge, 블록체인의 블록 해시까지 광범위하게 쓰이죠. NIST의 해시 함수 정책도 서로 다른 시스템 사이의 호환성이 필요한 용도라면 최소한 SHA-256을 구현하도록 권고합니다.

같은 SHA-2 계열의 SHA-384는 384비트(16진수 96자), SHA-512는 512비트(16진수 128자)의 해시값을 만듭니다. 출력이 길수록 충돌을 찾는 데 필요한 계산량은 늘어나지만, 언제나 가장 긴 알고리즘을 고를 필요는 없습니다. 프로토콜이 요구하는 보안 강도와 호환성을 먼저 보고 선택하면 됩니다.

SHA-3도 현재 사용할 수 있는 표준 알고리즘입니다. SHA-2와 다른 스펀지 구조(sponge construction)를 사용해 한 계열에서 약점이 발견되더라도 다른 선택지를 남겨 둡니다. 다만 이름과 달리 SHA-2를 몰아낸 후속 버전은 아닙니다. NIST도 SHA-2에서 SHA-3로 전환할 필요는 없다고 밝힙니다. 특별한 요구가 없다면 널리 지원되는 SHA-256을 쓰고, 다른 내부 구조나 SHA-3 지원이 필요한 환경에서 SHA-3를 선택하면 됩니다.

예전에 많이 썼지만 피해야 할 알고리즘

MD5와 SHA-1은 과거에는 파일 검증과 인증서, 디지털 서명에 널리 쓰였지만 새로운 보안 설계에서는 제외해야 합니다. 두 알고리즘이 결정성이나 일방향성까지 잃은 것은 아닙니다. 문제는 서로 다른 입력에서 같은 해시값을 만드는 충돌 공격이 실용적인 위협이 되었다는 점입니다.

MD5는 128비트(16진수 32자)의 해시값을 만듭니다. 이제는 공격자가 내용이 다른 두 파일에 같은 MD5 해시를 붙이는 충돌을 의도적으로 만들 수 있는데요. 공격자가 같은 해시값을 갖는 정상 파일과 악성 파일을 미리 만든다면, 정상 파일을 대상으로 받은 서명을 악성 파일에 붙여도 검증을 통과시킬 수 있습니다. IETF도 RFC 6151에서 충돌 저항성이 필요한 용도에는 MD5를 사용할 수 없다고 명시합니다.

SHA-1은 160비트(16진수 40자)로 MD5보다 출력이 길지만 충돌 저항성이 깨졌다는 결론은 같습니다. 실제 충돌 공격이 공개된 뒤 NIST는 새로운 디지털 서명에 SHA-1을 쓰지 말라고 이미 권고했습니다. NIST의 전환 계획에 따르면 2030년 말까지 암호학적 보호에 쓰이는 SHA-1을 자체 표준에서 모두 퇴출할 예정입니다. 2030년은 그때까지 안전하다는 보증이 아니라 기존 시스템이 전환을 마쳐야 하는 기한입니다. 새 시스템이라면 기다릴 이유 없이 SHA-256이나 SHA-3를 선택해야 합니다.

Git 저장소에서 SHA-1 기반의 40자 객체 ID(object ID)를 마주치거나 오래된 파일이 MD5 체크섬을 제공할 수는 있습니다. 이는 기존 데이터와의 호환성이나 우발적인 전송 오류 확인을 위한 흔적이지, 두 알고리즘이 공격자를 상대하는 보안 용도로 안전하다는 뜻은 아닙니다. 기존 식별자를 읽어야 할 때와 새로운 보안 기능에 알고리즘을 선택할 때를 구분해야 합니다.

무결성 검사부터 식별자까지

가장 흔한 쓰임새가 무결성 검사입니다. 파일을 다운로드한 뒤 해시값이 제공된 값과 일치하는지 비교하면, 중간에 변조되지 않았다는 사실을 확인할 수 있죠. 오픈 소스 배포물이나 컨테이너 이미지가 거의 빠짐없이 SHA-256 해시를 함께 공개하는 이유입니다. 이처럼 원본과 비교하려고 공개하는 짧은 값을 흔히 체크섬이라고 부릅니다.

다운로드 파일의 해시 비교
shasum -a 256 downloaded-file.tar.gz
# 출력된 값과 배포처가 공개한 값을 비교

식별자로도 자주 쓰이는데, Git 커밋 해시가 대표적인 예입니다. Git은 객체 모델에 따라 파일 내용을 블롭 객체(blob object)에, 디렉터리 구조를 트리 객체(tree object)에 저장합니다. 커밋 객체(commit object)에는 최상위 트리의 ID, 부모 커밋의 ID, 작성자와 커미터(committer) 정보, 커밋 메시지가 들어갑니다. Git은 객체의 종류와 길이가 담긴 헤더에 내용을 붙여 해시하고, 그 값을 객체 ID로 사용합니다.

파일 하나를 고치면 블롭의 해시가 바뀌고, 이를 가리키는 트리와 커밋의 해시도 차례로 달라집니다. 커밋은 부모 커밋의 ID까지 담고 있어서 과거 커밋을 바꾸면 그 뒤에 이어지는 커밋의 ID도 모두 달라지죠. 신뢰할 수 있는 경로로 미리 알고 있는 커밋 ID와 비교하면 파일이나 이력이 달라졌는지 알아낼 수 있는 이유입니다. SHA-1 기반 저장소에서 온전한 객체 ID가 40자로 표시되는 것도 160비트 출력을 16진수로 나타낸 결과입니다.

객체 생성과 해시 계산, 원격 저장소와 객체를 주고받는 일은 git commit, git clone, git fetch, git push 같은 명령이 알아서 처리합니다. 사용자는 특정 커밋을 가리키거나 비교할 때 짧은 커밋 ID를 쓰는 정도라서 해시 알고리즘을 직접 관리할 일은 거의 없습니다. Git의 해시 함수 전환 문서에 따르면 SHA-256 저장소도 지원하지만 객체 ID와 내부 참조 형식이 달라집니다. 이전 버전의 Git은 SHA-256 저장소를 읽지 못할 수 있어 저장소와 도구의 호환성을 함께 확인해야 합니다.

여기서 커밋 ID는 내용이 바뀌었는지 확인하는 식별자이지 작성자의 신원을 보증하는 디지털 서명은 아닙니다. 누가 커밋했는지까지 검증하려면 서명된 커밋이나 태그처럼 별도의 신뢰 수단이 필요합니다.

캐시 키나 정적 자산 파일명에 콘텐츠 해시를 박아두는 패턴도 흔합니다. Cache-Control 글에서 봤듯, 파일명 해시는 캐시 무효화 전략의 핵심 도구로도 쓰입니다.

디지털 서명에서도 해시가 핵심 역할을 합니다. 큰 메시지에 직접 서명하기는 비싸기 때문에, 메시지의 해시를 먼저 만들고 그 짧은 해시에 서명하는 식인데요. 받는 쪽도 같은 방식으로 해시를 만들어 검증하니, 메시지가 한 글자만 바뀌어도 해시가 완전히 달라져 서명 검증이 실패합니다.

웹에서 자주 보이는 또 다른 응용이 SRI(Subresource Integrity)입니다. HTML이 외부 CDN에서 자바스크립트나 CSS를 불러올 때, integrity 속성에 해시를 적어 두면 브라우저가 받은 파일의 해시를 비교해 변조 여부를 검증합니다.

SRI 속성으로 무결성 검증
<script
  src="https://cdn.example.com/lib.js"
  integrity="sha384-abc123..."
  crossorigin="anonymous"
></script>

CDN 자체가 침해되어도 해시가 안 맞으면 브라우저가 스크립트를 실행하지 않아, 공급망 공격에 대한 한 겹의 방어막이 되어 줍니다.

비밀번호에는 전용 해시 함수가 필요합니다

비밀번호는 AES처럼 복호화할 수 있는 암호문으로 저장하면 안 됩니다. 로그인 서버에는 원래 비밀번호를 알아낼 기능이 필요하지 않고, 사용자가 입력한 값이 저장된 결과와 일치하는지만 확인하면 되기 때문인데요.

그렇다고 SHA-256이나 SHA-512를 한 번 계산해 저장하는 것도 안전하지 않습니다. 범용 해시 함수의 빠른 속도 덕분에 데이터베이스를 훔친 공격자도 많은 비밀번호 후보를 짧은 시간에 시험할 수 있습니다.

비밀번호 저장에는 Argon2id, scrypt, bcrypt, PBKDF2처럼 의도적으로 계산 비용을 높인 전용 함수를 사용합니다. 사용자마다 다른 소금과 조정 가능한 비용 매개변수를 적용하고, 직접 조합하기보다 검증된 라이브러리의 고수준 API에 맡기는 것이 안전합니다.

저장 문자열의 구성과 로그인 검증 흐름, 알고리즘 선택 기준은 Argon2id로 비밀번호를 안전하게 저장하기에서 자세히 살펴보세요.

콘텐츠 주소 지정과 머클 트리

해시의 결정성을 응용하는 흥미로운 패턴이 콘텐츠 주소 지정(content addressing)입니다. 파일 자체의 해시를 그 파일의 주소로 쓰는 방식인데, 같은 내용이라면 어디에 있어도 같은 주소가 되는 셈이죠. 중복 저장을 자연스럽게 막아 주고, 주소만 알면 어느 노드에서 받든 위변조를 검증할 수 있어서 IPFS, Git, Docker 이미지 레지스트리 같은 시스템이 이 패턴을 쓰고 있습니다.

여러 데이터의 무결성을 효율적으로 묶는 도구가 머클 트리(Merkle tree)입니다. 데이터를 잘게 나눠 각 조각의 해시를 만들고, 그 해시들을 둘씩 묶어 다시 해시하는 식으로 트리를 쌓아 올리는데요. 트리의 꼭대기에 있는 루트 해시 하나만 있으면, 어느 한 조각이 바뀌어도 루트가 달라져 즉시 알아챌 수 있습니다. 블록체인의 트랜잭션 검증, 분산 파일 시스템의 동기화, Git의 트리 객체 같은 자리에 머클 트리가 자리 잡고 있죠.

HMAC으로 메시지를 인증합니다

해시 기반 메시지 인증 코드(Hash-based Message Authentication Code, HMAC)는 해시 함수에 비밀 키를 결합해 만드는 도구입니다. 같은 메시지라도 비밀 키가 다르면 다른 출력을 내고, 받는 쪽이 같은 키로 같은 결과를 만들 수 있는지를 확인해 메시지가 변조되지 않았는지를 검증하죠.

HMAC 계산
HMAC(key, message) = H((key ⊕ opad) || H((key ⊕ ipad) || message))

JWT의 HS256 알고리즘이 정확히 이 방식이고, 쿠키에 서명을 붙이거나 API 요청에 신원을 증명할 때도 자주 등장합니다. 공개 키 기반 서명보다 가볍고 빠르지만, 양쪽이 같은 비밀 키를 공유해야 하므로 키 관리가 다른 이슈가 됩니다. 이 키 관리 문제는 다음에 다룰 대칭키 암호화 글에서 깊이 살펴보게 됩니다.

어떤 해시를 골라야 할까

선택지가 많아 처음에는 헷갈리지만, 자리가 정해지면 후보가 거의 한두 개로 좁혀집니다.

일반적인 무결성 검사, 식별자, 캐시 키, 머클 트리에는 SHA-256이 기본 선택지입니다. 출력 길이가 충분하고 거의 모든 언어와 라이브러리가 표준으로 제공하니, 특별한 이유가 없다면 SHA-256을 그대로 쓰는 것이 안전한 출발점이죠. 더 강한 보안이 요구되거나 출력 길이가 더 필요한 경우에만 SHA-384 / SHA-512를 골라 갑니다.

비밀번호 저장에는 범용 암호학적 해시가 아니라 Argon2id 같은 전용 알고리즘을 골라야 합니다. 알고리즘별 선택 기준과 비용을 올리는 방법은 앞서 소개한 비밀번호 저장 글에서 확인할 수 있습니다.

메시지 인증에는 널리 지원되는 HMAC-SHA-256이 무난한 선택입니다. JWTHS256과 웹훅, 여러 API의 요청 인증에서 이 조합을 만날 수 있습니다. 키를 관리하고 재전송 공격까지 막는 방법은 HMAC 글에서 더 자세히 살펴보세요.

마치며

암호학적 해시 함수는 단순해 보이지만 무결성 검사, 식별, 디지털 서명, 비밀번호 저장까지 굉장히 넓은 자리에 자리 잡고 있습니다. “같은 입력은 같은 출력, 출력에서 입력은 못 찾는다”는 두 줄짜리 약속만 손에 익으면, 보안 관련 코드를 읽을 때 어떤 자리에 어떤 해시가 어울리는지를 가늠할 수 있게 되는데요. 다만 비밀번호처럼 특별한 조건이 있는 자리에서는 범용 암호학적 해시 대신 전용 알고리즘을 쓰는 것이 출발점이라는 점만 기억해두면 좋습니다.

다른 암호 도구가 궁금하다면 대칭키 암호화비대칭키 암호화 글을 이어서 읽어 보세요. 암호학적 해시 함수의 더 깊은 이론이 궁금하다면 NIST의 해시 함수 페이지를 출발점으로 추천합니다.

This work is licensed under CC BY 4.0CCBY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord