HMAC 입문: 서버는 값이 바뀌었다는 사실을 어떻게 알아낼까?

웹 서비스를 만들다 보면 이런 값들을 자주 다룹니다.

로그인 상태를 나타내는 쿠키

결제나 알림을 전달하는 웹훅 요청

만료 시각이 포함된 다운로드 링크

다음 화면으로 전달하는 사용자 식별자와 권한 정보

이 값들은 브라우저나 외부 네트워크를 거치기 때문에 사용자가 직접 볼 수 있고, 경우에 따라 수정할 수도 있습니다. 그렇다면 서버는 돌아온 값이 자신이 만들었던 그대로인지 어떻게 확인할까요?

이때 사용하는 대표적인 도구가 **HMAC(Hash-based Message Authentication Code)**입니다.

먼저 한 문장으로 이해하기

HMAC은 메시지와 비밀키를 함께 계산해 만든 위조하기 어려운 확인표입니다.

투명한 봉투를 떠올려봅시다. 봉투 안의 내용은 누구나 볼 수 있지만, 봉투 입구에는 서버만 만들 수 있는 특수한 봉인이 붙어 있습니다. 누군가 내용을 role=user에서 role=admin으로 바꾸면 원래 봉인과 맞지 않게 됩니다. 서버는 봉인을 다시 계산해 비교하는 것만으로 변조 사실을 알아챌 수 있습니다.

중요한 점이 하나 있습니다. HMAC은 투명한 봉투이지 금고가 아닙니다. 내용을 숨기지 않고, 내용이 바뀌지 않았음을 확인합니다.

해시만 사용하면 안 될까?

일반 해시는 입력값을 고정 길이의 지문으로 바꿉니다.

SHA-256("role=user") → 어떤 고정 길이의 해시값

파일이 손상됐는지 확인하는 용도로는 유용합니다. 하지만 공격자가 메시지와 해시값을 모두 바꿀 수 있는 상황이라면 충분하지 않습니다. 공격자도 role=admin의 SHA-256을 새로 계산할 수 있기 때문입니다.

HMAC에는 서버만 아는 비밀키가 추가됩니다.

인증 태그 = HMAC-SHA-256(비밀키, 메시지)

공격자는 메시지를 볼 수 있고 HMAC 알고리즘도 알 수 있습니다. 그래도 비밀키가 없기 때문에 바뀐 메시지에 맞는 올바른 인증 태그를 만들 수 없습니다. 안전성은 알고리즘을 숨기는 데서 나오지 않고, 충분히 강한 키를 안전하게 관리하는 데서 나옵니다.

HMAC 검증은 어떻게 진행될까?

예를 들어 서버가 다음 값을 브라우저에 전달한다고 가정해봅시다.

message = "userId=42&role=user&expires=1785340800"

서버는 이 메시지와 비밀키로 HMAC을 계산한 뒤 메시지와 인증 태그를 함께 보냅니다.

message + authentication_tag

값이 다시 돌아오면 서버는 같은 비밀키와 메시지로 HMAC을 한 번 더 계산합니다.

받은 메시지로 예상 HMAC을 계산합니다.

받은 HMAC과 예상 HMAC을 비교합니다.

둘이 같으면 변조되지 않은 값으로 판단합니다.

다르면 내용을 신뢰하지 않고 요청을 거부합니다.

사용자가 role=user를 role=admin으로 바꾸는 순간 계산 결과가 완전히 달라집니다. 공격자가 예전 인증 태그를 그대로 붙여도 검증에 실패합니다.

우리가 말하는 ‘서명값’은 정확히 무엇일까?

실무에서는 HMAC이 붙은 값을 편하게 서명값 또는 서명된 값이라고 부르곤 합니다. 다만 엄밀히 말하면 HMAC은 공개키 기반 전자서명과 다릅니다.

HMAC은 생성자와 검증자가 같은 비밀키를 공유합니다. 따라서 키를 가진 시스템끼리 빠르고 간단하게 무결성과 출처를 확인하는 데 적합합니다. 반면 공개키 전자서명은 개인키로 서명하고 공개키로 검증하므로, 비밀키를 공유하지 않은 제3자도 검증할 수 있습니다.

HMAC의 결과물은 기술적으로 **MAC 또는 인증 태그(authentication tag)**라고 부르는 편이 더 정확합니다. 그래도 애플리케이션 코드에서는 signed value라는 표현이 널리 쓰입니다.

해시·HMAC·암호화·전자서명의 차이

기술 | 주된 목적 | 비밀키 | 내용이 숨겨지는가
일반 해시 | 데이터의 지문 계산 | 없음 | 아니요
HMAC | 무결성과 공유키 기반 출처 확인 | 공유 비밀키 | 아니요
암호화 | 허가받지 않은 사람이 내용을 읽지 못하게 함 | 대칭키 또는 공개키 체계 | 예
공개키 전자서명 | 개인키 서명과 공개키 검증 | 개인키·공개키 | 아니요

따라서 개인정보나 액세스 토큰처럼 노출되면 안 되는 데이터를 HMAC만 붙여 전달하면 안 됩니다. 내용을 숨겨야 한다면 인증된 암호화를 사용해야 합니다. 비밀번호 저장은 또 다른 문제이므로 HMAC이나 일반 해시 대신 salt와 비용 조절 기능이 있는 전용 비밀번호 해시를 사용해야 합니다.

같은 값도 용도에 따라 다르게 서명해야 한다

