JWTの生成/検証/改ざん検出をコマンドラインでやってみる(共通鍵暗号方式編)
このレクチャーでは、JWT をコマンドラインで生成/検証し、そしてもし改ざんした場合どうなるのかという一連の流れを体験してみましょう。
目次
準備
JWT の header と payload を JSON 形式で用意します。
JWT のヘッダーを用意する
{
"alg": "HS256",
"typ": "JWT"
}
JWT のペイロードを用意する
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}
JWT 生成
それでは、JWT を生成していきます。
① ヘッダーを Base64URL エンコードする
cat jwt-header-plaintext.json | jq -c | base64 | tr '+/' '-_' | tr -d '=' | tee jwt-header.txt
| No. | コマンド | 役割 |
|---|---|---|
| 1 | cat jwt-header-plaintext.json |
JSONファイルを読み込む |
| 2 | jq -c |
JSONを1行に圧縮。-c オプション(compact-output)を使用 |
| 3 | base64 |
Base64エンコード |
| 4 | tr '+/' '-_' |
+→-に、/→_に変換する(Base64URL形式への変換) |
| 5 | tr -d '=' |
末尾の = を削除する(Base64URLへの変換) |
| 6 | tee jwt-header.txt |
出力を表示とファイル保存 |
ここまで完了すると、jwt-header.txt の中身は次のようになります:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Cg
② ペイロードを Base64URL エンコードする
ペイロードもヘッダーと同じように Base64URL エンコードします。
cat jwt-payload-plaintext.json | jq -c | base64 | tr '+/' '-_' | tr -d '=' | tee jwt-payload.txt
| No. | コマンド | 役割 |
|---|---|---|
| 1 | cat jwt-payload-plaintext.json |
JSONファイルを読み込む |
| 2 | jq -c |
JSONを1行に圧縮。-c オプション(compact-output)を使用 |
| 3 | base64 |
Base64エンコード |
| 4 | tr '+/' '-_' |
+→-に、/→_に変換する(Base64URL形式への変換) |
| 5 | tr -d '=' |
末尾の = を削除する(Base64URLへの変換) |
| 6 | tee jwt-payload.txt |
出力を表示とファイル保存 |
ここまで完了すると、jwt-payload.txt の中身は次のようになります:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0K
③ 署名対象データを作成する
署名の対象データを作成します。ヘッダーとペイロードをドットで連結した文字列(${header}.${payload} の形式)を用意します。この文字列全体が署名(signature)を計算する元データになります。
printf '%s.%s' "$(cat jwt-header.txt)" "$(cat jwt-payload.txt)" | tee jwt-header-payload.txt
| No. | コマンド | 役割 |
|---|---|---|
| 1 | printf '%s.%s' "$(cat jwt-header.txt)" "$(cat jwt-payload.txt)" |
ヘッダーとペイロードを . で連結した文字列を生成 |
| 2 | tee jwt-header-payload.txt |
生成した文字列を標準出力しつつ、jwt-header-payload.txt に保存 |
ここまで完了すると、jwt-header-payload.txt の中身は次のようになります:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Cg.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0K
④ 署名を作成する
署名は、ヘッダーとペイロードを連結した文字列に秘密鍵を使って HMAC-SHA256 で計算します。
HMAC-SHA256の秘密鍵(シークレットキー)には、テスト用として abc123 を指定しています
cat jwt-header-payload.txt | openssl dgst -binary -sha256 -hmac "abc123" | openssl base64 -A | tr '+/' '-_' | tr -d '=' | tee jwt-signature.txt
| No. | コマンド | 役割 |
|---|---|---|
| 1 | cat jwt-header-payload.txt |
ヘッダーとペイロードを連結したファイルを読み込む |
| 2 | openssl dgst -binary -sha256 -hmac "abc123" |
HMAC-SHA256 アルゴリズムで署名を生成(バイナリ形式で出力) |
| 3 | openssl base64 -A |
署名結果を Base64 でエンコード(改行なし) |
| 4 | tr '+/' '-_' |
Base64URL 形式へ変換する(’+’→’-’, ‘/’→’_’) |
| 5 | tr -d '=' |
末尾の = を削除する(Base64URL 化の仕上げ) |
| 6 | tee jwt-signature.txt |
署名の値を出力&保存 |
ここまで完了すると、jwt-signature.txt の中身は次のようになります:
TO322quK2fLK-nZ7InkmVYO8_R7iXsWqC2g683UiCPg
⑤ ヘッダー・ペイロード・署名を連結して JWT を作る
仕上げに3つのパートを連結すると、JWTの完成です。
printf '%s.%s.%s' "$(cat jwt-header.txt)" "$(cat jwt-payload.txt)" "$(cat jwt-signature.txt)" | tee jwt.txt
| No. | コマンド | 役割 |
|---|---|---|
| 1 | printf '%s.%s.%s' "$(cat jwt-header.txt)" "$(cat jwt-payload.txt)" "$(cat jwt-signature.txt)" |
ヘッダー・ペイロード・署名をドット区切りで連結する |
| 2 | tee jwt.txt |
生成したJWT文字列を標準出力&jwt.txtに保存 |
ここまで完了すると、 jwt.txt の中身は次のようになります:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Cg.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0K.TO322quK2fLK-nZ7InkmVYO8_R7iXsWqC2g683UiCPg
JWT 検証
JWT の検証とは、「署名を受信側で再計算して、改ざんされていないかをチェックする」ことです。
このとき、検証側も送信側と同じ秘密鍵(シークレットキー)を知っている必要があります。
この例では、署名時に使った秘密鍵 abc123 を検証時にも使います。
具体な手順としては、
- 「ヘッダー」と「ペイロード」を取り出し
- 同じ鍵でHMAC-SHA256による署名を再計算します
- その結果がJWTに含まれる署名部分と一致するかどうかで、正当なトークンか、改ざんされていないかを確認します
① JWT のヘッダーとペイロードを切り出す
まずは、受け取った JWT からヘッダーとペイロードを取り出します。ドット区切りで1つ目と2つ目の部分が対象です。
cat jwt.txt | cut -d. -f1,2 | tr -d '\n' | tee jwt-header-payload-when-verified.txt
| No. | コマンド | 役割 |
|---|---|---|
| 1 | cat jwt.txt |
JWT全体をファイルから読み込む |
| 2 | cut -d. -f1,2 |
.(ドット)で分割し、1つ目(ヘッダー)・2つ目(ペイロード)部分を抽出 |
| 3 | tr -d '\n' |
途中に入る改行コード(\n)を除去して1行にまとめる |
| 4 | tee jwt-header-payload-when-verified.txt |
結果をファイルに保存&標準出力にも表示 |
このコマンドを実行すると、jwt-header-payload-when-verified.txt の中身はこのようになります:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Cg.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0K
② JWT の署名を切り出す
JWT から署名を取り出します。この受け取った署名とこれから手元で再計算した署名を比較します。
cat jwt.txt | cut -d. -f3 | tr -d '\n' | tee jwt-header-payload-when-verified.txt
| No. | コマンド | 役割 |
|---|---|---|
| 1 | cat jwt.txt |
JWT全体をファイルから読み込む |
| 2 | cut -d. -f3 |
ドット区切りで3つ目(署名部分)を抽出する |
| 3 | tr -d '\n' |
途中に入る改行コード(\n)を除去して1行にまとめる |
| 4 | tee jwt-signature-when-verified.txt |
結果をファイルに保存&標準出力にも表示 |
このコマンドを実行すると、jwt-signature-when-verified.txt の中身はJWTから取り出した署名部分になります:
TO322quK2fLK-nZ7InkmVYO8_R7iXsWqC2g683UiCPg
③ ヘッダーとペイロードから署名を計算する
JWTのヘッダーとペイロードと秘密鍵(シークレットキー)で署名を再計算します。
cat jwt-header-payload-when-verified.txt | openssl dgst -binary -sha256 -hmac "abc123" | openssl base64 -A | tr '+/' '-_' | tr -d '=' | tee jwt-signature-when-verified.txt
| No. | コマンド | 役割 |
|---|---|---|
| 1 | cat jwt-header-payload-when-verified.txt |
headerとpayloadが連結されたテキストを読み込む |
| 2 | openssl dgst -binary -sha256 -hmac "abc123" |
HMAC-SHA256アルゴリズムで署名を計算(バイナリ出力) |
| 3 | openssl base64 -A |
署名バイト列をBase64でエンコード(一行で出力) |
| 4 | tr '+/' '-_' |
Base64の + → - 、/ → _ に変換(Base64URL形式に変換) |
| 5 | tr -d '=' |
パディングの = を削除(Base64URL形式に仕上げる) |
| 6 | tee jwt-signature-when-verified.txt |
出力を表示しつつファイルに保存 |
このコマンドを実行すると、jwt-signature-when-verified.txt はこのようになります
TO322quK2fLK-nZ7InkmVYO8_R7iXsWqC2g683UiCPg
④ JWT に付与されている署名と手元で再計算した署名を比較する
JWT に含まれていた署名と、手元で再計算した署名が同一であれば改ざんされていないと判断できます。
diff jwt-signature.txt jwt-signature-when-verified.txt && echo "VERIFIED" || echo "INVALID"
| No. | コマンド | 役割 |
|---|---|---|
| 1 | diff jwt-signature.txt jwt-signature-when-verified.txt |
JWTに含まれていた署名と手元で再計算した署名を比較する。違いがなければ何も出力しない(正常終了)、違いがあれば差分を出力(異常終了) |
| 2 | && echo "VERIFIED" |
diffコマンドが正常終了(両ファイルが同一)だった場合、「VERIFIED」と表示する |
| 3 | || echo "INVALID" |
diffコマンドが異常終了(両ファイルが異なる)だった場合、「INVALID」と表示する |
このコマンドを実行すると、VERIFIED と表示されるはずです。検証の結果、改ざんされていないと判定されました。
JWT 改ざん検出
次はJWTが改ざんされているケースの挙動を確かめてみましょう。
準備1:ペイロードを改ざんする
JWT の payload の sub を "1234567890" から "attacker" に書き換えてみます。これにより、JWTを利用するシステム側には、他人(attacker)になりすました偽造トークンが渡ることになります。
JWT の検証では、このようなケースを検出する必要があります。
cat jwt-payload-plaintext.json | jq -c '.sub = "attacker"' | base64 | tr '+/' '-_' | tr -d '=' | tee jwt-payload-tampered.txt
| No. | コマンド | 役割 |
|---|---|---|
| 1 | cat jwt-payload-plaintext.json |
payloadのJSONファイルを読み込む |
| 2 | jq -c '.sub = "attacker"' |
subを"attacker"に書き換えて1行JSON(compact)に変換 |
| 3 | base64 |
Base64エンコード |
| 4 | tr '+/' '-_' |
Base64の + → - 、/ → _ に変換(Base64URL形式に変換) |
| 5 | tr -d '=' |
パディングの = を削除(Base64URL形式に仕上げる) |
| 6 | tee jwt-payload-tampered.txt |
出力を表示&ファイル保存 |
このコマンドを実行すると、jwt-payload-tampered.txt にはこのような内容になっています:
eyJzdWIiOiJhdHRhY2tlciIsIm5hbWUiOiJKb2huIERvZSIsImFkbWluIjp0cnVlLCJpYXQiOjE1MTYyMzkwMjJ9Cg
準備2:改ざんされたJWTを作る
改ざんしたペイロードをもともとのヘッダーと署名に連結して改ざんされた JWT を作ります。
printf '%s.%s.%s' "$(cat jwt-header.txt)" "$(cat jwt-payload-tampered.txt)" "$(cat jwt-signature.txt)" | tee jwt-tampered.txt
| No. | コマンド | 役割 |
|---|---|---|
| 1 | printf '%s.%s.%s' "$(cat jwt-header.txt)" "$(cat jwt-payload-tampered.txt)" "$(cat jwt-signature.txt)" |
各ファイルからヘッダー、改ざんペイロード、元の署名を読み出し、. で区切って1つのJWT形式文字列として合成する |
| 2 | tee jwt-tampered.txt |
合成した改ざんJWTを画面表示しつつ、jwt-tampered.txt というファイルに保存する |
このコマンドにより、「元のヘッダー.改ざんペイロード.元の署名」という構造の"改ざんJWT"が得られます。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Cg.eyJzdWIiOiJhdHRhY2tlciIsIm5hbWUiOiJKb2huIERvZSIsImFkbWluIjp0cnVlLCJpYXQiOjE1MTYyMzkwMjJ9Cg.TO322quK2fLK-nZ7InkmVYO8_R7iXsWqC2g683UiCPg
① 改ざんされたJWTのヘッダーとペイロードを切り出す
cat jwt-tampered.txt | cut -d. -f1,2 | tr -d '\n' | tee jwt-header-payload-tampered-when-verified.txt
| No. | コマンド | 役割 |
|---|---|---|
| 1 | cat jwt-tampered.txt |
改ざんされたJWTが格納されたファイルを読み込む |
| 2 | cut -d. -f1,2 |
. (ドット) を区切り文字として1列目(ヘッダー)と2列目(ペイロード)を取り出す |
| 3 | tr -d '\n' |
途中で入る改行コード(LF)をすべて削除し、1行の文字列にする |
| 4 | tee jwt-header-payload-tampered-when-verified.txt |
標準出力に表示しつつ、ファイルにも保存する |
このコマンドを実行すると、jwt-header-payload-tampered-when-verified.txt は次のような内容になっています
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Cg.eyJzdWIiOiJhdHRhY2tlciIsIm5hbWUiOiJKb2huIERvZSIsImFkbWluIjp0cnVlLCJpYXQiOjE1MTYyMzkwMjJ9Cg
② ヘッダーとペイロードから署名を計算する
JWTのヘッダーとペイロードと秘密鍵(シークレットキー)で署名を再計算します。
cat jwt-header-payload-tampered-when-verified.txt | openssl dgst -binary -sha256 -hmac "abc123" | openssl base64 -A | tr '+/' '-_' | tr -d '=' | tee jwt-signature-tampered-when-verified.txt
| No. | コマンド | 役割 |
|---|---|---|
| 1 | cat jwt-header-payload-tampered-when-verified.txt |
改ざんJWTのヘッダーとペイロード部分をファイルから取得する |
| 2 | openssl dgst -binary -sha256 -hmac "abc123" |
HMAC-SHA256アルゴリズムで署名を計算(秘密鍵は"abc123"、バイナリ出力) |
| 3 | openssl base64 -A |
署名のバイナリをBase64でエンコード(改行なし) |
| 4 | tr '+/' '-_' |
‘+’→’-’, ‘/’→’_’ に変換しBase64URL形式に変換 |
| 5 | tr -d '=' |
Base64URL形式仕上げ。末尾の’=‘パディングを除去 |
| 6 | tee jwt-signature-tampered-when-verified.txt |
出力結果をファイル保存および標準出力 |
このコマンドを実行すると jwt-signature-tampered-when-verified.txt は次のようになっています
dsAT74ukY9mAXZiQqc-M-QIj-6LrYuFcTT9Q1fZSxRY
③ JWT に付与されている署名と手元で再計算した署名を比較する
JWT に含まれていた署名と、手元で再計算した署名が異なっていればJWTは改ざんされていると判断できます。
diff jwt-signature.txt jwt-signature-tampered-when-verified.txt && echo "VERIFIED" || echo "INVALID"
| No. | コマンド | 役割 |
|---|---|---|
| 1 | diff jwt-signature.txt jwt-signature-tampered-when-verified.txt |
2つの署名ファイル(元・改ざん検証用)を比較し、違いがあれば非ゼロ終了値(=異なる)を返す |
| 2 | && echo "VERIFIED" |
差分がなければ(同一なら)“VERIFIED” と出力 |
| 3 | || echo "INVALID" |
差分があれば(異なれば)“INVALID” と出力 |
このコマンドを実行すると、INVALID と表示されるはずです。検証の結果、改ざんが検出されました。