목차
지금까지는 감추는 이야기였다. 여기서는 방향을 튼다 - 감추지 않고 바뀌었는지만 확인하는 도구, 해시다.
되돌릴 수 없는 지문
해시 함수는 임의 길이의 입력을 고정 길이의 값으로 뭉갠다. 같은 입력은 늘 같은 값을, 조금만 달라도 완전히 다른 값을 낸다.
"hello" → 2cf24dba5fb0a30e26e83b2ac5b9e29e... (SHA-256, 항상 이 값)
"hellO" → d8ad3f0e9f4c1b7a2e6f... (한 글자 바꿨는데 완전히 딴판)이 값을 해시·다이제스트·지문이라 부른다. 암호화가 푸는 문제 글에서 봤듯 되돌릴 수 없다 - 지문에서 원본을 복원하는 게 아니라, 원본을 다시 해시해 지문이 같은지 맞춰볼 뿐이다.
좋은 해시의 세 가지 성질
암호용 해시(SHA-256 등)는 세 가지를 만족해야 한다.
- 단방향성 - 지문에서 원본을 되찾을 수 없다.
- 눈사태 효과 - 입력이 1비트만 바뀌어도 출력의 절반쯤이 뒤집힌다. 그래서 “비슷한 입력 → 비슷한 지문” 같은 실마리가 안 남는다.
- 충돌 저항성 - 다른 두 입력이 같은 지문을 내는 쌍을 찾기가 사실상 불가능하다.
셋째가 왜 중요한지가 다음 이야기다.
충돌은 반드시 있다, 다만 못 찾을 뿐
입력은 무한한데 출력은 고정 길이(유한)다. 비둘기집 원리로, 서로 다른 입력이 같은 지문을 내는 경우(충돌)는 반드시 존재한다. 없앨 수가 없다.
그래서 해시의 안전은 “충돌이 없음”이 아니라 **“충돌을 일부러 만들어낼 수 없음”**이다. 좋은 해시는 같은 지문을 내는 두 입력을 찾으려면 우주 나이가 걸린다.
이게 깨진 예가 MD5·SHA-1이다. 연구자들이 원하는 충돌을 실제로 만들어냈다 - 서로 다른 두 파일이 같은 MD5를 갖게. 그래서 이 둘은 보안 용도에서 퇴출됐다. 지금은 SHA-256(SHA-2 계열) 이상을 쓴다.
어디에 쓰나
해시의 쓸모는 “같은지 빠르게 확인”이다.
- 파일 무결성 - 내려받은 파일의 해시가 게시된 값과 같으면 도중에 안 깨졌다.
- 변경 감지·식별 - git의 커밋 ID가 내용 해시다. 내용이 바뀌면 ID가 바뀐다.
- 중복 제거 - 같은 해시면 같은 내용이니 한 벌만 저장한다.
여기까지는 실수로 바뀐 것을 잡는다. 그런데 일부러 바꾸는 상대는 어떨까.
해시만으로는 위조를 못 막는다
메시지와 그 해시를 함께 보낸다고 하자. 중간자가 메시지를 고치면?
보냄: [ 메시지 ] + [ 해시 ]
공격자가 메시지를 바꾸고
→ 바뀐 메시지의 해시를 "다시 계산해" 붙인다
받음: [ 바뀐 메시지 ] + [ 앞뒤 맞는 해시 ] ← 받는 쪽은 통과시킨다해시 계산법은 공개돼 있으니 누구나 다시 계산할 수 있다. 그래서 해시는 “실수로 안 바뀌었나”는 증명해도 “악의로 안 바뀌었나”는 증명하지 못한다. 빠진 건 하나 - 아무나 못 만들게 할 비밀이다.
HMAC: 비밀키를 섞은 지문
해시에 둘만 아는 비밀키를 섞어 지문을 만든다. 이게 HMAC이다.
비밀키를 모르면 바뀐 메시지에 맞는 지문을 만들 수 없다. 그래서 HMAC은 두 가지를 한 번에 준다 - 무결성(안 바뀌었다)과 진짜 보낸 사람(비밀키를 아는 쪽이 만들었다).
다만 비밀키를 양쪽이 나눠 갖는 대칭 방식이라, “둘 중 누가 만들었는지”까지는 못 가린다 - 둘 다 만들 수 있으니까. 그 한 걸음(부인 방지)은 다음 글의 디지털 서명이 맡는다.
왜 그냥 이어 붙이면 안 되나
“비밀키랑 메시지를 그냥 이어서 해시하면 되잖아?” 싶지만, 그렇게 하면(해시(비밀키 + 메시지)) 길이 확장 공격에 샌다 - 일부 해시 구조에선 원본 비밀키를 몰라도 뒤에 내용을 덧붙인 유효한 지문을 만들 수 있다.
HMAC은 그걸 막게 비밀키를 두 번, 정해진 방식으로 섞도록 설계됐다. 세부는 넘겨도 되지만 교훈은 분명하다 - 직접 조합하지 말고 HMAC을 쓴다. 암호화가 푸는 문제 글에서 말한 “직접 만들지 마라”가 여기서도 그대로다.
비교도 한 글자씩 하면 샌다
지문을 검증할 때 받은 지문과 계산한 지문을 비교하는데, 흔한 문자열 비교는 다른 글자가 나오는 순간 멈춘다. 그러면 “몇 글자까지 맞았나”가 응답 시간 차이로 새어, 공격자가 한 글자씩 맞춰갈 수 있다(타이밍 공격).
그래서 지문 비교는 길이에 상관없이 늘 같은 시간이 걸리는 방식(상수 시간 비교)을 쓴다. 라이브러리가 compare_digest 같은 함수로 제공한다 - 직접 ==로 비교하지 않는다.
실무에서: 서명된 요청
HMAC은 시스템 사이 통신에 흔하다.
- 웹훅 검증 - 결제사가 보내는 알림에 HMAC 지문을 붙인다. 받는 쪽은 공유한 비밀키로 다시 계산해 맞으면 “진짜 그 결제사가, 안 바뀐 채로” 보냈다고 믿는다.
- JWT의 HS256 - 토큰의 서명 조각이 HMAC이다. 그래서 그 글의 “서명은 잠그는 게 아니라 봉인이다”가 정확히 이 이야기다 - 감춘 게 아니라 바뀌면 티 나게 한 것.
- 지문 알고리즘은 SHA-256 이상, 비교는 상수 시간으로.
정리
- 해시는 되돌릴 수 없는 고정 길이 지문 - 단방향·눈사태·충돌 저항.
- 충돌은 반드시 있지만 좋은 해시는 그걸 못 찾게 한다. MD5·SHA-1은 찾혀서 퇴출, 지금은 SHA-256 이상.
- 해시만으론 악의적 위조를 못 막는다(누구나 다시 계산하니까). 비밀키를 섞은 HMAC이 무결성 + 진짜 보낸 사람을 준다.
- HMAC은 대칭이라 부인 방지는 못 한다 - 그건 서명의 몫이다.
다음 글은 개인키로 서명하고 공개키로 검증하는 디지털 서명, 그리고 그 공개키를 믿게 해주는 인증서로 간다.