読者です 読者をやめる 読者になる 読者になる

まったり技術ブログ

Technology is power.

【セキュリティ】jwt-goを使ってみる - その1

f:id:motikan2010:20170512014516p:plain

前回、RailsのJWTライブラリである"knock"を使って署名アルゴリズムにNoneを使えるのかを試してみた(使えなかった)ので、今回はGo言語のJWTライブラリである"jwt-go“を使って、署名アルゴリズムに「SHA256」と「none」を試した(使えた)ことを書く。 motikan2010.hatenadiary.com

github.com

下記のソースコードをベースにしていろいろ試してみます。
要認証URLでは、トークン内のデータ文字列をクライアントに返すようにしています。ここでは「ゲスト」という文字列で固定しています。
今回はginを使って記述しています。

main.go
package main

import (
    "fmt"
    "time"

    jwt "github.com/dgrijalva/jwt-go"
    "github.com/dgrijalva/jwt-go/request"
    "github.com/gin-gonic/gin"
)

var secretKey = "75c92a074c341e9964329c0550c2673730ed8479c885c43122c90a2843177d5ef21cb50cfadcccb20aeb730487c11e09ee4dbbb02387242ef264e74cbee97213"

func main() {
    r := gin.Default()

    r.GET("/api/", func(c *gin.Context) {
        /*
          アルゴリズムの指定
       */
        token := jwt.New(jwt.GetSigningMethod("HS256"))
     
        token.Claims = jwt.MapClaims{
            "user": "ゲスト",
            "exp":  time.Now().Add(time.Hour * 1).Unix(),
        }

        /*
          トークンに対して署名の付与
       */
        tokenString, err := token.SignedString([]byte(secretKey))
        if err == nil {
            c.JSON(200, gin.H{"token": tokenString})
        } else {
            c.JSON(500, gin.H{"message": "Could not generate token"})
        }
    })

    r.GET("/api/private/", func(c *gin.Context) {
        /*
          署名の検証
       */
        token, err := request.ParseFromRequest(c.Request, request.OAuth2Extractor, func(token *jwt.Token) (interface{}, error) {
            b := []byte(secretKey)
            return b, nil
        })

        if err == nil {
            claims := token.Claims.(jwt.MapClaims)
            msg := fmt.Sprintf("こんにちは、「 %s 」さん", claims["user"])
            c.JSON(200, gin.H{"message": msg})
        } else {
            c.JSON(401, gin.H{"error": fmt.Sprint(err)})
        }
    })

    r.Run(":8080")
}

実際に署名アルゴリズムに「HS256」と「none」を指定した動作を確認していきます。

動作確認

サンプルなどでよく見られる"HS256"を使用

f:id:motikan2010:20170512014541j:plain

トークン必要URLにアクセス
$ curl -v http://example.jp:8080/api/private/
GET /api/private/ HTTP/1.1
Host: example.jp:8080

HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Date: Thu, 11 May 2017 14:09:56 GMT
Content-Length: 40

{"error":"no token present in request"}
トークンを取得
$ curl -v http://example.jp:8080/api/
GET /api/ HTTP/1.1
Host: example.jp:8080

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Thu, 11 May 2017 13:51:09 GMT
Content-Length: 144

