Lento con forza

大学生気分のIT系エンジニアが色々書いてく何か。ブログ名決めました。

eyJから始まらないJWTを作りたい

eyJから始まる文字列を見たら https://jwt.io/ を開きたくなる id:kouki_dan です、こんにちは。

この記事ははてなエンジニア Advent Calendar 2024の8日目の記事です。

さて、JWTといえば、eyJから始まる文字列です。なぜかというと、JWTはJSON文字列のBase64URLエンコードを元に作られていて、JSONの最初の3文字になりがちな {"(a-zA-Z)は、どれもBase64エンコードするとeyJから始まる文字列になるからですね。

% echo "{\"a" | base64
eyJhCg==
% echo "{\"z" | base64
eyJ6Cg==
% echo "{\"A" | base64
eyJBCg==
% echo "{\"Z" | base64
eyJaCg==

JWTの最初の方の文字列はヘッダ部で、Base64をデコードすると以下のようになっていることが多いと思います。 {"alg": くらいまではだいたい一緒だと思うので、本当は eyJhbGciOi くらいまで一致することが多いのではないでしょうか。プロはエンコードされた文字列を見ただけで、どのアルゴリズムが使われているのかどうかがわかったりするのでしょうね。僕はそのレベルには達していませんが・・・。

{
  "alg": "HS256",
  "typ": "JWT"
}

ということで、eyJから始まる文字列は、実際はJSONをBase64エンコードしたものの可能性が高く、JWTはJSONをBase64URLエンコードしたものを元にしているので、eyJから始まりがち、ということになりますね。


ここで本題、eyJから始まるとパッと見でJWTっぽく見えて嫌ですよね。*1 Base64デコードしてJSONになればいいので、何かしら対策のしようがあるのではないでしょうか。

まず思いつくのは、改行や空白、タブ文字を入れてもJSONとしては正しそう*2、というもの。この作戦で行きましょう。

最初の文字がeyJじゃなければJWT感を感じなくなると思うので、ヘッダー部分だけ改ざんしてあげると良いでしょう。 https://jwt.io/ にデフォルトで入っているJWTのヘッダー部分を改行付きJSONに変えてBase64URLエンコードしてみましたが、Invalid Signatureとなってしまいました。

ewogICJhbGciOiJIUzI1NiIsCiAgInR5cCI6IkpXVCIKfQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

それもそのはず、JWTの署名はヘッダーとボディのどちらにも改ざんがないことを示すものです。署名の対象の文字列はエンコード済みの文字列(今回の場合だとewogICJhbGciOiJIUzI1NiIsCiAgInR5cCI6IkpXVCIKfQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ)です。ヘッダー部を改ざんしたので、署名が通らないのは当然ですね。

通すためには、署名してあげると良さそうです。

と、いうわけで、サクッと署名をしてあげて*3

import hashlib
import hmac
import base64

SECRET_KEY = b"your-256-bit-secret" 
MESSAGE = b"ewogICJhbGciOiJIUzI1NiIsCiAgInR5cCI6IkpXVCIKfQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJzdGFydHNfZnJvbSI6IndlbyJ9"

hmac_message = hmac.new(key=SECRET_KEY, msg=MESSAGE, digestmod=hashlib.sha256).digest()
hmac_message_base64 = base64.urlsafe_b64encode(hmac_message).decode().replace("=", "")

print(MESSAGE.decode() + "." + hmac_message_base64)

出てきた文字列がこちら。ちゃんと https://jwt.io でも Signature Verifiedになってますね。めでたしめでたし。

ewogICJhbGciOiJIUzI1NiIsCiAgInR5cCI6IkpXVCIKfQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJzdGFydHNfZnJvbSI6IndlbyJ9.t8P9RXbdyjmRrZylXluUnR6SMk5uIKWfQxYhLFrvNE8

ewoから始まっていますが、ewの並びがなんかeyっぽくてJWTみがあるのが気になります。やはりeから始まっている文字列という時点でJWTかもしれないと思っていそうな気もします。先頭に空白を入れるといいのかなぁ。でもそうすると世の中のJWTライブラリが処理できなくなる可能性もありそうで怖くて悩んでいます。ちなみに先頭を空白にすると、最初の3文字は IHs になって、一応Validになっています*4

軽く調べた感じ、JWTに空白を切り詰めたJSONを使用することという仕様はないように思える*5けど、実際はなんかライブラリによってはうまく動かなかったりするかも。一発ネタのつもりなので、実用する場合はちゃんと調べてください!

まだまだ続くはてなエンジニア Advent Calendar 2024、明日は id:gurrium です!

*1:本当に?本当は別に嫌じゃないんですが、ネタとして・・・

*2:BOMも考えたけど、明確に禁止されていた

*3:既存のJWTライブラリを使うと自動的に空白が切り詰められるので、手動で・・・

*4:https://jwt.io/#debugger-io?token=IHsgICJhbGciOiJIUzI1NiIsCiAgInR5cCI6IkpXVCIKfQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJzdGFydHNfZnJvbSI6IklIcyJ9.lh3NK5QN3TTSO8nMflog0tDBC3GaJv_ejLUNgHj703s

*5:RFC 7519 - JSON Web Token (JWT) には This JSON object MAY contain whitespace and/or line breaks before or after any JSON values or structural characters, in accordance with Section 2 of RFC 7159 と書いてあるから大丈夫な気はするけど。実際にセクション3.1で書かれているJWTの例には改行を含んでいることが明示的になっているし。 7.1には no canonicalization need be performed before encoding. とも書いてある。