セキュリティ

OTPとは?HOTP/TOTPの生成アルゴリズムと実装・SMS OTPの限界を実装視点で解説

OTPとは?HOTP/TOTPの生成アルゴリズムと実装・SMS OTPの限界を実装視点で解説

OTP(ワンタイムパスワード)は、一度きりで使い捨てられる認証コードで、固定パスワードに追加する second factor として広く実装されています。多くはサーバと利用者が事前に共有した秘密鍵に、時刻またはカウンターという同期要素を混ぜ、その場限りの数字を計算する仕組みです。この記事では、HOTP(RFC 4226)とTOTP(RFC 6238)の生成アルゴリズム、otpauth:// URIによるシークレットの受け渡し、pyotp 2.10系とotplib 13系で書く発行・検証コード、クロックずれを吸収する許容窓の決め方、そしてSMS OTPが抱える弱さと採用判断までを、実装者の視点で整理します。数値は原典のRFCと米国NISTのガイドラインに当たって確認しました。

まとめ:OTPの生成方式とTOTP/HOTPの選択・実装で外せない要点

OTPは、サーバと端末が共有する秘密鍵(シークレット)に、TOTPなら現在時刻を、HOTPならカウンター値を混ぜてHMACで計算し、その結果を6桁前後の数字に切り詰めたものです。鍵と同期要素が両側で一致するからこそ、通信路にコードを流さず同じ値を突き合わせられます。生成の実体は標準化されており、TOTPはRFC 6238、その土台のHOTPはRFC 4226で定義されています。

実装の勘所は4点です。シークレットの受け渡しはotpauth:// URIのQRコード化が事実上の標準で、桁数・周期・issuerごと認証アプリと相互運用できること。検証では時刻ずれを見越した許容窓を持たせつつ、同じコードの再利用を必ず弾くこと。その許容窓の単位はライブラリで異なり、pyotpは「ステップ数」、otplib 13系は「秒」を受け取るため移植時に取り違えやすいこと。そしてSMSで送るOTPはSIMスワップやリアルタイムフィッシングに抜かれます。米国のNIST SP 800-63B(第4版)も「OTP authentication is not phishing-resistant」と明記しており、OTPは所持要素の一手段であって単独で万能ではない前提で設計します。

OTPの基礎:使い捨てコードを支える共有シークレットと同期の仕組み

なぜ毎回違うコードが両側で一致するのかを先に押さえておくと、後の検証ロジックで迷いません。鍵は「共有シークレット」と「同期要素」の2つです。

ワンタイムパスワードが固定パスワードと異なる使い捨ての性質と役割

固定パスワードは一度漏れると変更するまで使われ続けますが、OTPは1回の認証か短い時間だけ有効で、使った瞬間に価値を失います。コードを覗かれても次の認証には別の値が要るため、再利用が効きません。だからOTPは固定パスワードの置き換えではなく、パスワード+OTPの二要素として重ねます。企業として二段階認証をどう位置づけるかは二段階認証とは?二要素認証・多要素認証との違いと企業の導入判断を解説で扱っており、本記事はその生成・検証の内部に踏み込みます。

共有シークレットと同期要素(時刻・カウンター)で成り立つ生成の前提

OTPの計算には、登録時に両側へ配った秘密鍵(Base32でエンコードされた共有シークレット)と、時刻またはカウンターという「毎回変わる値」を使います。サーバと端末が同じ鍵と同じ同期要素を持てば、通信路にコードそのものを流さなくても、両者が独立に同じ数字を導けます。OTPはネットワーク越しに正解を送り合う方式ではありません。

鍵の長さには下限があります。NIST SP 800-63B 第4版の単一要素OTPの節は、秘密鍵とそのアルゴリズムが SP 800-131A の最小セキュリティ強度(同文書の記載時点で112ビット)を満たすことを求めています。RFC 4226 も共有鍵を最低128ビット、推奨160ビットとしており、Base32で20バイト(32文字)を配るのが落としどころです。鍵が漏れれば攻撃者も同じコードを作れます。

SMS・認証アプリ・ハードウェアトークンという配布経路の違い

同じOTPでも、コードをどう届けるかで安全性と実装難度が変わります。選ぶ経路は主に次の3つです。

配布経路 生成主体 特徴
SMS・音声 サーバ側 導入が容易だが傍受・転送に弱い
認証アプリ 端末側(TOTP) オフライン生成・通信不要
ハードウェアトークン 専用端末 物理隔離で堅牢・配布コスト高

