2013年5月25日土曜日

YubiKey ウェブサイトでの2要素認証考察

YubiKey を使ってウェブサイトでの2要素認証をどのように実現すればよいのか?基本的な概要設計について考察してみたいと思います。

題材としては、ブラウザをクライアントとして使った一般的な業務用サーバシステムの認証を YubiKey で行う場合を検討してみましょう。架空のシステムを作るのは、ちょっとアレなので、認証部分や、管理部分を中心としたポイントとなりそうな考え方のみをまとめてみます。




まず、2要素認証というものがどういったものなのかを整理しましょう。


一般的なシステムやサービスは、ユーザのIDが発行され、IDを基準に利用者の特定を実施するわけですが、一度発行した ID はほぼ不変の内容であるため、ID だけではなく、パスワードを使って認証をするのが一般的と言えます。

しかし、何らかの理由でパスワードが漏れてしまった場合や、システムに対する正面からの攻撃によってパスワードが破られた場合に問題が起こってしまいます。

そこでパスワードの代わりに、よりセキュリティ強度が高いと言える認証用の物理的デバイス(YubiKey などのトークン以外にもカードなり様々なもの)を使った方法が考えられます。デバイスに ID を紐付け、デバイスのみで認証することもできますが、この場合、デバイス自体が盗まれた場合には、直ちにデバイスからのログインに対して停止措置などを取らなければ完全にアカウントの乗っ取りや、なりすましができてしまいます。盗まれない限り安全ではありますが、まだ不安がありますね。

ちなみに、ここまでの方法が1要素認証とか、単一要素認証と呼ばれる方法です。

そこで、更に強固な方法として、パスワードと、デバイスの2つの要素を併用して認証を行う。というもので、この方法が2要素認証Two-factor authentication)と呼ばれるもののようです。この場合は、両方が揃わなければ認証の壁を突破されることはないため、別々に管理できるものであれば、以前に比べて格段に固いセキュリティであると言えます。

この2要素認証の関係は、クレジットカードにおいて、カード番号、自分が設定したパスワード、カード自体の裏面に記載されているセキュリティコードのような関係だったり、銀行口座において口座番号、自分が設定した暗証番号、利用者毎の変換表やワンタイムパスワードトークンなど、一定以上のセキュリティが要求される場では、既に実運用されており、案外自分が身近に使っているものもあるかもしれません。


具体的にブラウザを介して YubiKey で2要素認証を行う場合は、ID は入力させず、YubiKey で生成したワンタイムパスワードと、ユーザのパスワードの2つの要素をサーバに投げるのが一般的なようですね。



2要素認証がどういうものかは解りましたが、次にウェブシステムにおいてどのように実現すれば良いのかを考えます。ブラウザでのログイン画面を想定してみると、ユーザパスワードとワンタイムパスワードを渡すだけで済むのであれば、2つのテキスト入力エリアを介してサーバに POST する形になるでしょう。

ブラウザ --> HTTP POST(pass, otp) --> サーバ
※ otp: ワンタイムパスワード

ということで、2要素認証のシステム実装においては、ウェブページの UI や HTTP 通信などの細かい話はどうでもよくて、サーバ側において渡されたユーザパスワードとワンタイムパスワードのセット情報を検証し、妥当であればユーザID なりユーザ情報を導き出す。という仕掛けが実現出来ればゴールに近づける。と言えます。イメージとしては以下の様なインターフェイスでしょうか?

user = authTwoFactor(pass, otp)

ちなみに、ワンタイムパスワードだけの1要素で認証する場合は以下の感じですね。

user = authOneFactor(otp)



さて、上記の機能を具体的に実現する上で必要となる個別要素に着目してみましょう。
システムに入力されたユーザパスワードと、ワンタイムパスワードを検証して、ユーザID を求めるには、少なくとも、ユーザID、ユーザパスワード、ワンタイムパスワード検証用の何か、という3つの要素がシステム上に事前登録されるべきと考えられます。どれが欠けても紐付けができなくなるため、マスタデータとしてサーバ側で登録管理すべきです。

