로그인 기능을 구현하면서 JWT( JavaScript Object Notation Web Token ) 를 도입하려 마음먹었던 경험이 있다.
서버 자원을 아끼고, 서버에 고정되지 않는다는 등의 장점이 아니라 단지 경험을 늘려보고 싶다는 생각에 도입을 결정했었다.
그러다보니 JWT에 대한 이해가 부족했고 인터넷에서 찾을 수 있는 코드들을 사용하면서 JWS, JWE 등 정체모를 비슷한 무언가들을 마주하게 되었다. 이번 포스팅에서는 당시의 아쉬움을 해소하기위해 RFC 7515, RFC 7516, RFC 7517, RFC 7518, RFC 7519 문서들을 통해 JWT에 대해 좀 더 명확한 이해를 하는 것을 목표로 삼을 예정이다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT는 Base64로 인코딩된 문자열 형태를 지닌다.
조금 더 자세하게 보자면 .를 구분자로 총 3개의 파트로 구성되며, 각 파트가 Base64로 인코딩된 형태이다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. // header
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. // payload
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c // signature
예를들어, 앞서 소개한 JWT는 위 형태와 같이 3개의 파트로 분리가 가능하다.
각 파트를 Base64 디코딩을 해보면 signature를 제외하고 어떤 값인지 확인이 가능하다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
=>{"alg":"HS256","typ":"JWT"}
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
=>{"sub":"1234567890","name":"John Doe","iat":1516239022}
마지막 Signature 부분은 Base64 인코딩된 Header와 Payload에 key를 포함하여 Header에 명시된 알고리즘으로 암호화되며, 위 예시에서 사용된 HS256 알고리즘은 SHA256 라는 해시알고리즘을 사용하기 때문에 복호화가 불가능하다.
즉, Header와 Payload 그리고 서버가 보유한 Key를 이용하여 암호화한 문자열이 동일한지 대조하는 방식으로 JWT를 검증한다.
대략적인 JWT의 구성은 알아봤고, 그렇다면 JWS( JSON Web Signature )와 JWE( JSON Web Encryption )은 무엇일까?
JWS와 JWE는 JWT를 구성하는 방식을 의미한다. JWS는 JWE는 일종의 추상클래스이고 이를 바탕으로 구현한 구현체가 JWT인 것이다.
예를들어, 위에 소개된 JWT는 JWS 방식으로 구현되어있다.
JWS( JSON Web Signature ) 는 전송되는 데이터가 외부에 노출되며 데이터의 변조여부를 검증할 수 있다.
JWE( JSON Web Encryption ) 는 전송되는 데이터가 암호화되며 인가된 사용자만 해독할 수 있다.
단순히 사용자 검증을 위한 목적으로는 JWS를 사용하는게 적합하다고 볼 수 있다.
상황에 따라서는 JWE를 통해 데이터를 암호화하고 암호화한 데이터를 JWS방식으로 서명하는 것도 가능하다.
- JOSE Header
- JWS Protected Header
- JWS Unprotected Header( JWS JSON Serialization 인 경우 )
- JWS Payload
- JWS Signature
JWS( JSON Web Signature ) 는 앞선 예시에서 본 JWT처럼 논리적으로 3개의 파트로 구분된다.
JWS 직렬화 형식( Compact, JSON )에 따라 구분되는 파트와 생성되는 토큰 형태가 다르다.
JWS Compact Serialization
JWS Unprotected Header가 없다eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9 . eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ . dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXkRFC 7515의 Compact 직렬화 예시
JWS JSON Serialization
{ "payload": "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGF tcGxlLmNvbS9pc19yb290Ijp0cnVlfQ", "signatures":[ {"protected":"eyJhbGciOiJSUzI1NiJ9", "header": {"kid":"2010-12-29"}, "signature": "cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZ mh7AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjb KBYNX4BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHl b1L07Qe7K0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZES c6BfI7noOPqvhJ1phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AX LIhWkWywlVmtVrBp0igcN_IoypGlUPQGe77Rw"}, {"protected":"eyJhbGciOiJFUzI1NiJ9", "header": {"kid":"e9bc097a-ce51-4036-9562-d2ade882db0d"}, "signature": "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8IS lSApmWQxfKTUJqPP3-Kg6NU1Q"}] }RFC 7515의 JSON 직렬화 예시
Flattened JWS JSON Serialization
{ "payload":"<payload contents>", "protected":"<integrity-protected header contents>", "header":<non-integrity-protected header contents>, "signature":"<signature contents>" }RFC 7515의 Flattened JSON 직렬화 예시
JWS는 Digital signature( 비대칭 ) 혹은 Message Authentication Codes (MACs, 대칭) 방식으로 데이터를 보호한다.
JOSE Header에는 이때 사용할 암호화 방식과 JWS Payload, 추가적인 속성들이 표기된다.
헤더의 파라미터 이름들은 Unique 해야하며, 만약 중복되는 파라미터 이름이 있다면 JWS Parser 에서 거부하거나 어휘적으로 마지막에 표기된 멤버이름을 반환한다.
IANA "JSON Web Signature and Encryption Algorithms" 에서 JWS와 JWE에서 JOSE Header에 표기할 수 있는 파라미터 목록을 제공한다.
alg( Algorithm )
JWS보안에 사용되는 보안 알고리즘으로, 반드시 포함되어 있어야한다.
만약 이 파라미터가 없다면 JWS Signature는 유효하지 않다. 파라미터의 값은 대소문자를 구문하는 ASCII 문자열이다.
IANA "JSON Web Signature and Encryption Algorithms"와 RFC 7518( JWA )에서 작성가능한 값 목록을 확인할 수 있다.jwk( JSON Web Key )
JWS 디지털 서명에 사용되는 공개 키를 나타낸다. 선택적으로 작성할 수 있다. RFC 7517jku( JWK Set URL )
JSON으로 인코딩된 공개 키 집합에 대한 리소스를 나타내는 URI를 나타낸다.
이 URI에 있는 공개 키 중 하나는 디지털 서명에 사용된다.
파라미터 값은 무결성을 보장해야하며, HTTP GET 요청의 경우 반드시 TLS 보안이 적용되어야 한다.kid( Key ID )
JWS 보안에 사용된 Key를 나타내는 힌트를 의미한다. 선택적으로 작성할 수 있다.
대소문자를 구분하는 문자열 형태여야 하며, JWK와 함께 사용하는 경우 JWK의kid값과 일치해야 한다.typ( Type )
JWS의 Media Type을 선언하는 용도로 사용된다. 선택적으로 작성할 수 있다.
현재 JWS가 어떤 용도로 사용되는지를 구분한다.cty( Content Type )
JWS Payload의 Media Type을 선언하는 용도로 사용된다. 선택적으로 작성할 수 있다.crit( Critical )
JOSE Header 파라미터 이름을 나열한 배열의 형태로 특정 파라미터가 처리되어야 한다는 것을 명시한다.
JWS를 처리하는 수신자가 반드시 명시된 파라미터들을 이해하고 처리해야함을 의미한다.
선택적으로 작성할 수 있으며, 빈 배열이 값으로 와서는 안된다.
보안대상이 되는 메시지를 의미한다. JWT의 Claims Set과 동일하다.
JWT에 의해 전달되는 JSON 객체를 나타내며, 각 Claim 이름은 Unique 해야한다.
외부에서 내용을 확인할 수 있으므로, 민감한 데이터를 넣어서는 안된다.
작성가능한 Claim 들은 IANA "JSON Web Token Claims"에 명시된 것들이 주로 사용된다.
iss( Issuer )
JWT의 발행자를 나타낸다.
값으로는 문자열이나 URI형태의 대소문자를 구분하는 문자열이 온다.
선택적으로 작성할 수 있다.sub( Subject )
JWT의 제목을 나타내며, Claim들을 일반적으로 표현할 수 있는 명칭이 온다.
값으로는 문자열이나 URI형태의 대소문자를 구분하는 문자열이 온다.
선택적으로 작성할 수 있다.aud( Audience )
JWT가 의도하는 수신자를 나타낸다.
값으로는 문자열이나 URI형태의 대소문자를 구분하는 문자열 배열이 온다.
선택적으로 작성할 수 있으며, 수신자가 이 Claim값과 동일하지 않으면 JWT를 거부해야한다.exp( Expiration Time )
JWT가 처리승인을 받을 수 있는, 만료시간을 나타낸다.
값으로는 NumericDate 값을 포함하는 숫자가 와야한다.
선택적으로 작성할 수 있다.nbf( Not Before )
JWT가 처리승인을 받을 수 있는, 유효해지는 시간을 나타낸다.
값으로는 NumericDate 값을 포함하는 숫자가 와야한다.
선택적으로 작성할 수 있다.iat( Issued At )
JWT가 발행된 시각을 나타낸다.
값으로는 NumericDate 값을 포함하는 숫자가 와야한다.
선택적으로 작성할 수 있다.jti( JWT ID )
JWT의 유일한 식별자를 나타낸다.
토큰 재사용을 막아주는 용도로 사용할 수 있다.
실수로 다른 데이터 객체에 할당될 가능성이 거의 없는 값이 와야하며, 대소문자를 구분하는 문자열이여야 한다.
선택적으로 작성할 수 있다.
이 외에 사용자가 원하는 이름의 Claim을 IANA에 등록할 수도 있고, 애플리케이션에서만 사용되는 Claim을 사용할 수도 있다.
단, 다른 Claim과 중복되지 않도록 주의해야한다.
JWT의 변조유무를 확인하기 위한 용도로 작성되며, 대칭키 또는 비대칭키를 이용한 방법이 존재한다.
Signature 생성에 사용되는 값은 아래와 같다
ASCII(BASE64URL(UTF8(JWS Protected Header)).BASE64URL(JWS Payload))alg에 따른 keyBase64 인코딩 과정에 공백, 줄바꿈이 포함되면 안된다
이렇게 생성된 Signature는 외부에 노출되지 않으며, 사용되는 알고리즘에 따라 Message Signature( 비대칭키 )혹은 MAC( 대칭키 )이 사용된다.
MAC(Message Authentication Code)는 대칭키가 사용된다.
Message Signature는 비대칭키가 사용된다.
좀 더 자세한 내용응 RFC 7518문서를 참고하자
{
"kty":"EC",
"crv":"P-256",
"x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
"y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0",
"kid":"Public key used in JWS spec Appendix A.3 example"
}
JSON 구조의 암호화 키로, 자신을 나타내는 속성 값들로 구성된다. ( 위 데이터는 JWK의 예시 )
여러 JWK를 Set의 형태로 표현할 수 있다.
IANA "JSON Web Key Parameters"에서 작성가능한 파라미터 목록을 확인할 수 있다.
kty( Key Type )
키와 함께 사용되는 암호 알고리즘( ex: RSA, EC )을 나타낸다.
보통 IANA "JSON Web Key Types"에 등록된 값들이 사용된다.
대소문자를 구분하는 문자열 형식의 값이 작성되며, 반드시 포함되어 있어야 한다.use( Public Key Use )
키의 사용목적을 나타낸다.
값으로 보통sig( Signatrue ) 혹은enc( Encryption )가 사용된다.key_ops( Key Operations )
키의 사용목적을 나타낸다.use파라미터와 함께 사용되면 안된다.
sign,verify,encrypt,decrypt,unwrapKey,deriveKey,deriveBits가 올 수 있다.
한 키에 여러개를 설정하는 것은 보안상의 취약점때문에 권장되지 않는다.alg( Algorithm )
키에 사용되는 암호 알고리즘을 나타낸다.
선택적으로 작성할 수 있다.kid( Key ID )
특정 키의 고유 식별자를 나타낸다.
예를들어, JWK Set에서 특정 키를 식별하는 용도로 사용된다.
JWE, JWS와 같이 사용되는 경우 JWK의kid와 동일한 값을 가져야한다.
선택적으로 작성할 수 있다.
- JOSE Header
- JWE Protected Header
- JWE Shared Unprotected Header( JWE JSON Serialization를 사용하는 경우 )
- JWE Per-Recipient Unprotected Header( JWE JSON Serialization를 사용하는 경우 )
- JWE Encrypted Key
- JWE Initialization Vector
- JWE Ciphertext
- JWE Authentication Tag
- JWE AAD( JWE JSON Serialization를 사용하는 경우 )
JWE( JSON Web Encryption ) 는 논리적으로 6개의 파트로 구분된다.
JWT에 암호화된 메시지가 저장되므로 기밀성이 높다.
직렬화 방식( Compact, JSON )에 따라 구분되는 파트가 다르고, 생성되는 토큰 형태가 다르다.
JWE Compact Serialization
BASE64URL(UTF8(JWE Protected Header)) || '.' || BASE64URL(JWE Encrypted Key) || '.' || BASE64URL(JWE Initialization Vector) || '.' || BASE64URL(JWE Ciphertext) || '.' || BASE64URL(JWE Authentication Tag)
JWE JSON Serialization
{ "protected":"<integrity-protected shared header contents>", "unprotected":<non-integrity-protected shared header contents>, "recipients":[ {"header":<per-recipient unprotected header 1 contents>, "encrypted_key":"<encrypted key 1 contents>"}, ... {"header":<per-recipient unprotected header N contents>, "encrypted_key":"<encrypted key N contents>"}], "aad":"<additional authenticated data contents>", "iv":"<initialization vector contents>", "ciphertext":"<ciphertext contents>", "tag":"<authentication tag contents>" }
Flattened JWE JSON Serialization
{ "protected":"<integrity-protected header contents>", "unprotected":<non-integrity-protected header contents>, "header":<more non-integrity-protected header contents>, "encrypted_key":"<encrypted key contents>", "aad":"<additional authenticated data contents>", "iv":"<initialization vector contents>", "ciphertext":"<ciphertext contents>", "tag":"<authentication tag contents>" }
일반 텍스트와 JWE의 추가속성에 적용될 암호화에 대한 내용이 작성된다.
헤더의 파라미터 이름들은 Unique 해야하며, 만약 중복되는 파라미터 이름이 있다면 JWS Parser 에서 거부하거나 어휘적으로 마지막에 표기된 멤버이름을 반환한다.
IANA "JSON Web Signature and Encryption Algorithms" 에서 JWS와 JWE에서 JOSE Header에 표기할 수 있는 파라미터 목록을 제공한다.
아래에는 JWE에만 적용되는 파라미터 목록이다. 공통되는 부분은 JWS의 JOSE Header 부분에 작성되어있다.
enc( Encryption Algorithm )
ciphertext와 Authentication Tag를 생성하기 위해 일반 텍스트에서 인증된 암호화를 수행하는데 필요한 알고리즘을 나타낸다.
이 알고리즘은 키 길이가 지정된 AEAD 알고리즘이어야 한다.
해당 파라미터에서 지원하지않는 알고리즘을 사용한 암호화된 컨텐츠는 유효하지 않다.
IANA "JSON Web Signature and Encryption Algorithms"와 RFC 7518( JWA )에서 작성가능한 값 목록을 확인할 수 있다.
값으로는 문자열이나 URI형태의 대소문자를 구분하는 문자열 배열이 온다.
반드시 포함되어야 한다.zip( Compression Algorithm )
암호화되기 이전의 평문에 적용되는 알고리즘을 나타낸다.
문자열 압축의 용도로 사용되며, 올 수 있는 값은DEF( DEFLATE 알고리즘 ) 가 있다.
만약 이 파라미터를 사용하는 경우, 반드시 무결성을 보장해야하 하므로 JWE Protected Header에만 작성된다.
선택적으로 작성할 수 있다.
JWE의 메시지 암호화는 평문을 ciphertext와 Authentication Tag로 암호화 하기위해 AEAD 알고리즘을 사용한다.
이때 AEAD 알고리즘을 위한 대칭키를 Content Encryption Key( CEK ) 라고 부른다.
이 CEK값을 결정짓기 위해 사용되는 메서드를 Key Management Mode라고 부르며, 올 수 있는 값은 아래와 같다.
- Key Encryption
- Key Wrapping
- Direct Key Agreement
- Key Agreement with Key Wrapping
- Direct Encryption
메시지 암호화 과정에서 Key Management Mode에 따라 약간의 동작 차이가 발생한다.
alg 헤더 파라미터를 통해 CEK 생성에 사용할 Key Management Mode 결정BASE64URL(JWE Encrypted Key) 계산BASE64URL(JWEInitialization Vector) 계산zip 헤더 파라미터가 있다면 평문을 압축하여 M으로 두고, 파라미터가 없다면 평문을 M으로 둠BASE64URL(UTF8(JWEProtected Header)) 계산. 만약, Protected Header가 없다면 빈 문자열로 둠ASCII(Encoded Protected Header || '.' || BASE64URL(JWE AAD))로 설정ASCII(Encoded Protected Header)로 설정M을 암호화,BASE64URL(JWE Ciphertext) 계산BASE64URL(JWE Authentication Tag) 계산BASE64URL(JWE AAD) 계산.를 구분자로 병합BASE64URL(UTF8(JWE Protected Header)) || '.' || BASE64URL(JWE Encrypted Key) || '.' || BASE64URL(JWE Initialization Vector) || '.' || BASE64URL(JWE Ciphertext) || '.' || BASE64URL(JWE Authentication Tag)메시지 복호화는 RFC 7516 문서를 확인하자