카테고리 없음

[JWT] 4. JSON Web Signatures (JWS)

noahkim_ 2025. 7. 22. 12:12

0. JWS

서명

  • 제3자에 의해 위조되지 않았음을 증명
  • 생성자가 누군지 확인할 수 있음

 

Validation

  • 헤더와 페이로드에 명시된 조건을 충족
  • 옵션) 사용자 정의 조건도 추가 가능

 

1 Structure of a Signed JWT

예시) Header

더보기
{
  "alg": "HS256",     // 서명 알고리즘
  "typ": "JWT"        // 타입 (JWT 고정)
}

 

예시) Payload

더보기
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

 

예시) Signature

더보기
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

 

알고리즘

알고리즘 서명 방식 키 구조 특징
HS256 HMAC + SHA-256 대칭 (하나의 비밀 키 공유) 빠름, 단순, 키 유출 시 위조 가능
RS256 RSA + SHA-256 비대칭 (개인 키/공개 키) 보안성 높음, 공개 검증 가능
ES256 ECDSA + SHA-256 비대칭 (개인 키/공개 키) 경량, 보안성 높고 성능 우수

 

 

Algorithm Overview for Compact Serialization

  • 각 부분별로 인코딩하여 합치는 구성을 가짐

 

코드) HMAC

더보기
const encodedHeader = base64(utf8(JSON.stringify(header)));
const encodedPayload = base64(utf8(JSON.stringify(payload)));

const signature = base64(
  hmac(`${encodedHeader}.${encodedPayload}`, secret, sha256)
);

const jwt = `${encodedHeader}.${encodedPayload}.${signature}`;
  • 공유 비밀키 사용

 

코드) RS256

더보기
const encodedHeader = base64(utf8(JSON.stringify(header)));
const encodedPayload = base64(utf8(JSON.stringify(payload)));

const signature = base64(
  rsassa(`${encodedHeader}.${encodedPayload}`, privateKey, sha256)
);

const jwt = `${encodedHeader}.${encodedPayload}.${signature}`;
  • 개인키로 서명

 

Practical Aspects of Signing Algorithms

구분 사용하는 키 누가 가능? 구조
서명 개인키로 서명, 공개키로 검증 검증자: 다수 Many-to-One
암호화 공개키로 암호화, 개인키로 복호화 암호화자: 다수 One-to-Many

 

JWS Header Claims

클레임 설명 주요 용도
jku 키 세트의 URL 키 참조 (외부 URL)
jwk 공개 키 객체 키 직접 포함
kid 키 식별자 다중 키 관리
x5u X.509 인증서 URL 외부 인증서 체인 참조
x5c X.509 인증서 체인 인증서 직접 포함
x5t, x5t#S256 인증서 해시 인증서 무결성 확인
typ 타입 지정 "JWT", "JOSE" 등
crit 필수 해석 항목 지정 구현체 확장 지원

 

JWS JSON Serialization

  • JSON 형식의 구조화된 표현 방식을 사용하는 서명 방식 (Compact Serialization ❌)
  • 하나의 JWT에 여러 개의 서명을 포함시킬 수 있음
    • 즉, 하나의 payload에 대해 여러 주체가 각각 서명한 결과를 담을 수 있음 
  • 예시
    • 여러 기관이 같은 데이터에 서명해야 할 때
    • 다양한 알고리즘을 동시에 사용할 때

 

예) JWS JSON Serialization

더보기
{
  "payload": "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ",
  "signatures": [
    {
      "protected": "eyJhbGciOiJSUzI1NiJ9",
      "header": { "kid": "2010-12-29" },
      "signature": "cP4hiU..."
    },
    {
      "protected": "eyJhbGciOiJFUzI1NiJ9",
      "header": { "kid": "e9bc097a..." },
      "signature": "DtEhU3..."
    }
  ]
}

 

설명
payload
JWT의 본문 데이터, Base64URL로 인코딩됨
signatures 여러 개의 서명을 담는 배열

 

signatures 필드 설명
protected
서명 대상인 헤더 (Base64Url 인코딩된 JSON)
header 서명되지 않는 헤더 (JSON 객체)
signature 실제 서명 값, Base64Url 인코딩

 

2. Signing and Validating Tokens

  • JavaScript에서 널리 쓰이는 jsonwebtoken 라이브러리를 사용해서 서명된 JWT 생성

 

HS256: HMAC + SHA-256

예시) 서명

더보기
const secret = 'my-secret';

const signed = jwt.sign(payload, secret, {
    algorithm: 'HS256',
    expiresIn: '5s' // if ommited, the token will not expire
});

 

예시) 검증

더보기
const decoded = jwt.verify(signed, secret, {
    // Never forget to make this explicit to prevent
    // signature stripping attacks
    algorithms: ['HS256'],
});

 

RS256: RSASSA + SHA256

bash) RSA 키 생성 (OpenSSL)

더보기
# 1. 개인키 생성 (2048비트)
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048

# 2. 공개키 추출
openssl rsa -pubout -in private_key.pem -out public_key.pem

 

예시) 서명

더보기
import jwt from 'jsonwebtoken';

// 개인키 (private_key.pem의 내용 전체를 문자열로 붙여 넣음)
const privateRsaKey = `-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----`;

const payload = {
  sub: "1234567890",
  name: "John Doe",
  admin: true
};

// JWT 서명 생성
const signed = jwt.sign(payload, privateRsaKey, {
  algorithm: 'RS256',
  expiresIn: '5s'
});

 

예시) 검증

더보기
// 공개키 (public_key.pem의 내용 전체)
const publicRsaKey = `-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----`;

// JWT 검증
const decoded = jwt.verify(signed, publicRsaKey, {
  algorithms: ['RS256'] // 꼭 명시적으로 지정할 것!
});

 

ES256: ECDSA using P-256 and SHA-256

bash) ECDSA 키 생성 (OpenSSL)

더보기
# ECDSA 개인키 생성 (prime256v1 = P-256)
openssl ecparam -name prime256v1 -genkey -noout -out ecdsa_private_key.pem

# 공개키 추출
openssl ec -in ecdsa_private_key.pem -pubout -out ecdsa_public_key.pem

 

예시) 서명

더보기
import jwt from 'jsonwebtoken';

// 개인키 PEM 내용 전체 붙여넣기
const privateEcdsaKey = `-----BEGIN EC PRIVATE KEY-----
...
-----END EC PRIVATE KEY-----`;

const payload = {
  sub: "1234567890",
  name: "John Doe",
  admin: true
};

const signed = jwt.sign(payload, privateEcdsaKey, {
  algorithm: 'ES256',
  expiresIn: '5s'
});

 

예시) 검증

더보기
// 공개키 PEM 내용 전체
const publicEcdsaKey = `-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----`;

const decoded = jwt.verify(signed, publicEcdsaKey, {
  algorithms: ['ES256'] // 꼭 명시! (서명 제거 공격 방지)
});