TOTPによるワンタイムパスワード
短いコードがどの値から作られ、どの要求・時刻へ結び付ける必要があるかを説明します。
説明
TOTPは時刻から求めたカウンターと共有秘密から、短い認証コードを計算する方式です。同じ時刻区間では同じコードになるため、一度使ったコードの再利用を止めるには検証側の状態も必要です。
RFC 6238の公開試料を使い、時刻59秒・30秒区間・8桁・HMAC-SHA-1の計算をバイト列から確認します。期限と使用済み状態を変えた後、push承認の要求IDと表示番号の対応も比較します。
短いコードがどの値から作られ、どの要求・時刻へ結び付ける必要があるかを説明します。
1時刻から8バイトのカウンターへ
時刻59を30で割った整数部分は1です。HMACへ渡すカウンターは8バイトのビッグエンディアンです。
from sc04 import details, OTP
d = details(59)時刻59の計算過程を取得する
print(d["counter"], d["message"])カウンターと入力バイト列を読む1と0000000000000001です。画面に表示する8桁コードと、HMACへ入力する8バイトは異なります。
2HMACと動的切出し位置
最後のバイトの下位4ビットをoffsetとし、その位置から4バイトを使います。
print(d["hmac"])20バイトのHMACを16進数で読む
print(d["offset"])切出し開始位置を読むHMAC全体は20バイトです。offsetはその中の切出し開始位置で、固定の先頭4バイトを使う方式ではありません。
331ビットの整数から8桁へ
切り出した4バイトの最上位ビットを外し、10の8乗で割った余りを8桁へそろえます。
print(d["binary"])最上位ビットを外した整数を読む
print(d["code"])8桁へ変換した結果を読むコードは94287082です。整数から8桁へ縮めるため衝突はあり、コードだけで利用者や要求を特定することはできません。
4同じ区間と次の区間
時刻59と60ではカウンターが1から2へ変わります。
print(details(58)["code"]同じ区間のコードを計算する, details(59)["code"]区間末尾を計算する, details(60)["code"]次の区間を計算する)58と59は同じコードで、60では変わります。実際の検証で許す時計のずれは、受入れ範囲として別途決める条件です。
5一度受け入れたコードを再利用しない
このOTPクラスは現在の区間だけを許可し、受理したカウンターを使用済みにします。
otp = OTP()未使用状態の検証器を作る
print(otp.accept(d["code"], 59)最初のコードを受け入れる)
print(otp.accept(d["code"], 59)同じコードを再度提示する)初回True、再利用Falseです。HMAC計算が一致するだけでは一回性を保証できず、検証側の使用済み管理が必要です。
6期限後は現在のコードを使う
次の区間60で古いコードを出し、その後に現在のコードを提示します。
print(otp.accept(d["code"], 60)古いコードを次区間へ提示する)
print(otp.accept(details(60)["code"], 60)現在区間のコードを提示する)古い値はFalse、現在の値はTrueです。時刻の照合と再利用防止は別々の条件です。
7先頭が0のコードを保持する
RFCの別の試料時刻で8桁表現を確認します。数値に変換して先頭0を落とさないよう、コードは文字列で扱います。
print(details(1111111109)["code"]公開試料の8桁コードを計算する)07081804です。8桁の先頭0も、照合する表現の一部です。
8push承認を要求へ結び付ける
二つの要求に表示番号を割り当てます。別要求の番号を見て承認した場合を、要求IDと番号の組で判定します。
pending =要求ごとの表示番号を用意する {"request-7":42,"request-8":83}
print(pending["request-7"] == 83別要求の番号を照合する)
print(pending["request-7"] == 42対応する番号を照合する)
pending.pop("request-7")完了した要求を待機集合から外す
print("request-7" in pending)同じ要求が残っていないことを読む別要求の番号はFalse、対応する番号はTrueです。完了した要求をpendingから外すと同じ要求の再承認も防げます。ただし番号照合だけでフィッシング耐性を保証するものではありません。
まとめ
TOTPは時刻カウンター・HMAC・動的切出しから作られます。
コード計算の一致と、一度だけ受け入れる処理は別です。
承認操作を、要求ID・時刻・利用先へ結び付ける必要があります。
操作履歴と解説を振り返れます。
Python 3.15 / cryptography 50.0.2 / 公開試料と固定時刻
from sc04 import details, OTP
d = details(59)
print(d["counter"], d["message"])1 0000000000000001
print(d["hmac"])
print(d["offset"])75a48a19d4cbe100644e8ac1397eea747a2d33ab 11
print(d["binary"])
print(d["code"])1094287082 94287082
print(details(58)["code"], details(59)["code"], details(60)["code"])94287082 94287082 37359152
otp = OTP()
print(otp.accept(d["code"], 59))
print(otp.accept(d["code"], 59))True False
print(otp.accept(d["code"], 60))
print(otp.accept(details(60)["code"], 60))False True
print(details(1111111109)["code"])07081804
pending = {"request-7":42,"request-8":83}
print(pending["request-7"] == 83)
print(pending["request-7"] == 42)
pending.pop("request-7")
print("request-7" in pending)False True False
演習完了