SteveSignalsDocs

Webhook

署名を検証する

Signals が Webhook に署名する正確なアルゴリズムと、よくある間違いを説明します。

すべての Webhook リクエスト(1 回の実行 Webhook)は、エンドポイントのシークレットで署名されています。ボディを信頼する前に、署名を検証してください。公開 URL には誰でも POST できますが、有効な署名を作れるのは Signals だけです。

アルゴリズム

Signals-Signature ヘッダーは次の形式です。

text
1Signals-Signature: t=<unix seconds>,v1=<hex>

ヘッダーを分割する

, で分割します。t(unix 秒のタイムスタンプ)と、すべての v1 の値を読み取ります。

タイムスタンプを確認する

現在時刻と t の差の絶対値が 300 秒を超えたら、リクエストを拒否します。これでリプレイ攻撃を防げます。

署名対象のペイロードを作る

${t}.${rawBody}: タイムスタンプ、ドット、そしてリクエストボディの正確なバイト列です。

HMAC を計算する

署名対象のペイロードの HMAC-SHA256 を、署名シークレット(whsec_… 全体をテキストとして)を鍵に計算します。小文字の 16 進数でエンコードします。

定数時間で比較する

各 v1 の値と、定数時間の関数で比較します。いずれかが一致すれば、リクエストを受け入れます。

再発行後は署名が 2 つになる

シークレットを再発行してから 24 時間は、ヘッダーに v1= の値が 2 つ入ります。先に新しいシークレットで、次に古いシークレットで署名したものです。どちらかが一致すれば受け入れてください。これで、イベントを落とさずに新しいシークレットをデプロイできます。24 時間後は、値は再び 1 つになります。

よくある間違い

間違い対処
解析後の JSON を検証している。再シリアライズした JSON が同じバイト列になることはまれです。受け取ったままの生のボディを使います。
ミドルウェアやプロキシが、空白、改行コード、エンコーディングを変更する。ボディに触れるミドルウェアより前に検証します。トリム、デコード、再エンコードはしないでください。
シークレットを再発行した後に、古いシークレットを使っている。アプリのシークレットを更新します。最初の 24 時間は古いシークレットでも一致するため、問題が表に出るのは 2 日目です。
署名の比較に == を使っている。定数時間で比較する関数を使います。
ボディだけ、または t だけに署名している。t、ドット、ボディの順に署名します。
whsec_ の接頭辞を取り除いたり、シークレットを base64 デコードしたりしている。鍵は whsec_… 全体をテキストとして使います。接頭辞を取り除いたり、base64 デコードしたりしないでください。
サーバーの時計が 5 分以上ずれている。NTP で時計を同期します。ずれがあると、すべてのリクエストがタイムスタンプの確認で失敗します。
最初の v1 だけを検証している。すべての v1 の値を確認します。

エンドポイントをテストする

自分で署名を作り、curl でエンドポイントに送って、200 が返ることを確認します。

bash
1SECRET='whsec_XXXX'
2BODY='{"version":3,"webhookId":"manual-test-1","type":"run.completed","eventType":"reviews","sentAt":"2026-01-01T00:00:00.000Z","run":{"id":"manual-run-1","trackerId":"manual-tracker-1","projectId":null,"source":"reviews","completedAt":"2026-01-01T00:00:00.000Z","competitor":{"name":"Example","domain":"example.com","subjectKey":"example"}},"pageSize":100,"events":{"reviews.new":{"total":0,"totalPages":0,"firstPageToken":null},"reviews.updated":{"total":0,"totalPages":0,"firstPageToken":null}}}'
3T=$(date +%s)
4SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
5
6curl -i https://your-app.com/signals-hook \
7 -H 'Content-Type: application/json' \
8 -H "Signals-Event-Version: 3" \
9 -H "Signals-Webhook-Id: manual-test-1" \
10 -H "Signals-Signature: t=$T,v1=$SIG" \
11 --data-binary "$BODY"

このボディにはトークンがないので、ページのループで取得するものはありません。次に、ボディを 1 文字変えて、400 が返ることを確認します。実際のデータで確認する最も簡単な方法はテストイベントを送信です。Webhook を設定するをご覧ください。