暗号化されたJWTをパースしてみる

セッション管理にJWTをCookieにセットして使う実装も増えてきて、いよいよ JWTが流行しそうな予感がする。セッション管理になると暗号化されたJWT (JWE) を使うので、その辺の仕組みを知っておく必要があるので、やってみた。

今回書いてみたコードは、draft-ietf-jose-json-web-encryption [1] Appendix A.2に出てくるRSA1_5 and A128CBC-HS256で暗号化されたJWE をパースするもの。

書いてみてポイントかなと思ったところは、

  • Content Encryption Key (CEK) の前半128bitをMAC_KEY、後半128bitをENC_KEY として使う。
  • ブロック暗号のパディングはPKCS#7 Paddingになっているので、 復号するときにPKCS#7 Paddingの仕様にあわせたパディング除去が必要。
  • AAD Lengthは、バイト長ではなくビット長を値として持つので、変換が必要。

といったところ。

ただ、”JOSE Header” と “JWE Protected Header” と “AAD” との関係というか 用語の使い分けがまだしっくりきていない。

from base64 import urlsafe_b64decode

from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
from Crypto.Cipher import AES
from Crypto.Hash import HMAC
from Crypto.Hash import SHA256
from Crypto.Util.number import long_to_bytes, bytes_to_long


# RSA Key described in A.2.3
#
jwk_n = 'sXchDaQebHnPiGvyDOAT4saGEUetSyo9MKLOoWFsueri23bOdgWp4Dy1Wl' \
        'UzewbgBHod5pcM9H95GQRV3JDXboIRROSBigeC5yjU1hGzHHyXss8UDpre' \
        'cbAYxknTcQkhslANGRUZmdTOQ5qTRsLAt6BTYuyvVRdhS8exSZEy_c4gs_' \
        '7svlJJQ4H9_NxsiIoLwAEk7-Q3UXERGYw_75IDrGA84-lA_-Ct4eTlXHBI' \
        'Y2EaV7t7LjJaynVJCpkv4LKjTTAumiGUIuQhrNhZLuF_RJLqHpM2kgWFLU' \
        '7-VTdL1VbC2tejvcI2BlMkEpk1BzBZI0KQB0GaDWFLN-aEAw3vRw'
jwk_e = 'AQAB'
jwk_d = 'VFCWOqXr8nvZNyaaJLXdnNPXZKRaWCjkU5Q2egQQpTBMwhprMzWzpR8Sxq' \
        '1OPThh_J6MUD8Z35wky9b8eEO0pwNS8xlh1lOFRRBoNqDIKVOku0aZb-ry' \
        'nq8cxjDTLZQ6Fz7jSjR1Klop-YKaUHc9GsEofQqYruPhzSA-QgajZGPbE_' \
        '0ZaVDJHfyd7UUBUKunFMScbflYAAOYJqVIVwaYR5zWEEceUjNnTNo_CVSj' \
        '-VvXLO5VZfCUAVLgW4dpf1SrtZjSt34YLsRarSb127reG_DUwg9Ch-Kyvj' \
        'T1SkHgUWRVGcyly7uvVGRSDwsXypdrNinPA4jlhoNdizK2zF2CWQ'
jwk_p = '9gY2w6I6S6L0juEKsbeDAwpd9WMfgqFoeA9vEyEUuk4kLwBKcoe1x4HG68' \
        'ik918hdDSE9vDQSccA3xXHOAFOPJ8R9EeIAbTi1VwBYnbTp87X-xcPWlEP' \
        'krdoUKW60tgs1aNd_Nnc9LEVVPMS390zbFxt8TN_biaBgelNgbC95sM'
jwk_q = 'uKlCKvKv_ZJMVcdIs5vVSU_6cPtYI1ljWytExV_skstvRSNi9r66jdd9-y' \
        'BhVfuG4shsp2j7rGnIio901RBeHo6TPKWVVykPu1iYhQXw1jIABfw-MVsN' \
        '-3bQ76WLdt2SDxsHs7q7zPyUyHXmps7ycZ5c72wGkUwNOjYelmkiNS0'
n = bytes_to_long(urlsafe_b64decode(jwk_n+'=='))
e = bytes_to_long(urlsafe_b64decode(jwk_e+'=='))
d = bytes_to_long(urlsafe_b64decode(jwk_d+'=='))
p = bytes_to_long(urlsafe_b64decode(jwk_p+'=='))
q = bytes_to_long(urlsafe_b64decode(jwk_q+'=='))
key = RSA.construct((n, e, d, p, q))


# Target JWT described in A.2.7
#
jwt = 'eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.' \
      'UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-kFm' \
      '1NJn8LE9XShH59_i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKxGHZ7Pc' \
      'HALUzoOegEI-8E66jX2E4zyJKx-YxzZIItRzC5hlRirb6Y5Cl_p-ko3YvkkysZIF' \
      'NPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8OtvzlV7elprCbuPhcCdZ6XDP0_F8' \
      'rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTP-cFPgwCp6X-nZZd9OHBv' \
      '-B3oWh2TbqmScqXMR4gp_A.' \
      'AxY8DCtDaGlsbGljb3RoZQ.' \
      'KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY.' \
      '9hH0vgRfYgPnAHOd8stkvw'.split('.')


# Decode JOSE Header (JWE Protected Header) [A.2.1]
#
header = urlsafe_b64decode(jwt[0]+'==').decode()
print('## JOSE Header\n', header)


