この記事について この記事は、生成AIを活用した自動生成フローで作成しています。Python公式のhmac仕様をもとに、署名の一致と注文の重複処理が別問題であることを、架空の注文データで解説します。
検証ステータス:📘 公式仕様を参考に構成・掲載コードのローカル実行確認済み
HMAC署名が一致しても二重注文は防げない?改ざんと再送をPythonで比べる
Webhookや外部システムから受け取る注文通知には、HMAC署名が付いていることがあります。「署名が正しいから、その注文を処理してよい」と判断したくなりますが、ここには重要な落とし穴があります。HMACが確認するのは、共有鍵を知る相手が作ったメッセージと、受信したメッセージが対応するかどうかです。同じ通知が二度届いたことや、その注文が既に処理済みかどうかまでは教えてくれません。
例えば、注文番号がA-100、数量が2という通知を受け取り、在庫を2個引き当てる場面を考えます。通信の再試行で全く同じ通知がもう一度届いても、署名は再び正しく検証されます。そこで再び在庫を減らすと、署名検証は成功しているのに業務処理は誤ります。この違いを、ネットワークに接続せず、標準ライブラリだけで確認します。
対象環境と実験の範囲
| 項目 | 内容 |
|---|---|
| 対象 | Python 3の標準ライブラリ hmac、hashlib |
| 入力 | 架空の注文番号・数量と実験用の固定鍵 |
| 外部通信 | なし |
| 変更対象 | メモリ上の集合のみ。実ファイルや注文システムは変更しない |
| 注意 | 掲載した固定鍵を本番で使用しない |
今回の成功条件は、正常な通知は受理し、数量を改ざんした通知は署名不一致になり、元の通知を再送した場合は署名が一致しても二重処理を拒否できることです。実験用のコードは教育目的の最小構成です。HTTPヘッダー、時刻の有効期限、鍵の安全な配布、永続的なデータベースは実装していません。
最初に予想する
署名を「改ざんされていないことを示す印鑑」と考えると理解しやすくなります。ただし、印鑑が正しい伝票を二枚持ち込まれたとき、同じ荷物を二度出荷してよいかは別の判断です。署名の検証と、伝票番号の処理済み確認を二段階に分ける必要があります。
ここでは注文を文字列として扱います。A-100|2は注文番号A-100、数量2を意味します。A-100|20は数量だけが違う別のバイト列です。たった一文字の変更でも、同じ署名では検証に通らないはずです。一方、元のA-100|2をそのまま繰り返すと、署名は当然一致するはずです。
安全な実験:三つの通知を比較する
次のコードをhmac_demo.pyとして保存し、Python 3で実行します。実在するAPI鍵や顧客情報は入力しないでください。実験はローカルのメモリ内で完結し、署名もその場で作成します。
import hashlib
import hmac
key = b"demo-key-not-for-production"
original = b"A-100|2"
tampered = b"A-100|20"
seen = set()
def sign(message):
return hmac.new(key, message, hashlib.sha256).digest()
def receive(message, signature):
expected = sign(message)
if not hmac.compare_digest(expected, signature):
return "REJECT: signature"
order_id = message.split(b"|", 1)[0]
if order_id in seen:
return "REJECT: duplicate"
seen.add(order_id)
return "ACCEPT: process once"
signature = sign(original)
print(receive(original, signature))
print(receive(tampered, signature))
print(receive(original, signature))
期待する表示は、順にACCEPT: process once、REJECT: signature、REJECT: duplicateです。最初は正しい通知なので処理され、二番目は元の署名のまま数量を変えたため拒否されます。三番目は元の通知と元の署名の組合せなので、署名自体は正しいままです。しかし、処理済みの注文番号が見つかるため、業務処理を拒否します。
なぜ署名の比較だけでは足りないのか
HMACは秘密鍵とメッセージを入力として、一定長の認証用の値を計算します。送信側と受信側が同じ鍵と同じバイト列を使えば、同じ値を計算できます。受信側は自分で計算した値と送られてきた署名を比較し、異なる場合に拒否します。暗号化ではないため、メッセージ本文を隠す機能はありません。通信の盗聴防止にはHTTPSなど別の仕組みが必要です。
コードのhmac.newは、実験用の鍵と通知本文からSHA-256を使ったHMACを計算します。digest()は生のバイト列を返します。hmac.compare_digestは通常の==の代わりに、比較処理の時間差から情報が漏れる危険を減らすために用意された関数です。ただし、引数の型や長さに関する情報まで完全に隠す万能な仕組みではありません。実際のAPIが16進文字列で署名を送る場合は、形式を正しく変換してから比較します。
署名が一致するということは、受信したバイト列と鍵から計算した認証値が合っている、という判定に過ぎません。受信時刻が古いか、注文が取消済みか、既に出荷されたかは、HMACの計算には入っていません。同じバイト列をもう一度受け取れば、同じ署名が一致することは、むしろ仕様どおりの正常動作です。
分岐を図で読む
次の図は、受信した通知をどの順番で検査するかを表しています。署名が正しくても、処理済みの注文番号を確認するまでは業務処理へ進まない点に注目してください。
flowchart TD
A[通知を受信] --> B{HMAC署名が一致?}
B -- いいえ --> C[改ざん等として拒否]
B -- はい --> D{注文IDは処理済み?}
D -- はい --> E[重複として拒否]
D -- いいえ --> F[一度だけ業務処理]
この順序を逆にして、署名を確認する前に注文番号を信頼してしまうと、認証されていない入力で処理済みデータを参照・更新する設計になりかねません。図は最小限の考え方を示したもので、実務では認証前の入力サイズ制限や構文チェックも必要です。
一つだけ条件を変える
三回目のreceive(original, signature)の直前にseen.clear()を入れて、もう一度実行してみてください。最後の表示はACCEPT: process onceに変わるはずです。署名の作り方も、注文本文も、秘密鍵も変えていません。変えたのは処理済み記録の有無だけです。つまり、二重処理防止は署名の性質ではなく、受信側が管理する状態に依存します。
この実験のsetはプログラム終了とともに消えます。サーバーが再起動したら過去の注文を忘れますし、複数台のサーバーで動かせば、それぞれ別の集合を持つため同じ注文を両方で処理する危険があります。実際の注文受付では、データベースの一意制約やトランザクションを利用し、複数プロセスが同時に受けても一度だけ確定する仕組みを設計します。
失敗したときに切り分ける
最初から署名不一致になる場合、鍵が同じか、本文を同じバイト列として扱っているかを確認します。文字列の前後に空白や改行が増えたり、JSONを読み込んで再出力したりすると、意味が同じでもバイト列が変わることがあります。実際のWebhookではサービスが指定した「署名対象の生のリクエスト本文」を使う必要があります。JSONのキー順や空白を勝手に正規化してから検証すると、正しい通知も拒否する場合があります。
改ざんしたのに受理される場合、署名の計算に改ざん後の本文を使って新しい署名まで作っていないか確認します。今回の比較実験では、必ず元のsignatureを使い回します。鍵を持つ送信側が本文を変更して正しい署名を再生成した場合、それはこの実験でいう第三者による改ざんとは違います。HMACだけでは、送信側の業務判断が正しいかまでは保証できません。
重複が拒否されない場合は、処理済み記録が保存されているか、注文番号の表記が統一されているかを調べます。A-100とa-100を同じ注文として扱うかどうかは業務仕様です。単純に大文字小文字を変換すると、別の正当な注文番号を誤って同一視する可能性もあります。受信側だけで判断せず、送信元のID仕様を確認する必要があります。
業務で使うときの設計判断
在庫や決済を伴うWebhookでは、まず署名の検証に成功した通知だけを受け付け、その後でイベントIDまたは注文IDを使って冪等性を実現します。注文IDは一つでも、注文作成と注文取消しという異なるイベントがある場合、注文IDだけを重複キーにすると取消しまで拒否してしまいます。送信元が提供する一意のイベントIDを使うか、イベント種別と注文IDを組み合わせるか、業務要件に合わせて決めます。
処理済み記録を書いた後で本処理が失敗すると、次の再送を誤って拒否する危険があります。逆に本処理を終えてから記録を書こうとして失敗すると、再送時に二度処理する危険があります。このため、単なる「集合に追加してから処理」では本番の整合性は保証できません。データベースの一意制約と処理状態、同じトランザクションで確定できる範囲、失敗時の再試行と補償処理を検討します。
また、再送攻撃を抑えるために署名対象へタイムスタンプやnonceを含める方式もあります。ただし、時刻だけを確認しても許容時間内の再送は起こり得ます。nonceの保存期間、時計ずれ、イベントIDの重複検知を組み合わせる必要があります。送信元サービスが定めた署名形式や再送ポリシーが最優先であり、独自の署名形式を勝手に付け足して互換性を壊さないようにします。
まとめ
HMAC署名の一致は、通知が同じ鍵と同じメッセージに対応することを確認する手段です。通知を一度だけ処理したかどうかは、別の状態管理が必要です。実験で数量を一文字変更すると署名検証が失敗する一方、元の通知をそのまま再送すると署名は一致します。この違いを知っておくと、Webhookの「認証成功なのに二重注文」という障害を、署名の問題と業務状態の問題に分けて調査できます。
