← Posts

Cryptography · Hash · HMAC

해시와 HMAC - 바뀌었는지를 안다

해시는 되돌릴 수 없는 지문이다. 충돌은 반드시 있지만 못 찾을 뿐. 그런데 해시만으론 위조를 못 막아서, 비밀키를 섞은 HMAC이 필요하다.

목차
  1. 되돌릴 수 없는 지문
  2. 좋은 해시의 세 가지 성질
  3. 충돌은 반드시 있다, 다만 못 찾을 뿐
  4. 어디에 쓰나
  5. 해시만으로는 위조를 못 막는다
  6. HMAC: 비밀키를 섞은 지문
  7. 왜 그냥 이어 붙이면 안 되나
  8. 비교도 한 글자씩 하면 샌다
  9. 실무에서: 서명된 요청
  10. 정리

지금까지는 감추는 이야기였다. 여기서는 방향을 튼다 - 감추지 않고 바뀌었는지만 확인하는 도구, 해시다.

되돌릴 수 없는 지문

해시 함수는 임의 길이의 입력을 고정 길이의 값으로 뭉갠다. 같은 입력은 늘 같은 값을, 조금만 달라도 완전히 다른 값을 낸다.

plaintext
"hello"   →  2cf24dba5fb0a30e26e83b2ac5b9e29e...   (SHA-256, 항상 이 값)
"hellO"   →  d8ad3f0e9f4c1b7a2e6f... (한 글자 바꿨는데 완전히 딴판)

이 값을 해시·다이제스트·지문이라 부른다. 암호화가 푸는 문제 글에서 봤듯 되돌릴 수 없다 - 지문에서 원본을 복원하는 게 아니라, 원본을 다시 해시해 지문이 같은지 맞춰볼 뿐이다.

좋은 해시의 세 가지 성질

암호용 해시(SHA-256 등)는 세 가지를 만족해야 한다.

  • 단방향성 - 지문에서 원본을 되찾을 수 없다.
  • 눈사태 효과 - 입력이 1비트만 바뀌어도 출력의 절반쯤이 뒤집힌다. 그래서 “비슷한 입력 → 비슷한 지문” 같은 실마리가 안 남는다.
  • 충돌 저항성 - 다른 두 입력이 같은 지문을 내는 쌍을 찾기가 사실상 불가능하다.

셋째가 왜 중요한지가 다음 이야기다.

충돌은 반드시 있다, 다만 못 찾을 뿐

입력은 무한한데 출력은 고정 길이(유한)다. 비둘기집 원리로, 서로 다른 입력이 같은 지문을 내는 경우(충돌)는 반드시 존재한다. 없앨 수가 없다.

그래서 해시의 안전은 “충돌이 없음”이 아니라 **“충돌을 일부러 만들어낼 수 없음”**이다. 좋은 해시는 같은 지문을 내는 두 입력을 찾으려면 우주 나이가 걸린다.

이게 깨진 예가 MD5·SHA-1이다. 연구자들이 원하는 충돌을 실제로 만들어냈다 - 서로 다른 두 파일이 같은 MD5를 갖게. 그래서 이 둘은 보안 용도에서 퇴출됐다. 지금은 SHA-256(SHA-2 계열) 이상을 쓴다.

어디에 쓰나

해시의 쓸모는 “같은지 빠르게 확인”이다.

  • 파일 무결성 - 내려받은 파일의 해시가 게시된 값과 같으면 도중에 안 깨졌다.
  • 변경 감지·식별 - git의 커밋 ID가 내용 해시다. 내용이 바뀌면 ID가 바뀐다.
  • 중복 제거 - 같은 해시면 같은 내용이니 한 벌만 저장한다.

여기까지는 실수로 바뀐 것을 잡는다. 그런데 일부러 바꾸는 상대는 어떨까.

해시만으로는 위조를 못 막는다

메시지와 그 해시를 함께 보낸다고 하자. 중간자가 메시지를 고치면?

plaintext
보냄:  [ 메시지 ] + [ 해시 ]
       공격자가 메시지를 바꾸고
       → 바뀐 메시지의 해시를 "다시 계산해" 붙인다
