【S2お茶会】MFA の実装

s2お茶会
健康は大事だとつくづく思います。

今回は弊社の吉津が「MFA の実装 」について話しました。

MFA の実装

MFAとは


MFA とは Multi Factor Authentication の略で、日本語にすると多要素認証ユーザーに2つ以上の検証要素を提供することを要求する認証方法です。
一般的な「ユーザー名 + パスワードによる認証」に、1つ以上の追加検証要素による認証が加わると多要素認証になります。

認証要素の種類には、
ユーザーが知っていること (知識)、たとえばパスワードや暗証番号
ユーザーが持っているもの (所有)、たとえばバッジやスマートフォン
ユーザー自身 (継承)、指紋または声解析など、バイオメトリクスを介して表示されます

多要素認証 (MFA) とは何でしょう?
https://www.onelogin.com/jp/learn/what-is-mfa

などがあり、これらを組み合わせて2つ以上の認証を行います。
例えば Google アカウントとか、何かのサービスにログインする時に2段階認証を求められることがあるので覚えがあるかもしれません。

多要素認証を導入する際、脆弱性対策のためにもパスワードや暗証番号などの知識的な認証に加え、「所有」もしくは「継承」の要素が必要です。
「継承」についてはPCやスマホの指紋・顔認証が使えればいいですが、少しハードルが高いケースもあるので「所有」の方を使うことが多いように思います。

知識の例所有の例継承の例
暗証番号SMS指紋認証
パスワードTOTP顔認証

TOTP


今回はそれらの認証の中から所有の例に挙げた TOTP について見ていこうと思います。

TOTP とは Time-based One-Time Password の略で、時間に基づいて生成されるワンタイムパスワード(6桁の数値)です。ユーザーは Google Authenticator などで簡単に生成することができます。
ちなみに TOTP の場合、認証に使われるのは秘密鍵で、秘密鍵を搭載しているスマホを持っているという意味での所有になります。

ユーザーはスマホ画面に表示された 6 桁の数値をサーバー側に送信。サーバー側はユーザーの秘密鍵と現在時刻で 6 桁の数値を作成し、送られてきた 6 桁の数値と同じかどうかを判定し認証を行います。

こちらは RFC 6238GoogleAuth を参考に書いた TOTP を生成するための実装例です。
my $hmac = hmac_sha1_hex(
    pack('H*', sprintf('%016x', int($timestamp / $interval))),
    _decode_base32($self->secret32),
);

# $hmac = 1f8698690e02ca16618550ef7f19da8e945b555a
# 20 Bytes Hex String = 40 chars

# 6 桁の数字にするアルゴリズムは HOTP と同じ
my $offset = hex(substr($hmac, -1)) * 2; # a => 10 * 2 = 20
my $snum = hex(substr($hmac, $offset, 8)); # 50ef7f19 => 1357872921
my $d = ($snum & 0x7fffffff) % 1000000; # 1357872921 % 1000000 (2147483647 と論理積を取って 31bit に収める)
return sprintf('%06d', $d); # 872921
何をやっているかと言いますと、まず $hmac という引数に現在時刻と秘密鍵を渡しています。
2行目の $timestamp / $interval現在時刻を30秒で割っているのが重要で、こうすることで30秒単位の間隔を取得することができます。

30 秒以内に発行されたパスコードを入力してくださいというシチュエーションがあるかと思いますが、その仕組みの部分です。必ずしも 30 秒でなくてはいけないわけではありませんが、時間が長くなるとそれだけセキュリティも甘くなってしまうので、一応 30 秒が推奨されています。

続いて、 $hmac に渡された40文字の文字列を 6 つの数字に変えなければいけません。そのアルゴリズムが RFC 4226 に定義されています。

10行目で $hmac の末尾4 Byte(最後の1文字)を取得しています。つまりここでは「a」ですね。
「a」は16進数なので、これを10進数に変換します。そうすると「10」になるのですが、Perlの文字列で offset に直すために *2 しています。
-------------------------------------------------------------
| Byte Number                                               |
-------------------------------------------------------------
|00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|
-------------------------------------------------------------
| Byte Value                                                |
-------------------------------------------------------------
|1f|86|98|69|0e|02|ca|16|61|85|50|ef|7f|19|da|8e|94|5b|55|5a|
-------------------------------***********----------------++|

Byte Value の 20 番目から 4 Byte 分を指定して 50ef7f19 を取得、それを 16 進数から 10 進数に変換する処理が 11 行目です。
6桁の数字がほしいので、先ほど変換した数字を 10 の 6 乗で割るのですが、その時に 31 Byte 以内に収まるように 0x7fffffff という 31 Byte の最大数と論理積をとっています。

バックエンドで必要なこと


実際に多要素認証を実装する時にバックエンドには大きく分けて、

  • 登録時に情報を作ること
  • 検証すること
  • 解除すること

これらの 3 つの機能が必要です。

登録時


これはSlackのものですが、登録時には下のような画面が表示されます。


ここで必要な情報としては、秘密鍵と諸々の情報を含んだ QR コードです。
秘密鍵と URL、そして URL の QR コードを生成しなければいけないのですが、そのステップがこちら。

  1. 秘密鍵の生成
  2. URLの生成
  3. QRコードの作成
  4. バックアップコードの生成
  5. 秘密鍵の保存
  6. 秘密鍵、QRコードを渡す

アプリにQRコードを登録すると、「確認コードを入力」のところに自動でコード(TOTP)が表示されるようになります。

検証


保存されていた秘密鍵でTOTPを計算し、送られてきた数値と一致するかどうかを確認します。
一致していたらMFAの登録はOKとして、バックエンドのフラグを立てます。

バックアップコードの場合は、DB に保存されていたバックアップコードを取得し、送られてきた値が含まれるかを確認。一致したものを取り除いて再度 DB に保存するといった流れです。

解除


解除はシンプルに DB に保存してある秘密鍵を含むレコードを削除します。


どれもモジュールを使えば簡単に実装できます。

ハマったポイント


Duo Mobile という二要素認証を管理するアプリで試すとどうも安定せず。
RFC を見返すと「ネットワークの遅延を考慮して許容可能なタイムステップの範囲を設定すべきである」「ただし広く取りすぎると攻撃を受けやすくなるので最大で1つが望ましい」という旨が書いてあったので、前後のタイムステップ、つまり現在と30秒前、30秒後の全てを検証してどれかに引っかかれば許可することで解決できました。