二要素認証に使われてるYubico OTP の仕組み
2要素認証に利用できるデバイスに、Yubico社が提供するYubikeyというものがあります。
Yubikeyでは様々な機能を使えますが、初期状態で利用できるものにYubico OTP(One-Time password)があります。 Yubico OTPは、非常に安全な仕組みだと思いますが、公式ドキュメントがやや分かりづらいこともあり書きました。
Yubico OTPとは
Yubico OTP は、Yubicoが定めるOTP(One-Time Password)の形式であり、Yubikeyから正常に生成されたOTPかどうかを検証することができます。
このOTPを「私が所持するYubikeyから生成されたものかどうか」を検証することで、二要素認証の要素として利用できます。 sshログインで二要素認証にYubico OTPの使い方は、他の方が書かれているので興味のある方は検索してみてください。
Yubikeyは、USBキーボードとして認識され、円の部分をタップすることでYubico OTPを生成し、キー入力されます。
OTP(One-Time Password)という名前からもわかる通り毎回生成される文字列が異なります。 例えば、2回生成をすると、以下のような文字列が生成されます。
vvvgutvinihgjelnlrctgrvukrkbhtbrrrtugdgndcif vvvgutvinihgjrcnkgkflerfghfdkijiudvltttuntji
異なる文字列が生成されていることがわかるかと思います。
Yubico OTPの検証サーバー
OTPが、Yubikeyから生成されたのか、でたらめな文字列なのかを検証する仕組みがYubico社から提供されています(YubiCloud)。 下図は、password + Yubikeyの2要素認証を行するフローで、青色の部分でOTPをYubiCloudに問い合わせています。
1要素目のpasswordが検証されたあとに、OTPの検証をしていることがわかるかと思います。この2つの要素の認証が両方成功して、この認証は成功になるという流れです。
https://developers.yubico.com/OTP/Libraries/Using_a_library.html
YubiCloudはOTPが正しいかどうかを検証するだけで、Yubikeyとユーザとの紐付けはServer側の責任となります。 Yubico OTPでは、どのYubikeyから生成されてるいるのか判別出来るように、public IDというYubikey毎に固定の値が先頭につきます。
OTPが「私が所持するYubikeyから生成されたものかどうか」は、
- サーバ側で、Yubikeyのpublic IDとユーザーの登録したpublic IDが正しいか
- YubiCloudによるOTPの検証結果
から分かります。
Modhex形式
ここからは、生成されたOTPについて説明していきます。
Yubico OTPは、16進数をModhex方式(Yubico社が定めたエンコーディング方式)で別の文字に置換されています。
YubikeyはUSBキーボードとして認識されるので、PCに設定されているキーボード設定によっては、入力がぶれてしまうことがあるそうです。 Modhex形式では、様々なキーボードでのぶれが起きにくい文字が使われているそうです。
16進数 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | a | b | c | d | e | f |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Modhex | c | b | d | e | f | g | h | i | j | k | l | n | r | t | u | v |
Modhex形式を16進数に戻すコードは以下のようになります。
d = {'b': '1', 'c': '0', 'd': '2', 'e': '3', 'f': '4', 'g': '5', 'h': '6', 'i': '7', 'j': '8', 'k': '9', 'l': 'a', 'n': 'b', 'r': 'c', 't': 'd', 'u': 'e', 'v': 'f'} def modhex2hex(x): return ''.join([d[i] for i in x])
この関数でOTPを16進数に変換するとこのようになります。
>>> print(modhex2hex('vvvgutvinihgjelnlrctgrvukrkbhtbrrrtugdgndcif')) fff5edf7b76583abac0d5cfe9c916d1cccde525b2074 >>> print(modhex2hex('vvvgutvinihgjrcnkgkflerfghfdkijiudvltttuntji')) fff5edf7b7658c0b9594a3c456429787e2fadddebd87
馴染みのある16進数の表現になりましたね。 これ以降のOTPは16進数に変換後の値を使います。
public IDと暗号文
Yubico OTPは44文字の文字列で、public ID(先頭12文字)と暗号文(残り32文字)からなります。正確にはpublic IDの長さは設定で変えられますが、YubiCloudで使えるYubico OTPに話を限定します。
public IDは、OTPの中で固定の値です。 設定で変更できますが、工場出荷状態で使える設定では、必ずユニークな値となっています。
暗号文には、OTPがYubikeyから生成されたものかを検証するための情報が含まれています。
先ほどの同じYubikeyから生成した2つのOTP
- fff5edf7b76583abac0d5cfe9c916d1cccde525b2074
- fff5edf7b7658c0b9594a3c456429787e2fadddebd87
をpublic ID と 暗号文に分けると以下のようになります。
public ID | 暗号文 |
---|---|
fff5edf7b765 | 83abac0d5cfe9c916d1cccde525b2074 |
fff5edf7b765 | 8c0b9594a3c456429787e2fadddebd87 |
public IDが同じで、暗号文は変わっていることがわかります。
暗号化方式とその鍵
暗号化にはAES-128が使われています。
鍵はYubikey内と検証サーバー(YubiCloud)に保存されています。 YubiCloudでは、public IDから鍵を取り出し復号します。
工場出荷時に、YubiCloudへpublic IDと鍵の登録はされているので、Yubico OTPはすぐに使うことができるというわけです。
また、Yubikeyは設定に対して、書き込みを行うためのインターフェースしか用意されておらず、読み込みができないため、鍵はYubico OTP設定時にのみ分かるようになっています。
初期状態では工場でYubico OTPの設定がされているので、購入者も鍵を知ることができません。 Yubikeyの利用者は、鍵を知らずに(知ることが出来ずに)OTPを生成し、Yubico OTPという仕組みを使用することができます。
鍵の情報漏洩を心配しなくて良いのはYubikeyなどのデバイスを通した認証の良いところですね。 鍵の入ったデバイスが盗まれてしまったら元も子もないですが、public IDに紐づく情報を無効化することで被害を抑えられます。
暗号文の中身
暗号文の中身で、検証に重要な値として、
- private ID
- Counter
があります。
private IDは、鍵と同様に、工場で設定されYubiCloudに登録されている値です。 public IDは公開情報なので、他人のpublic IDになりすまそうとすることができますが、暗号文内のpublic IDに紐づいているprivate IDの値は分からないため、なりすましを防ぐことができます。
Counterは、YubikeyでOTPを生成する毎にカウントアップされる値です。YubiCloudでは、最後に検証したCounterを保持されており、入力されたOTPのカウンターと最後に使用したカウンターの値を比較することで、使い回しなどを防ぐことができます。 また、過去に生成したOTPが漏洩してしまったとしても、そのあとに生成したOTPを使用することで簡単に漏洩したOTPを使用できなくすることができます。
結局、YubiCloudでは、暗号文を復号し、
- private IDが正しいか
- Counterの値が新しいか
を検証します。
以下は、Yubicoのサイトに記載されている検証の流れを示した図です。
https://developers.yubico.com/OTP/OTPs_Explained.html
ここまでの、まとめ
- Yubico OTPは、YubiCloudによって検証できる
- 暗号部分はAESで暗号化されており、購入者も鍵を知ることができないので、鍵の漏洩を心配しなくて良い
- 暗号化された情報で改ざん防止や使い回しの防止がされている
暗号文の中身
ここからは、暗号文の中身を見ていくことで、Yubico OTPの詳細な仕様を見ていこうと思います。
暗号文の中身は16バイトで以下の値が入っています。
field名 | バイト数 | |
---|---|---|
uid | 6 | private(secret) ID |
useCtr | 2 | Usage counter |
tstp | 3 | Timestamp |
sessionCtr | 1 | Session usage counter |
rnd | 2 | Random number |
crc | 2 | CRC16 checksum |
https://mowa-net.jp/wiki/YubiKey#Yubico_OTP.2BMG5O1WnY-
- uid: public IDと1対1で対応したID。private ID。
- useCtr: USBを抜き差しするとカウントアップされる不揮発な値。
- tstp: デバイス起動時からカウントされる8Hzのタイマーの値。
- sessionCtr: 揮発性の値で、USBが差さっている間に、Yubico OTPを生成するとカウントされる。
- rnd: 乱数。
- crc: 16bit ISO13239 1st complement checksum だそうです。チェックサム。
カウンターの上限について
Yubico OTPのカウンターは、不揮発性のuseCtrと揮発性のsessionCtrの2つあります。
最後に使用したuseCtrより大きければOK、小さければNGです。useCtrが同じ場合は、sessionCtrが大きければOK、同じか小さければNGとなっています。
sessionCtrは、1バイトなので256回タップすると上限に達します。次のタップでは、useCtrがカウントアップし、sessionCtrは0になります(確認済み)。
useCtrは、2バイトなので、65535まで値を保持できます。sessionCtr 1日10回抜き差しすると大体17年くらい使える感じです。 上限に達する前に(かなり丈夫に作られてるとはいえ)壊れてしまいそうですが、上限に達したらもう一度設定し直すしかないらしいです。
Yubikey Personalization tools
暗号文の詳細を追ってくために、実際に暗号文を復号してみたいと思います。
Yubikey Personalization tools というYubikeyの設定ツールがあります。 これを使うとYubico OTPの設定を変えることができ、public ID、private IDと鍵を知ることができます。
新しく生成したpublic IDとprivate IDと鍵のペアをYubiCloudにアップロードすることで、Yubico OTPが使えるようになります。
自分で生成したものは先頭がvvとなり、工場で生成されたものはccから始まるよう決められており、自分で生成したものかはpublic IDから判別することが可能です。
暗号文の実際の値
鍵が a9 e2 29 33 2e 87 0f 26 1e a5 5a 2a bd ef da e0
に設定されたYubico OTPの中身を見て行きたいと思います。
以下が、復号や暗号化するコードです。
以下の5つのOTPを復号してみようと思います。
vvntibfekfkkuvrvubtictldndbenurgrgbukhkutild vvntibfekfkkcgfeljervjjcejvjkvttthndftrtbdrf vvntibfekfkkbnkhcdiuhbbbflbuitdnecbkbnlkchgv vvntibfekfkkbevrttebkucvbdrntikdicluudifdgil vvntibfekfkkjfvttcrfvdkrrrvdidrrrdlcdefvhege
結果は、
$ python Yubico.py < public_id:ffbd71439499 private_id:8a00555dd7db useCtr:0001 tstp:f011a4 sessionCtr:00 rnd:adfd crc:7855 > < public_id:ffbd71439499 private_id:8a00555dd7db useCtr:0001 tstp:fe11a4 sessionCtr:01 rnd:cd73 crc:1308 > < public_id:ffbd71439499 private_id:8a00555dd7db useCtr:0001 tstp:1312a4 sessionCtr:02 rnd:d3dd crc:1599 > < public_id:ffbd71439499 private_id:8a00555dd7db useCtr:0002 tstp:5bd808 sessionCtr:00 rnd:f2d7 crc:ae1f > < public_id:ffbd71439499 private_id:8a00555dd7db useCtr:0002 tstp:7fd808 sessionCtr:01 rnd:7326 crc:121d >
このようになります。 最初の3個は、同じuseCtrで、sessionCtrがカウントアップされていることがわかります。 4個目のYubico OTPを生成する前に、Yubikeyを抜き差ししました。4個目のuseCtrが2になり、sessionCtrが0に戻っていることが分かります。
checksumの計算
自分でOTPを計算していきたいと思います。
crcは、それ以外のfieldの値を使ってchecksumを計算します。 マニュアルに載っているコードは、一部間違っているようでしたので注意が必要です。
以下のコードが、チェックサムの計算と検証するコードです。
def update_crc(crc, x): crc ^= x for _ in range(8): flag = crc & 1 crc >>= 1 if flag != 0: crc ^= 0x8408 return crc & 0xffff def get_crc(s): assert len(s) == 14, "Invalid length" crc = 0x5af0 for i in s: crc = update_crc(crc, i) return crc def verify_crc(s): assert len(s) == 16, "Invalid length" crc = 0xffff for i in s: crc = update_crc(crc, i) return crc == 0xf0b8
YubiCloudで検証
YubikeyによるYubico OTPの生成をみてきましたが、これらの情報が検証サーバ(YubiCloud)で正しく検証されるか見ていきたいと思います。
Validation Protocol に書かれていますが、
http://api.yubico.com/wsapi/2.0/verify?otp=vvvvvvcucrlcietctckflvnncdgckubflugerlnr&id=87&timeout=8&sl=50&nonce=askjdnkajsndjkasndkjsnad
にリクエストを送ることで、OTPが正しいかを検証することができます。 OTPをotpというパラメータにセットし、それ以外のパラメータはそのままでリクエストを送ります。
YubiCloudに登録している、public IDで生成した正常なOTPを送って見ます。
$ curl http://api.yubico.com/wsapi/2.0/verify\?otp\=vvvgutvinihgvbbrtbdjebcblfherenbdkkbfrhdhghr\&id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad status=OK
status=OK
から、正しいOTPであることが分かります。
同じOTPを再度、送って見ます
$ curl http://api.yubico.com/wsapi/2.0/verify\?otp\=vvvgutvinihgvbbrtbdjebcblfherenbdkkbfrhdhghr\&id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad status=REPLAYED_REQUEST
status=REPLAYED_REQUEST
より、再送されたものであることが分かります。
その他にも、 過去のOTPを送ると
$ curl http://api.yubico.com/wsapi/2.0/verify\?otp\=vvvgutvinihgddvrncverchrujjdnfferuccvdficvcf\&id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad status=REPLAYED_OTP
一文字書き換えたOTPを送ると
$ curl http://api.yubico.com/wsapi/2.0/verify\?otp\=vvvgutvinihgddvrncverchrujjdnfferuccvdgicvcf\&id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad status=BAD_OTP
となり、きちんとNGが返ってきていることが確認できます。
自分で生成したOTPを検証する
以下のpublic ID(ff39a66437d3) と private ID(065c6ad736b4)と鍵でYubiCloudに登録し、以下のOTPを検証したとします。
# vveklhhfeitehunflcgniugkjencelvjcfgjrublthei < public_id:ff39a66437d3 private_id:065c6ad736b4 useCtr:0001 tstp:aa4865 sessionCtr:02 rnd:03cd crc:fc31 >
useCtr が1、sessionCtrが2なので、タップすればuseCtrが1、sessionCtrが3のOTPが生成されるます。 この条件で、OTPを生成し、検証してみます。
y1 = Yubico.calc_otp(key, 0xff39a66437d3, 0x065c6ad736b4, 0x01, 0x03) print(y1.otp) # => vveklhhfeiteefcuirblkthjbctldteunthliutfrlhl
これを実行することで、vveklhhfeiteefcuirblkthjbctldteunthliutfrlhl が得られます。
# 普通に生成したOTPの検証。useCtr が1、sessionCtrが2。 $ curl http://api.yubico.com/wsapi/2.0/verify\?id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad\&otp\=vvvgutvinihgichgvrljjhrdbtdrbnrnnnttehnebjbc h=pNQ0bQIYZx2iBHoktz5aVjDGFvk= t=2016-12-30T03:56:09Z0559 otp=vvvgutvinihgichgvrljjhrdbtdrbnrnnnttehnebjbc nonce=askjdnkajsndjkasndkjsnad sl=50 status=OK # 自分で計算したOTPの検証。useCtr が1、sessionCtrが3。 $ curl http://api.yubico.com/wsapi/2.0/verify\?id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad\&otp\=vveklhhfeiteefcuirblkthjbctldteunthliutfrlhl h=6jnCfVVKSa0wBSlZWdZcfoIKL3k= t=2016-12-30T04:00:39Z0699 otp=vveklhhfeiteefcuirblkthjbctldteunthliutfrlhl nonce=askjdnkajsndjkasndkjsnad sl=50 status=OK # 普通に生成したOTPの検証。useCtr が1、sessionCtrが3。 $ curl http://api.yubico.com/wsapi/2.0/verify\?id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad\&otp\=vveklhhfeiteribeddtufhbtvnhnttivubkkfugekhvh h=k3b7zIJOLmAflOJmwYOPzyl8wSw= t=2016-12-30T04:00:51Z0931 otp=vveklhhfeiteribeddtufhbtvnhnttivubkkfugekhvh nonce=askjdnkajsndjkasndkjsnad status=REPLAYED_REQUEST
二つ目の結果から自分で生成したOTPが、検証でOKを返していることがわかります。このリクエストにより、YubiCloudのカウンターが更新され、3個目にYubickey経由で生成したOTPにも関わらず検証が失敗していることがわかります。
せっかくなのでカウンターがマックスまでいったとき、オバーフローとかして、また最初から使えないかみてみます。
# useCtr が0xffff、sessionCtrが0xff。 $ curl http://api.yubico.com/wsapi/2.0/verify\?id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad\&otp\=vveklhhfeitejcvfeneitrvgujrihuiutgvgvgvefdcf h=gEwRfJPLZ6PTjTwnyY0lXvsgvZ4= t=2016-12-30T04:15:05Z0073 otp=vveklhhfeitejcvfeneitrvgujrihuiutgvgvgvefdcf nonce=askjdnkajsndjkasndkjsnad sl=50 status=OK # useCtr が0x0、sessionCtrが0x0。 $ curl http://api.yubico.com/wsapi/2.0/verify\?id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad\&otp\=vveklhhfeitejcvfeneitrvgujrihuiutgvgvgvefdcf h=7bNNnRVlNpH/CFPbKKF943MM1JI= t=2016-12-30T04:15:17Z0875 otp=vveklhhfeitejcvfeneitrvgujrihuiutgvgvgvefdcf nonce=askjdnkajsndjkasndkjsnad status=REPLAYED_REQUEST
サーバ側でオバーフローなどはしないので、やはり設定をし直すしかなさそうですね。
後半の、まとめ
- Yubico OTPの暗号化されている中身を詳細にみてみた
- 実際に暗号文を復号し、挙動を確かめた
- 暗号化するコードを作り、正しく検証されるか確かめた
さいごに
Yubico OTPは、普通の使い方では鍵がもれる心配をしなくてよいためとても安全な仕組みかと思います。 公式マニュアルがややわかりにくかったのとYubicoの独自認証プロトコルなので資料がほぼなかったので書いてみました。 何かしら興味をもっていただければ幸いです。
参考
- 公式
- マニュアル
- これが一番詳しいと思います。
- ただし、公式サイトから、この資料のリンクを探し出すのは困難なので注意してください。
- Yubico OTP
- Yubikey Personalization tools
- 検証サーバー関連
- YubiKey
- 日本語のブログ
- 本当にこのページがあって助かりました:bow:
- 公式の素材用の画像
その他にもYubikeyのことが書かれた日本語のブログはお世話になりました。
もし公式ドキュメントを読みたい方は、
- ドキュメント量は多いが整理されていないので注意する
- 名称のブレがとにかく激しいので、ノリで理解する
- 例えば、public ID, Yubikey Token, publicname は全部同じものを指します。
に注意してください。
どこか間違っている点や疑問点などあればコメントいただけると助かります。