使い方
- 「アルゴリズム」を選びます。初期設定では HMAC-SHA256 が選択されており、ほとんどの Webhook や API で使われているのもこのアルゴリズムです。
- 「秘密鍵」を入力し、「鍵の形式」を「テキスト」(UTF-8 として読み取ります)、「16進数」、「Base64」のいずれかに設定します。鍵がパスワードのような文字列ではなく生のバイト列の場合は、「16進数」または「Base64」を使います。
- メッセージを入力または貼り付けます。生のバイト列の場合は入力形式を「16進数」に切り替え、ファイルの場合は「ファイルを開く」を使います。HMAC は入力に合わせてリアルタイムに更新されます。
- 「出力形式」で「16進数(小文字)」「16進数(大文字)」「Base64」のいずれかを選んで結果をコピーします。受け取った署名を「比較対象」に貼り付けると、「一致」または「不一致」が表示されます。
HMAC のしくみ
HMAC(hash-based message authentication code、ハッシュベースのメッセージ認証コード、RFC 2104)は、暗号学的ハッシュ関数と秘密鍵を組み合わせたものです。メッセージに対して正しい値を作れるのは鍵を知っている人だけなので、HMAC が一致すれば、メッセージが改ざんされていないことと、鍵の所有者から送られたものであることの 2 点が同時に証明されます。
計算式は H((K ⊕ opad) ‖ H((K ⊕ ipad) ‖ message)) です。鍵はハッシュ関数のブロックサイズ(MD5、SHA-1、SHA-256 では 64 バイト、SHA-384 と SHA-512 では 128 バイト)より長い場合は先にハッシュ化され、そのサイズになるまでゼロで埋められます。次に、ipad(0x36 のバイト列)と XOR を取ったものでメッセージに対する内側のハッシュを計算し、opad(0x5c のバイト列)と XOR を取ったもので内側の結果に対する外側のハッシュを計算します。単純な SHA-256(key ‖ message) には外側のハッシュがないため、タグを 1 つ見た人なら誰でも、鍵を知らなくてもメッセージにデータを追加して有効な新しいタグを計算できてしまいます(伸長攻撃)。
出力の長さは元のハッシュ関数と同じで、HMAC-SHA256 なら 32 バイト(16進数で 64 文字)です。RFC 2104 では少なくともこの長さの鍵を使うよう推奨されており、ランダム Base64 生成ツールで適切な鍵を作成できます。
HMAC 署名が使われる場面
- GitHub の Webhook。
X-Hub-Signature-256ヘッダーは、sha256=の後に、リクエストの生のボディに対して Webhook のシークレットを鍵として計算した HMAC-SHA256 を16進数で続けたものです。ヘッダーの値全体を「比較対象」に貼り付けてもかまいません。sha256=プレフィックスは無視されます。 - Stripe の Webhook。
Stripe-Signatureヘッダーには、タイムスタンプtと署名v1が含まれます。署名は、タイムスタンプ、ドット、生のボディをつなげたものに対し、エンドポイントのシークレット(whsec_...)をテキストのまま鍵として計算した HMAC-SHA256 の16進数です。 - Shopify の Webhook。
X-Shopify-Hmac-Sha256には、生のボディに対してアプリのクライアントシークレットを鍵として計算した HMAC-SHA256 が Base64 で入っています。「出力形式」で「Base64」を選ぶか、ヘッダーの値を「比較対象」に貼り付けてください。 - AWS 署名バージョン 4。署名鍵は HMAC-SHA256 を連鎖させて導出します。まず
AWS4とシークレットアクセスキーをつなげたものを鍵として日付の HMAC を計算し、続いてリージョン、サービス、aws4_requestを、それぞれ直前の結果を鍵として順に処理します。あるステップの出力を次のステップの鍵として使うには、「鍵の形式」を「16進数」に設定してください。 - JSON Web Token。HS256、HS384、HS512 は、エンコード済みのヘッダー、ドット、エンコード済みのペイロードをつなげたものに対する HMAC-SHA256、-384、-512 です。トークンの 3 番目の部分はその HMAC を URL セーフの Base64 で表したもので、「比較対象」にそのまま貼り付けられます。トークン全体の検証には JWT デコーダーをご利用ください。
HMAC は必ず、受信したとおりの生のボディに対して計算してください。JSON を解析してから再びシリアライズすると、空白やキーの順序が変わります。改行が 1 つ増えただけでも、まったく異なる HMAC になります。
コードで HMAC を計算する
# Python
import hmac, hashlib
sig = hmac.new(b'key', b'message', hashlib.sha256).hexdigest()
hmac.compare_digest(sig, received) # 定数時間で比較
// Node.js
const crypto = require('crypto');
const sig = crypto.createHmac('sha256', 'key').update('message').digest('hex');
// timingSafeEqual は長さが異なると例外を投げるため、先に長さを確認します
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(received));
// PHP:メッセージを鍵より先に渡します
$sig = hash_hmac('sha256', 'message', 'key');
hash_equals($sig, $received);
# OpenSSL:テキストの鍵と、同じ鍵を16進数で指定する例
printf '%s' 'message' | openssl dgst -sha256 -hmac 'key'
printf '%s' 'message' | openssl dgst -sha256 -mac HMAC -macopt hexkey:6b6579
# テストベクター:鍵 "key"、メッセージ "The quick brown fox jumps over the lazy dog"
# HMAC-SHA256 = f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8
独自のコードで署名を検証する場合は、上の例のような定数時間の比較を使用してください。単純な == は最初に異なる文字が見つかった時点で比較を終えるため、その時間差を手がかりに、攻撃者が有効な署名を 1 バイトずつ突き止められるおそれがあります。Stripe のようにタイムスタンプを含む方式では、タイムスタンプが数分以上前のリクエストも拒否してください。これでリプレイ攻撃を防げます。
よくある質問
HMAC-MD5 や HMAC-SHA1 は今でも安全ですか?
衝突攻撃によって MD5 と SHA-1 は単体のハッシュ関数としては破られていますが、それで HMAC を偽造できるわけではありません。HMAC の安全性は、ハッシュ関数の別の性質と秘密鍵に依存しているためです(HMAC-MD5 については RFC 6151 で論じられています)。HMAC-SHA1 を使う TOTP コードのように、これらが必須の連携はそのまま使い続けて問題ありません。新しく設計する場合は HMAC-SHA256 を使用してください。
HMAC が Webhook の署名と一致しないのはなぜですか?
よくある原因は、生のボディではなく解析して再シリアライズした JSON をハッシュ化している、メッセージの末尾に改行が入っている、シークレットや鍵の形式が間違っている(Base64 や16進数のシークレットを「テキスト」として入力している、またはその逆)、アルゴリズムが違う、といったものです。16進数か Base64 かの違いは問題になりません。「比較対象」は大文字・小文字どちらの16進数も、標準の Base64 も、URL セーフの Base64 も受け付けます。
HMAC と通常のハッシュ値の違いは何ですか?
HMAC はデジタル署名と同じものですか?
いいえ。HMAC では双方が 1 つのシークレットを共有するため、タグを検証できる人はタグを作成することもできます。RSA や ECDSA(JWT の RS256 や ES256)のような署名は、本人だけが持つ秘密鍵で作成し、公開鍵で検証するため、検証する側が署名を偽造することはできません。
秘密鍵はサーバーに送信されますか?
いいえ。HMAC-SHA1 と SHA-2 系の HMAC はブラウザの Web Crypto API で、HMAC-MD5 はページ上の JavaScript で計算されます。鍵とメッセージがお使いのデバイスの外に出ることはありません。それでも本番環境のシークレットは慎重に扱い、漏えいした可能性のある鍵はローテーションしてください。