TOTP 認証アプリのコードは実際どう動くのか

TOTP (RFC 6238) が共有秘密と現在時刻をどうやって 6 桁のコードに変えるか、なぜ両者が一致するか、そして何を守り何を守らないかを整理します。

認証アプリが 6 桁のコードを表示し、サーバーがそれを受け入れるとき、両者の間で ネットワーク呼び出しは起きていません。スマホとサーバーは、どちらもすでに持って いる 2 つの入力から同じ数字を独立に計算しました。登録時に固定された共有秘密と、 現在の時刻です。それが TOTP の背後にある発想のすべてです。ネットワークの要らない 第 2 の要素で、飛行機の中でも、サーバーに到達できなくても動きます。両者が同じ 時計に対して同じ決定論的な関数を走らせるからです。

TOTP は RFC 6238 です。HOTP (RFC 4226) の上の薄い層で、HOTP 自体が HMAC の上の 薄い層です。それを理解するとは、互いの上に積み重なったちょうど 3 つを理解する ことです。

HOTP: カウンタの鍵付きハッシュ

HOTP は共有秘密 K とカウンタ C からコードを作ります。カウンタは 8 バイトの 整数です。構成は次のとおりです。

  1. HMAC-SHA1(K, C) を計算します。20 バイトの MAC です。
  2. その 20 バイトを数字に崩すために 動的切り詰め を適用します。
  3. その数字を 10^digits で割った余りを取り、表示するコードを得ます。

動的切り詰めが唯一自明でない手順です。HMAC の最後のバイトの下位ニブル (0〜15 の 値) をオフセットとして読みます。そのオフセットから始めて HMAC から 4 バイトを 取り、結果を正に保つために最上位ビットをマスクし、得られた 31 ビットの整数を 10^digits で割った余りを取ります。デフォルトの 6 桁なら mod 1000000 で、 000000 から 999999 までの数字が残ります。

mac      = HMAC-SHA1(secret, counter)   # 20 バイト
offset   = mac[19] & 0x0f                # 最後のニブル、0..15
chunk    = mac[offset .. offset+3]       # そのオフセットから 4 バイト
binary   = chunk & 0x7fffffff            # 符号ビットをマスク -> 31 ビット
code     = binary % 10^digits            # デフォルト digits = 6

オフセットがハッシュ自体から導かれるので 動的 と呼びます。常に同じウィンドウを 読む代わりに、標本の 4 バイトを MAC 全体に散らし、出力が固定された切片ではなく ダイジェスト全体を反映するようにします。

TOTP: カウンタを時刻に置き換える

HOTP はカウンタを手動で増やします。ハードウェアトークンを押すたびに C が 1 上がり、サーバーが期待値を追跡します。その同期は壊れやすいです。押下を取りこぼす と両者がずれるからです。

TOTP は手動のカウンタをなくします。ユーザーが進める数字の代わりに、カウンタが すなわち 時計です。

counter = floor(currentUnixTime / period)    # period のデフォルトは 30
code    = HOTP(secret, counter)

30 秒周期でカウンタは 1 分に 2 回変わります。12:00:00 から 12:00:29 までは 1 つの 値を保ち、12:00:30 に次へ進みます。スマホとサーバーはどちらも自分の時計を読み、 30 で割り、切り捨て、その整数を上記の正確な HOTP 構成に入れます。両者の時計が ウィンドウ内で一致すれば、互いに一言も交わさずに同じ 6 桁を計算します。

それがメカニズムのすべてです。TOTP は文字どおり現在時刻の鍵付きハッシュを 6 桁に 切り詰めたものです。

登録: QR コード

共有秘密は設定時にちょうど一度、両側に届かなければなりません。標準の運び手は otpauth:// URI で、ほぼ常に QR コードとして配られます。

otpauth://totp/Example:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=Example&algorithm=SHA1&digits=6&period=30

フィールドは次のとおりです。

  • secret — 共有鍵、Base32 エンコード。手で打たれ、フォントで表示されても 耐えるアルファベットだからです。デコードされたバイトが HMAC に与える K です。
  • issueraccount — アプリが表示するラベル (「Example」、 「[email protected]」)。装飾的で、計算には入りません。
  • algorithmSHA1SHA256、または SHA512。デフォルトは SHA1。
  • digits6 または 8。デフォルトは 6。
  • period — 1 ステップあたりの秒。デフォルトは 30。

