HMAC を Base64 で計算する方法
- HMAC モードと SHA-256 が選択されています。API が SHA-1、SHA-384、SHA-512、MD5 を使うなら、アルゴリズム で別のものを選びます。
- 秘密鍵 を入力し、鍵の形式 を設定します。普通のシークレット文字列ならテキスト(UTF-8)、鍵がエンコードされたバイト列として渡されているなら16進数か Base64 です。
- テキスト にメッセージを入力するか貼り付けます。または 入力 を ファイル に切り替えて、ペイロードのファイルそのものをドロップします。
- 署名を Base64、Base64URL、16進数のいずれかでコピーします。すべてのアルゴリズム では、すべてのハッシュでの HMAC を一度に表示します。どれが使われているかドキュメントの記述があいまいなときに役立ちます。
空の鍵も受け付けて結果を出しますが、そのような HMAC は何も保護しないという注意が表示されます。
値の例
鍵 key とメッセージ The quick brown fox jumps over the lazy dog の場合:
| アルゴリズム | HMAC(Base64) |
|---|---|
| HMAC-MD5 | gAcHE0Y+d0m5DC3CSRHidQ== |
| HMAC-SHA1 | 3nybhbi3iqa8ino29wqQcBydtNk= |
| HMAC-SHA256 | 97yD9DBThCSxMpjmqm+xQ+9NWaFJRhdZl0edvC0aPNg= |
| HMAC-SHA512 | tCrwkFe6weLUFwjkipAuCbX/fxKrQopP6GZTxz3SSPuC+UilSfe3kaW0GRXuTR7Dk1NX5OIxclDQNyr6Lr7rOg== |
HMAC-SHA256 の値を16進数で書くと f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8 で、Wikipedia の HMAC の記事でおなじみのテストベクターです。鍵のエンコードによって結果が変わります。テキスト a2V5 を鍵にすると 5O+7AtpguFrgIRw1qXGU+m0OaTs1oV1kwRQ/hI1WAUE= になりますが、a2V5 を Base64 として読むと key のバイト列になり、再び 97yD9DBT… になります。
Webhook の署名:Base64 か16進数か
プロバイダーは HMAC を使う点では一致していますが、出力のエンコードや署名の対象はまちまちです。ドキュメントが充実している例をいくつか挙げます。
| プロバイダー | ヘッダー | アルゴリズム | エンコード | 署名の対象 |
|---|---|---|---|---|
| Shopify | X-Shopify-Hmac-Sha256 | HMAC-SHA256 | Base64 | 生のリクエスト本文 |
| Standard Webhooks(Svix) | webhook-signature: v1,… | HMAC-SHA256 | Base64 | id.timestamp.body、鍵は whsec_ の Base64 から |
| Twilio | X-Twilio-Signature | HMAC-SHA1 | Base64 | URL とソートした POST パラメーター |
| GitHub | X-Hub-Signature-256: sha256=… | HMAC-SHA256 | 16進数 | 生のリクエスト本文 |
| Stripe | Stripe-Signature: t=…,v1=… | HMAC-SHA256 | 16進数 | timestamp.body |
このツールの値が16進数では一致するのにコードでは Base64 で比較している、あるいはその逆の場合、計算は正しく、エンコードが違うだけです。何が署名されるかはプロバイダーのドキュメントで確認してください。タイムスタンプの接頭辞は、古いリクエストのリプレイを防ぐためにあります。
コードで署名を検証する
生のバイト列を定数時間で比較し、本文は受け取ったとおりにハッシュしてください。
const crypto = require("crypto");
function verifyWebhook(rawBody, secret, signatureHeader) {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest();
const given = Buffer.from(signatureHeader, "base64");
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}
rawBody を fox の文の Buffer、秘密鍵を key、署名を上の表の Base64 値にすると true を返します。生の本文の代わりに JSON.stringify(JSON.parse(body)) を渡すと、プロバイダーの JSON に、シリアライザーが落とす空白が 1 つでもあれば false になります。Express なら、Webhook のルートでは express.raw() で本文を読むということです。
Python で同じことをするには、hmac.new(key, body, hashlib.sha256).digest() を Base64 エンコードし、hmac.compare_digest で比較します。コマンドラインでは openssl dgst -sha256 -hmac 'key' -binary | base64 で同じ Base64 が得られます。鍵が16進数なら -mac HMAC -macopt hexkey:… を使ってください。強い鍵には、Base64 鍵生成ツール の HMAC シークレットのプリセットで 32 バイトのランダムな鍵を作れます。リクエストではなくトークンに署名するなら、JWT エンコーダー をご覧ください。
よくある質問
Webhook の HMAC が一致しないのはなぜですか?
署名はリクエスト本文の正確なバイト列に対して計算されます。フレームワークが JSON をパースし、それを再シリアライズしたものをハッシュすると、空白やキーの順序が変わって結果が違ってきます。生の本文をハッシュし、プロバイダーが16進数と Base64 のどちらを想定しているか、秘密鍵を先にデコードする必要があるかを確認してください。
HMAC の秘密鍵はテキストですか、Base64 ですか?
プロバイダーによります。大半は UTF-8 のテキストとして使う文字列を渡します。中には Base64 でエンコードされた鍵を、whsec_ のような接頭辞付きで渡すものもあり、その場合は使う前にバイト列にデコードする必要があります。鍵の形式 をそれに合わせてください。
ハッシュと HMAC の違いは何ですか?
メッセージの SHA-256 は誰でも計算できるので、ハッシュで検出できるのは偶発的な変更だけです。HMAC は秘密鍵を混ぜ込むので、有効な値を作れるのは鍵を持つ人だけになり、署名として機能します。
HMAC-MD5 や HMAC-SHA1 を使ってもよいですか?
どちらも古い API で今も使われていて、素の MD5 や SHA-1 よりはるかに強力です。衝突がそのまま HMAC の偽造につながるわけではないからです。新しく設計するなら HMAC-SHA256 を選んでください。