JWT 서명과 JWS — 서명은 어떻게 만들어지고 검증되나
JWT 서명과 JWS — 서명은 어떻게 만들어지고 검증되나
토큰의 서명 부분이 정확히 어떤 원리로 만들어지는지 궁금한 적 있나요. JWT의 서명은 별개의 스펙인 JSON Web Signature(JWS, RFC 7515)가 정의해요. JWT는 사실 서명된 클레임 집합이라기보다, JWS 구조의 페이로드로 클레임을 실어 보내는 방식이에요. 서명 계산과 검증 흐름을 알면 라이브러리가 내부적으로 뭘 하는지 보여요.
본문
JWS는 JSON 기반 데이터 구조로 내용을 디지털 서명이나 메시지 인증 코드(MAC)로 보호해요. HTTP Authorization 헤더나 URI 쿼리 파라미터처럼 공간이 제약된 환경을 위한 **컴팩트 직렬화(JWS Compact Serialization)**가 가장 흔히 쓰이고, 같은 내용에 여러 서명을 붙일 수 있는 JSON 직렬화도 있어요. 둘 다 암호학적 바탕은 같아요.
JWT에서 쓰는 컴팩트 형태는 다음 세 부분이에요.
BASE64URL(UTF8(JWS Protected Header)) || '.' || BASE64URL(JWS Payload) || '.' || BASE64URL(JWS Signature)
Signing Input — 무엇이 서명 대상인가
서명 계산에서 가장 중요한 개념이 JWS Signing Input이에요. 헤더와 페이로드를 각각 Base64Url로 인코딩한 뒤 점(.)으로 이어 붙인 값이 바로 서명 입력이에요.
ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.' || BASE64URL(JWS Payload))
헤더가 서명에 포함되기 때문에, 헤더의 alg(알고리즘) 값조차 서명 대상이에요. 이게 나중에 알고리즘 교체 공격을 막는 중요한 바탕이 돼요.
서명 계산
JWS를 만들 때는 다음 순서를 밟아요. 의존성이 없는 단계는 순서가 바뀌어도 상관없어요.
- JWS Payload로 쓸 내용을 만든다.
- JOSE 헤더(보호 헤더)를 만든다.
- Signing Input을 계산해 서명 또는 MAC을 만든다.
인코딩된 헤더 || '.' || 인코딩된 페이로드 || '.' || 인코딩된 서명순서로 잇는다.
HMAC SHA-256이라면 signing input을 SHA-256 해시와 비밀 키로 HMAC해 서명값을 얻어요.
서명 검증
검증할 때는 서명을 만든 쪽이 쓰던 알고리즘과 키로 같은 signing input을 다시 계산해 대조해요. 검증 단계 중 하나라도 실패하면 서명이나 MAC은 검증되지 않아요. 핵심 단계는 다음과 같아요.
- 보호 헤더를 Base64Url 디코딩하고, 알고리즘과 키 형식이 이해되는지 확인한다.
- signing input과 서명, 서명자의 공개 키(또는 공유 비밀 키)를 검증 알고리즘에 넘긴다.
- 재계산한 서명이 토큰에 담긴 서명과 일치하면 검증 성공.
alg헤더가 지원하는 알고리즘인지 반드시 확인한다.
alg 값이 검증 대상에 포함되지 않는 구현은, 공격자가 alg를 자신이 아는 값으로 바꿔 서명을 위조할 수 있는 알고리즘 혼동 공격에 노출되기 쉬워요.
디지털 서명과 MAC의 차이
- MAC(예: HMAC): 발신자와 수신자가 같은 비밀 키를 공유해요. 키를 아는 쪽이면 누구나 서명을 만들 수도, 검증할 수도 있어요. 누가 만들었는지 증명하기보다 '키를 가진 쪽이 만들었다'는 사실만 보장해요.
- 디지털 서명(예: RSA): 개인 키로 서명하고 공개 키로 검증해요. 개인 키를 가진 쪽만 만들 수 있으므로 부인 방지가 가능해요.