{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTQ1MTQyNjksInVzZXIiOiLjgrLjgrnjg4gifQ.jI9Sc22DdF3EclflQZyqxuR1mGjj3YcqljsBo2IRn4Y"}
base64デコード
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 {“alg”:“HS256”,“typ”:“JWT”}
eyJleHAiOjE0OTQ1MTQyNjksInVzZXIiOiLjgrLjgrnjg4gifQ {“exp”:1494514269,“user”:“ゲスト”}
jI9Sc22DdF3EclflQZyqxuR1mGjj3YcqljsBo2IRn4Y 署名(シグネチャ)

本来であれば「ゲスト」という部分にユーザIDなどの識別子が格納され、ユーザを識別するために使用されます。

③発行されたトークンを送信
$ curl -v http://example.jp:8080/api/private/ -H "Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTQ1MTQyNjksInVzZXIiOiLjgrLjgrnjg4gifQ.jI9Sc22DdF3EclflQZyqxuR1mGjj3YcqljsBo2IRn4Y"
GET /api/private/ HTTP/1.1
Host: example.jp:8080
User-Agent: curl/7.43.0
Accept: */*
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTQ1MTQyNjksInVzZXIiOiLjgrLjgrnjg4gifQ.jI9Sc22DdF3EclflQZyqxuR1mGjj3YcqljsBo2IRn4Y

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Thu, 11 May 2017 13:53:30 GMT
Content-Length: 56

{"message":"こんにちは、「 ゲスト 」さん"}

「ゲスト」を「管理者」という文字列に改ざんしてリクエストを送信したらどうなるでしょうか?

base64エンコード
{“exp”:1494514269,“user”:“管理者”} eyJleHAiOjE0OTQ1MTQyNjksInVzZXIiOiLnrqHnkIbogIUifQo=

f:id:motikan2010:20170512014603j:plain

トークンを改ざんして送信
curl -v http://example.jp:8080/api/private/ -H "Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTQ1MTQyNjksInVzZXIiOiLnrqHnkIbogIUifQo=.jI9Sc22DdF3EclflQZyqxuR1mGjj3YcqljsBo2IRn4Y"
GET /api/private/ HTTP/1.1
Host: example.jp:8080
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTQ1MTQyNjksInVzZXIiOiLnrqHnkIbogIUifQo=.jI9Sc22DdF3EclflQZyqxuR1mGjj3YcqljsBo2IRn4Y

HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Date: Thu, 11 May 2017 14:03:53 GMT
Content-Length: 33

{"error":"signature is invalid"}

署名の検証が行われ、結果的に改ざんされたことが検出されました。
ステータスコードも「401 Unauthorized」となっていることが確認できます。

危険と言われている署名の検証なし"None"を使用

jwt-goではアルゴリズムの指定に「none」を指定することが可能になっています。
「none」を指してトークンを発行することにより、トークンの検証を行われずに改ざんが行われたトークンが受け入れられるようになります。
f:id:motikan2010:20170514012635j:plain

「none」を指定するには、jwt.UnsafeAllowNoneSignatureTypeを指定します。 https://github.com/dgrijalva/jwt-go#user-content-compliance
上記ソースコード内の3箇所を編集します。以下のようになります。

/*
   アルゴリズムの指定
*/
//token := jwt.New(jwt.GetSigningMethod("HS256"))
token := jwt.New(jwt.GetSigningMethod("none")) // https://github.com/dgrijalva/jwt-go/pull/79/files
/*
  トークンに対して署名の付与
*/
//tokenString, err := token.SignedString([]byte(secretKey))
tokenString, err := token.SignedString(jwt.UnsafeAllowNoneSignatureType)
/*
   署名の検証
*/
token, err := request.ParseFromRequest(c.Request, request.OAuth2Extractor, func(token *jwt.Token) (interface{}, error) {
    //b := []byte(secretKey)
    b := jwt.UnsafeAllowNoneSignatureType
    return b, nil
})
トークン必要URLにアクセス
$ curl -v http://example.jp:8080/api/private/
GET /api/private/ HTTP/1.1
Host: example.jp:8080

HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Date: Thu, 11 May 2017 14:34:36 GMT
Content-Length: 40

{"error":"no token present in request"}
トークンを取得
$ curl -v http://example.jp:8080/api/
GET /api/ HTTP/1.1
Host: example.jp:8080

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Thu, 11 May 2017 14:37:51 GMT
Content-Length: 100

{"token":"eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0k.eyJleHAiOjE0OTQ1MTcwNzEsInVzZXIiOiLjgrLjgrnjg4gifQ."}
base64デコード
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0k {“alg”:“none”,“typ”:“JWT}
eyJleHAiOjE0OTQ1MTcwNzEsInVzZXIiOiLjgrLjgrnjg4gifQ {“exp”:1494517071,“user”:“ゲスト”}
※署名なし

ここで受け取ったトークンをそのまま返しても正常にレスポンスが返ってくるのは確実なので、早速改ざんしたデータを送ってみます。

ここで「ゲスト」を「管理者」という文字列に改ざんしてリクエストを送信したらどうなるでしょうか? この時に、トークンが発行された時のように、シグネチャはリクエストに含めません。

base64エンコード
{“exp”:1494517071,“user”:“管理者”} eyJleHAiOjE0OTQ1MTcwNzEsInVzZXIiOiLnrqHnkIbogIUifQ==
トークンを改ざんして送信
$ curl -v http://example.jp:8080/api/private/ -H "Authorization: eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0k.eyJleHAiOjE0OTQ1MTcwNzEsInVzZXIiOiLnrqHnkIbogIUifQ==."
GET /api/private/ HTTP/1.1
Host: example.jp:8080
Authorization: eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0k.eyJleHAiOjE0OTQ1MTcwNzEsInVzZXIiOiLnrqHnkIbogIUifQ==.

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Thu, 11 May 2017 14:44:57 GMT
Content-Length: 56

{"message":"こんにちは、「 管理者 」さん"}

レスポンス内に「{“message”:“こんにちは、「 管理者 」さん”}」が表示され、改ざんしたリクエストが正常処理されてしまったことが確認できます。 このような動作をすることから「UnsafeAllowNoneSignatureType」の指定が必要となります。

つづく・・・