September 10, 2021

One-Time Password(OTP)にまつわる仕様

最近ふと気になって読んだOTP(One-Time Password)の仕様についてまとめようと思います。

OTPにまつわる仕様

OTPそのもの仕様は1998年にRFC2289で定義されています。しかし、実際に使われているのはこれから紹介するHOTP(HMAC-Based One Time Password Algorithm)とTOTP(Time-Based One-Time Password Algorithm)です。 HOTPはRFC4426で定義されていて、それを拡張したのが私達が普段使っているRFC6238で定義されているTOTPです。

そもそもHMACとは

HMAC(Hash Based Message Authentication Code)はいろんなところで使われているので馴染み深い技術の一つですが簡単におさらいをします。

HMACとは、1997年にRFC2104で定義された、二者間でメッセージの完全性を担保するために使われる共通暗号方式を使った技術です。処理の流れとしては、送信者はメッセージ本文と暗号鍵からMAC値を算出して本文と贈ります。そして、受信者は受け取ったメッセージとあらかじめ送信者から提供された暗号鍵を元にMAC値を照らし合わせて改ざんされていないことをチェックします。このようにして完全性を担保する仕組みです。

HOTPとは

HOTPとは、2005年にOATH(Open AuTHentication)によってHMACを使用したOTPの生成方法を定義したもので、仕様内で”Air Gap Solution”と呼ばれているようにインターネットにアクセスが必要なくても、どんなソフトウェア・ハードウェアであってもパスワードを生成できる仕組みです。

簡単に紹介すると以下のような数式でHOTPの値は決まります。

HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))

ここでKは認証しようとしているバリデーター(HOPT validator)とジェネレータ(HOPT generator)で共通な鍵(シークレット)で、C8バイトのカウンターです。またTruncate関数はSection 5.3で定義されている関数で、非常に簡単な処理です。またここではハッシュ関数としてSHA1を使用しましたが、バリデーターとジェネレーターで共通のハッシュ関数を使えれば違うハッシュ関数を使っても問題はありません。

なぜHMACを使う必要があるのか

“Air Gap”がある環境、すなわちインターネット環境がない状況でOTPを生成して、それをバリデーターで「確かにジェネレーターで生成された」ことの検証にはHMACの仕組みが使われています。

そもそもOTPとは完全性が担保できてやっと実現できる仕組みなので、HMACがいい役割を果たしています。

なぜRSAは使えないのか

共通鍵暗号方式であるHMACではなく、公開鍵暗号方式を使うことはできないのでしょうか。答えは「できない」です。 というのもアルゴリズムにその理由があります。

HMACはRFC2104Section 5. Truncated outputで規定されているように、出力をTruncateすることができます。TruncateができるからこそMAC値をそのまま入力したりするのではなく、6桁の数字で表現をすることができるのです。これによってヒューマンフレンドリーな出力となり、わたしたちがストレス感じることなくOTPを使ったりすることができます。RSAの方式ではTruncateをすることができない、すなわち文字列が欠損するとバリデーターで検証をすることができなくなるためRSAをOTPを仕組みに容易に使うことができません。

TOTPとは

TOTPとは、HOTPを時間を使って拡張をした仕組みのことで、先程のカウンターCをそのときの時間Tで置き換えたものです。

TOTP(K,T) = Truncate(HMAC-SHA-1(K,T))

TOTPはUNIX時間をHOTPのカウンターに使用したもの、ということができます。

入力Tの詳細

Google AuthenticatorなどのOTP生成機では30秒ごとにOTPが生成されると思います。そのため、Tを「現在の時刻」とすると毎秒OTPが変わってしまいよくなさそうです。一体、どのようなTを入力したらいいのでしょうか。

実際に任意の定数Tを選択して、床関数をかけることによってTを設定します。またT0というOTP設定時のカウンターをサーバーとクライアントで共有することになります(T0はデフォルトでは0です)。

T = |(現時刻 - T0) / X|

Goで実装するななら以下のような感じです。

// counter
c := uint64(math.Floor(float64(time.Now().Unix()) / float64(X))))

この値をカウンターCとして使えば良いです。

TOTPの種類

TOPTにはいろんな種類があります。以下のような違いがあります。

  • ジェネレータとバリデーターの時間動機方法
  • アルゴリズムの違い

Google Authenticatorなどのジェネレータの仕様

Google AuthenticatorもTOTPのジェネレータにあたります。これらのジェネレータで違いはあるのでしょうか。

Google AuthenticatorのWikiにも乗っているのですが、以下のような仕様になっているようです。

サービスごとにちょっとずつ仕様(ハッシュ関数や有効時間など)の違いがあるので自前でジェネレーターも合わせる必要があります。

QRコードとの関係

QRコードを読み取ると以下のようなURIが取得できます。

otpauth://totp/GitHub:KeisukeYamashita?secret=xxxxxxxxx&issuer=GitHub

重要なのはsecretパラメーターです。これが先程のサーバーとクライアントで共有されるシークレットになります。 これを各サービスの生成感覚XでカウンターCを求めてやるとTOTP値が取得できます。つまりQRコードには共通鍵を受けたしする機能があるといえます。

URIのフォーマット

Google Authenticatorの場合はWikiにURIの仕様が載っていました。Key Uri Format。いくつかの重要なパラメータがあります。

  • Algorithm: 仕様するハッシュ関数の種類です。デフォルトではSHA1です。
  • Digits: OTPの桁数のことです。デフォルトでは6です。
  • Type: OTPの種類です。totphotpです。デフォルトではtotpです。
  • Issuer: だれがバリデーターなのかを定義したものです。
  • Counter: Typehotpのときに設定されるものです。デフォルトではTypetoptなのでありません。
  • Period: TOTPの生成間隔Xのことです。デフォルトでは30秒です。

つまり、Google Authenticatorに対応したければこのURIの仕様に沿えばHOTPやTOTPをいろんな間隔(Period)やアルゴリズム(Algorithm)で作ることができるようになるというわけです。

Secretの注意点

そのままの文字列では使えません。

GitHub, Google, etcのジェネレーターではBase32でエンコードしてあるのでsecretパラメータの値をデコードして鍵を取り出す必要があることに注意です。またDropboxなどではパディングがBase32が定義されているRFC 3548 Section 2.2通りになっていないことが報告されています(参考: Missing padding is not re-added)。この場合は、自分で=のパディングをつける必要があることに注意です。

これから

OTPは便利なものの、いろんなセキュリティ的な懸念があったり、Google Authenticatorや1Passwordなどといったようなジェネレーターを用意する必要があるなど、改善の余地がまだまだあります。いろんな論文が出ているのですが、今度はそれらの紹介をしたいなと思っています。

参考文献

© KeisukeYamashita 2023