以下の記事ではGo言語のライブラリ slack-go を使ってSlackのリクエスト署名検証を行うサンプルアプリを作成しましたが、今回はその検証処理の詳細を確認していきたいと思います。
simple-minds-think-alike.moritamorie.com
なお、Slackのリクエスト署名に関しては以下の公式記事を参考にしました。
前提
- go: v1.17
- slack-go: v0.10.3
リクエスト署名の概要
公式の記事によると
リクエスト署名は、Slack がアプリにデータを送信する API メソッドでサポートされています。これには、スラッシュコマンド、Events API リクエスト、インタラクティブ・メッセージ、アクション、メッセージボタン、メッセージメニュー、レガシー Outgoing Webhook が含まれます。
ということで、基本的にSlackからアプリに送信されるHTTPリクエストはリクエスト署名検証できるようになっているようです。
リクエスト署名検証の仕方(理論)
また、リクエスト署名検証には以下の3つが必要なようです。
X-Slack-Signature
HTTP ヘッダー- アプリ固有のシークレットキー(
Signing Secret
) - 基本文字列
- バージョン番号 (現在は常に v0)
- X-Slack-Request-Timestamp HTTP ヘッダーの値
- リクエスト自体のメッセージ本文
X-Slack-Signature
HTTP ヘッダー
公式記事に以下のように記載されていました。
このヘッダーには、HMAC-SHA256 キー付きハッシュが設定されています。このハッシュは、アプリ固有のシークレットをキーとして使って計算されます。
具体的にはv0=c11b919c9ea488395441d317....
のような値になります。基本文字列とアプリ固有のシークレットでSHA256 ハッシュ計算されたものです。アプリ側でも同じ計算を行い、ハッシュが同じ値になることを確認することで署名検証できます。
アプリ固有のシークレットキー( Signing Secret
)
xoxb-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
のような値がアプリ毎に発行され、Slack APIのOAuth & Permissions
から取得できます。
基本文字列
バージョン番号 (現在は常に v0)、X-Slack-Request-Timestamp HTTP ヘッダーの値、リクエスト自体のメッセージ本文の順で、コロン (:) で区切って連結し、基本文字列を作成します。
例えば、基本文字列は次のようになります v0:1538352000:token=abc123def456xyz....
リクエスト署名検証の仕方(実装)
上記の検証を行なっている アプリとslack-go の実装を確認していきます。
アプリ側の実装
作成したサンプルアプリの middleware.go
から署名検証を行なっている箇所を抜粋し、処理内容をコメントで追記しています。
以下の①〜③の番号は次の slack-go
の実装と関連する箇所を明示しています。
// ①リクエスト署名検証に必要な情報(基本文字列のメッセージ本文以外)を設定し、 // シークレット検証機を生成 sv, err := slack.NewSecretsVerifier(r.Header, os.Getenv("SLACK_SIGNING_SECRET")) if err != nil { w.WriteHeader(http.StatusBadRequest) return } // ②シークレット検証機に不足情報(基本情報の文字列のメッセージ本文)を追加 if _, err := sv.Write(body); err != nil { w.WriteHeader(http.StatusInternalServerError) return } // ③SHA256 ハッシュ計算し、ハッシュが同じ値になることを確認 if err := sv.Ensure(); err != nil { w.WriteHeader(http.StatusUnauthorized) return }
slack-go
の実装
署名検証は、security.goの実装に含まれており、そこからの抜粋になります。
①リクエスト署名検証に必要な情報(基本文字列のメッセージ本文以外)を設定し、シークレット検証機を生成
シークレット検証機生成とタイムスタンプのチェックをしています。
func NewSecretsVerifier(header http.Header, secret string) (sv SecretsVerifier, err error) { var ( timestamp int64 ) stimestamp := header.Get(hTimestamp) // シークレット検証機生成 if sv, err = unsafeSignatureVerifier(header, secret); err != nil { return SecretsVerifier{}, err } // タイムスタンプを10進数(64ビット長)に変換 if timestamp, err = strconv.ParseInt(stimestamp, 10, 64); err != nil { return SecretsVerifier{}, err } // リクエストヘッダーのタイムスタンプ(X-Slack-Request-Timestamp HTTP ヘッダーの値)と現在時刻を比較し、 // 5分より長ければエラーにする diff := absDuration(time.Since(time.Unix(timestamp, 0))) if diff > 5*time.Minute { return SecretsVerifier{}, ErrExpiredTimestamp } return sv, err }
NewSecretsVerifier
の中から呼ばれている unsafeSignatureVerifier
の実態は以下の通りです。
const ( hSignature = "X-Slack-Signature" hTimestamp = "X-Slack-Request-Timestamp" ) func unsafeSignatureVerifier(header http.Header, secret string) (_ SecretsVerifier, err error) { var ( bsignature []byte ) // HTTPヘッダーから署名とリクエストタイムスタンプを取得 signature := header.Get(hSignature) stimestamp := header.Get(hTimestamp) if signature == "" || stimestamp == "" { return SecretsVerifier{}, ErrMissingHeaders } // 署名をデコード if bsignature, err = hex.DecodeString( strings.TrimPrefix(signature, "v0=")); err != nil { return SecretsVerifier{}, err } // シークレットをキーとしてHMACハッシュを生成する hash := hmac.New(sha256.New, []byte(secret)) // ハッシュに"v0:【リクエストタイムスタンプ】"を書き込む if _, err = hash.Write( []byte(fmt.Sprintf("v0:%s:", stimestamp))); err != nil { return SecretsVerifier{}, err } return SecretsVerifier{ signature: bsignature, hmac: hash, }, nil }
http.Header
とsecretを引数にとり、検証機を生成して返します。なお、ここで使用している HMAC-SHA-256
というアルゴリズムに関しては、別の記事で概要を記載していますので、もしよろしければ参考にしてみてください。
simple-minds-think-alike.moritamorie.com
②シークレット検証機に不足情報(基本情報の文字列のメッセージ本文)を追加
func (v *SecretsVerifier) Write(body []byte) (n int, err error) { return v.hmac.Write(body) }
③SHA256 ハッシュ計算し、ハッシュが同じ値になることを確認
func (v SecretsVerifier) Ensure() error { // ハッシュ値を算出 computed := v.hmac.Sum(nil) // 署名と算出したハッシュ値が同じになることを確認 if hmac.Equal(computed, v.signature) { return nil } if v.d != nil && v.d.Debug() { v.d.Debugln(fmt.Sprintf("Expected signing signature: %s, but computed: %s", hex.EncodeToString(v.signature), hex.EncodeToString(computed))) } return fmt.Errorf("Computed unexpected signature of: %s", hex.EncodeToString(computed)) }
Best Plactice for security
他のSlack APIに関するセキュリティの対応は Best Plactice for security
という公式の記事にまとめられています。
2022年6月時点での目次を転記しておきます。
- Security considerations for Slack apps (Slackアプリのセキュリティに関する考慮点)
- Safe Token Storage (セーフトークンストレージ)
- Thinking of things in the 7-Layer OSI model (7層OSIモデルで物事を考える)