서론

Json Web Token(이하 JWT)은 전자 서명을 통해 json의 변조를 방지하는 Claim 기반의 토큰입니다. 이번 글에서는 제가 RS256을 다루면서 한참을 헤맸던 부분을 조금 정리해보고자 합니다.

본 글에서는 Google Colab에서 Python 3.7.12 PyJWT 2.3.0을 사용하여 테스트하였습니다. 결과값이 다르다면 동일한 환경에서 시도해주세요.

### RS256이란

JWT는 크게 header, payload, signature의 세 부분으로 구성됩니다. header은 해당 json이 JWT이며 어떤 알고리즘으로 서명하였는지, payload는 JWT에 담고자 한 내용이, signature은 header와 payload의 정보를 서명한 결과가 URLsafe base64 인코딩되어 담겨있습니다. (마지막의 패딩인 =는 제외합니다.)

signature은 보통 메세지를 SHA256 해시 처리한 후 HMAC, RSA, ECC 등의 알고리즘에 입력하는 과정을 거쳐 만들어집니다.

RS256은 SHA256으로 메세지를 해싱하고 RSA의 비밀키로 서명하는 알고리즘입니다.

RS256 구조 분석

자 그렇다면, 정말 RS256이 이와 같이 동작하는지 검증해봅시다.

비밀키는 PEM 형식으로 다음과 같은 키를 사용합니다.

prv = """-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj
MzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu
NMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ
qgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg
p2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR
ZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi
VuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV
laAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8
sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H
mQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY
dgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw
ta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ
DM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T
N0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t
0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv
t8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU
AhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk
48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL
DY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK
xt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA
mNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh
2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz
et6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr
VBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD
TQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc
dn/RsYEONbwQSjIfMPkvxF+8HQ==
-----END PRIVATE KEY-----"""

사용할 payload는 {"id":"sean9892"}입니다. 이를 RS256으로 인코딩하면…

import base64
def b64pad(s): # recover padding
  return s+"="*((-len(s))%4)

header,payload,signature = token.split(".")
print("header(urlsafe-b64-encoded)     :",header)
print("payload(urlsafe-b64-encoded)    :",payload)
print("signature(urlsafe-b64-encoded)  :",signature)
print("header(urlsafe-b64-decoded)     :",base64.urlsafe_b64decode(b64pad(header)).decode())
print("payload(urlsafe-b64-decoded)    :",base64.urlsafe_b64decode(b64pad(payload)).decode())
"""
Output:
header(urlsafe-b64-encoded)     : eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9
payload(urlsafe-b64-encoded)    : eyJpZCI6InNlYW45ODkyIn0
signature(urlsafe-b64-encoded)  : mx97KL1A6sOzBGrC9Bauoq7KIXDt9fZr5ooItkm-0mNzQcB8V8O5wWCPzOObqRH8jV4OQfrvr23_W_xFGJ9NsG0G_qyPtUaVfkLJfMVaWs8Cc6C7884-tanh9dR1KTktsy5izHcwULrMTVxjnaoHmPeRFfnm5Ns-83uEw9BV6rh7TNJvtR6LdKP3xWym2fR2HXhSyODj9qSa8bC5-4YXnvS3RedHpNrY4XeYazJ7Fju0YtN7J6Gu2gGbVnc_J7_-sSzROi3CGnzmMH6zKLmhdy6-yxv3wCsdPly_GR6FriheRGHken7XyqJJm7LISlxGPLEl_5SOBHgT6wb1h1RP9Q
header(urlsafe-b64-decoded)     : {"typ":"JWT","alg":"RS256"}
payload(urlsafe-b64-decoded)    : {"id":"sean9892"}
"""

잘 나오네요! 그럼 검증을 위해 eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpZCI6InNlYW45ODkyIn0를 SHA256 처리 후 Big Endian 기준으로 정수형 변환하고, private exponent로 처리해줍시다.

from Crypto.PublicKey import RSA
from Crypto.Hash import SHA256
from Crypto.Util.number import long_to_bytes

prv_key = RSA.import_key(prv)
m = b"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpZCI6InNlYW45ODkyIn0"
m = SHA256.new(m).digest()
m = int.from_bytes(m,byteorder="big")
s = pow(m,prv_key.d,prv_key.n)
s = base64.urlsafe_b64encode(long_to_bytes(s)).decode().strip("=")
print("m :",hex(m))
print("s :",s)
print(signature == s)
"""
Output:
m : 0xefae4155320433a301f6d23bc456a7cca8b9a6f649f0242040c89eb0a03abf6b
s : KvEY5yotkwEE0DJ2SV5S1jgjnTJaIU_pz3cas1AGW4PdA5EKYofoYaBgP71m9UG4x9_0urFTZOIyusrEqcekcWrKqKL9ftx-uFSNjbcSXM1H450PVVLIxXFLCFNlv2m_A1QgLofWIrB13yzQ7ubn7URQZwjyBgwafUNj7qgacn_nwVuErr20cCWjrkfPhOtrgOrarghHYc5gSH59C8JyXYfSFoODgUvgxAj6EQfGjDre273Dmt_KULNuc2DFUL07310DzmEWMSHU280Pb7fs0Y-DxtPS9fXk89p3P0s94XEls-TjImMLft_90Z2N8w9uJVbKlv-JTOkkecKjmZEk_w
False
"""

서명 결과값이 다르네요! 무언가 빠뜨린 과정이 있나봅니다.

빠진 과정을 여러 라이브러리의 구현체를 통해 살펴보면 pkcs1_15._EMSA_PKCS1_V1_5_ENCODE라는 함수를 거쳐야 함을 알 수 있습니다. 따라서 이를 거쳐주면

from Crypto.PublicKey import RSA
from Crypto.Hash import SHA256
from Crypto.Util.number import long_to_bytes, size, ceil_div

prv_key = RSA.import_key(prv)
m = b"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpZCI6InNlYW45ODkyIn0"
m = pkcs1_15._EMSA_PKCS1_V1_5_ENCODE(SHA256.new(m),ceil_div(size(prv_key.n),8))
m = int.from_bytes(m,byteorder="big")
s = pow(m,prv_key.d,prv_key.n)
s = base64.urlsafe_b64encode(long_to_bytes(s)).decode().strip("=")
print("m :",hex(m))
print("s :",s)
print(signature == s)
"""
Output:
m : 0x1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff003031300d060960864801650304020105000420efae4155320433a301f6d23bc456a7cca8b9a6f649f0242040c89eb0a03abf6b
s : mx97KL1A6sOzBGrC9Bauoq7KIXDt9fZr5ooItkm-0mNzQcB8V8O5wWCPzOObqRH8jV4OQfrvr23_W_xFGJ9NsG0G_qyPtUaVfkLJfMVaWs8Cc6C7884-tanh9dR1KTktsy5izHcwULrMTVxjnaoHmPeRFfnm5Ns-83uEw9BV6rh7TNJvtR6LdKP3xWym2fR2HXhSyODj9qSa8bC5-4YXnvS3RedHpNrY4XeYazJ7Fju0YtN7J6Gu2gGbVnc_J7_-sSzROi3CGnzmMH6zKLmhdy6-yxv3wCsdPly_GR6FriheRGHken7XyqJJm7LISlxGPLEl_5SOBHgT6wb1h1RP9Q
True
"""
yubin.choi's profile image

최유빈(yubin.choi)

2021-11-01 12:00

Read more posts by this author