Simple minds think alike

より多くの可能性を

【Golang】Cookieを認証・暗号化するライブラリ gorilla/cookie を試してみた

最近、仕事でCookie使ったGo言語コードを見かけた際に gorilla/securecookie が使われていたので、このライブラリに関してを調べてみました。その時に調べた内容を書きたいと思います。

ginecho といったGo言語でよく使われているWebフレームワークの内部でも gorilla/securecookie を使っているので、gorilla/securecookie の役割/仕組みを理解することで、goのWebアプリケーションのCookie管理の仕組みを把握できると思います。

前提

概要

公式の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の値

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の値が変わり、そのままでは読めなくなっていることが分かります。

 securecookie使用時の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の名前を"cookie-name"、Cookieのデータ(Name: "title", Value: "SPY x FAMILY")とした場合の認証のフローです。

このフローではオプションである暗号化は考慮していないので機密性が上がるわけではありません。自身のサーバーがハッシュキーを使って保存したものか認証できるようになるだけです。

Cookieの値作成・保存

以下の図のフローでCookieの値を作成、保存します。 Cookieの値作成・保存までのフロー図

ハッシュ値を算出

基本文字列を"cookie-name|【タイムスタンプ】|Cookieデータ"とし、ハッシュキーと合わせてHMACハッシュ値(mac)を算出します。

Cookieに設定する値を作り、保存

次に "【タイムスタンプ】|Cookieデータ|ハッシュ値(mac)"をbase64エンコードし、Cookieとして保存します。

Cookieの取得・検証

以下の図のフローでCookieの値を取得・検証します。

Cookieの取得・検証までのフロー図

ハッシュ値を取り出し

Cookieから取り出した"【タイムスタンプ】|Cookieデータ|ハッシュ値(mac)"をbase64デコードし、ハッシュ値(mac)を取り出します。

Cookieの検証

次に、"cookie-name"を追加して"cookie-name|タイムスタンプ|Cookieデータ"というデータを作り、ハッシュキーと合わせてHMACでハッシュ値(mac)を算出し、取り出したハッシュ値と値が一致するかで検証できます。

Cookieの内容を改変し、わざと検証失敗させてみる

開発者ツールを使ってCookieの値を改変してみます。

Cookieの値の検証

再度Webブラウザ/show-cookie にアクセスすると、検証エラーになるかことを確認できます。

Cookieの値の検証で失敗した状態

securecookieの暗号化の挙動を確認

securecookieを使ってコードを書き直し、暗号化してみる

securecookie.Newの第2引数には暗号化のためのブロックキーを指定できます。

hashKey := []byte("hash-key")
blockKey := []byte("blocoooooock-key")
s := securecookie.New(hashKey, blockKey)

ぱっと見では、認証だけの場合と何も変わっていないように見えます。 securecookieを使った場合のCookieの値

暗号化/複合化の仕組み

オプションである暗号化を有効にすることで機密性が上がります。

以下の図のように認証処理の前に、Cookieのデータに対してAES(CTRモード)で暗号化を行うことで、Webブラウザには平文が保存されないようになります。

 securecookie使用時の暗号化/複合化の仕組み

複合時は、Cookieの署名検証が問題なければ暗号化されたCookieデータをブロックキーで複合化します。

参考記事