SMSや音声通話はサーバがコードを生成して送る方式で、番号さえあれば始められます。送信をSaaSに寄せるならTwilio Verifyとは|SMS・音声認証の実装手順と日本向け料金に料金と実装手順をまとめました。一方、Google Authenticatorのような認証アプリは端末内でTOTPを計算するため、コードが通信路を流れません。

第4版で線引きが変わった箇所があります。旧版では帯域外認証の一経路だったメールが、「Email SHALL NOT be used for out-of-band authentication」と明確に禁じられました。パスワードだけでアクセスでき、転送経路で傍受されうるためです。メール宛にコードを送る実装が残っているなら、置き換える対象になります。上位の枠組みは多要素認証(MFA)とは?3つの認証要素と実装方式・耐フィッシングMFAを実装視点で解説で整理しています。

HOTPとTOTPの生成アルゴリズム:RFC 4226とRFC 6238の違い

OTPの中核は生成アルゴリズムです。認証アプリが実装するのは、カウンター基準のHOTPと、それを時刻基準に置き換えたTOTPの2系統に整理できます。まず土台のHOTPから見ます。

HOTP(RFC 4226)のカウンター基準とHMAC・動的切り詰めの生成手順

HOTPは2005年12月のRFC 4226で標準化された、カウンター値を同期要素に使う方式です。共有シークレットと8バイトのカウンターをHMAC-SHA-1に通し、得られた20バイトのハッシュから4バイトを取り出して数字に変換します。処理の流れは次のとおりです。

  1. 共有シークレットとカウンターをHMAC-SHA-1で計算する
  2. ハッシュ末尾4ビットが指す位置から4バイトを取り出す(動的切り詰め)
  3. 最上位ビットを落として31ビットにし、10のべき乗で剰余して6桁などに整える
  4. 認証成功のたびに両側のカウンターを1つ進める

最上位ビットを落とすのは、符号付き整数として解釈する処理系との差異を避けるためだとRFC 4226の Section 5.3 が明示しています。カウンター方式の弱点は同期ずれです。生成したコードがサーバへ届かなければ、カウンターが片側だけ進みます。サーバが数個先まで試す「先読み窓(look-ahead window)」で追従させます。

TOTP(RFC 6238)の時刻基準とタイムステップ30秒・SHA1既定の仕組み

TOTPは2011年5月のRFC 6238で定義され、HOTPのカウンターを「現在のUNIX時刻から起点時刻T0を引き、タイムステップXで割った商」に置き換えた方式です。RFCは既定値としてT0=0、X=30秒を挙げており、30秒ごとに値が1つ進むためコードが自動で切り替わります。ボタンを押さなくても更新される点が、認証アプリで広く採用された理由です。

時刻を同期要素にしたことで、カウンターの取りこぼしは起きません。代わりに時計がずれると値が合わなくなるため、検証側は現在ステップの前後を少し許容します。桁数とアルゴリズムはSHA-1・6桁のまま運用されることが多いものの、RFC 6238 上はHMAC-SHA-256やHMAC-SHA-512も認められています。ただし認証アプリ側の対応にばらつきがあり、相互運用を優先するなら既定値から動かさない判断が無難でしょう。

HOTPとTOTPの同期方式・ずれ耐性・用途で分かれる比較観点

2方式は同じHMACベースでも、同期のとり方が異なるため向く用途が分かれます。

観点 HOTP TOTP
同期要素 カウンター 時刻(30秒)
原典 RFC 4226(2005年) RFC 6238(2011年)
有効期間 使うまで有効 タイムステップで失効
ずれ対策 先読み窓 時刻の許容窓
主な用途 ハードトークン 認証アプリ

選択の分岐は明快です。時計を持たない安価なハードウェアトークンや、生成回数を絞りたい局面ではHOTPが向きます。スマートフォンの認証アプリで数十秒ごとにコードを切り替える一般的な二要素認証なら、失効まで自動化できるTOTPが素直でしょう。新規に組むならTOTP、特殊要件でHOTPという順序です。

TOTPを実装する手順:シークレット生成からotpauth URI発行まで

登録フェーズでやることは4工程です。シークレットを作り、otpauth:// URIに詰め、QRとして利用者に渡し、暗号化して保管する。

pyotp 2.10系でBase32シークレットを生成しQRを配る作業手順

PythonならpyotpのTOTPクラスで完結します。2026年9月時点の最新は2.10.0(2026年6月14日公開)で、Python 3.8以降が対象です。

