試運転ブログ

技術的なあれこれ

二要素認証に使われてるYubico OTP の仕組み

2要素認証に利用できるデバイスに、Yubico社が提供するYubikeyというものがあります。

f:id:otameshi61:20161230002407j:plain

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を生成し、キー入力されます。

f:id:otameshi61:20161230002451g:plain

OTP(One-Time Password)という名前からもわかる通り毎回生成される文字列が異なります。 例えば、2回生成をすると、以下のような文字列が生成されます。

vvvgutvinihgjelnlrctgrvukrkbhtbrrrtugdgndcif
vvvgutvinihgjrcnkgkflerfghfdkijiudvltttuntji

異なる文字列が生成されていることがわかるかと思います。

Yubico OTPの検証サーバー

OTPが、Yubikeyから生成されたのか、でたらめな文字列なのかを検証する仕組みがYubico社から提供されています(YubiCloud)。 下図は、password + Yubikeyの2要素認証を行するフローで、青色の部分でOTPをYubiCloudに問い合わせています。

1要素目のpasswordが検証されたあとに、OTPの検証をしていることがわかるかと思います。この2つの要素の認証が両方成功して、この認証は成功になるという流れです。

f:id:otameshi61:20161230002515p:plain https://developers.yubico.com/OTP/Libraries/Using_a_library.html

YubiCloudはOTPが正しいかどうかを検証するだけで、Yubikeyとユーザとの紐付けはServer側の責任となります。 Yubico OTPでは、どのYubikeyから生成されてるいるのか判別出来るように、public IDというYubikey毎に固定の値が先頭につきます。

OTPが「私が所持するYubikeyから生成されたものかどうか」は、

  1. サーバ側で、Yubikeyのpublic IDとユーザーの登録したpublic IDが正しいか
  2. 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では、暗号文を復号し、

  1. private IDが正しいか
  2. Counterの値が新しいか

を検証します。

以下は、Yubicoのサイトに記載されている検証の流れを示した図です。

f:id:otameshi61:20161230002549p:plain 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と鍵を知ることができます。

s_Screen Shot 2016-11-20 at 14.50.54.png

新しく生成した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の中身を見て行きたいと思います。

以下が、復号や暗号化するコードです。

gist.github.com

以下の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の独自認証プロトコルなので資料がほぼなかったので書いてみました。 何かしら興味をもっていただければ幸いです。

参考

その他にもYubikeyのことが書かれた日本語のブログはお世話になりました。

もし公式ドキュメントを読みたい方は、

  • ドキュメント量は多いが整理されていないので注意する
  • 名称のブレがとにかく激しいので、ノリで理解する
    • 例えば、public ID, Yubikey Token, publicname は全部同じものを指します。

に注意してください。

どこか間違っている点や疑問点などあればコメントいただけると助かります。