GoでJWT(JSON Web Token)を生成してみた
目次
1.はじめに
JWTとは
JSON Web Token(JWT)とは、RFC 7519で標準化されている認証情報などを運ぶためのトークンのことです。JWTの読み方は「JOT」と呼ぶことが推奨されているらしく、「ジョット」「ジョゥト」など色々と人によって呼び方の正解が変わっているようです。日本語的な発音であれば正解はないと思っていますが、周りの詳しい方たちは「ジョット」と呼ぶので自分もそう呼んでいます。
トークン自体は名前の通りJSON形式となっており、特定の使い方をする際の型としてのJWTであると考えるとよいでしょう。どこで使われているのかというと、有名どころで言うとOpenID ConnectのID Tokenの形式はJWTとなっています。他にも、LINEのMessaging APIのトークンとしても用いられています。
記事の概要
本記事では、JWTについて説明と、JWTに関連する攻撃について説明をした後、実際にJWTを使うためのハンズオンとしてGoでJWTを生成するプログラムを紹介していきます。
2.JWTの内容
JWTはヘッダー・ペイロード・署名の3つで構成されており、それらをBase64 URL encodingし、ピリオドで結合したものが実際に送信されます。
署名アルゴリズムやJWTの形式を示すキーとバリューが存在し、それぞれを「Claim Name」と「Claim Value」と呼びます。
JWTのホームページでは実際にエンコードとデコードができる場所があるので、気になった方はぜひ覗いてみてください。
ヘッダー
ヘッダーとはJWTとして使用するために必要な情報が配置されており、以下のような形をしています。ここではClaimとしてalg
とtyp
が設定されていますね。
{
"alg": "HS256",
"typ": "JWT"
}
alg
というパラメータでは、署名に用いるアルゴリズムが指定されています。ここではHS256、つまりハッシュ関数にSHA-256を用いたHMACによって署名を行う、ということをここで指定しています。トークンを受けとった側は、ここでアルゴリズムを判定して送られてきたトークンが想定されたユーザーからのものなのかを判断するための処理を行います。
ペイロード
ペイロードにはユーザーの情報やidなど、送受信者がやりとりしたいデータが格納されています。
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
署名
署名はJSON形式ではなく、署名アルゴリズムによって出力された値です。ヘッダーとペイロードと暗号化を行うための鍵を入力して得られた値が以下のものです。
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Base64 URL encoding
Base64 URL encodingとは、通常のBase64 encoding とは違い、URLセーフな出力を得ることができます。+
や/
といった特別な意味として捉えられる文字列を別の文字列に置き換えることで、URLクエリパラメータなどにJWTを加えることができます。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
3.JWTに関連する攻撃
"alg=None"攻撃
"alg=None"攻撃とは、ヘッダーにあるalg
のパラメータをNoneに書き換えることによって、署名を回避する攻撃のことです。署名を回避することができれば内容が改ざんされたことを認識できなくなるため、いくらでも中身をいじることができてしまいます。
脆弱性ではないのかと思ってしましますが、一応仕様として"alg=None"があるのでJWT自体の脆弱性ではなく実装時の脆弱性ということになります(でもほぼJWTの脆弱性じゃん…思ってしまう)。汎用性を高めようとした結果としてこのような仕様になったのではないかと筆者は考えています。
共有鍵をシークレットとして使用する攻撃
ヘッダーの検証アルゴリズムを書き換えることで、通らないはずの検証が通ってしまう攻撃です。
公開鍵による検証を行うことを想定しており、サーバー側は検証の鍵として公開鍵をセットしていたとする。
jwt.verify(TOKEN, KEY)
上記のようにプログラムを書いていた場合に、"alg=None"攻撃のときと同様にHS256のような共通鍵暗号化方式に書き換えたとします。
攻撃者が何らかの方法によって公開鍵を手に入れることができた場合、検証アルゴリズムが通ってしまし、改ざんが可能になってしまいます。
4.参考サイト
-
JSON Web Token(JWT)の紹介とYahoo! JAPANにおけるJWTの活用
- 2017年の記事なので古いですが、当時YahooでのJWTの活用例がまとめられています。
-
JWT形式を採用したChatWorkのアクセストークンについて
- JWTのアクセストークンの失効管理について議論がされています。
-
SPAセキュリティ入門~PHP Conference Japan 2021
- 徳丸さんの発表スライドです。
5. JWTの生成のしかた
本記事ではGoを用いてJWTを生成する手順について解説をします。途中まではLINE Messaging APIのドキュメントを参考にしています。
以下のコードはGoのライブラリである jwx によって秘密鍵と公開鍵のペアを生成したあと、秘密鍵を用いて署名を行うコードです。秘密鍵と公開鍵のペアはLINE Messaging APIのドキュメントに書いてある方法で作成できます。
package main
import (
"fmt"
"io/ioutil"
"time"
"github.com/lestrrat-go/jwx/jwa"
"github.com/lestrrat-go/jwx/jwk"
"github.com/lestrrat-go/jwx/jwt"
)
func main() {
// private.keyファイルのパス
keyFilePath := "private.key"
// private.keyファイルを読み込む
keyData, err := ioutil.ReadFile(keyFilePath)
if err != nil {
fmt.Println("ファイルの読み込みエラー:", err)
return
}
// JWKをparse
set, err := jwk.Parse(keyData)
if err != nil {
fmt.Println("JWKのパースエラー:", err)
return
}
// ペイロードを設定
claims := jwt.New()
claims.Set("iss", "2893740928")
claims.Set("sub", "2893740928")
claims.Set("aud", "https://api.line.me/")
claims.Set("exp", time.Now().Add(time.Hour*1).Unix())
claims.Set("token_exp", time.Now().Add(time.Hour*24*30).Unix())
// 秘密鍵を取得
key, ok := set.Get(0)
if !ok {
fmt.Println("秘密鍵が見つかりません")
return
}
// JWTを署名
token, err := jwt.Sign(claims, jwa.RS256, key)
if err != nil {
fmt.Println("JWT署名エラー:", err)
return
}
// 作成したJWTを表示
fmt.Println("JWT:", string(token))
}
6.さいごに
JWTについて調べる機会があったので軽い説明とともにGoでJWTを作成するプログラムを書きました。実際にJWTを使う際はサービスや用途によって起こりうる脅威を想定した設計を行わなければならないので、本記事に書いたもの以外にも知るべきことはたくさんあります。
初めて記事を書いたので間違っている部分などがあればご指摘ください。
Discussion