ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [OAuth 2.0 API 보안] JWS를 이용한 메시지 수준 보안
    보안/OAuth 2.0 2022. 6. 23. 02:08

    0. JSON 웹 서명을 이용한 메시지 수준 보안

    JSON : JavaScript Object Notation


    1. JWT의 이해

    JWT(JSON Web Token)은 데이터를 전송하기 위한 컨테이너를 정의한다.

    OIDC가 이 JWT를 사용해서 ID 토큰을 나타낸다.

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
    

    JWT는 세 가지 요소로 구성된다.

    각 요소는 Base64url 방식으로 인코딩되고 마침표(.)로 구분된다.

    • 서명 및 암호화(JOSE) 헤더
    • 페이로드/클레임
    • 서명

    1.1. JOSE 헤더

    JOSE 헤더는 JWT 클레임에 적용된 암호화에 관련된 속성을 정의한다.

    {
      "alg": "HS256",
      "typ": "JWT",
    	//"kid": "f7b87..."
    }
    

    alg, kid 파라미터는 JWT가 아닌 JSON 웹 서명(JWS, JSON Web Signature)에 정의된 사양이다.

    • alg : 서명에 사용할 해시 알고리즘 정의
    • 사용할 알고리즘은 JSON 웹 알고리즘(JWA)에 정의된 알고리즘을 사용한다.
    • kid : 키 식별자
    • typ : JWT의 미디어 유형 정의JWT 구현부에서는 JWT 내부 값을 확인하지 않지만 JWT 앱은 내부 값을 파싱하고 데이터를 확인한다.
    • JWT를 처리하는 구성 요소에는 JWT 구현부와 JWT 앱 두 가지 유형이 있다.
    • cty : JWT 구조 정보 정의

    1.2. 페이로드/클레임

    {
      "sub": "1234567890",
      "name": "John Doe",
      "iat": 1516239022
    }
    

    데이터를 담는 객체를 base64url로 인코딩한 값이다.

    인증된 사용자의 정보는 이곳에 포함된다.

    <tag1><tag2/></tag1>
    <tag1><tag2></tag2></tag1>
    

    XML의 경우에는 같은 의미의 값도 다른 구조, 형태를 가질 수 있다.

    위의 예시는 같은 의미를 가지지만 형태가 다르다.

    이는 정규화 과정을 통해서 하나의 표준 형태로 변환해야 한다.

     

    JWT의 경우에는 공백을 포함할 수 있고 base64url 인코딩 이전에 정규화를 진행할 필요가 없다.

    JWT 클레임은 등록된 클레임, 공개 클레임, 개인 클래엠 세 가지 클래스를 가진다.

     

    등록된 클레임은 JWT에서 필수적으로 사용할 필요는 없다.

    어떤 요소가 필수인지를 결정하기 위해서는 JWT를 기반으로 구축된 시스템에 따라 달라진다.

    OIDC의 경우에는 iss가 필수 사항 클레임이다.

    • iss : JWT의 발행자
    • 대소문자를 구분하는 문자열
    • sub : 식별 엔티티
    • 대소문자를 구분하는 문자열
    • aud : 토큰 대상자
    • exp : 토큰 만료 시간
    • nbf : 시간 비교 값
    • nbf값이 현재 시간보다 큰 경우 토큰 수신자는 이를 거부해야함
    • iat : 토큰 발행자가 계산한 JWT의 발행 시간
    • jti : 토큰의 고유 ID

     

    JWT 클레임은 공개 클레임으로도 정의할 수 있다. 이 경우에는 충돌이 발생할 수 있으니 클레임 레지스트리에 등록하거나 적절한 네임스페이스를 활용해여 충돌 방지 정책을 정의해야 한다.

     

    비공개 클레임의 경우에는 토큰 발급자와 수신자 사이에서만 공유해야 한다.

    충돌 가능성이 있으니 적절한 회피책을 마련해야 한다.

     

    1.3 서명

    JWT의 서명을 위한 부분이며 base64url로 인코딩된다.

    서명 자체는 디코딩하더라도 읽을 수는 없지만 JWT 자체의 유효성을 판단할 수 있다.

    이때, 비밀 키를 통해서 JWT의 유효성을 판단하며 이는 안전하게 관리해야 한다.

    public static String buildPlainJWT() {
    
    	// build audience restriction list.
    	List aud = new ArrayList();
    	aud.add("<https://app1.foo.com>");
    	aud.add("<https://app2.foo.com>");
    
    	Date currentTime = new Date();
    
    	// create a claim set.
    	JWTClaimsSet jwtClaims = new JWTClaimsSet.Builder().
    			// set the value of the issuer.
    			issuer("<https://apress.com>").
    			// set the subject value - JWT belongs to this subject.
    			subject("john").
    			// set values for audience restriction.
    			audience(aud).
    			// expiration time set to 10 minutes.
    			expirationTime(new Date(new Date().getTime() + 1000 * 60 * 10)).
    			// set the valid from time to current time.
    			notBeforeTime(currentTime).
    			// set issued time to current time.
    			issueTime(currentTime).
    			// set a generated UUID as the JWT identifier.
    			jwtID(UUID.randomUUID().toString()).build();
    
    	// create plaintext JWT with the JWT claims.
    	PlainJWT plainJwt = new PlainJWT(jwtClaims);
    
    	// serialize into string.
    	String jwtInText = plainJwt.serialize();
    
    	// print the value of the JWT.
    	System.out.println(jwtInText);
    
    	return jwtInText;
    }
    

    송신 측은 위와 같이 JWT 클레임을 정의하고 토큰을 만든다.

    public static PlainJWT parsePlainJWT() throws ParseException {
    
    	// get JWT in base64-encoded text.
    	String jwtInText = buildPlainJWT();
    
    	// build a plain JWT from the bade64 encoded text.
    	PlainJWT plainJwt = PlainJWT.parse(jwtInText);
    
    	// print the JOSE header in JSON.
    	System.out.println(plainJwt.getHeader().toString());
    
    	// print JWT body in JSON.
    	System.out.println(plainJwt.getPayload().toString());
    
    	return plainJwt;
    }
    

    수신측은 JWT를 파싱한다.

    이때 토큰의 검증 과정이 추가될 수 있다.

     


    2. JWS

    JWS는 전자 서명이나 MAC(HMAC)을 통한 인증을 받는 메시지, 페이로드를 의미한다.

    JWS의 사양에 따라서 두 가지 방식의 직렬화(Serialization)로 나타낼 수 있다.

    • Compact Serialization
    • JSON Serializaion

     

    구글의 OIDC의 경우에는 Compact 직렬화를 사용한다.

     

     

    2.1 JWS Compact Serialization

    서명된 JSON 페이로드를 URL-Safe 문자열로 표현한다.

    마침표(.)로 구분되는 헤더/페이로드/서명의 요소를 가지고 있다.

    JSON 페이로드가 Compact 직렬화를 사용하는 경우 헤더와 JWS 페이로드는 단일 서명을 가진다.

     

    헤더의 경우 다음의 파라미터를 가진다.

    • alg : 페이로드 서명을 위한 알고리즘
    • jku : JSON 웹 키 셋을 가리키는 URL키 셋 검색을 위한 프로토콜은 무결성 보호를 제공해야 한다.
    • 이 키 셋 중 하나가 페이로드 서명에 사용된다.
    • jwk : JSON 페이로드 서명을 위한 공개키
    • kid : JSON 페이로드 서명을 위한 키 식별자
    • x5u : x.509 인증서나 인증서 체인을 가리킴
    • x5c : JSON 페이로드 서명을 위한 개인키에 해당하는 x.509 인증서 또는 인증서 체인
    • x5t : JSON 페이로드 서명을 위한 키의 X.509 인증서의 base64url 인코딩, SHA-1 의 결과
    • typ : 완성된 JWS 유형
    • cty : 보안 콘텐츠의 유형
    • crit : 추가적인 사용자 파라미터 유무

    헤더는 페이로드를 어떻게 서명할 것인가를 정의하는 부분으로 볼 수 있다.

    하지만 이 파라미터 외에도 추가적인 파라미터를 사용할 수 있으며 공개 헤더 파라미터와 개인 헤더 파라미터가 있다.

    이런 추가적인 파라미터는 충돌을 방지하는 정책을 도입해야 한다.

     

    페이로드는 헤더로 정의한 서명 방식을 통해 서명 해야 하는 메시지다.

    이 값이 꼭 JSON 페이로드일 필요는 없지만 송-수신 사이에 결정된 방식을 사용해야 한다.

    JWS 서명은 헤더에서 정의한 방식을 직접 서명하는 곳이다.

     

    2.2 JWS JSON Serialization

    JWS JSON 직렬화는 동일한 JWS 페이로드에 여러 서명을 생성할 수 있다.

    직렬화의 마지막 단계에서 서명된 페이로드와 관련 메타데이터를 함께 JSON 객체로 만든다.

    이 객체에는 payload, signatures와 signatures의 protected, header, signature가 포함된다.

    {
    	"payload" : "...",
    	"signatures" : [
    		{
    			"protected" : "...",
    			"header" : {"kid" : "..."},
    			"signature" : "..."
    		},
    		{
    			"protected" : "...",
    			"header" : {"kid" : "..."},
    			"signature" : "..."
    		}
    	]
    }
    

    JWS 페이로드는 전체 JWS 페이로드의 base64url 인코딩 값이 최상위 요소로 포함된다.

    JWS 보호 헤더는 서명이나 MAC을 통해 무결성을 보장해야 하는 파라미터를 가진다.

    이때, protected 파라미터가 JWS 보호 헤더의 base64url 인코딩 값을 가진다.

     

    JWS 비보호 헤더는 무결성 보호를 받지 않는 헤더 파라미터를 가진다.

    header 파라미터가 JWS 보호 헤더의 base64url 인코딩 값을 가진다.

     

    보호 헤더와 비 보호헤더를 결합하여 서명에 해당하는 JOSE 헤더를 만들 수 있다.

    // create a claim set.
    JWTClaimsSet jwtClaims = new JWTClaimsSet.Builder().
            // set the value of the issuer.
            issuer("<https://apress.com>").
            // set the subject value - JWT belongs to this subject.
            subject("john").
            // set values for audience restriction.
            audience(aud).
            // expiration time set to 10 minutes.
            expirationTime(new Date(new Date().getTime() + 1000 * 60 * 10)).
            // set the valid from time to current time.
            notBeforeTime(currentTime).
            // set issued time to current time.
            issueTime(currentTime).
            // set a generated UUID as the JWT identifier.
            jwtID(UUID.randomUUID().toString()).build();

    위와 같은 방식으로 클레임 셋을 만들 수 있다.

     


     

    JWT는 OAuth 2.0 을 통해서 소셜 로그인을 적용할 때만 열어보는 값이었는데 여러 방식, 여러 파라미터가 있다는 것은 이번에 처음 알았다.

    이런 파리미터를 적절히 활용할 수 있는 환경이 이미 구축되어있겠다 생각하니 참 배울게 많다고 느껴진다.

    이 부분도 아직 이해가 완벽하지는 않다. 추가적으로 학습하면서 내용을 보완해보려고 한다.

    댓글

Designed by Tistory.