pip install pyotp qrcode[pil]

import pyotp, qrcode

# 1. Base32で32文字(20バイト=160ビット)の共有シークレットを生成
secret = pyotp.random_base32()

# 2. otpauth URI を組み立てる(issuer とラベルは必ず入れる)
uri = pyotp.TOTP(secret).provisioning_uri(
    name="[email protected]",
    issuer_name="Issoh",
)

# 3. QR画像にして登録画面へ渡す
qrcode.make(uri).save("enroll.png")

print(uri)
# otpauth://totp/Issoh:[email protected]?secret=...&issuer=Issoh

random_base32 は既定で32文字を返し、Base32復号後は20バイトになります。RFC 4226の推奨鍵長と一致するため、桁数を絞る理由がなければ手を入れる必要はありません。引数と戻り値の定義はpyotpの公式APIドキュメントに一覧があります。

otpauth URIのパラメータ設計とissuer重複で起きる登録事故

認証アプリへシークレットを登録するとき、事実上の標準がotpauth:// で始まるKey URI Formatです。仕様はGoogle Authenticatorのリポジトリで公開されており、Key Uri Format のドキュメントが原典にあたります。

典型的な形は otpauth://totp/Issoh:[email protected]?secret=BASE32SECRET&issuer=Issoh&digits=6&period=30&algorithm=SHA1 です。方式(totp)とラベル(サービス名:アカウント)に続けて、secret(Base32の共有シークレット)、issuer、digits(桁数)、period(タイムステップ)、algorithmを並べます。

事故が起きやすいのはラベルとissuerの扱いです。Key Uri Format はラベル側のプレフィックスとクエリの issuer を両方指定するよう推奨しており、片方だけだとアプリ上でどのアカウントか判別できません。ステージングと本番で同じissuer文字列を使えば、端末に見分けのつかない項目が2つ並びます。環境名を含めておく運用が無難です。

共有シークレットの暗号化保管と鍵長16バイト下限が引く実装の境界

生成したシークレットは平文で置かず、KMSやアプリ側の鍵で暗号化して保管します。復号できるのは検証処理だけに絞り、管理画面からは参照できないようにします。登録用QRの再表示や再発行には、本人確認の再認証を挟んでください。

鍵長には実装レベルの下限もあります。otplib 13系はBase32復号後16バイト(128ビット)未満のシークレットを SecretTooShortError で弾く仕様になりました。解説記事で定番の JBSWY3DPEHPK3PXP は復号すると10バイトしかなく、そのまま貼るとエラーになります。チュートリアルのコードが動かないときは、まずここを疑ってください。

TOTPコードを検証する手順:ずれ許容の設定と同一コードの遮断

検証フェーズで判断が要るのは2点だけです。時刻ずれをどこまで許すか、一度使われたコードをどう弾くか。ここは明示的に設計します。

pyotpとotplib 13系で異なるずれ許容パラメータの指定方法

pyotpの検証は TOTP.verify(otp, for_time=None, valid_window=0) で、valid_window は「前後いくつのカウンターティックまで受け入れるか」を表します。既定は0、つまり現在ステップのみ。1を指定すると前後1ステップ(合計90秒幅)まで通ります。

一方、Node.js側のotplibは13.5.0(2026年8月21日公開)が最新で、v13で全面的に書き直されました。単独の authenticator パッケージは廃止され、検証は非同期で VerifyResult を返すため result.valid を見る形に変わっています。ずれ許容の単位もステップ数ではなく秒の epochTolerance になりました。period が30なら epochTolerance: 30 で前後1周期ぶん、5 なら小さなドリフトだけを吸収します。HOTP側は counterTolerance で、こちらはカウンター個数です。

npm install otplib

import { verify } from "otplib";

// TOTP: epochTolerance は「秒」。period 30 なら 30 で前後1周期ぶん
const result = await verify({
  secret,
  token,
  epochTolerance: 30,
});

if (result.valid) {
  // 認証成功。ここで last_step を記録して再利用を遮断する
}

// 過去のみ許容する非対称指定(未来のコードを拒否する)
const strict = await verify({
  secret,
  token,
  epochTolerance: [5, 0],
});

公式のAdvanced Usage ガイドは、標準的な2要素認証で epochTolerance 30 / counterTolerance 5、高セキュリティ用途で 5 または [5, 0] / 0 という目安を示しています。v12の数値をそのまま持ち込むと、意図の10倍以上の窓が開きます。移植時は単位を必ず確認してください。