안전한 구현은 메시지만 HMAC에 넣지 않고 **용도(purpose)**도 함께 넣습니다.

HMAC-SHA-256(비밀키, "cookie.v1" + 구분자 + 메시지)
HMAC-SHA-256(비밀키, "webhook.v1" + 구분자 + 메시지)

메시지가 같아도 쿠키용 HMAC과 웹훅용 HMAC은 달라집니다. 덕분에 한 기능에서 발급된 값을 다른 기능으로 옮겨 사용하는 공격을 막을 수 있습니다. 이를 **도메인 분리(domain separation)**라고 합니다.

버전과 용도, 메시지 경계를 모호하지 않게 구분하는 것도 중요합니다. 문자열을 아무 구분 없이 이어 붙이면 서로 다른 입력 조합이 같은 바이트열이 되는 실수를 만들 수 있기 때문입니다.

짧은 코드로 보는 HMAC

Node.js에서는 다음과 같은 형태로 HMAC을 계산할 수 있습니다.

import { createHmac, timingSafeEqual } from "node:crypto";

function createTag(message: string, secret: Buffer) {
return createHmac("sha256", secret)
.update("cookie.v1")
.update("\0")
.update(message)
.digest();
}

function verifyTag(message: string, received: Buffer, secret: Buffer) {
const expected = createTag(message, secret);
return received.length === expected.length
&& timingSafeEqual(received, expected);
}

이 예제의 핵심은 세 가지입니다.

검증할 때도 메시지와 용도를 똑같이 사용합니다.

일반 문자열 비교 대신 상수 시간 비교 함수를 사용합니다.

비밀키를 코드에 직접 적지 않고 안전한 키 저장소나 비밀 관리 환경에서 주입합니다.

실제 서비스에서는 여기에 값의 형식, 최대 길이, 버전, 키 식별자, 만료 정책까지 함께 설계해야 합니다.

HMAC이 자주 쓰이는 곳

1. 웹훅 검증

외부 서비스가 요청 본문을 HMAC으로 서명하고 수신 서버가 이를 검증합니다. 전송 중 본문이 바뀌지 않았고 공유키를 가진 발신자가 만들었다는 것을 확인할 수 있습니다.

2. 서명된 쿠키와 상태값

서버가 공개돼도 괜찮은 작은 상태값에 HMAC을 붙여 클라이언트에 보관할 수 있습니다. 사용자가 값을 임의로 바꾸면 서버가 즉시 감지합니다.

3. 만료 시간이 있는 링크

리소스 경로와 만료 시각을 함께 서명하면 사용자가 경로나 만료 시각을 바꿀 수 없습니다. 단, 링크가 유출됐을 때의 재사용까지 HMAC이 자동으로 막아주지는 않습니다.

4. 내부 서비스 요청

공유키를 안전하게 관리할 수 있는 제한된 시스템 사이에서 요청 내용의 무결성을 확인하는 데 사용할 수 있습니다. 서비스가 많아져 키 공유 범위가 넓어진다면 공개키 서명이나 별도의 서비스 인증 체계가 더 적합할 수 있습니다.

HMAC이 해결하지 않는 문제

HMAC은 강력하지만 만능은 아닙니다.

기밀성: 메시지는 그대로 보입니다. 민감정보는 암호화해야 합니다.

재생 공격: 유효한 요청을 복사해 다시 보내는 공격은 별도 대책이 필요합니다. 타임스탬프, nonce, 요청 ID, 일회성 소비 기록을 함께 사용합니다.

키 유출: 공유 비밀키가 노출되면 공격자도 정상 HMAC을 만들 수 있습니다. 키 교체와 접근 통제가 필요합니다.

제3자 증명: 생성자와 검증자가 같은 키를 가지므로 어느 쪽이 값을 만들었는지 제3자에게 독립적으로 증명하기 어렵습니다.

실무에서 놓치기 쉬운 다섯 가지

직접 만든 SHA256(secret + message)를 사용하지 않습니다. 검증된 HMAC 구성을 사용합니다.

받은 원본 바이트를 기준으로 검증합니다. 특히 웹훅 JSON은 파싱 후 다시 문자열로 만들면 공백이나 키 순서가 달라질 수 있습니다.

만료 시각과 권한 관련 값도 서명 범위에 포함합니다. 서명되지 않은 필드는 공격자가 바꿀 수 있습니다.

비교는 상수 시간 함수로 수행합니다. 비교 시간의 차이가 인증 태그 추측에 힌트를 주지 않게 합니다.

키 식별자와 교체 전략을 준비합니다. 새 값은 새 키로 만들고, 기존 값은 제한된 기간 동안 이전 키로 검증한 뒤 자연스럽게 재발급합니다.

정리

HMAC은 “이 값이 비밀인가?”가 아니라 **“이 값이 내가 만든 뒤 바뀌지 않았는가?”**에 답하는 기술입니다.

메시지는 볼 수 있지만 비밀키 없이는 올바른 인증 태그를 다시 만들 수 없습니다. 그래서 웹훅, 서명된 쿠키, 만료 링크처럼 외부에 노출되는 값의 변조를 감지하는 데 잘 맞습니다. 다만 기밀성, 재생 방지, 키 관리까지 자동으로 해결해주지는 않으므로 각각의 보안 목적에 맞는 도구와 함께 사용해야 합니다.

더 깊이 살펴보고 싶다면 RFC 2104의 HMAC 정의와 DUOLABS AUTH의 서명값 · HMAC 모듈을 참고할 수 있습니다.