NTP による時刻同期 - ネットワーク越しにミリ秒精度を実現する仕組み
NTP (Network Time Protocol) の階層構造、往復遅延の補正アルゴリズム、セキュリティ上の課題から、chrony や systemd-timesyncd など現代の実装選択肢まで実務的に解説します。
TOTP (Time-based One-Time Password) は、サーバーとユーザーの端末が共有する秘密鍵と現在時刻を組み合わせて、30 秒ごとに変化する 6 桁の数字を生成する認証方式です。Google Authenticator、Authy、Microsoft Authenticator、1Password など、現在主流の二要素認証アプリはほぼすべてこの方式を採用しています。RFC 6238 として 2011 年に標準化され、ベンダーロックインのない相互運用可能な仕様になっています。
TOTP の最大の利点は、ネットワーク通信を必要としないことです。サーバーとアプリの間で秘密鍵を最初に共有しておけば、その後はそれぞれが時刻を知っているだけで同じコードを生成できます。SMS 認証のように電波状況に左右されず、1 通ごとの送信料も発生しないため、大規模に運用しても SMS のワンタイムパスワードより費用がかさみません。SMS の SIM スワッピング攻撃のリスクも回避できます。
TOTP を理解するには、まず先行仕様の HOTP (HMAC-based One-Time Password、RFC 4226) を押さえる必要があります。HOTP はカウンタ値と秘密鍵を HMAC-SHA-1 でハッシュ化し、結果から 6 桁の数字を導出するアルゴリズムです。サーバーと端末がカウンタを同期させ、認証成功のたびにカウンタを 1 ずつ増やしていきます。コード自体は時刻に依存しないため、時計の精度が悪い環境でも動作する利点があります。
HOTP の弱点は、カウンタ同期が崩れた場合の復旧の難しさです。ユーザーがアプリで複数回コードを生成したのに認証に使わなかった場合、サーバーと端末でカウンタがずれてしまいます。サーバー側で「次の数個分のコード」を先読みして照合する必要があり、運用が複雑です。TOTP はこの問題を「カウンタの代わりに時刻を使う」というシンプルなアイデアで解決しました。
| 観点 | HOTP | TOTP |
|---|---|---|
| 標準仕様 | RFC 4226 | RFC 6238 (2011 年に標準化) |
| カウンタの正体 | コードを数える値。端末は生成するたび、サーバーは認証に成功するたびに 1 増やす | Unix 時刻を 30 で割った商。30 秒ごとに 1 増える |
| 時計の必要性 | 不要 - 時計を持たないハードウェアトークンでも動く | 必要 - 端末とサーバーの双方が正しい時刻を知っている前提 |
| ずれる原因 | 使わずにコードを生成すると端末側のカウンタだけが進む | 端末またはサーバーの時刻ずれ |
| ずれたときの対処 | サーバーが次の数個分を先読みして照合する | 時刻を正しく同期し直す |
共有鍵を HMAC-SHA-1 にかけて 6 桁を取り出す部分は両者で共通です。違いは HMAC に渡すカウンタを「回数」から作るか「時刻」から作るかだけで、TOTP は HOTP の計算をそのまま流用しています。
TOTP の核となる計算は、現在の Unix 時刻を 30 で割った商をカウンタとして HOTP を実行することです。Unix 時刻 1716000000 (2024-05-18 02:40:00 UTC) であれば、カウンタは 57200000 となります。この値を秘密鍵と組み合わせて HMAC-SHA-1 を計算し、結果から 6 桁の数字を抽出します。30 秒経過してカウンタが 57200001 になれば、まったく別の 6 桁が生成されます。
30 秒というステップ幅は RFC 6238 のデフォルト値で、セキュリティと使いやすさのバランス点として選ばれました。短すぎるとユーザーが入力する間にコードが変わってしまい、長すぎると総当たり攻撃の窓が広がります。多くのサービスはサーバー側でも前後 1 ステップ (合計 90 秒) を許容範囲として照合するのが一般的です。一部の企業向けハードウェアトークンでは 60 秒のステップ幅が使われるなど、実装による違いもあります。
共有鍵を一度だけ交換する - 二要素認証を有効化するときの QR コード (otpauth URI) に Base32 の共有鍵が入っています。以後この鍵はネットワークに流れません。
Unix 時刻を 30 で割る - 商がカウンタです。Unix 時刻 1716000000 (2024-05-18 02:40:00 UTC) ならカウンタは 57200000 になります。
カウンタと共有鍵を HMAC-SHA-1 にかける - 出力は 20 バイトのハッシュ値です。ここは HOTP と同じ計算です。
ハッシュから 4 バイトを切り出す - 最終バイトの下位 4 ビットが切り出し位置を指し、取り出した 4 バイトの先頭ビットを落として 31 ビットの正の整数にします。
1000000 で割った余りを 6 桁で表示する - 足りない桁は 0 で埋めます。30 秒後はカウンタが 57200001 になり、まったく別の 6 桁が現れます。
サーバーは受け取った 6 桁と、自分で同じ手順を踏んで求めた 6 桁を比べます。多くの実装は前後 1 ステップ (合計 90 秒) までを一致とみなし、これが端末とサーバーのわずかな時刻ずれを吸収しています。桁数は otpauth URI の digits パラメータで 8 桁を指定できる仕様もありますが、対応するクライアントは限られます。
二要素認証を有効化するときに表示される QR コードは、otpauth:// で始まる URI 文字列をエンコードしたものです。例えば otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example のような形式で、Base32 エンコードされた共有鍵 (secret) と発行元 (issuer)、ユーザー識別子が含まれます。Google Authenticator や類似アプリはこの URI をパースして秘密鍵を保存します。
QR コードを写真撮影されると秘密鍵が漏洩するため、設定画面の表示は一度限り、画面共有中は避けるべきセキュリティ事項です。多くのサービスは「バックアップコード」を併せて発行し、端末紛失時の復旧手段を用意しています。バックアップコードは TOTP とは独立した使い捨てパスワードで、一度使用したら無効化される設計が一般的です。両方を別の安全な場所に保管するのが、ユーザーのリスク管理として推奨される運用です。
TOTP の認証失敗で最も多い原因は、サーバーと端末の時刻ずれです。サーバー側は通常 NTP で UTC に同期されていますが、ユーザーのスマートフォンが手動設定で時刻をいじっていたり、海外旅行で時刻設定が混乱したりすると、コードが合わなくなります。Google Authenticator にはかつてアプリ内設定に「時刻補正」機能がありましたが、バージョン 7.0 以降は提供されておらず、現在は端末 OS の時刻設定をそのまま使います。端末側の対策は OS の自動時刻同期を有効にしておくことです。
サーバー側の時刻ずれも問題で、コンテナの時刻が NTP 同期されていない構成では数十秒のずれが生じることがあります。Kubernetes の Pod は通常ホストの時刻を参照しますが、仮想化のレイヤーで誤差が累積するケースもあります。TOTP を実装するサーバーでは、定期的な NTP 同期と時刻同期失敗時のアラート設定が運用上の必須事項です。サーバー側の時刻ずれは特定の利用者に限らず、全ユーザーが同時にログインできなくなる形で顕在化しうる点が厄介です。
TOTP の標準では HMAC-SHA-1 が使われますが、これは「OTP の用途には十分」とされている一方で、新規実装では SHA-256 や SHA-512 を使う選択肢もあります。otpauth URI には algorithm パラメータでハッシュアルゴリズムを指定でき、対応するクライアントは適切に処理します。ただしクライアント側の互換性に注意が必要です。Google Authenticator は algorithm パラメータ自体を無視して SHA-1 として扱う実装のため、互換性を優先して SHA-1 のままにしているサービスが多数派です。
サーバー側の実装では、ユーザーが過去に使ったコードを再利用させない「リプレイ攻撃対策」も重要です。30 秒間に同じコードが 2 回成功してしまうと、攻撃者が傍受したコードを使って認証を通せる可能性があります。最後に成功したカウンタ値をユーザーごとに保存し、それより新しいカウンタ値しか受け付けないロジックを入れるのが定石です。コードの長さは 6 桁が標準で、digits パラメータで 8 桁を指定できる仕様もありますが、対応するクライアントが限られるため採用例は少数です。
この記事は役に立ちましたか?