最近、仕事でCookie使ったGo言語コードを見かけた際に gorilla/securecookie が使われていたので、このライブラリに関してを調べてみました。その時に調べた内容を書きたいと思います。
gin や echo といったGo言語でよく使われているWebフレームワークの内部でも gorilla/securecookie
を使っているので、gorilla/securecookie
の役割/仕組みを理解することで、goのWebアプリケーションのCookie管理の仕組みを把握できると思います。
前提
- go: v1.18
- gorilla/securecookie: v1.1.1
概要
公式のREADMEを読むと以下のように書いてありました。
securecookie encodes and decodes authenticated and optionally encrypted cookie values.
securecookie は認証され、オプションで暗号化されたCookieの値をエンコードおよびデコードします。
Secure cookies can't be forged, because their values are validated using HMAC. When encrypted, the content is also inaccessible to malicious eyes. It is still recommended that sensitive data not be stored in cookies, and that HTTPS be used to prevent cookie replay attacks.
セキュアCookieは、その値がHMACを使用して認証されるため、偽造することができません。暗号化された場合、コンテンツは悪意のある目からアクセスすることもできません。
securecookie
は、標準パッケージの net/httpのcookie にはない認証と暗号化の仕組みを提供してくれるようです。
標準パッケージの挙動を確認
標準パッケージを使ってCookieを設定するだけのシンプルなコードを試してみました。
package main import ( "fmt" "log" "net/http" ) func main() { http.HandleFunc("/set-cookie", func(w http.ResponseWriter, r *http.Request) { cookie := &http.Cookie{ Name: "title", Value: "SPY x FAMILY", } http.SetCookie(w, cookie) fmt.Fprintf(w, "Cookieをセットしました") }) log.Fatal(http.ListenAndServe(":8080", nil)) }
Webブラウザでパス: /set-cookie
、ポート: 8080にアクセスすると以下のようになり、想定したCookieが設定されていることが確認できます。
Cookieはこのようにブラウザツールで簡単に参照、改変できるためセキュリティが低い状態になっています。
securecookie
の認証の挙動を確認
securecookie
を使ってコードを書き直す
gorilla/securecookie
を使ってコードを書き直すと、セキュリティを向上できることを確認します。
securecookie.New
の第1引数にはHMACのハッシュキーを指定します。第2引数には暗号化のためのブロックキーを指定できますが、今回は nil
を指定し、暗号化しないようにしてみます。
package main import ( "fmt" "log" "net/http" "github.com/gorilla/securecookie" ) func main() { hashKey := []byte("hash-key") s := securecookie.New(hashKey, nil) http.HandleFunc("/set-cookie", func(w http.ResponseWriter, r *http.Request) { values := map[string]string{ "title": "SPY x FAMILY", } encoded, err := s.Encode("cookie-name", values) if err == nil { cookie := &http.Cookie{ Name: "cookie-name", Value: encoded, } http.SetCookie(w, cookie) fmt.Fprintf(w, "Cookieをセットしました") } }) log.Fatal(http.ListenAndServe(":8080", nil)) }
同じようにWebブラウザでアクセスするとCookieの値が変わり、そのままでは読めなくなっていることが分かります。
以下のハンドラーを追加し、Cookie内のハッシュ値を検証し、通れば値をデシリアライズしてCookieの値を表示します。
http.HandleFunc("/show-cookie", func(w http.ResponseWriter, r *http.Request) { if cookie, err := r.Cookie("cookie-name"); err == nil { value := make(map[string]string) err = s.Decode("cookie-name", cookie.Value, &value) if err == nil { fmt.Fprintf(w, "値は%qです。", value["title"]) } else { fmt.Fprintf(w, err.Error()) } } })
WebブラウザでアクセスするとCookieから取得した値が表示されることを確認できます。
認証の仕組み
上記のコードと同様に、Cookieの名前を"cookie-name"、Cookieのデータ(Name: "title", Value: "SPY x FAMILY"
)とした場合の認証のフローです。
このフローではオプションである暗号化は考慮していないので機密性が上がるわけではありません。自身のサーバーがハッシュキーを使って保存したものか認証できるようになるだけです。
Cookieの値作成・保存
以下の図のフローでCookieの値を作成、保存します。
①ハッシュ値を算出
基本文字列を"cookie-name|【タイムスタンプ】|Cookieデータ"とし、ハッシュキーと合わせてHMACでハッシュ値(mac)を算出します。
②Cookieに設定する値を作り、保存
次に "【タイムスタンプ】|Cookieデータ|ハッシュ値(mac)"をbase64エンコードし、Cookieとして保存します。
Cookieの取得・検証
以下の図のフローでCookieの値を取得・検証します。
①ハッシュ値を取り出し
Cookieから取り出した"【タイムスタンプ】|Cookieデータ|ハッシュ値(mac)"をbase64デコードし、ハッシュ値(mac)を取り出します。
②Cookieの検証
次に、"cookie-name"を追加して"cookie-name|タイムスタンプ|Cookieデータ"というデータを作り、ハッシュキーと合わせてHMACでハッシュ値(mac)を算出し、取り出したハッシュ値と値が一致するかで検証できます。
Cookieの内容を改変し、わざと検証失敗させてみる
開発者ツールを使ってCookieの値を改変してみます。
再度Webブラウザで /show-cookie
にアクセスすると、検証エラーになるかことを確認できます。
securecookieの暗号化の挙動を確認
securecookie
を使ってコードを書き直し、暗号化してみる
securecookie.New
の第2引数には暗号化のためのブロックキーを指定できます。
hashKey := []byte("hash-key") blockKey := []byte("blocoooooock-key") s := securecookie.New(hashKey, blockKey)
ぱっと見では、認証だけの場合と何も変わっていないように見えます。
暗号化/複合化の仕組み
オプションである暗号化を有効にすることで機密性が上がります。
以下の図のように認証処理の前に、Cookieのデータに対してAES(CTRモード)で暗号化を行うことで、Webブラウザには平文が保存されないようになります。
複合時は、Cookieの署名検証が問題なければ暗号化されたCookieデータをブロックキーで複合化します。