デフォルトは SHA1、6 桁、30 秒で、実務では事実上必須です。仕様は SHA-256 や SHA-512 と 8 桁を許可しますが、多くの人気の認証アプリは algorithmdigits のパラメータを黙って無視し、デフォルトを仮定します。だから 8 桁の SHA-256 の 秘密を発行するサーバーは、ユーザーの半分のアプリが決して検証されない 6 桁の SHA-1 のコードを生成するのを目にすることがあります。両端を制御できるなら、より 強いオプションを使えますし、ユーザーが自分の認証アプリを持ち込むなら、デフォルト を守ってください。

(ここでの HMAC-SHA1 は SHA-1 の衝突問題ではありません。TOTP は HMAC に依存し、 HMAC は SHA-1 を署名用に破った衝突とは別の、はるかに強い攻撃に対する SHA-1 の 耐性に依存します。HMAC-SHA1 は今でも安全と見なされています。それは Webhook 署名 を認証するのに使われる同じ 鍵付きハッシュのプリミティブで、TOTP はメッセージが現在時刻であるその構成です。)

時計のずれと検証ウィンドウ

時計はずれます。時刻が 40 秒進んだスマホは、サーバーがまだ現在のステップにいる 間に 次の ステップのコードを計算します。サーバーが自分の現在のステップのコード しか受け入れなければ、そのユーザーたちは毎回ログインに失敗します。

そこでサーバーはウィンドウを受け入れます。よくある選択は ±1 ステップです。 サーバーは現在のカウンタ、前のもの、次のものを調べ、その 3 つのどれに一致しても 受け入れます。30 秒周期で、これは約 ±30 秒のずれを許容しつつ、どの瞬間でも有効な コードの数を 3 つに保ちます。

ウィンドウを広げるのは直接のトレードオフです。

  • 広く — ずれによる正当な失敗は減りますが、より多くのコードが同時に有効に なり、盲目的に推測する攻撃者はより大きな的を持ち、中継されたコードがより長く 使えます。
  • 狭く — より厳しいセキュリティですが、時計がいい加減なユーザーが締め出され ます。

±1 が普通の落としどころです。慢性的なずれを見るサーバーは、全員のためにウィンドウ を広げるより、各ユーザーの測定したオフセットを記録してウィンドウをずらすことも あります。

TOTP が守るもの、守らないもの

TOTP は 1 つの特定のものを守ります。パスワードを盗んだ攻撃者が、コードなしでは 依然としてログインできず、コードは 30 秒ごとに変わります。クレデンシャル スタッフィングやデータベースの漏洩に対して、それは実質的で大きな改善です。 強い第 1 要素のパスワード の後ろの 自然な第 2 要素です。

限界も同じく特定です。

  • フィッシングされえます。 コードはユーザーが読んで打つ短い文字列です。 リアルタイムのフィッシングサイトはそれを尋ね、同じウィンドウ内で本物の サーバーへ中継できます。TOTP の何も、コードを要求するサイトに結び付けません。 資格情報がオリジンに暗号学的に限定され、ほかの場所で再生できない WebAuthn/パスキーとは違います。フィッシング耐性が必要なら、TOTP はそれでは ありません。
  • ウィンドウ内のリプレイ。 コードはステップ全体 (と受け入れられた隣) で 有効です。サーバーは成功した瞬間にコードを使用済みと印を付け、そのウィンドウの 残りの間、同じコードを拒否すべきです。さもなければ上記の中継が攻撃者に数十秒を 買います。
  • 保存時の秘密のリスク。 サーバーはスマホが持つのと同じ秘密を保存します。 TOTP の秘密を露出するデータベースの侵害は、攻撃者がすべての被害者のコードを 生成できるようにします。これらの秘密は保存時の暗号化と、パスワードのハッシュと 同じ取り扱いの規律が必要です。動くコードに戻せるので、むしろそれ以上です。

その注意点があっても、TOTP は SMS のワンタイムコードを決定的に上回ります。SMS は SIM スワップや SS7 攻撃で傍受されえ、キャリアの配信に依存し、ログインのたびに 秘密をネットワークに送ります。TOTP は秘密を一度交換し、二度と送信しません。 パスキーが選択肢にないとき、アプリベースの TOTP が正しいデフォルトです。

一行のまとめ

TOTP は時計の HMAC です。秘密を一度共有し、時刻に合意し、鍵付きハッシュを 6 桁に 切り詰め、ずれのために小さなウィンドウを受け入れてください。残りのすべて、つまり QR コード、Base32、検証ウィンドウは、その中核を取り囲む配管です。メカニズムを 直接見たければ、私たちの TOTP 生成器 に Base32 の秘密を 貼り付け、コードが 30 秒の境界ごとに切り替わるのを眺めてください。上記の正確な 構成をブラウザ内で走らせ、サーバーには何も送りません。