認証アプリの6桁コードはどう作られるか:TOTPの計算手順

30秒ごとに変わる認証アプリのコードがサーバーと一致する仕組みを、RFCのテスト値でたどります。時間ステップ、HMAC、動的切り捨て、サーバー側の検証、フィッシングに弱い理由まで整理しました。

開発セキュリティ

認証アプリの6桁のコードは30秒ごとに変わります。アプリはサーバーに問い合わせずにコードを作りますが、サーバーはその数字を受け付けます。両者が同じ秘密鍵と同じ時間ステップの規則でそれぞれ計算し、双方の時計が十分にそろっているからです。この計算が RFC 6238 の TOTP(Time-Based One-Time Password)です。

計算の手順

まず現在の Unix 時間をカウンター T = floor((現在時刻 - T0) / X) に変えます。既定値は X=30秒、T0=0 なので、59秒なら T=1、60秒なら T=2 です。

その先はカウンター方式のアルゴリズム HOTP(RFC 4226)と同じです。共有秘密鍵で8バイトのカウンター T の HMAC-SHA-1(RFC 2104)を計算し、20バイトを得ます。HMAC は鍵を混ぜてハッシュ関数を2回適用するメッセージ認証コードで、RFC 6238 は SHA-256 や SHA-512 を使ってもよい(MAY)としています。続く動的切り捨て(dynamic truncation)では、最後のバイトの下位4ビットをオフセットとして4バイトを読み、符号の扱いの混乱を避けるため先頭ビットを0にします。この31ビット整数を10^桁数で割った余りがコードで、桁が足りなければ先頭を0で埋めます。

RFC のテスト鍵 12345678901234567890 と T=1 で計算した HMAC-SHA-1 は 75a48a19d4cbe100644e8ac1397eea747a2d33ab です(RFC 4226 付録 D)。最後のバイト ab からオフセット11が得られ、その位置の c1 39 7e ea の先頭ビットを0にすると1,094,287,082です。10^6 で割った余りは287082、10^8 で割った余りは RFC 6238 の表にある59秒の値94287082です。

検証と再利用の防止

サーバーは受け取った時刻から T を計算します。RFC 6238 は過去の時間ステップとも比較しつつ、ネットワーク遅延として認めるのは最大1ステップにするよう推奨しています(RECOMMEND)。範囲が広いほど攻撃に使える時間も長くなるからです。検証に成功した OTP を再び受け付けてはなりません(MUST NOT)。米国連邦政府のシステム向けガイドラインである NIST SP 800-63B-4 も、OTP を有効な間に一度だけ受け付けるよう求め(SHALL)、64ビットより短い OTP には試行回数の制限を求めています(SHALL)。100万通りしかない6桁のコードはこれに当たり、特に定めがなければ連続失敗は100回以下に制限します。サーバーも同じ鍵でコードを計算し直すので、保存した鍵は厳重に保護する必要があります。

フィッシングには弱い

TOTP はパスワードだけを盗んだ攻撃者は防げますが、コードそのものを受け取る攻撃は防げません。NIST SP 800-63B-4 は OTP 認証をフィッシング耐性がないものと分類しています。手で入力するコードは認証中のセッションに結び付かないため、偽サイトが有効な間にコードを本物のサイトへ渡せば、ログインが成立しえます。このガイドラインでは、フィッシング耐性はチャネルバインディングか、WebAuthn のような検証者名バインディングを備えた暗号認証でしか得られません。米国連邦のシステムでは、AAL2 でフィッシング耐性のある手段を少なくとも一つ提供する必要があり(SHALL)、AAL3 ではフィッシング耐性が必須です。

RFC 6238、4226、2104 は相互運用のための Informational RFC で、標準化トラックの文書ではありません。NIST の SHALL 要件が規範的なのは米国政府のシステムの範囲内に限られます。

参考

  • RFC 6238, TOTP: Time-Based One-Time Password Algorithm (2011)
  • RFC 4226, HOTP: An HMAC-Based One-Time Password Algorithm (2005)
  • RFC 2104, HMAC: Keyed-Hashing for Message Authentication (1997)
  • NIST SP 800-63B-4 (2025)

ブログへ