HMACによるメッセージ認証
鍵の保有者、認証するバイト列、再利用を止める状態を分けて設計します。
説明
HMACは共有秘密鍵とメッセージから認証タグを計算します。鍵を知らない相手による本文の作り替えを検出できますが、本文の暗号化や再送の拒否を自動で行うものではありません。
注文メッセージへタグを付け、本文・鍵・表現を変えます。その後、同じ要求を再送し、署名対象の要求IDと使用済み記録が一回性にどう関わるかを確認します。
鍵の保有者、認証するバイト列、再利用を止める状態を分けて設計します。
1共有鍵でタグを作る
教材の公開鍵文字列は秘密として使えません。ここでは共有鍵が同じである場合の計算を観察します。
from sc06 import *
message = b"id=7&amount=100"
t = tag(message)本文にHMAC-SHA-256を付ける
print(message)
print(t.hex())本文はそのまま読めます。HMACは本文の秘匿ではなく、鍵を使った照合値の生成です。
2受信側で同じ鍵を使う
受信側は受領本文からタグを再計算し、受領したタグと比較します。
print(verify(message,t)受領本文とタグを検証する)Trueです。比較には一定時間で比較するcompare_digestを使っています。鍵共有者は同じタグを生成できます。
3金額を変更する
タグを保ったまま本文の100を900へ変えます。
changed = b"id=7&amount=900"
print(verify(changed,t)改変した金額を元のタグと照合する)Falseです。正しいタグは元の本文のバイト列に対応しており、新しい金額には対応しません。
4別の鍵で検証する
本文とタグが同じでも、共有鍵が一致しなければ検証できません。
print(verify(message,t,b"different-public-example-key"別の公開試料鍵を指定する))Falseです。鍵を取り違えた場合と、本文・タグが改変された場合は、検証結果だけでは原因を特定できません。
5意味が同じでもバイト列を変える
パラメーターの順序を入れ替えます。アプリが同じ意味と扱っても、HMACの入力バイト列は変わります。
print(verify(b"amount=100&id=7"同じ値を別の順序で表現する,t))
print(message.hex())Falseです。送受信で同じ正規化規則を使うか、受領した元のバイト列を認証対象にする必要があります。
6同じ要求は再び検証できる
本文とタグをそのまま再送します。
print(verify(message,t),verify(message,t)同じ要求を2回検証する)どちらもTrueです。HMACが正しいことと、まだ実行していない要求であることは別の条件です。
7認証した要求IDを一度だけ受け入れる
要求IDはHMAC対象に含まれています。検証後にIDを取り出し、受付記録と業務更新を一体で確定する設計が必要です。ここでは単一処理の集合で受付を表します。
seen = set()
request_id =認証対象の本文からIDを取り出す message.split(b"&")[0]
accepted = verify(message,t) and request_id not in seen
if accepted: seen.add(request_id)初回受付を記録する
print(accepted, sorted(seen))
print(verify(message,t) and request_id not in seen)再送を拒否する最初だけTrueで、再提示はFalseです。並行する実システムでは、検査と登録の競合を一意制約やトランザクションで防ぎます。
8鍵共有者なら別の本文にもタグを作れる
正当な受信側も共有鍵を知っています。金額900の新しいタグを作って検証します。
new_tag = tag(changed)鍵保有者として新しい本文へタグを作る
print(verify(changed,new_tag))Trueです。HMACだけでは共有者のどちらが作ったかを第三者へ証明できません。秘密鍵と公開検証鍵を分ける電子署名とはこの点が異なります。
まとめ
HMACは鍵と正確なバイト列に対応する認証タグです。
本文の暗号化と再送拒否は別に設計します。
鍵共有者は全員タグを作れるため、作成者の第三者向け証明にはなりません。
操作履歴と解説を振り返れます。
Python 3.15 / cryptography 50.0.2 / 公開試料と固定時刻
from sc06 import *
message = b"id=7&amount=100"
t = tag(message)
print(message)
print(t.hex())b'id=7&amount=100' 6947f8de1629535f641b730388f5108db3fa14828c323825b97cef0295d33ff6
print(verify(message,t))True
changed = b"id=7&amount=900"
print(verify(changed,t))False
print(verify(message,t,b"different-public-example-key"))False
print(verify(b"amount=100&id=7",t))
print(message.hex())False 69643d3726616d6f756e743d313030
print(verify(message,t),verify(message,t))True True
seen = set()
request_id = message.split(b"&")[0]
accepted = verify(message,t) and request_id not in seen
if accepted: seen.add(request_id)
print(accepted, sorted(seen))
print(verify(message,t) and request_id not in seen)True [b'id=7'] False
new_tag = tag(changed)
print(verify(changed,new_tag))True
演習完了