HMAC Webhookがあなたが嘘をついていないことを知る方法
GitHub、Stripe、またはShopifyがサーバーにWebhookを送信するたびに、それらはパラメータをHMACで署名します。正確にその仕組みと、自らのコード内でそれを検証する方法について説明します。
POSTリクエストを受け取り、それがStripeからのものだと主張しています。パラメータには支払いが成功したとあり、注文を処理し、商品を配送します。しかし3日後にそのリクエストが偽造されていたことに気づきます。おっしゃあ。
これが、すべての信頼できるWebhookプロバイダーが、そのパラメータを HMACで署名することの理由です。HMACを理解しているなら、GitHub、Stripe、Shopify、Twilio、そしてほぼすべての現代的なAPIがこれを使用している理由、そして自らのサーバーのコードでその署名を検証する方法を理解できます。
HMACが実際に何であるか
HMACは ハッシュベースメッセージ認証コードです。1つの質問に答えます。「このメッセージを送った人が共有された秘密を知っていたか?」
それは、秘密鍵とメッセージ本文の組み合わせに対して暗号ハッシュ関数(通常はSHA-256)を実行することで実現されます。出力は、固定長の文字列で、次の通りです:
- まったく変化する メッセージ内の1バイトでも変更された場合
- 秘密鍵を知らなければ生成できない 秘密鍵を知らなければ生成できない
- 元のメッセージや鍵を復元できない 復元できない
その式はコンパクトです: HMAC(key, message) = H((key ⊕ opad) || H((key ⊕ ipad) || message))。内部の詳細を覚える必要はありません — すべての言語には標準ライブラリで実装されています — しかし、その意図を理解することは役に立ちます。鍵はハッシュに2回、異なる方法で混ぜられます。これにより、長さ拡張攻撃というクラスの攻撃を防げます。
Webhookプロバイダーがどのように使用するか
Webhookエンドポイントを登録すると、プロバイダーはあなたに 署名秘密鍵 を提供します — あなたとプロバイダーだけが知っているランダムな文字列です。イベントが発生したとき:
- プロバイダーはイベントパラメータをJSON(または標準化された文字列)にシリアライズします。
- そして
HMAC-SHA256(secret, payload). - それを計算します。
X-Hub-Signature-256GitHubの場合、Stripe-SignatureStripeの場合、そしてその他の場合も同様です。
あなたの側では、rawリクエストボディに対して同じ計算を行い、比較します。一致すれば、パラメータは信頼できるものと判断できます。一致しない場合は、それを無視します。
コードでの検証
以下は、最も一般的な言語で検証がどのように見えるかを示しています。パターンはすべての言語で同じです:計算し、定時間関数で比較します。
Node.js
const crypto = require('crypto');
function verifyWebhook(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody) // rawBody must be a Buffer or string — NOT parsed JSON
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
パイソン
import hmac
import hashlib
def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
PHP
function verifyWebhook(string $rawBody, string $signature, string $secret): bool {
$expected = hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signature);
}
重要な点は、rawボディを使用することです
最も一般的な実装ミスは、 パースされ、再シリアライズされた パラメータをHMAC関数に渡すことですが、元のrawバイトを使用しなければなりません。キーの順序、空白、Unicodeエスケープなどすべてハッシュに影響します。プロバイダーは実際にネットワーク経由で送ったバイトを署名しました — あなたはそのまったく同じバイトをハッシュしなければなりません。
Node.js(Express)では、その意味は、ボディパーサーをrawバッファを保持するように設定することです:
app.use('/webhooks', express.raw({ type: 'application/json' }));
Djangoでは request.body を使用します。 request.dataではなく request.get_data().
Flaskでは
なぜ単純なハッシュを使わないのか SHA256(payload) パラメータの単純なSHA-256ハッシュは何の証明もできません — 何者でもそのハッシュを計算できます。秘密鍵がなければ、HMACは単なるチェックサムではなく、 認証 認証コードです。これは「誰が送ったか」という質問に答えます。単に「送信中に損傷されたか」という質問に答えているだけではありません。
なぜ非対称署名(RSAなど)を使わないのか
RSAやECDSAでは、受信者は秘密鍵を知らなくても署名を検証できます — これは公開ブロードキャスト(例:コード署名)に非常に価値があります。Webhookでは、署名を検証する必要があるのはあなたとプロバイダーの2つの側だけです。共有秘密はシンプルで、速く、そのモデルにおいて同等に安全です。一部のプロバイダー(Svix、Clerk)は、サーバー上で秘密を安全に保存できない場合に、非対称Webhook署名を提供しています。
リプレイ攻撃 — そしてそれを防ぐ方法
有効なHMAC署名は信頼性を保証しますが、新鮮さを保証しません。攻撃者が正当な署名付きリクエストをキャプチャして、後に再送することができます。Stripeはその対策として、署名にタイムスタンプを含め、そのタイムスタンプとパラメータボディを一緒にハッシュします。あなたの側では、タイムスタンプが5分以上古いリクエストはすべて拒否します。 Stripe-Signature 自作Webhookシステムを構築している場合は、同じように実装してください。署名されたメッセージに単調に増加するノンスまたはUnixタイムスタンプを含め、サーバー側で古いリクエストを拒否してください。
タイムイングセーフな比較はオプションではありません
HMAC署名を単純な等価性チェック(
)で比較することは絶対にしないでください。文字列比較の短絡は、どの先頭バイトが一致しているかを漏らす情報になります — 攻撃者が数千回リクエストを送ることで、期待される署名を1バイトずつ再構築できます。常に定時間比較を使用してください:===, ==PHP:
- Node.js:
crypto.timingSafeEqual() - Python:
hmac.compare_digest() - Go:
hash_equals() - Ruby:
hmac.Equal() - すべてを組み合わせて、プロダクション用のWebhookハンドラー
ActiveSupport::SecurityUtils.secure_compare()
GitHubの
ヘッダーを使用したNode.jsの完全な例です: X-Hub-Signature-256 簡単なリファレンス:誰が何を使用しているか
const express = require('express');
const crypto = require('crypto');
const app = express();
const WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET;
// Keep the body as raw bytes — critical!
app.use('/github/webhook', express.raw({ type: 'application/json' }));
app.post('/github/webhook', (req, res) => {
const sigHeader = req.headers['x-hub-signature-256'];
if (!sigHeader) return res.status(401).send('Missing signature');
const sig = sigHeader.replace('sha256=', '');
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// Safe to process the event now
console.log('Event type:', req.headers['x-github-event']);
res.sendStatus(200);
});
app.listen(3000);
プロバイダー
| 署名にタイムスタンプあり? | ヘッダ | アルゴリズム | X-Hub-Signature-256 |
|---|---|---|---|
| GitHub | Stripe-Signature | HMAC-SHA256 | いいえ |
| Stripe | X-Shopify-Hmac-Sha256 | HMAC-SHA256 | はい |
| Shopify | X-Twilio-Signature | HMAC-SHA256 | いいえ |
| Twilio | X-Slack-Signature | HMAC-SHA1 | いいえ |
| スラック | Paddle | HMAC-SHA256 | はい |
| Paddle-Signature | HMAC: Webhookがあなたが嘘をついていないことを知る2 | HMAC-SHA256 | はい |
あなたも好きかもしれません
恵 スコアボードが到着しました!
スコアボード ゲームを追跡する楽しい方法です。すべてのデータはブラウザに保存されます。さらに多くの機能がまもなく登場します!
