Simple minds think alike

より多くの可能性を

【Golang】go-oidcでHS256に対応する方法

以下の記事でgo-oidcというサードパーティライブラリを使用してOpenID ConnectでAuth0連携アプリケーションを構築する記事を書きましたが、JWT(JSON Web Token)署名に非対称暗号アルゴリズムを使っているRS256の対応方法だけしか書いていませんでした。今回は対称暗号アルゴリズムを使っているHS256に対応する方法を書いてみようと思います。

simple-minds-think-alike.moritamorie.com

前提

仕様

go-oidcを利用した実装の前にどのような署名検証を実現したいか、仕様の面を書いていきたいと思います。

HS256はどんなアルゴリズム

まず、HS256はどのようなアルゴリズムかというOpenID Connectとは直接的には関係がない一般的な話を書いてみようと思います。

HS256はHMAC-SHA-256 のことです。

HMACとは

まず、MAC (Message Authentication Code)はメッセージ認証コードのことです。

メッセージ認証コードは、暗号技術の一つです。対称・非対称暗号を用いてメッセージを暗号化すると機密性を得られますが、例えば通信経路の中で改竄が行われるとそれを検知できないので、暗号化によって完全性は得られません。メッセージ認証コードを使うと完全性を得られるようになります。

メッセージ認証コードは様々な方法で実現可能ですが、HMACは一方向ハッシュ関数(先頭HはHashの頭文字を取ったもの)を使って実現します。 HMAC-SHA-256(HS256) は、一方向ハッシュ関数にSHA-256を使ってメッセージ認証コードを計算する方法のことです。

HMACのプロセスと完全性の検証方法

計算方法など具体的な解説は省きますが、ResearchGateから引用したHMACのプロセス図を掲載しておきます。 https://www.researchgate.net/publication/346634579/figure/fig2/AS:965112139636738@1607112073858/Hash-Message-Authentication-Code-HMAC-process-332-Overview-of-SHA-256-SHA-256.png

メッセージ(Message)と共通鍵(Key)を元に、この図のプロセスで計算すると最終的にMAC値が算出されます。一方向ハッシュ関数の性質により、同じ鍵を持つ人ではないと同じMAC値を算出できないので、メッセージの受信者は改竄されたかを後から検証できます。(完全性が得られる。)

図の一方向ハッシュ関数(HASH Function)にSHA-256を使ったものがHMAC-SHA-256(HS256)というアルゴリズムです。

OpenID ConnectにおけるJWT署名

さて、話を一般的なメッセージ認証コードの話からOpenID Connectに戻し、JWTの署名がどのようなものになっているか、その中でHS256がどのように使われているかを確認していきます。

JWTのEncoding

JWTのEncodingの例は以下に記載されています。

ざっくり書くと以下の図のように、ヘッダー・ペイロード・署名の内容をBase64エンコードして.(ピリオド)で繋げて表現します。

JWTの最後の部分が署名部分だということが分かります。

JWTの具体的な例

ざっくりとしたEncodingの説明では具体的なイメージが湧かないと思いますので、jwt.ioというツールを使ってJWTを作ってみたいと思います。

jwt.ioを開いて、右側のDecodedのところに上からJWTのヘッダー・ペイロード・共通鍵(以下の図ではmy-common-keyの箇所)を入力すると、左側のEncodedでJWTが得られます。

ヘッダーのalgHS256になっているので、ペイロードをメッセージ、my-common-keyを共通鍵としてHS256で算出されたMAC値が署名部分になります。

署名検証の方法は以下に記載されており、JWTの受信者は共通鍵を使用することで署名検証できます。

実装

ここまで go-oidc を使って実現したいHS256の署名検証の方法を書いてきましたが、2022年1月時点で go-oidcHS256 をサポートしていないため部分的に独自で実装する必要があります。(参照)

HS256がサポートされていない背景

以下のissueを読んだところ、対称アルゴリズムと非対称アルゴリズムを混ぜるとAPIの誤用に繋がりかねないため、意図的な制約としてサポートしていないようです。

github.com

しかし、内部で署名検証に利用している go-jose ではHS256をサポートしているため、これを利用して実装できます。

実装

以下のように独自のVerifierを実装すると実現できます。 go-joseのREADMEを読むとSupported key types(Algorithm: HMAC)は []byte と記載されているので、共通鍵をバイトのスライスに変換して Verify関数の引数に渡します。

type HS256KeySet struct {
    CommonKey string
}

func (r *HS256KeySet) verify(_ context.Context, jws *jose.JSONWebSignature) ([]byte, error) {
    return jws.Verify([]byte(r.CommonKey))
}

func (r *HS256KeySet) VerifySignature(ctx context.Context, jwt string) ([]byte, error) {
    jws, err := jose.ParseSigned(jwt)
    if err != nil {
        return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
    }
    return r.verify(ctx, jws)
}

func callback(code, state string) {

    // rawIDToken取得

    oidcConfig := &oidc.Config{
        ClientID:             "my-client-id",
        SupportedSigningAlgs: []string{"HS256"},
    }

    verifier := oidc.NewVerifier("https://issuer-domain.com/", 
        &HS256KeySet{CommonKey: "my-common-key"}, oidcConfig)
    idToken, err := verifier.Verify(context.Background(), rawIDToken)

    // 署名検証後のNonceの検証等
}

独自に作ったVerifierのVerifySignature関数は以下のタイミングで実行されます。

このように実装することで、OIDC認証の要件に必要なExpiryやIssuer, ClientIDのチェック等はサードパーティライブラリ(go-oidc)に任せられるようになり、署名検証の部分だけ独自のVerifierに実装できます。

参考資料