業務システムであれば、データベースを使って管理するのが一般的ですが、考察する時点においては、そんなことはどうでもいい話ですので、ユーザID とユーザパスワードはシステム上、登録と管理がされるという認識で一旦置いておきましょう。一般的なシステムではユーザID を軸とした、利用者の登録管理機能を持たせるのが普通ですから、管理面ではワンタイムパスワード検証用の何かを管理する機能に注力すべきと言えるでしょう。


『ワンタイムパスワード検証用の何か』は、結局どういったワンタイムパスワードの方式を使うかによって、検証方法も違ってくるでしょうから、方式を定めなければ、検証用の情報が1つのデータで済むのか?複数のデータが必要なのか?決めようもありません。

そこで、今回はワンタイムパスワードに RFC 4226 の OATH-HOTP を使う前提で考えてみましょう。この方式は以前、意図したパスワードが生成できるところまでは確認しましたが、今回は検証も考えてみます。


検証のために、HOTP について整理が必要ですね。

HOTP の場合、細かいパラメータを省いて単純に言えば、秘密鍵とパスワードの生成回数のカウンタを基準にワンタイムパスワードを生成する方式です。生成されるワンタイムパスワードの内容は6桁あるいは8桁の整数を使うのが一般的で、2回目以降の生成には直前のワンタイムパスワードの値を渡すようです。(これにより連続性を持たせて安易に割り出せないようにしているようですね)

RFC 4226 では、具体的な Java のソースコードが載っているため、それを使って実際に確認してみるのも良いでしょう。なお、今回は6桁を想定しておきます。単純化したインターフェイスとしては以下のイメージになるでしょうか?

hash = easyHOTP(secret, counter, [prevHash = 0])


ちなみに、1分間で6桁の数値が変わるセキュリティトークンなどは HOTP のカウンタ部分を時間に変えた TOTP あるいは、それに類似する方式と捉えて問題ないと思われます。



サーバ側としては、HOTP で利用する秘密鍵については事前に登録されている必要があります。また、パスワード生成カウンタや直前の値を 0 から始めるにしても、一旦運用が始まった後は当然、カウンタや、直前のワンタイムパスワードの値も正確に把握していなければ制御できなくなります。そういったことを踏まえて、とりあえず、ユーザ情報登録に関するレコード定義は以下の様なイメージを想定しました。ワンタイムパスワードに絡む情報としては、秘密鍵だけを持たせておきます。

UserRecord = {
  id: String       // ユーザID
, pass: String    // ユーザパスワード (平文ではなくハッシュ値を持たせるべきか?)
, secret: String // 秘密鍵

  // 以下、表示上や必要な情報や属性があれば付ける
, name: String
, position: String
  :
}

もし、システムに拡張する形で外部に持たせるならば、以下の様な形も考えられます。

OtpMasterRecord = {
  id: String        // ユーザを特定するキー情報(当然内部IDでも良い)
, secret: String
  // 必要な情報があれば付ける
, entryTime       // 利用開始日時
, disabled          // 無効化フラグ
}


更に、HOTP の課題としては、YubiKey などのトークン側において、好きなタイミングでワンタイムパスワードが生成できてしまうため、順番にカウントアップした値で生成されたワンタイムパスワードが送られてくるとは限りません。いくつかカウンタが飛び飛びで渡される事も考慮し、常にカウンタの同期を取りながら検証を進めていく事が重要と言えます。

ここまでで感の良い人であれば、既に気づいていると思いますが、外部から入力されるパラメータはカウンタではなく、ワンタイムパスワードそのものです。そのため、大量のユーザ情報が登録されている状況では、順番に各ユーザのワンタイムパスワードを生成していき、該当する内容が出るまで検証し続ける。というのは、残念な方法と言えます。特にカウンタが飛ぶ可能性があるため、毎回1回で検証できる保証が無く、この方法は絶対に使うべきではありません。

現実的な方法としては、事前に各ユーザ毎のこれから渡される可能性のあるワンタイムパスワードを、ユーザ情報に対する紐付けをした上で何回分か計算して蓄えておく形でしょう。


事前準備するワンタイムパスワードには、カウンタやユーザIDも持たせたレコード定義をしておくべきでしょう。以下の様な構造のイメージです。