同一コードの再提出を弾くlast_step記録とロックアウトの設計

許容窓を開けた分だけ、同じコードが有効な時間は延びます。NIST SP 800-63B 第4版は、検証者が同一のOTPを二度受け付けないこと、および連続失敗回数を制限するレート制限機構を備えることを要求しています。認証が通ったタイムステップを保存し、次回以降それ以下のステップは通しません。

CREATE TABLE totp_state (
  user_id      BIGINT PRIMARY KEY,
  secret_enc   BYTEA       NOT NULL,
  last_step    BIGINT,
  fail_count   INT         NOT NULL DEFAULT 0,
  locked_until TIMESTAMPTZ
);

# 検証側(pyotp)
step = totp.timecode(datetime.now(timezone.utc))
if row.last_step is not None and step <= row.last_step:
    raise ReplayDetected
if not totp.verify(code, valid_window=1):
    increment_fail_and_maybe_lock(row)
    raise InvalidCode
save_last_step(row, step)

6桁は100万通りしかありません。30秒の窓でも自動化された総当たりには耐えられないため、失敗回数の上限とロックアウトは検証ロジックとセットで実装します。正規利用者が締め出される副作用には、失敗ごとに待ち時間を延ばす方式を SP 800-63B 第4版が代替として挙げています。

時刻ずれの問い合わせが出たときにNTPと許容窓で切り分ける手順

「コードが合わない」という問い合わせは、端末側とサーバ側の時計ずれが混ざって届きます。切り分けは次の順で進めます。

  • サーバのNTP同期を確認する(ここがずれると全利用者で失敗が出る)
  • 特定利用者だけなら端末側を疑い、認証アプリの時刻補正機能を案内する
  • 再現するなら許容窓を一時的に広げ、どのステップが一致したかログに残す
  • 広げた窓は恒久化せず、原因の解消後に戻す

SP 800-63B 第4版は、時刻ベースOTPの有効期間を「認証器の想定クロックドリフト+ネットワーク遅延+利用者の入力時間」から定めるべきだとしています。窓の広さを勘で決めず、この3要素で説明できる値にしておけば、監査でも障害の振り返りでも根拠を示せます。

OTPのセキュリティ限界と採用・見送りを分ける判断基準の整理

OTPは固定パスワード単独より堅くなりますが、万能ではありません。どの経路が何に弱いかを知らないと、守ったつもりで抜けが残ります。ここは判断を言い切ります。

SMS OTPのSIMスワップとNIST SP 800-63Bが制限扱いとする背景

もっとも導入が容易なSMS OTPは、もっとも弱い経路でもあります。攻撃者が携帯キャリアを欺いて番号を自分のSIMへ移すSIMスワップに遭えば、届くコードごと奪われかねません。SS7など通信網の脆弱性を突いた傍受や、端末のSMS転送設定の悪用も報告されています。

NIST SP 800-63B 第4版は、公衆交換電話網(PSTN)を使った帯域外認証を独立した節に切り出し、「Use of the PSTN for out-of-band verification is restricted」と制限扱いを明記しました。制限付き認証手段には、リスク評価・利用者への告知・代替手段の提供という追加義務が課されます。帯域外認証そのものにも10分以内という完了期限が置かれました。SMS OTPは導入初期の妥協策と割り切り、守りたい資産が大きいなら端末生成のTOTPやパスキーへ移行する計画をセットで持つべきです。

リアルタイムフィッシングにOTPが抜かれる限界と耐フィッシング認証の位置づけ

TOTPやハードトークンでも越えられない壁が、リアルタイムのフィッシングです。偽サイトが利用者からIDとOTPをその場で受け取り、正規サイトへ即座に中継すれば、有効時間内に本物の認証を通されます。OTPは「正しい相手と通信しているか」を検証しないため、この中間者攻撃を原理的に防げません。SP 800-63B 第4版が「OTP authentication is not phishing-resistant」と書き切っているのは、この構造上の限界を指しています。

ここを埋めるのが、認証先のドメインに鍵を結び付けるFIDO/WebAuthn系の耐フィッシング認証です。仕組みの詳細はFIDO認証・FIDO2とは?仕組みとパスキー・導入判断を解説で、ブラウザAPIを叩く側の実装はWebAuthnとは?パスキー認証を実装する仕組みと登録・認証フロー・RP実装の要点で解説しており、公開鍵暗号でオリジンを検証するためフィッシング中継が成立しません。フィッシング耐性を要件に含めるなら、OTPを最終形にせずパスキーへの移行を前提に設計します。