# Decrypt CEK usign the RSAES-PKCS1-V1_5 algorithm [A.2.3][A.2.2]
#
cek = PKCS1_v1_5.new(key).decrypt(urlsafe_b64decode(jwt[1]+'=='), None)
print('## CEK\n', list(cek))


# Decode Initializtion Vector [A.2.4][B.4]
#
iv = urlsafe_b64decode(jwt[2]+'==')
print('## IV\n', list(iv))


# Decode Additional Authenticated Data (use JWE Protected Header) [A.2.5]
#
aad = jwt[0].encode()
print('## AAD\n', list(aad))


# Extract MAC_KEY and ENC_KEY [B.1]
#
mac_key = cek[:16]
enc_key = cek[16:]
print('## MAC_KEY\n', list(mac_key))
print('## ENC_KEY\n', list(enc_key))


# Decrypt Ciphertext [B.2]

## Decode Ciphertext
ct = urlsafe_b64decode(jwt[3]+'==')
print('## Ciphertext\n', list(ct))

## Decrypt Ciphertext
aes = AES.new(enc_key, AES.MODE_CBC, iv)
text = aes.decrypt(ct).decode()

## Remove PKCS#7 padding
for n in range(1, aes.block_size+1):
    padding = chr(n) * n
    if text.endswith(padding):
        text = text[:-n]

print('## Plaintext\n', text)


# AAD Length value [B.3]
#
al = long_to_bytes(len(aad)*8, 8)
print('## AL value\n', list(al))


# Input to HMAC Computation [B.5]
#
print('## Input to HMAC Computation (AAD + IV + Ciphertext + AL value)\n',
      list(aad+iv+ct+al))


# Compute HMAC Value [B.6]
#
m = HMAC.new(mac_key, aad+iv+ct+al, SHA256).digest()
print('## Computed HMAC Value\n', list(m))


# Compute Authentication Tag [B.7]
#
t = m[:16]
tag = urlsafe_b64decode(jwt[4]+'==')
print('## Computed Authentiction Tag\n', list(t))
print('## Authentication Tag in JWT\n', list(tag))

https://gist.github.com/paoneJP/7fa72ab0458091681b30 にも同じものを貼っておく。

実行結果

$ python3 parse_jwe_sample.py
## JOSE Header
 {"alg":"RSA1_5","enc":"A128CBC-HS256"}
## CEK
 [4, 211, 31, 197, 84, 157, 252, 254, 11, 100, 157, 250, 63, 170, 106, 206, 107, 124, 212, 45, 111, 107, 9, 219, 200, 177, 0, 240, 143, 156, 44, 207]
## IV
 [3, 22, 60, 12, 43, 67, 104, 105, 108, 108, 105, 99, 111, 116, 104, 101]
## AAD
 [101, 121, 74, 104, 98, 71, 99, 105, 79, 105, 74, 83, 85, 48, 69, 120, 88, 122, 85, 105, 76, 67, 74, 108, 98, 109, 77, 105, 79, 105, 74, 66, 77, 84, 73, 52, 81, 48, 74, 68, 76, 85, 104, 84, 77, 106, 85, 50, 73, 110, 48]
## MAC_KEY
 [4, 211, 31, 197, 84, 157, 252, 254, 11, 100, 157, 250, 63, 170, 106, 206]
## ENC_KEY
 [107, 124, 212, 45, 111, 107, 9, 219, 200, 177, 0, 240, 143, 156, 44, 207]
## Ciphertext
 [40, 57, 83, 181, 119, 33, 133, 148, 198, 185, 243, 24, 152, 230, 6, 75, 129, 223, 127, 19, 210, 82, 183, 230, 168, 33, 215, 104, 143, 112, 56, 102]
## Plaintext
 Live long and prosper.
## AL value
 [0, 0, 0, 0, 0, 0, 1, 152]
## Input to HMAC Computation (AAD + IV + Ciphertext + AL value)
 [101, 121, 74, 104, 98, 71, 99, 105, 79, 105, 74, 83, 85, 48, 69, 120, 88, 122, 85, 105, 76, 67, 74, 108, 98, 109, 77, 105, 79, 105, 74, 66, 77, 84, 73, 52, 81, 48, 74, 68, 76, 85, 104, 84, 77, 106, 85, 50, 73, 110, 48, 3, 22, 60, 12, 43, 67, 104, 105, 108, 108, 105, 99, 111, 116, 104, 101, 40, 57, 83, 181, 119, 33, 133, 148, 198, 185, 243, 24, 152, 230, 6, 75, 129, 223, 127, 19, 210, 82, 183, 230, 168, 33, 215, 104, 143, 112, 56, 102, 0, 0, 0, 0, 0, 0, 1, 152]
## Computed HMAC Value
 [246, 17, 244, 190, 4, 95, 98, 3, 231, 0, 115, 157, 242, 203, 100, 191, 23, 164, 159, 52, 126, 166, 142, 192, 23, 79, 73, 181, 252, 116, 123, 198]
## Computed Authentiction Tag
 [246, 17, 244, 190, 4, 95, 98, 3, 231, 0, 115, 157, 242, 203, 100, 191]
## Authentication Tag in JWT
 [246, 17, 244, 190, 4, 95, 98, 3, 231, 0, 115, 157, 242, 203, 100, 191]
$

脚注

[1]JSON Web Encryption (JWE)