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/^.* //')56curl -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 を設定するをご覧ください。