OTPを採用すべき場面と見送るべき場面の条件付きでの切り分け

採用判断は条件で切り分けられます。既存のログインに低コストで二要素を足したい、利用者の端末にアプリを入れてもらえる、守る対象が過度に高リスクではない——この条件ならTOTPベースのOTPが素直な選択です。オフライン生成で通信も要らず、運用負荷も軽く済みます。

逆に、金融取引や管理者権限のように奪取の被害が甚大な領域では、OTPを主軸に据えるのは見送ります。耐フィッシングのパスキーを一次手段に置き、OTPは端末を失ったときのバックアップに降格させる構成が妥当です。SMS OTPだけで高リスク領域を守る構成は避けてください。移行先を比べる段階ならパスワードレス認証とは?方式4分類の実装難度と移行判断を実装者向けに解説が実装難度の順で並べています。製品選定など企業目線の判断はワンタイムパスワードとは?仕組みと種類・企業が選ぶ導入方式を解説に整理しました。OTPやMFAを組み込んだ認証基盤は、認証基盤・ID管理システム開発としてシークレット保管や失効設計ごと受託しています。要件整理の段階からご相談ください。

よくある質問

OTPの実装検討でよく挙がる疑問を、開発者の視点でまとめます。

OTPとTOTPとHOTPの違いは何ですか?

OTPは使い捨てパスワードの総称で、TOTPとHOTPはその生成アルゴリズムの名前です。HOTP(RFC 4226)はカウンターを同期要素に使い、認証のたびに1つ進めます。TOTP(RFC 6238)はそのカウンターを現在時刻ベースに置き換えた方式で、RFCの既定値では30秒ごとにコードが切り替わります。認証アプリの多くが実装するのはTOTPです。

SMSで届くOTPは安全ですか?

他の経路に比べて弱い部類です。SIMスワップで番号を乗っ取られる、通信網の傍受やSMS転送の悪用でコードを抜かれる、という経路があります。NIST SP 800-63B 第4版もPSTN経由の帯域外認証を「restricted」と位置づけ、リスク評価と代替手段の提供を求めました。同じ版でメール宛の送信は禁止されています。守る資産が大きい場合はTOTPやパスキーへ寄せる前提で設計してください。

TOTPの時刻ずれで認証が通らないときはどうしますか?

まずサーバのNTP同期を確認し、次に検証側の許容窓を見ます。pyotpは valid_window にステップ数、otplib 13系は epochTolerance に秒を渡す仕様で、単位が違う点に注意してください。既定値が0なら前後1ステップ相当まで広げるとずれた端末を救えます。ただし窓を広げるほど有効時間が延びるため、恒久化はしません。

OTPはフィッシングを防げますか?

完全には防げません。偽サイトがOTPをその場で受け取り正規サイトへ中継するリアルタイムフィッシングには、有効時間内に本物の認証を通されます。NIST SP 800-63B 第4版もOTP認証はフィッシング耐性を持たないと明記しました。要件に含めるなら、認証先ドメインに鍵を結び付けるFIDO/WebAuthn系のパスキーを併用または主軸にしてください。

OTPの実装は自作すべきですか、ライブラリを使うべきですか?

ライブラリを使う判断が無難です。生成・検証はRFC 4226/6238で標準化されており、Pythonのpyotp(2.10.0)、Node.jsのotplib(13.5.0)など実績あるものが揃っています。自作すると動的切り詰めや許容窓の実装ミスが入りやすく、そこが脆弱性になりかねません。周辺のリプレイ遮断・レート制限・シークレット保管に開発リソースを回すほうが効きます。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.09 テックブログ IDCFクラウド(IDCフロンティア)不正アクセス・ランサムウェア:影響先・復旧・データは戻るか
  2. 2026.10.08 テックブログ 大阪公立大学のランサムウェア被害と仮想化基盤の停止|全授業休講に至った経緯とバックアップを守る設定
  3. 2024.11.08 テックブログ OpenAPI GeneratorでJavaコードを自動生成する方法|CLI導入からSpring・ライブラリ選択まで
  4. 2026.10.09 テックブログ 京王電鉄のランサムウェア被害とグループ共通基盤:決済・ポイント・予約が止まった範囲と遮断の初動
  5. 2026.10.09 テックブログ ニッスイのサイバー攻撃で日水物流の入出荷停止|委託先クラウド障害に荷主が備える手順

RELATED POSTS 関連記事

目次