概要
以前JWTを認証用トークンに使う時に調べたこと - Carpe Diemで紹介した内容の具体的な実装の紹介です。
環境
- golang 1.8.1
署名アルゴリズムと鍵長は以下とします。
署名アルゴリズム | 鍵長 |
---|---|
RSA-SHA256 | 4096bit |
成果物
今回の完成形はこちら
公開鍵認証のためのキーペア作成
秘密鍵の生成
$ openssl genrsa 4096 > secret.key
秘密鍵から公開鍵の生成
$ openssl rsa -pubout < secret.key > public.key
今回は簡単のためソースコードに貼り付けます。
var ( rawPublicKey = []byte(`-----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAw8eiDb307pWnvR2XL0CW ... Horlxxaj4cGE2OYxaJmRpL0CAwEAAQ== -----END PUBLIC KEY-----`) rawSecretKey = []byte(`-----BEGIN RSA PRIVATE KEY----- MIIJKgIBAAKCAgEAw8eiDb307pWnvR2XL0CWOD/Nscc5ZTN7peZzuyjOQSzqEYjE a4XxUjI9KgdJD6zYW5h5BSA0Xa2quagJtZCSj5BjZ5YCF9rMaLAeUAmifKJyGj0Z ... XtrgK2rkGLqvf0is3zma6Fc0m/C2Jg/mv8vHNeYzqLw2uf8AZX8GJMNjFxkCtQ== -----END RSA PRIVATE KEY-----`) )
文字列からGoのPublicKey, SecretKeyへ変換
rsa.PublicKey
、rsa.SecretKey
に変換して使う必要があるので、その処理を入れます。
func parseKeys(rawSK []byte, rawPK []byte) error { var err error privateKeyBlock, _ := pem.Decode(rawSK) if privateKeyBlock == nil { return errors.New("Private key cannot decode") } if privateKeyBlock.Type != "RSA PRIVATE KEY" { return errors.New("Private key type is not rsa") } secretKey, err = x509.ParsePKCS1PrivateKey(privateKeyBlock.Bytes) if err != nil { return errors.New("Failed to parse private key") } publicKeyBlock, _ := pem.Decode(rawPK) if publicKeyBlock == nil { return errors.New("Public key cannot decode") } if publicKeyBlock.Type != "PUBLIC KEY" { return errors.New("Public key type is invalid") } publicKey, err = x509.ParsePKIXPublicKey(publicKeyBlock.Bytes) if err != nil { return errors.New("Failed to parse public key") } return nil }
JWTの生成
JWTのメリットはペイロードの改竄が検知できる点です。
トークンとして使う場合は、userIDなどを入れるのが一般的だと思います。
またトークンに期限を持たせるため、IssuedAt
やExpiresAt
を入れた方が良いです。
func GenerateToken(userID string, now int64) (tokenString string, err error) { claims := jwt.StandardClaims{ Subject: userID, IssuedAt: now, ExpiresAt: time.Unix(now, 0).AddDate(0, 0, expiryDays).Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) // sign by secret key tokenString, err = token.SignedString(secretKey) if err != nil { err = errors.Wrap(err, "Failed to sign token") return } return }
userID, unixtimeをいれて実行すると以下のようなJWTが生成されます。
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTIyODI1MTksImlhdCI6MTQ5MjE5NjExOSwic3ViIjoidXNlcjAwMSJ9.H1Vh6B3Y-wCwiFDRENz7YDxahXrlAu3DAFIP3HwcLId32DsjshMtOoSNYMWsXtuW36rf5SOOBFCeGQFI1b9cXDB9XOTykmN0MBJg6x2_0dWgaWrQrgaA4qRcc2_FvRSAxg_bDzqdKOoAI3mx5mZVb7voBPBFSxcymgz_x5kOKFXwQZ7s5PwASsnELpYpUg0cottehw0h4TYJHzD0Itxbp97-lWOvj3ncmR1UJnCCMH1H9Hj7yrNGs7f4RCd8weANoVBi-jvScoOjNvZowbQrg09phwuYf1tM81uvAARE_S6hVSCujFaSMii-8c122ckfcsOvM5PZveUhvGOpe2RX856tBtmV4wU-cAwkq7qtNKNc6ebp1v2vdBZFSPoijK-9b94BKkattLnIY1PARCUmNYTWFUSBh0a-CJcRr-RXEWatt3w-DyuHyp1ESoj0tidxtlX_W-bvlyaaTVCHPiPg1vFcWDl3PDEJaGjlanBChcrdVfhFAbvNBY5ujjJvJom2GqMvuh9JDUXAporMqMylxxhFt1jsO2haK28hbVpfL6xcE_lHfwowamtdgsvSzNDxO7r3FHMeKl0MxKV5h1bbAYaqZqBRqTL1N0Oi3--RRhQ1kxMSuvjGq3YAvqWVQJh73UT9sj4efNdYpTwGBhr-Pc5bZs2TDbJJIR8bLVtCCw4
生成された署名の検証
func VerifyToken(tokenString string) (*jwt.Token, error) { parsedToken, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { // check signing method if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { err := errors.New("Unexpected signing method") return nil, err } return publicKey, nil }) if err != nil { err = errors.Wrap(err, "Token is invalid") return nil, err } if !parsedToken.Valid { return nil, errors.New("Token is invalid") } return parsedToken, nil }
jwt.Parse()
関数のcallbackの中で公開鍵を戻り値に設定します。
HMACなどの共通鍵でのアルゴリズムを採用していれば共通鍵を返すことになります。
また内部で使用するアルゴリズムのチェックをしています。
// check signing method if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { err := errors.New("Unexpected signing method") return nil, err }
これはJWSで見落としがちな以下の脆弱性のためです。
JWS 実装時に作りがちな脆弱性パターン - OAuth.jp
ただ、今回使用しているjwt-goというライブラリは、デフォルトでnone
アルゴリズムを禁止しています。
またHS*アルゴリズムで置換する方法もGoでは型が違うため(RSAはrsa.PublicKey、HMACは[]byte)、処理に失敗してくれます。
なので正直なところ無くても問題ないですが、念のため書いておきます。
動作結果
署名をいじらなければきちんと値を得ることができます。
{ "sub":"user001", "exp":1.492282997e+09, "iat":1.492196597e+09 }
githubのソースにはテストコードも付けてあり、そちらでは
- ペイロードを改竄した
- algを改竄した(none, HS*)
- ExpiresAtが切れている
といった場合にエラーになることを証明しています。