JWT 서명 검증, 제대로 이해하기

JWT를 디코딩하는 방법을 아는 개발자는 많지만, 서명이 정확히 무엇을 증명하고 왜 알고리즘마다 검증 방식이 다른지 아는 개발자는 상대적으로 적습니다. 이 글은 그 간극을 채웁니다.

서명이 증명하는 것, 증명하지 않는 것

JWT의 헤더와 페이로드는 Base64URL로 인코딩된 것이지 암호화된 것이 아닙니다. 누구나 디코딩해서 내용을 읽을 수 있습니다. 서명이 보장하는 것은 딱 하나, "이 헤더와 페이로드는 서명 이후 변조되지 않았고, 이 키를 가진 누군가가 서명했다"는 사실뿐입니다. 내용을 숨기지도, 발급자가 신뢰할 만한지도 보장하지 않습니다. 이 전제를 잊으면 아래 나올 취약점들이 왜 위험한지 이해하기 어렵습니다.

HS256과 RS256/ES256, 검증 방식이 왜 다른가

JWT 서명 알고리즘은 크게 두 계열로 나뉩니다.

브라우저에서 이걸 실제로 수행하는 코드는 생각보다 짧습니다. 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) 표준은 서명을 rs를 이어붙인 고정 길이 바이트로 인코딩하고, 이게 Web Crypto API가 기대하는 형식과 정확히 일치해서 별도 변환 없이 바로 검증할 수 있습니다.

Nymphsoft의 JWT 디코더는 이 로직을 그대로 구현해서, 토큰을 붙여넣고 비밀키/공개키만 입력하면 브라우저 안에서 즉시 검증합니다. 키는 서버로 전송되지 않습니다.

프로덕션에서 흔히 발생하는 alg 관련 취약점

1. alg: none 공격

JWT 표준은 서명이 없는 alg: "none"도 정의합니다. 검증 로직이 헤더의 alg 값을 그대로 믿고 분기한다면, 공격자가 algnone으로 바꾸고 서명 부분을 비운 토큰을 보냈을 때 서버가 이를 "서명 없음이니 검증 생략"으로 처리해 그대로 통과시켜버릴 수 있습니다. 방어는 간단합니다 — 서버가 기대하는 alg를 화이트리스트로 명시하고, none은 항상 거부해야 합니다.

2. 알고리즘 혼동 공격 (Algorithm Confusion)

더 미묘한 사례입니다. 서비스가 원래 RS256(비대칭키)으로 토큰을 발급하고, 검증 시에도 헤더의 alg를 그대로 읽어 검증 알고리즘을 결정한다고 가정해봅시다. RS256의 공개키는 이름 그대로 공개되어 있으므로 누구나 얻을 수 있습니다. 공격자는 토큰의 algHS256으로 바꾸고, 그 공개키 문자열 자체를 HMAC 비밀키로 사용해 서명을 위조합니다. 검증 서버가 "alg가 HS256이니 HMAC으로 검증하자"며 그 공개키를 비밀키인 것처럼 사용해버리면 위조 서명이 그대로 통과합니다. 이 취약점은 2015년 여러 주요 JWT 라이브러리에서 실제로 발견되어 널리 알려졌고, 지금도 alg를 검증 시점에 동적으로 신뢰하는 구현에서 재발합니다. 방어 원칙은 동일합니다 — 토큰이 아니라 서버 설정이 사용할 alg를 결정해야 합니다.

이런 이유로 대부분의 성숙한 JWT 라이브러리(예: jsonwebtoken, jose)는 verify() 호출 시 algorithms: ["RS256"]처럼 허용 알고리즘을 명시적으로 요구합니다. 이 옵션을 생략하거나 비워두지 마세요.

브라우저 검증만으로는 충분하지 않은 이유

Nymphsoft의 JWT 도구로 서명을 검증할 수 있게 됐다고 해서, 클라이언트 측 검증이 인증의 대체 수단이 될 수는 없습니다. 브라우저에서 서명을 확인하는 건 디버깅과 학습 목적에 유용하지만, 실제 인증/인가 판단은 반드시 서버(또는 신뢰할 수 있는 백엔드)에서 이루어져야 합니다. 이유는 단순합니다 — 클라이언트 코드는 사용자가 완전히 통제할 수 있는 환경에서 실행되므로, "검증 결과"를 클라이언트가 스스로 보고하는 방식은 언제든 조작될 수 있습니다.

프로덕션 체크리스트

자주 묻는 질문

JWT를 왜 세션 대신 쓰나요?

세션은 서버가 상태를 저장해야 하지만(Stateful), JWT는 토큰 자체에 필요한 정보를 담아 서명만 검증하면 되므로 서버가 상태를 저장할 필요가 없습니다(Stateless). 대신 발급된 JWT는 만료 전까지 즉시 무효화하기 어렵다는 트레이드오프가 있습니다.

대칭키(HS256)와 비대칭키(RS256) 중 뭘 써야 하나요?

발급과 검증을 같은 서버(또는 완전히 신뢰하는 서버들)가 수행한다면 HS256으로 충분합니다. 여러 마이크로서비스나 제3자 클라이언트가 "검증만" 해야 한다면, 비밀키를 나눠 갖지 않아도 되는 RS256/ES256이 안전합니다.

Nymphsoft JWT 도구로 검증한 결과를 인증에 사용해도 되나요?

아니요. 이 도구는 디버깅과 학습을 위한 것입니다. 실제 서비스의 인증/인가는 반드시 서버 측에서, 화이트리스트로 고정된 alg와 검증된 라이브러리로 처리해야 합니다.