OtpRecord = {
  id: String
, counter: Int
, otp: Int
}

もし、上記を DB テーブルとして定義するならば id と counter をキーに、otp に単体でインデックスを貼るなんてのが良いかもしれません。

なお、準備しておくワンタイムパスワードの『何回分』というのがカウンタのズレに対する『許容値』と言えます。この値は利用者に不便が無い程度の回数で、かつ大き過ぎない値に留めておく方が良いでしょう。その理由は、大きすぎると他のハッシュと突合してしまう確率が高くなってくるため、セキュリティの質を落とすことに繋がるからです。ここは HOTP の仕様上避けられない課題かもしれません。
※ 具体的なシステム実装やらを調べたわけではないため、上手い解決策があるかも?


こういったレコードがシステム上で管理できているならば、入力されたワンタイムパスワードから、該当する OtpRecord を全て取り出し(1個以上該当する可能性アリ)紐づく各 UserRecord のユーザパスワードと入力されたユーザパスワードが一致すれば、ユーザを特定できます。

ユーザが特定できた OtpRecord が存在する場合は、そのカウンタで該当した。ということなので、その OtpRecord から、許容値分の OtpRecord を再度準備し、古い内容は削除すべきです。

ユーザが特定できずに、該当する OtpRecord が1つ以上存在した場合は、下手に OtpRecord を削除するのは危険な場合があるため、各 OtpRecord をそれぞれ1つ分、追加しておくのが無難でしょう。(ここの考え方はシステムに対する攻撃の糸口を与えかねないため難しいですね)


以上の流れで、意図した範囲でカウンタの同期が取れている間はユーザの特定ができると考えられます。しかし、実運用まで考えると、大きくカウンタがズレてしまった例外的な状況も十分に起こりうるでしょう。これに対する解決策としてはサーバ側のシステム管理者の機能として、強制的にカウンタの同期を行うものを整備したり、サーバの登録情報と YubiKey の双方を初期化する運用か仕掛けを用意すべきです。



また、更に中長期の運用では、YubiKey が故障したり、紛失した場合の救済策も必要となるでしょう。これに対しては、予備のYubiKey を常備しておく。あるいはシステム自体が YubiKey を使わない認証方式にも切り替えられる。などなど、例外的だけれども十分に起こりうる事象への対策は必須と言えます。



よりよい制御、管理方式もあるとは思いますし、他のシステムで具体的にどう実装しているのかは?少し気になる所ではありますが、これだけ情報があれば、2要素認証をウェブシステムに組み込むことが、別段無茶な話では無いことが解ると思います。



最後に YubiKey の OATH-HOTP の設定について補足しておきます。
YubiKey の OATH-HOTP では、6bytes の OATH Token Identifier というものが付与できるようになっています。これは6桁および8桁の HOTP の結果の先頭に強制的に 12桁の固定ID を付けるというものです。2要素では特に付けなくても運用できると思いますが、1要素での認証を考えた場合は、必ず付けて運用すべきです。なぜなら扱うユーザ数にもよりますが、OATH-HOTP はワンタイムパスワードが最低6桁の数値である以上、システム利用者が最大100万ユーザ存在した時点で確実に重複が出ますし、少数のユーザだけで運用したとしても中長期の運用を行う限り、ワンタイムパスワードからユーザが一意に絞れない状況が確実に出てしまうでしょう。そこで、そのような状況の発生を回避するために OATH Token Identifier を付与するべきです。また、その固定ID をユーザ毎、YubiKey毎に重複せず発行できていれば、先頭12桁を判別することで、(なりすまされている可能性も否定はできませんが)どのユーザが持つ YubiKey からのアクセスであるかを絞ることができます。更に、今回述べた OtpRecord の判定時、YubiKey が特定できる場合は、候補が複数存在するようなことは無く、必ず1件あるかないか?という形に絞れますので、よりシンプルな形でシステム化できるでしょう。以上の理由から、運用上の管理との兼ね合いになるとは思いますが、固定ID は付けておくのが無難でしょうね。


次回は Yubico OTP について調べてみたいと思います。

0 件のコメント:

コメントを投稿