先日ソシャゲとしての運用を想定したPrivateAPIを作ろうとした時に、Golangにおけるhmac認証について調べたら意外と情報が少なかったので、今回ハンズオン形式でまとめておきます!
Golang歴がまだ短い若輩者なので、もし間違いなどあればコメントにてご指摘いただけると嬉しいです😭
##HMAC署名とは
In cryptography, an HMAC (sometimes expanded as either keyed-hash message authentication code or hash-based message authentication code) is a specific type of message authentication code (MAC) involving a cryptographic hash function and a secret cryptographic key. It may be used to simultaneously verify both the data integrity and the authentication of a message, as with any MAC. Any cryptographic hash function, such as SHA-256 or SHA-3, may be used in the calculation of an HMAC; the resulting MAC algorithm is termed HMAC-X, where X is the hash function used (e.g. HMAC-SHA256 or HMAC-SHA3). The cryptographic strength of the HMAC depends upon the cryptographic strength of the underlying hash function, the size of its hash output, and the size and quality of the key.
引用:https://en.wikipedia.org/wiki/HMAC
HMACは正式には、Hash-based Message Authentication Code
と呼ばれる、いわゆる「メッセージ認証符号」の一つ。
クライアント⇄APIサーバー間でメッセージが送受信される際に「第三者によるメッセージ改竄が行われたかどうか」を認証することができます。
###署名の仕組み
引用:https://ja.wikipedia.org/wiki/メッセージ認証符号
メッセージ(本文)を送受信する際に、共通鍵とハッシュ関数(SHA256, SHA512 etc..)を用いて認証用のハッシュ値を算出し、送信元が本人であるかどうかを、受信先が確認するための署名としてメッセージと一緒にAPIへ送ります。
メッセージ+署名を受け取った受信先は送られてきたメッセージを元に、同様に共通鍵とハッシュ関数を用いて認証用のハッシュ値を算出し、そのハッシュ値が送信元から署名として送られてきたハッシュ値と一致するかどうかでメッセージの改竄を検出します。
あくまでもメッセージの改竄を検出するためのものであり、メッセージを暗号化するものではないのでその点注意しましょう。
###crypto/hmac
引用:https://golang.org/pkg/crypto/hmac/
GolangにはHMAC認証を行うためのパッケージが標準で用意されています。
Java, Ruby, Pythonなどサーバーサイド言語では標準でサポートされている様子。Swiftは非公式のライブラリが作られていましたが公式のサポートがない様子でした。
関数自体も少なく簡単なので、実装するのは意外と楽です!
というわけでハンズオンやっていきましょう↓
HMAC認証を行うmiddlewareを実装する
今回は簡単なAPIが既に構築されている前提で、「middlewareの一部としてHMAC認証処理を追加する」ということをやっていこうと思います。
ちなみに、今回はmiddlewareの全体的な処理はmiddleware.go
、HMAC認証処理はvalidater.go
に記述しています。
###1. request.Headerから送られてきた署名(sign)を受け取る
まずはmiddlewareで、http.requestからHMAC認証に必要な情報を受け取ります。
受け取る情報は以下です。
- 受信した署名 ... Access-Sign
- タイムスタンプ ... Access-Timestamp
- メソッド ... request.Method
- エンドポイント ... request.URL.path
- メッセージ本文 ... request.Body
- 共通鍵 ... sercretKey
ちなみに共通鍵については、事前に認証TokenやUserIDを使ってDBから取得してくる処理が必要です。
今回は明示的にするために変数に組み込んでます。
タイムスタンプやメソッド、エンドポイントについても改竄検知の対象として検証用MACの生成に使用します。
func Middleware(nextFunc http.HandlerFunc) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
ctx := request.Context()
if ctx == nil {
ctx = context.Background()
}
// ioutilでメッセージをbyteで取得
requestBodyByte, err := ioutil.ReadAll(request.Body)
if err != nil {
log.Println("Failed to Load Request")
response.BadRequest(writer, "Failed to Load Request")
return
}
// 本来はDBでUserIDと紐づけるなどしてDBに保存します。
sercretKey := "TestSercretKey"
// request.Headerから署名とタイムスタンプを取得
timestamp := request.Header.Get("Access-Timestamp")
inputSign := request.Header.Get("Access-Sign")
nextFunc(writer, request.WithContext(ctx))
}
}
###2. requestと秘密鍵を用いて署名(sign)を再現する
middlewareで値を受け取れたので、次は送られてきたHMAC署名が正常なものがどうかを検証する関数(Validater)を作っていきます。ちょっと引数がイケてないですがそこはご愛敬😇😇
package validater
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"strconv"
"time"
)
func RequestValidater(inputSign, timestamp, method, endpoint, key string, body []byte) bool {
// 受け取った署名をStringからByteへ変換
inputSignByte, err := hex.DecodeString(inputSign)
if err != nil {
return false
}
// メッセージの再現(連結の順番を間違えると検証が通りません)
message := timestamp + method + endpoint + string(body)
// macの生成
mac := hmac.New(sha256.New, []byte(key))
mac.Write([]byte(message))
validSignByte := mac.Sum(nil)
// 確認用に出力
fmt.Println("Input-Sign: " + inputSign)
fmt.Println("Valid-Sign: " + hex.EncodeToString(validSignByte))
// 一致or不一致でBoolを返す
if !hmac.Equal(inputSignByte, validSignByte) {
return false
}
return true
}
###3. hmac.Equalで比較し、検証結果を返す
2で受け取った情報を入力すれば検証結果を返してくれるValidaterができたので、これをmiddlewareで実行し、if文で分岐させればメッセージ改竄検出機能が完成です🎉
package middleware
import (
"context"
"io/ioutil"
"log"
"net/http"
)
func Middleware(nextFunc http.HandlerFunc) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
ctx := request.Context()
if ctx == nil {
ctx = context.Background()
}
requestBodyByte, err := ioutil.ReadAll(request.Body)
if err != nil {
log.Println("Failed to Load Request")
response.BadRequest(writer, "Failed to Load Request")
return
}
sercretKey := "TestSercretKey"
timestamp := request.Header.Get("Access-Timestamp")
inputSign := request.Header.Get("Access-Sign")
// 一致していなければ、BadRequestを返す
if !validater.RequestValidater(inputSign, timestamp, request.Method, request.URL.Path, sercretKey, requestBodyByte) {
log.Println("invalid Access")
response.BadRequest(writer, "Request is Forged Probably")
return
}
nextFunc(writer, request.WithContext(ctx))
}
}
HMACをAPIで使用する上での注意点
hmac認証を取り入れるとしたらオープンなWebAPIより、ソシャゲや仮想通貨等のセキュアな通信が求められるプライベートなAPIが主な使い所になると思いますが、それを前提としてHMAC認証が抱える問題をここで紹介しておきます。
共通鍵の共有による鍵配送問題
クライアント側もAPI側も、HMACの署名生成には共通鍵を用いるためHMAC認証を行う上では各自で鍵を所持していることが前提となります。
すなわち共通鍵をお互いに持ち合うために、初回は必ずどちらかが生成した鍵をもう一方へ配送しなければならず、セキュリティ上問題となっています。
ちなみにこうした共通鍵暗号方式の鍵配送問題を解決するために作られたのが「公開鍵暗号方式」なのですが、公開鍵方式を使用した署名は「デジタル署名」と呼ばれるものになり、HMACとはまた別のものになります。
Golangにおけるデジタル署名についてはこちらの記事が詳しく書かれていますので参考にしてください。
なのでHMACにおける共通鍵の取り扱いについては、有効期限を設ける等のセキュアに保つ仕組みが必要になります。
(ベストプラクティスをご存知の方いらっしゃいましたらコメントで教えていただけると嬉しいです...!)
まとめ
- GolangでHMAC認証は割と簡単に実装できる
- HMACはメッセージの完全性のみを保証する
- タイムスタンプで再生攻撃を防止
- 鍵配送問題や否認防止性なども解決するにはデジタル署名が良さそう
というわけで、以上GolangでHMAC認証を実装するハンズオンでした!