TOTP/HOTP の内部動作(時刻同期ワンタイムパスワードの数理)
認証アプリの6桁はどう生まれるのか。共有秘密と HMAC からコードを導く HOTP、時刻ステップで自動更新する TOTP、切り捨ての数理まで原理で押さえれば、ドリフトや総当たりの設計を正しく判断できる。
- 1.HOTP は共有秘密 K と8バイトのカウンタ C を HMAC-SHA1 にかけ、その出力を dynamic truncation で31ビットに削り、10^d で割った余りを d 桁コードにする方式(RFC 4226)。同じ K と C なら端末とサーバーが通信なしで同じ値を導く。
- 2.TOTP はカウンタ C を「(現在時刻 - T0) / 時間ステップ X」の整数商に置き換えただけ(RFC 6238)。既定は X=30秒で、コードが時間とともに自動更新される。クロックドリフトは前後数ステップを許容する検証窓で吸収する。
- 3.dynamic truncation は HMAC 出力の末尾4ビットをオフセットに使い、毎回違う位置から4バイトを抜き出す。総当たり耐性はコード桁数(6桁なら最大10^6通り)とレート制限・試行回数ロックで担保し、HMAC 自体の強度とは別問題。
ワンタイムパスワードが解こうとしている問題
認証アプリに表示される6桁コードは、毎回違う「使い捨ての秘密」です。狙いは、ネットワークを流れる認証情報を再利用不能にすること。固定パスワードは盗聴・流出すれば何度でも悪用されますが、ワンタイムパスワード(OTP)は一度使えば(あるいは一定時間で)無効になるため、傍受してもリプレイできません。これが多要素認証の「所持」要素として OTP が広く使われる理由です(要素の組み合わせは 多要素認証(MFA) を参照)。
OTP には大きくチャレンジ・レスポンス型と同期型がありますが、認証アプリが採るのは同期型です。サーバーと端末が共有秘密を一度だけ交換しておき、以後は通信なしで同じコードを独立に計算して照合します。鍵となる発想は「共有秘密と、双方が一致させられるカウンタ(イベント回数や現在時刻)を、鍵付きハッシュに通す」こと。HOTP がイベント版、TOTP が時刻版です。
HOTP:カウンタを HMAC に通す(RFC 4226)
**HOTP(HMAC-based One-Time Password, RFC 4226)**は、すべての OTP の土台です。入力は二つだけ。K(端末とサーバーが共有する秘密鍵、通常 Base32 で配布)と、C(8バイトのカウンタ、初期値0で認証成功ごとに +1)です。
HOTP(K, C) の手順:
1. C を 8 バイトのビッグエンディアン整数に変換する
2. HS = HMAC-SHA1(K, C) # 20 バイトの擬似ランダム列
3. Sbits = DynamicTruncate(HS) # 31 ビットの整数を抽出
4. code = Sbits mod 10^d # d は桁数(既定 6)
→ 例: d=6 なら "287082" のように 0 埋めした 6 桁
要点は HMAC-SHA1(K, C) が擬似ランダム関数として働くことです。鍵 K を知らない攻撃者には、カウンタを1進めた出力が前の出力と無相関なランダム列にしか見えません(HMAC が PRF として振る舞う根拠は MAC と HMAC の設計原理 を参照)。だからカウンタという単純で予測可能な値を入れても、出てくるコードは予測不能になります。RFC 4226 は SHA-1 を基準としていますが、構成上 H は差し替え可能で、TOTP では SHA-256/SHA-512 も選べます。
HMAC-SHA1 の出力は20バイト(160ビット)で、人が入力するには長すぎます。OTP は人間が画面を見て手で打ち込むことが前提なので、6〜8桁程度に縮める必要があります。単純に先頭数バイトを取る固定切り出しでも桁数は減らせますが、RFC 4226 はあえて出力依存で抽出位置を変える dynamic truncation を採用しました。固定位置だと、もし将来 HMAC 出力の特定バイトに偏りが見つかったとき常にその弱い場所を使い続けてしまうためで、抽出位置を出力自身でばらつかせる保険になっています。
dynamic truncation:毎回違う位置から抜き出す
桁数を減らす中核が **dynamic truncation(動的切り捨て)**です。20バイトの HMAC 出力から、その出力自身が指すオフセット位置の4バイトを抜き出します。
DynamicTruncate(HS[0..19]):
offset = HS[19] AND 0x0f # 末尾バイトの下位 4 ビット → 0〜15
P = (HS[offset] AND 0x7f) << 24 # 最上位ビットを落とす(符号対策)
| (HS[offset+1] AND 0xff) << 16
| (HS[offset+2] AND 0xff) << 8
| (HS[offset+3] AND 0xff)
return P # 31 ビットの非負整数
仕組みは二段です。まず末尾バイトの下位4ビットをオフセット(0〜15)とし、20バイトのどこから読むかを決めます。HMAC 出力は擬似ランダムなので、このオフセットも実質ランダムに散らばります。次にその位置から連続4バイト(32ビット)を読み、先頭バイトの最上位ビットを 0x7f でマスクして落とし、31ビットの非負整数 P を得ます。最上位ビットを落とすのは、言語によって最上位ビットが符号として解釈され、実装間で値がずれるのを防ぐための互換性対策です。
最後に P mod 10^d で桁数に丸めます。6桁なら 10^6 = 1000000 で割った余りなので 000000〜999999、8桁なら 10^8 です。31ビット(最大約21.4億)を100万で割るため、剰余の偏りは無視できる水準に収まります。
123 mod 10^6 = 123 のように剰余が桁数に満たない場合は、必ず左を 0 で詰めて固定長にします("000123")。可変長にすると桁数から値域が漏れたり照合がずれたりするため、出力は常に d 桁の文字列として扱うのが RFC の規定です。
TOTP:カウンタを時刻に置き換える(RFC 6238)
**TOTP(Time-based OTP, RFC 6238)**は、HOTP を一切作り替えていません。カウンタ C の決め方だけを、イベント回数から「現在時刻を一定間隔で割った整数」へ差し替えた特殊化です。
T = floor((現在のUNIX時刻 - T0) / X)
TOTP(K) = HOTP(K, T)
T0 = 起点時刻(既定 0、つまり 1970-01-01 00:00:00 UTC)
X = 時間ステップ秒(既定 30 秒)
たとえば UNIX 時刻が 1718000000 秒、X=30 なら T = floor(1718000000 / 30) = 57266666。この T をカウンタとして HOTP(K, T) を計算します。30秒の間は T が一定なので同じコードが出続け、次の30秒境界で T が +1 されコードが一斉に切り替わります。端末とサーバーは同じ K を持ち、各自が自分の時計から T を計算するだけなので、やはり通信は不要です。
時間ステップ X は安全性と使い勝手のトレードオフです。短くすると盗まれたコードの有効時間が縮みますが、入力が間に合わずユーザーが打ち損じます。長くすると入力は楽ですが攻撃者の利用窓が広がります。30秒は両者の妥協点として広く採用されています。
クロックドリフト:ずれた時計をどう許容するか
TOTP の弱点は、端末とサーバーの時計のずれ(クロックドリフト)です。端末の時計が数十秒ずれていると、双方が計算する T が食い違い、正しい秘密を持っているのに照合が通りません。RFC 6238 はこれを検証窓で吸収します。
サーバー側の検証(後方/前方に W ステップ許容):
自分の T を中心に T-W 〜 T+W を順に試す
for delta in [-W .. +W]:
if TOTP(K, T + delta) == 入力コード:
認証成功(delta を記録しておくとよい)
W=1 なら T-1, T, T+1 の3ステップ、X=30 秒環境では実質前後90秒ぶんを許容します。窓を広げるほどドリフトに寛容になりますが、同時に有効なコードが増える=攻撃者の当たり判定も広がるため、安全性が下がります。実務では W=1(前後1ステップ)が一般的な落としどころです。
ドリフトが頻発するからと窓を大きくするのは安易な解決です。窓を W に広げると有効コードが 2W+1 個になり、総当たりの成功確率がそのぶん上がります。さらに TOTP は時間内なら同じコードが何度も有効なので、一度使われたコードはそのステップが終わるまで再受理しない(リプレイ防止)処理を併用すべきです。慢性的なドリフトには、認証成功時の delta を記録してそのユーザーの基準時刻を補正する方が、窓を広げるより安全です。
HOTP のカウンタ同期:イベント版特有の問題
時刻同期の TOTP と違い、HOTP はカウンタ C のずれが起きます。端末側でコードを生成したのに送信・入力されず破棄されると、端末の C だけが進み、サーバーの C と食い違います。対策はサーバー側の**先読み窓(look-ahead window)**です。
サーバー C = 100 のとき、look-ahead s を許して照合:
C, C+1, ..., C+s まで HOTP(K, ·) を試す
一致した位置 C+k で認証成功 → サーバー C を C+k+1 に更新(リシンク)
ただし HOTP はコードが時間で失効しないのが構造的な弱点です。生成されたコードは次に使われるまで永続的に有効で、肩越しに見られたり書き留められたりすれば後から悪用されます。時間で自動失効する TOTP がモバイル認証アプリの主流になったのは、この使い勝手と安全性の両面が理由です。
| 観点 | HOTP(RFC 4226) | TOTP(RFC 6238) |
|---|---|---|
| カウンタの正体 | イベント回数(成功ごとに +1) | (時刻 - T0) / X の整数商 |
| コードの更新契機 | 認証成功などのイベント時 | 時間ステップ X ごとに自動 |
| 失効 | 使われるまで永続的に有効 | ステップ経過で自動失効 |
| ずれの吸収 | カウンタ先読み窓(look-ahead) | 前後ステップの検証窓(ドリフト許容) |
| 主な用途 | ハードウェアトークン、オフライン端末 | 認証アプリ(Google Authenticator 等) |
総当たり耐性:強さはどこから来るか
OTP の総当たり耐性は、HMAC の暗号強度ではなく、コードの桁数とサーバー側のレート制限で決まります。ここを取り違えると設計を誤ります。
6桁コードは値域が 10^6 = 100万通りしかありません。攻撃者が1回の試行で当てる確率は 1/1000000。さらに TOTP では、検証窓 W のぶん有効コードが 2W+1 個に増えるので、W=1 なら成功確率は 3/1000000 に上がります。桁数だけでは防御として薄いため、必ずサーバー側で試行回数のレート制限とアカウントロックを併用します(レート制限の設計 も参照)。たとえば「1ステップあたり数回まで、超えたら一時ロック」とすれば、100万分の数という当たり確率では実用的な総当たりが成立しません。
OTP の安全性は「桁数 vs 試行回数」の積で考えます。6桁を8桁にすれば値域は100倍になりますが、入力負担も増えます。それより1コードあたりの試行回数を厳しく制限するほうが、実用上は桁を増やすより効きます。正規ユーザーは1〜2回しか打ち間違えないので、数回でロックしても支障は小さい。総当たり耐性は値域とレート制限の合わせ技で、片方だけでは不十分です。
HOTP/TOTP の安全性は、究極的には共有秘密 K が漏れないことにすべて懸かっています。K が漏れれば攻撃者は任意の時刻のコードを自由に計算でき、OTP は完全に無力化されます。だからこそ K は十分なエントロピーを持つ乱数で生成し(CSPRNG とエントロピー を参照)、サーバー側では平文ではなく暗号化または HSM 内で保管し、登録時の QR/Base32 受け渡しも盗聴・スクリーンショットに注意します。なお TOTP/HOTP は入力する文字列が存在するため、コードごと偽サイトに打ち込ませる中継フィッシングには原理的に弱く、フィッシング耐性が必要ならパスキー(FIDO2)が上位互換です。
まとめ
HOTP は「共有秘密 K とカウンタ C を HMAC に通し、dynamic truncation で31ビットに削り、10^d の剰余で d 桁にする」だけの仕組みです。HMAC が PRF として働くので、単純なカウンタから予測不能なコードが生まれます。TOTP はそのカウンタを (時刻 - T0) / X に置き換えただけの特殊化で、時間による自動失効を得る代わりにクロックドリフトの問題を抱え、前後ステップの検証窓で吸収します。総当たり耐性は HMAC の強度ではなく桁数とレート制限の積で決まり、共有秘密 K の管理がすべての前提です。これらは 多要素認証(MFA) の「所持」要素を支える実装であり、HMAC という土台を理解すれば内部動作はほぼ自明に追えます。
セキュリティ Article
TOTP/HOTP の内部動作(時刻同期ワンタイムパスワードの数理)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
TOTP
比較で見る軸
難易度: advanced / カテゴリ: セキュリティ / タグ数: 6
導入後に効く点
TOTP はカウンタ C を「(現在時刻 - T0) / 時間ステップ X」の整数商に置き換えただけ(RFC 6238)。既定は X=30秒で、コードが時間とともに自動更新される。クロックドリフトは前後数ステップを許容する検証窓で吸収する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- セキュリティ
- タグ数
- 6
判断チェックリスト
- 自社の用途が「TOTP / HOTP」に近いか確認する。
- 強みである「HOTP は共有秘密 K と8バイトのカウンタ C を HMAC-SHA1 にかけ、その出力を dynamic truncation で31ビットに削り、10^d で割った余りを d 桁コードにする方式(RFC 4226)。同じ K と C なら端末とサーバーが通信なしで同じ値を導く。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。