JWT 서명 검증, 제대로 이해하기
JWT를 디코딩하는 방법을 아는 개발자는 많지만, 서명이 정확히 무엇을 증명하고 왜 알고리즘마다 검증 방식이 다른지 아는 개발자는 상대적으로 적습니다. 이 글은 그 간극을 채웁니다.
서명이 증명하는 것, 증명하지 않는 것
JWT의 헤더와 페이로드는 Base64URL로 인코딩된 것이지 암호화된 것이 아닙니다. 누구나 디코딩해서 내용을 읽을 수 있습니다. 서명이 보장하는 것은 딱 하나, "이 헤더와 페이로드는 서명 이후 변조되지 않았고, 이 키를 가진 누군가가 서명했다"는 사실뿐입니다. 내용을 숨기지도, 발급자가 신뢰할 만한지도 보장하지 않습니다. 이 전제를 잊으면 아래 나올 취약점들이 왜 위험한지 이해하기 어렵습니다.
HS256과 RS256/ES256, 검증 방식이 왜 다른가
JWT 서명 알고리즘은 크게 두 계열로 나뉩니다.
- 대칭키 (HS256/HS384/HS512, HMAC) — 서명과 검증에 동일한 비밀키를 사용합니다. 발급자와 검증자가 같은 비밀을 공유해야 하므로, 보통 하나의 서버가 발급과 검증을 모두 수행하는 구조(자체 로그인 시스템 등)에 적합합니다.
- 비대칭키 (RS256/384/512, PS256/384/512, ES256/384/512) — 발급자는 개인키로 서명하고, 검증자는 공개키만으로 검증합니다. 공개키는 유출되어도 안전하므로, 여러 마이크로서비스나 외부 클라이언트가 검증만 수행해야 하는 구조(OAuth/OIDC ID 토큰 등)에 적합합니다.
브라우저에서 이걸 실제로 수행하는 코드는 생각보다 짧습니다. HMAC 검증은 이렇게 동작합니다.
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"]
);
const valid = await crypto.subtle.verify(
"HMAC", key, signatureBytes, new TextEncoder().encode(`${header}.${payload}`)
);
RS256/ES256처럼 공개키를 쓰는 경우는 importKey에 "spki" 형식과 PEM에서 추출한 바이트를 넘기고, RSASSA-PKCS1-v1_5 또는 ECDSA 알고리즘으로 검증한다는 점만 다릅니다. 흥미로운 점은 ECDSA인데, JOSE(JWT) 표준은 서명을 r과 s를 이어붙인 고정 길이 바이트로 인코딩하고, 이게 Web Crypto API가 기대하는 형식과 정확히 일치해서 별도 변환 없이 바로 검증할 수 있습니다.
Nymphsoft의 JWT 디코더는 이 로직을 그대로 구현해서, 토큰을 붙여넣고 비밀키/공개키만 입력하면 브라우저 안에서 즉시 검증합니다. 키는 서버로 전송되지 않습니다.
프로덕션에서 흔히 발생하는 alg 관련 취약점
1. alg: none 공격
JWT 표준은 서명이 없는 alg: "none"도 정의합니다. 검증 로직이 헤더의 alg 값을 그대로 믿고 분기한다면, 공격자가 alg를 none으로 바꾸고 서명 부분을 비운 토큰을 보냈을 때 서버가 이를 "서명 없음이니 검증 생략"으로 처리해 그대로 통과시켜버릴 수 있습니다. 방어는 간단합니다 — 서버가 기대하는 alg를 화이트리스트로 명시하고, none은 항상 거부해야 합니다.
2. 알고리즘 혼동 공격 (Algorithm Confusion)
더 미묘한 사례입니다. 서비스가 원래 RS256(비대칭키)으로 토큰을 발급하고, 검증 시에도 헤더의 alg를 그대로 읽어 검증 알고리즘을 결정한다고 가정해봅시다. RS256의 공개키는 이름 그대로 공개되어 있으므로 누구나 얻을 수 있습니다. 공격자는 토큰의 alg를 HS256으로 바꾸고, 그 공개키 문자열 자체를 HMAC 비밀키로 사용해 서명을 위조합니다. 검증 서버가 "alg가 HS256이니 HMAC으로 검증하자"며 그 공개키를 비밀키인 것처럼 사용해버리면 위조 서명이 그대로 통과합니다. 이 취약점은 2015년 여러 주요 JWT 라이브러리에서 실제로 발견되어 널리 알려졌고, 지금도 alg를 검증 시점에 동적으로 신뢰하는 구현에서 재발합니다. 방어 원칙은 동일합니다 — 토큰이 아니라 서버 설정이 사용할 alg를 결정해야 합니다.
이런 이유로 대부분의 성숙한 JWT 라이브러리(예: jsonwebtoken, jose)는 verify() 호출 시 algorithms: ["RS256"]처럼 허용 알고리즘을 명시적으로 요구합니다. 이 옵션을 생략하거나 비워두지 마세요.
브라우저 검증만으로는 충분하지 않은 이유
Nymphsoft의 JWT 도구로 서명을 검증할 수 있게 됐다고 해서, 클라이언트 측 검증이 인증의 대체 수단이 될 수는 없습니다. 브라우저에서 서명을 확인하는 건 디버깅과 학습 목적에 유용하지만, 실제 인증/인가 판단은 반드시 서버(또는 신뢰할 수 있는 백엔드)에서 이루어져야 합니다. 이유는 단순합니다 — 클라이언트 코드는 사용자가 완전히 통제할 수 있는 환경에서 실행되므로, "검증 결과"를 클라이언트가 스스로 보고하는 방식은 언제든 조작될 수 있습니다.
프로덕션 체크리스트
- 검증 시 허용할
alg를 서버 설정에서 화이트리스트로 고정한다 (토큰 헤더 값을 신뢰하지 않는다). alg: none은 항상 거부한다.- 서명뿐 아니라
exp(만료),nbf(활성 시작)를 매 요청마다 검사한다. - 발급자가 여러 곳이거나 여러 클라이언트를 대상으로 한다면
iss(발급자),aud(대상) 클레임도 함께 검증한다. - 비대칭키를 쓴다면
kid(Key ID) 헤더로 키 로테이션을 지원하고, 폐기된 키의kid는 거부한다. - 직접 서명/검증 로직을 구현하기보다, 검증된 JWT 라이브러리를 사용한다.
자주 묻는 질문
JWT를 왜 세션 대신 쓰나요?
세션은 서버가 상태를 저장해야 하지만(Stateful), JWT는 토큰 자체에 필요한 정보를 담아 서명만 검증하면 되므로 서버가 상태를 저장할 필요가 없습니다(Stateless). 대신 발급된 JWT는 만료 전까지 즉시 무효화하기 어렵다는 트레이드오프가 있습니다.
대칭키(HS256)와 비대칭키(RS256) 중 뭘 써야 하나요?
발급과 검증을 같은 서버(또는 완전히 신뢰하는 서버들)가 수행한다면 HS256으로 충분합니다. 여러 마이크로서비스나 제3자 클라이언트가 "검증만" 해야 한다면, 비밀키를 나눠 갖지 않아도 되는 RS256/ES256이 안전합니다.
Nymphsoft JWT 도구로 검증한 결과를 인증에 사용해도 되나요?
아니요. 이 도구는 디버깅과 학습을 위한 것입니다. 실제 서비스의 인증/인가는 반드시 서버 측에서, 화이트리스트로 고정된 alg와 검증된 라이브러리로 처리해야 합니다.