받음:  [ 바뀐 메시지 ] + [ 앞뒤 맞는 해시 ]   ← 받는 쪽은 통과시킨다

해시 계산법은 공개돼 있으니 누구나 다시 계산할 수 있다. 그래서 해시는 “실수로 안 바뀌었나”는 증명해도 “악의로 안 바뀌었나”는 증명하지 못한다. 빠진 건 하나 - 아무나 못 만들게 할 비밀이다.

HMAC: 비밀키를 섞은 지문

해시에 둘만 아는 비밀키를 섞어 지문을 만든다. 이게 HMAC이다.

diagramdiagram

비밀키를 모르면 바뀐 메시지에 맞는 지문을 만들 수 없다. 그래서 HMAC은 두 가지를 한 번에 준다 - 무결성(안 바뀌었다)과 진짜 보낸 사람(비밀키를 아는 쪽이 만들었다).

다만 비밀키를 양쪽이 나눠 갖는 대칭 방식이라, “둘 중 누가 만들었는지”까지는 못 가린다 - 둘 다 만들 수 있으니까. 그 한 걸음(부인 방지)은 다음 글의 디지털 서명이 맡는다.

왜 그냥 이어 붙이면 안 되나

“비밀키랑 메시지를 그냥 이어서 해시하면 되잖아?” 싶지만, 그렇게 하면(해시(비밀키 + 메시지)) 길이 확장 공격에 샌다 - 일부 해시 구조에선 원본 비밀키를 몰라도 뒤에 내용을 덧붙인 유효한 지문을 만들 수 있다.

HMAC은 그걸 막게 비밀키를 두 번, 정해진 방식으로 섞도록 설계됐다. 세부는 넘겨도 되지만 교훈은 분명하다 - 직접 조합하지 말고 HMAC을 쓴다. 암호화가 푸는 문제 글에서 말한 “직접 만들지 마라”가 여기서도 그대로다.

비교도 한 글자씩 하면 샌다

지문을 검증할 때 받은 지문과 계산한 지문을 비교하는데, 흔한 문자열 비교는 다른 글자가 나오는 순간 멈춘다. 그러면 “몇 글자까지 맞았나”가 응답 시간 차이로 새어, 공격자가 한 글자씩 맞춰갈 수 있다(타이밍 공격).

그래서 지문 비교는 길이에 상관없이 늘 같은 시간이 걸리는 방식(상수 시간 비교)을 쓴다. 라이브러리가 compare_digest 같은 함수로 제공한다 - 직접 ==로 비교하지 않는다.

실무에서: 서명된 요청

HMAC은 시스템 사이 통신에 흔하다.

  • 웹훅 검증 - 결제사가 보내는 알림에 HMAC 지문을 붙인다. 받는 쪽은 공유한 비밀키로 다시 계산해 맞으면 “진짜 그 결제사가, 안 바뀐 채로” 보냈다고 믿는다.
  • JWT의 HS256 - 토큰의 서명 조각이 HMAC이다. 그래서 그 글의 “서명은 잠그는 게 아니라 봉인이다”가 정확히 이 이야기다 - 감춘 게 아니라 바뀌면 티 나게 한 것.
  • 지문 알고리즘은 SHA-256 이상, 비교는 상수 시간으로.

정리

  • 해시는 되돌릴 수 없는 고정 길이 지문 - 단방향·눈사태·충돌 저항.
  • 충돌은 반드시 있지만 좋은 해시는 그걸 못 찾게 한다. MD5·SHA-1은 찾혀서 퇴출, 지금은 SHA-256 이상.
  • 해시만으론 악의적 위조를 못 막는다(누구나 다시 계산하니까). 비밀키를 섞은 HMAC이 무결성 + 진짜 보낸 사람을 준다.
  • HMAC은 대칭이라 부인 방지는 못 한다 - 그건 서명의 몫이다.

다음 글은 개인키로 서명하고 공개키로 검증하는 디지털 서명, 그리고 그 공개키를 믿게 해주는 인증서로 간다.