最近ふと気になって読んだ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)で共通な鍵(シークレット)で、C
は8
バイトのカウンターです。またTruncate
関数はSection 5.3で定義されている関数で、非常に簡単な処理です。またここではハッシュ関数としてSHA1
を使用しましたが、バリデーターとジェネレーターで共通のハッシュ関数を使えれば違うハッシュ関数を使っても問題はありません。
なぜHMACを使う必要があるのか
“Air Gap”がある環境、すなわちインターネット環境がない状況でOTPを生成して、それをバリデーターで「確かにジェネレーターで生成された」ことの検証にはHMACの仕組みが使われています。
そもそもOTPとは完全性が担保できてやっと実現できる仕組みなので、HMACがいい役割を果たしています。
なぜRSAは使えないのか
共通鍵暗号方式であるHMACではなく、公開鍵暗号方式を使うことはできないのでしょうか。答えは「できない」です。 というのもアルゴリズムにその理由があります。
HMACはRFC2104のSection 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の種類です。
totp
かhotp
です。デフォルトではtotp
です。 - Issuer: だれがバリデーターなのかを定義したものです。
- Counter:
Type
がhotp
のときに設定されるものです。デフォルトではType
はtopt
なのでありません。 - 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などといったようなジェネレーターを用意する必要があるなど、改善の余地がまだまだあります。いろんな論文が出ているのですが、今度はそれらの紹介をしたいなと思っています。