HMAC Signature

개요

HMAC 서명 인증은 요청 자체를 공유 비밀키로 서명해, 보낸 주체의 신원과 요청의 무결성(변조되지 않음)을 동시에 보장하는 방식입니다. AWS Signature V4가 대표적이며, 결제·웹훅·서버 간 API처럼 요청 위변조와 리플레이를 막는 것이 중요한 곳에서 씁니다.

전송 방식

Authorization: <scheme> Credential=..., Signature=... (+ signed headers, timestamp, nonce)

자세히

클라이언트와 서버는 비밀키를 공유합니다. 클라이언트는 요청의 표준화된 형태(메서드, 경로, 정렬된 헤더, 타임스탬프, 본문 해시 등)를 하나의 문자열(string-to-sign)로 만들고, HMAC-SHA256으로 서명한 값을 Authorization 헤더에 담아 보냅니다. 서버는 같은 비밀키로 동일한 문자열을 재구성해 서명을 다시 계산하고, 두 값이 일치하는지 상수 시간 비교로 확인합니다. 비밀키 자체는 절대 네트워크에 실려 가지 않습니다.

Bearer 토큰이 '토큰만 있으면 통과'라 탈취에 취약한 반면, HMAC은 요청의 각 필드를 서명에 묶기 때문에 중간에 파라미터 하나만 바뀌어도 서명이 깨집니다. 여기에 timestamp와 nonce(일회성 값)를 서명에 포함하면, 지난 요청을 그대로 다시 보내는 리플레이 공격을 막을 수 있습니다. 서버는 보통 '타임스탬프가 현재로부터 ±5분 이내이고, 이 nonce가 처음 보는 값일 때만' 요청을 받아들입니다.

장점은 자격 증명(비밀키)이 전송되지 않고 요청 전체가 서명·보호된다는 점이고, 단점은 클라이언트·서버 양쪽이 정확히 같은 정규화(canonicalization) 규칙을 구현해야 해 구현이 까다롭다는 점입니다. 헤더 정렬·인코딩·본문 해시가 조금만 어긋나도 서명이 불일치합니다.

요청 / 응답 예시

Authorization: AWS4-HMAC-SHA256 Credential=AKIA.../20260716/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024

코드로 보기

import hmac, hashlib, time
ts = str(int(time.time()))
body = request_body
msg = f'{method}\n{path}\n{ts}\n{hashlib.sha256(body).hexdigest()}'
sig = hmac.new(SECRET.encode(), msg.encode(), hashlib.sha256).hexdigest()
headers = {'X-Timestamp': ts, 'Authorization': f'HMAC keyId={KEY_ID}, signature={sig}'}
import crypto from 'crypto';
const expected = crypto.createHmac('sha256', SECRET)
  .update(`${method}\n${path}\n${ts}\n${bodyHash}`).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
if (!ok || Math.abs(Date.now()/1000 - ts) > 300) res.status(401).end();

보안 고려사항

관련 헤더

AuthorizationX-Amz-DateX-Amz-Content-Sha256DigestDate

관련 상태코드