HMAC かデジタル署名かは、あらゆるメッセージ認証方式で最も重要な設計上の判断であり、問われるのは速度ではなく信頼です。HMAC のタグは同じ共通鍵で計算・検証されるため、検証できる者は誰でも偽造もできます。署名は秘密鍵で作成され公開鍵で検証されるため、検証者は新たに有効なメッセージを作成できません。
比較表
| HMAC(RFC 2104、FIPS 198-1) | デジタル署名 | |
|---|---|---|
| 鍵 | 1 つの共有秘密 | 秘密鍵で署名し、公開鍵で検証 |
| 有効なタグや署名を作成できる者 | シークレットを持つ全員(すべての検証者を含む) | 秘密鍵の保有者のみ |
| 否認防止(第三者に証明できるか) | なし | あり(鍵と署名者の信頼できる紐付けがある場合) |
| 鍵の配布 | シークレットをすべての検証者に秘密裏に届ける必要あり | 公開鍵は公開できる(JWKS、証明書、DNS) |
| 証明のサイズ | HMAC-SHA256 で 32 バイト | 64 バイト(Ed25519、ECDSA P-256)、256〜512 バイト(RSA) |
| 速度(Apple M4 Max の 1 コア) | 256 バイトのメッセージで毎秒約 7,000,000 タグ(OpenSSL 3.6.5) | Ed25519 で毎秒約 60,000 署名、P-256 で毎秒約 87,000 署名 |
| 主な用途 | webhook、セッション Cookie、AWS SigV4、TOTP、HS256 の JWT、TLS 1.2 のレコード MAC | TLS 証明書とハンドシェイク、OIDC の ID トークン、パッケージ署名、Git、パスキー |
| ポスト量子 | SHA-256 なら安全(Grover のアルゴリズムでも原像計算の労力が半分になるだけ) | 従来の方式は Shor のアルゴリズムで破られるため、ML-DSA などが必要 |
HMAC のスループットは openssl speed -hmac sha256 で測定しました(256 バイトの入力で約 1.83 GB/s)。署名の速度は openssl speed で測定しました。
違いは信頼モデルにある
HMAC は鍵付きハッシュで、H((K ⊕ opad) ‖ H((K ⊕ ipad) ‖ m)) と計算されます。その安全性の目標は、鍵を持たない者には有効なタグを生成できないことです。ちょうど 2 者が鍵を共有し、互いに偽造しないと信頼している場合は、これで十分です。その場合、有効なタグは「相手がこれを送った」ことを意味するからです。3 者以上になると、この前提は崩れます。どの検証者も他の参加者に対して送信者になりすますことができ、紛争になれば、送信者はタグを作ったのは受信者だと主張できます。
デジタル署名は、署名する能力と検証する能力を切り離します。検証鍵を公開しても、それで誰かが署名できるようになるわけではありません。そのため 1 つの発行者が数千もの検証者に対応でき、証明書、鍵ディレクトリ、アカウントの記録によって鍵が署名者に紐付けられていれば、署名を出所の証拠として監査人や裁判所に提示できます。多数のサービスが利用する ID 関連のトークン(OpenID Connect の ID トークンや、リソースサーバーが検証するアクセストークン)が署名を使うのも、検証側の設定が漏洩しても署名なら無害なのに HMAC では致命的になるのも、この分離があるからです。
実務上のポイント
鍵管理:HMAC シークレットは、ランダムに生成し(HMAC-SHA256 では 256 ビット以上。RFC 7518 §3.2 は HS256 にこれを要求しています)、秘密裏に伝送し、すべての検証者が保存し、すべての場所で一斉に、または 2 つの有効なシークレットを併用する猶予期間を設けてローテーションしなければなりません。署名鍵では、秘密鍵を 1 か所(理想的には HSM や KMS)に保管し、公開された鍵セット内の鍵識別子を使ってローテーションします。
性能とサイズ:HMAC はハッシュ計算 2 回分で、1 コアあたり毎秒数百万回実行でき、タグは 32 バイトです。署名には数十マイクロ秒かかり、サイズは 64 バイト以上です。ほとんどの Web トラフィックでは、どちらも問題になりません。問題になるのはパケット単位の認証や非常に大量の内部メッセージングで、そこでは HMAC か AEAD を使うのが一般的です。
落とし穴:HMAC は定数時間で比較しなければならず、hash(key ‖ message) で代用してはいけません(SHA-256 では伸長攻撃によって破られます)。また、受信したバイト列そのもの(再シリアライズした JSON オブジェクトではなく、生のリクエストボディ)を認証しなければなりません。署名は、検証者が固定したアルゴリズムで検証しなければならず、メッセージ側が選んだアルゴリズムを使ってはいけません。
Node.js 26.10 で実行した次のスクリプトは、同じ webhook のボディを両方の方式で認証します。
import { createHmac, generateKeyPairSync, randomBytes, sign, timingSafeEqual, verify } from "node:crypto";
const body = Buffer.from('{"event":"invoice.paid","id":"evt_123"}');
// HMAC: sender and receiver share one secret (in production it comes from a secret store).
const secret = randomBytes(32);
const tag = createHmac("sha256", secret).update(body).digest();
const expected = createHmac("sha256", secret).update(body).digest();
console.log("HMAC-SHA256 tag:", tag.length, "bytes, valid:", timingSafeEqual(tag, expected));
// Signature: sender keeps the private key, receivers hold only the public key.
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
const signature = sign(null, body, privateKey);
console.log("Ed25519 signature:", signature.length, "bytes, valid:", verify(null, body, publicKey, signature));
HMAC-SHA256 tag: 32 bytes, valid: true
Ed25519 signature: 64 bytes, valid: true
HMAC のシークレットはすべての検証者と共有しなければなりません。一方、Ed25519 の秘密鍵は送信者の手元から出ることがなく、配布されるのは 32 バイトの公開鍵だけです。
webhook と API の実際
ほとんどの webhook プロバイダーは、購読者ごとに専用のシークレットを発行するため HMAC を使います。GitHub は、X-Hub-Signature-256: sha256= の後にボディの HMAC-SHA256 を 16 進数で付けて送ります。Stripe は、受信側がリプレイを拒否できるよう、タイムスタンプとボディを合わせたものに署名し、Stripe-Signature ヘッダーで送ります。購読者ごとのシークレットが一意であるため、多者間の問題は生じません。HTTP メッセージ署名(RFC 9421)や JWS に基づく署名付き webhook が使われるのは、1 つの送信者が、シークレットを共有すべきでない多数の受信者とやり取りする場合や、受信側がシークレットを用意せずに検証したい場合です。AWS Signature Version 4 は HMAC ベースのリクエスト署名です。OAuth DPoP と Web Bot Auth が署名を使うのは、検証者がクライアントになりすませてはならないからです。
どちらを選ぶべきか
- 同じサービスが発行と検証を行う(セッション Cookie、CSRF トークン、社内の JWT):HMAC-SHA256 を使います。
- 送信者も受信者も 1 つで、シークレットを帯域外で共有済み(顧客ごとの webhook):署名対象のデータにタイムスタンプを含めた HMAC を使います。
- 発行者が 1 つで、検証者が多数(ID トークン、公開 API、サーバーのデータを検証するモバイルアプリ):署名を使い、鍵は JWKS で公開します。
- 第三者に対する証拠(契約書、監査ログ、ソフトウェアのリリース):署名に信頼できるタイムスタンプを組み合わせます。
- 互いになりすませてはならないクライアント(DPoP、ボット認証、デバイスアテステーション):クライアントごとの鍵による署名を使います。
- 信頼できる経路での非常に高いメッセージレート:HMAC または AEAD を使います。
移行時の注意点
HMAC から署名への移行は、たいてい 2 つ目の検証者が現れたときか、シークレットが漏洩したときに行われます。
- アルゴリズム識別子を導入する:形式にまだない場合は追加します(JOSE の
alg、HTTP 署名のalgパラメーター、バージョンを示すプレフィックスなど)。こうすれば検証者は、メッセージを盲目的に信頼することなく、どの方式が使われているかを判断できます。 - 公開鍵を公開する:鍵識別子を付けて公開し(JWKS、HTTP 署名の鍵ディレクトリ)、検証者が取得するための時間を確保します。
- 移行期間中は二重に署名する:HMAC のヘッダーと署名の両方を送り、検証者には署名を優先させます。
- 検証だけを行う検証者からは共有秘密を削除します。これこそがセキュリティ上の利点なので、「念のため」に残しておいてはいけません。
- 1 つの鍵を両方の方式に使ってはいけません。RSA 公開鍵を HMAC シークレットとして流用することこそ、JWT ライブラリに脆弱性をもたらしたアルゴリズム混同です。HS256 と RS256 の比較を参照してください。