#はじめに
本記事は認証認可技術 Advent Calendar 2019の22日目の記事です。
CTAPについて、自分の過去の投稿をまとめる感じで書いてみました。
#CTAPって何?
CTAP(シータップ)です。
認証のやつです。FIDOの関係です。
CTAPはFIDOアライアンスに策定されている仕様で、外部認証器(FIDOキー)のインタフェース仕様です。
YubikeyとかBioPassとかTitanとかはこの仕様に基づいて作られて、FIDOアライアンスの認定をとって「FIDO対応」として販売しています。
Webの人でプログラム作る人はWebAuthn-APIをJavaScriptからコールしてFIDOしているかと思います。
- navigator.credentials.create()
- navigator.credentials.get()
ですね。
で、これらのAPIをコールすると「セキュリティキーを挿入してください」とかメッセージボックスが出てきてFIDOキーと対話できますが、このときブラウザが内部でFIDOキーと通信するときにCTAPでやっています。
ですんで、WebでFIDOしている人はCTAPのことは知らなくても全く問題ありません。
とはいえ、物事を土台から理解することは大切なことです。また、興味本意でマニアックな領域を追求することって楽しいですよね。
ということで、ここではCTAPについて超ざっくり語って、CTAPを使ってWindowsのネイティブアプリで何かやってみたんだけど、という話を書きたいと思います。
#CTAPの位置付け
FIDO用語を使って言うと、
WindowsとかAndroidとかのPlatform
の中でChromeとかEdgeとかのBrowser
が動いていて、Browser
はWebAuthn
を実装していて、そのWebAuthn
の内部ではAuthenticator(FIDOキー)
とCTAPで通信します。
#CTAPスペック超ざっくり
CTAPの仕様はFIDOアライアンスで公開されています。
本投稿時点で2019/1/30版が最新です。
CTAP仕様 Client to Authenticator Protocol (CTAP) Proposed Standard, January 30, 2019
以降ではスペックに書いてある以下のことを超ざっくり説明していきます。
- CTAP1/U2FとCTAP2がある
- CBORでエンコーディングする
- CTAPauthenticatorコマンド
- USB HIDとNFCとBLEがある
##CTAP1/U2FとCTAP2がある
FIDOはFIDO1とFIDO2という仕様的なバージョンがあるんですが、CTAPにもCTAP1/U2FとCTAP2があります。
- FIDO1=CTAP1/U2F
- FIDO2=CTAP2
という理解でいいと思います。
CTAP2ではResidentKeyというキーの中にデータを保存する仕様が追加されていたりします。
ちなみに、FIDOキー欲しいなと思ってネットで製品を検索するとU2F対応とかFIDO2対応とか書いてありますが、それがこのことです。U2Fとしか書いていないものはFIDO2ではないと思った方がいいかと思います。
本記事はCTAP2のことだけ書いています。CTAP1のことは書いていませんのでご注意ください。
##CBORでエンコーディングする
以降で説明するCTAPauthenticatorコマンドをHID,NFC,BLEのプロトコルのPayloadに乗っけて送信してレスポンスをGETするんですが、CTAPauthenticatorコマンドの形式がCBOR
というものになります。
COBOLではありません,CBOR(The Concise Binary Object Representation ※1)
です。
CBOR
はCBOR
です。**細かいことは知りません。**Windowsだったらnugetにライブラリがあるんでそれ使えばいいです。
※1 シーボルと言う、たぶん。雰囲気jsonにちょっと似てます。
以下参考です。
[CWT入門その1] CBORによるオブジェクトのバイナリ表現
##CTAPauthenticatorコマンド
CTAPコマンド群をざっくり紹介します。
- authenticatorMakeCredential
- authenticatorGetAssertion
- authenticatorGetNextAssertion
- authenticatorGetInfo
- authenticatorClientPIN
- authenticatorReset
###authenticatorMakeCredential(0x01)
WebAuthnでいうところのnavigator.credentials.create()
がこのコマンドに対応しています。
指定されたrp(Relying Party)情報をキーにしてクレデンシャル(秘密鍵と公開鍵のキーペア)を生成して秘密鍵はキー内部に保管し、公開鍵とクレデンシャルIDなどが入ったAttestation構造体をレスポンスしてきます。Attestationには署名も含まれていて、署名を検証するとちゃんとしたものだと確認することができます。
このコマンドを通すためには、PIN(記憶要素)か生体要素のいずれかでの本人確認-UV(UserVerification)が必要です(※2)。
PIN認証はパラメータにPINを文字列で指定するとかではなく、authenticatorClientPIN
で求めたpinAuth(0x08)
を指定します、これはいわゆるトークンみたいなものです。
生体認証する場合はpinAuth(0x08)
は不要ですがoptions(0x07)
のuvをtrueにします。こうすると生体認証をFIDOキーが勝手にやってくれます。
options(0x07)
のrk(ResidentKey)オプションをtrueにしとくとuser(0x03)
の内容をFIDOキーに保存することができます。このrkがFIDO2の便利機能です。
ところで、このコマンドはキーペアを生成して秘密鍵をFIDOキーに保管すると言いましたが、不思議なことに、FIDOキーのメモリを消費することは無いです。
どうやらレスポンス(Attestation)に入っているCredentialID
があれば秘密鍵を再生成できるようです。**「えーそれって秘密鍵を外に出しているじゃん!」**というツッコミがありそうなんですが、誰もツッコマないので問題ないんだろう(※3)。
そんなわけで、FIDOキーには何も書き込まないので、無限にMakeCredentialできます。ただし、これはrkしない場合(Non ResidentKey)。
rkすると、user(0x03)
の内容と秘密鍵をFIDOキーに保管(Write)します。なのでrkしまくるとそのうちFIDOキーがパンクします(※4)。
ちなみに、uvとは別のバラメータでupっていうのがあり、最初はこんがらがるので注意です。
- uv=UserVerification=ユーザー本人かどうかPINとか生体とかで検証すること
- up=UserPresence=ユーザーがいるかどうかタッチで確認すること(本人かどうかは気にしない)
upはFIDOキーのタッチセンサー使います。Yubikeyはいかにも指紋認証してそうですが、upです、ただ接触をみているだけです。
upは3要素(記憶、所持、生体)にはカウントされないのでupだけで多要素認証とかいうとセキュリティおじさんに怒られます。
※2 FIDOキーにPINが未設定の場合(リセット直後とか初期状態のとき)はuv無しでもコマンドが通ったりします。
※3 問題あったら困るのでちゃんとしてると思います。
※4 パンクしたらどうなるのか、やってみましょう。
###authenticatorGetAssertion(0x02)
WebAuthnでいうところのnavigator.credentials.get()
がこのコマンドに対応しています。
rp情報、クレデンシャルID、チャレンジをパラメータで渡して認証します。authenticatorMakeCredential
で登録したクレデンシャルがあればすれば認証OKとなりAsssertion構造体をレスポンスしてきます。
Assertionには秘密鍵で署名されたチャレンジが入っていますので、署名をRPサーバ側で保管されている公開鍵で検証することで登録したときのFIDOキーと同じものだよ、ということが確認できます。これが公開鍵暗号の仕組みです。
さて、このコマンドをパスするにはauthenticatorMakeCredential
と同じくPINか生体のいずれかで本人確認します(※5)。
authenticatorMakeCredential
でrkしてた場合はクレデンシャルIDを指定しないでもAssertionをGETでき、user情報もGETできます。この辺がFIDO2のええ感じなところです。
※5 options(0x05)
でuvをFalseにするとPINも生体も無しでAssertionをGETできますが、AssertionのauthData(0x02)
にuvしたかどうかの結果(Flags)が返ってきます。ここをちゃんと見る必要があります。
参考
CTAP2 お勉強メモ#4 - 認証(Authentication)
###authenticatorGetNextAssertion(0x08)
authenticatorMakeCredential
でrpをキーにしてクレデンシャルを生成するんですが、1個のrpにつき1個のクレデンシャルしか生成できないわけではなく複数のクレデンシャルを生成することが可能です。
authenticatorGetAssertion
でrpとクレデンシャルIDを指定すれば引っ張ってくるクレデンシャルは必ず1個なんですが、rk(ResidentKey)して同じrp情報でauthenticatorGetAssertion
を何回か実行するとFIDOキーの中にはrpに対して複数のクレデンシャルが保存されます。
この状態でauthenticatorGetAssertion
でrpだけを指定して(クレデンシャルIDを指定しないで)GETするとrpに紐づくAssertionが複数ヒットし、レスポンスのnumberOfCredentials (0x05)
にAssertionの総数が入ってきてAssertion自体は1個だけ取れます。こんな時authenticatorGetNextAssertion
で2つ目以降のAssertionをGETすることができます。
参考
CTAP2 お勉強メモ#6 - Use Case - 5. rk,uvについて
###authenticatorClientPIN(0x06)
PINをアレコレするコマンドです。以下のようなサブコマンドで構成されています。
- getRetries(0x01)
- getKeyAgreement(0x02)
- setPIN(0x03)
- changePIN(0x04)
- getPINToken(0x05)
getRetries
はPINのリトライ回数を問合せます。
MakeCredential、GetAssertion、GetNextAssertionではパラメータにPinAuth
というものを指定して認証することができるのですが、このPinAuth
を生成するのがgetPINToken
です。
PINを変更したりはsetPIN
、changePIN
で行います。
重要なのがgetKeyAgreement
で、ECDHという超謎な方法でPINを暗号化する共通鍵(SharedSecret)をジェネレートするためのKeyAgreement
というパラメータを取得します(※6)。
そんで、setPIN
、changePIN
、getPINToken
はパラメータにはKeyAgreement
を使って復元不可能な暗号化をしたPINを指定する必要があります。
getPINToken
はレスポンスでpinToken
をGETできます(※7)。
※6 説明が難しい!
※7 pinAuth=LEFT(HMAC-SHA-256(pinToken,clientDataHash),16)
百聞は一見にしかず、ということでこちら参照ください。
PIN周りのシーケンス図
引用元5.5.8.3. Without pinToken in authenticatorGetAssertion
###authenticatorGetInfo(0x04)
FIDOキーの基本情報をGETするコマンドです。
接続しているFIDOキーがどんな機能を持っているかがわかります。
参考
CTAP2 お勉強メモ#2 - 接続(GetInfo)
###authenticatorReset(0x07)
FIDOキーをリセットするコマンドです。
パラメータが無いのでauthenticatorGetInfo(0x04)
とほぼ一緒です。
コマンドは簡単ですがクレデンシャルやらPINやら綺麗さっぱりクリアされるので注意。
CTAP仕様を直訳するとこんな感じです。
クライアントはこのメソッドを使用して認証システムを工場出荷時のデフォルト状態にリセットし、生成されたすべての資格情報を無効にします。 このメカニズムの偶発的なトリガーを防ぐために、何らかの形式のユーザー承認がオーセンティケーター自体で実行される場合があります。つまり、クライアントはリセットが実行されるまでデバイスをポーリングする必要があります。 リセットを実行する実際のユーザーフローは、オーセンティケーターとこの仕様の範囲外に応じて異なります。
参考
5.6. authenticatorReset (0x07)
##USB HIDとNFCとBLEがある
さて、CBOR形式でCTAPauthenticatorコマンドを作りました。これをFIDOキーに送りつけてやるのですが、HID
だとか、NFC
リーダーで読み取るとか、BLE
で無線でやるとかの仕様があります。
それぞれのインターフェース毎の低レベルのプロトコルがあり、そのPayloadにCBORエンコードしたCTAPauthenticatorコマンドを乗っけるイメージです。
送ったコマンドの応答もPayloadはCBORエンコードされています。
###USB HID(Human Interface Device)
FIDOセキュリティキーで世の中に一番出回っているタイプです。
このプロトコルがかなりめんどくさいです。
- HIDの通信は1回の送信で64byte単位なので、
初期パケット(initialization packets)
と継続パケット(continuation packets)
に分けて送信する。 - このPacketの中に
CTAPHIDコマンド
なる形式のデータをつっこむ。 -
CTAPHIDコマンド
はいくつかあり、CTAPHID_INIT(0x06)
、CTAPHID_CBOR(0x10)
などある。 - 基本的には
CTAPHID_INIT
でCID(Channel identifier-セッションIDみたいなもの)
を取得してそのほかのCTAPHIDコマンではCID
をヘッダに入れて投げるという方式。 -
CTAPHID_CBORコマン
ドにCBORエンコードしたCTAPauthenticatorコマンドを突っ込んで送る。
参考
CTAP2 お勉強メモ#2 - 接続(GetInfo)
###NFC(Near Field Communication)
NFCはAPDU(Application Protocol Data Unit)
というICカードと通信する標準コマンドで通信しますが、この中にCBORエンコードしたCTAPauthenticatorコマンドを突っ込みます。
この通信で重要なのがFIDOアプリの**AID(Application ID)**です。
FIDOアプリのAID
A0:00:00:06:47:2f:00:01:00
通信はまずFIDOアプリにSELECT FILE
してから、CBORエンコードしたCTAPauthenticatorコマンドを送信し、応答を受信します。
###BLE(Bluetooth Low Energy)
BLEはおなじみ非接触の無線通信です。これでFIDO認証できたらカッコいいですよね。
BLE通信はGATT(Generic attribute profile-ガット)
です。この中にCBORエンコードしたCTAPauthenticatorコマンドを格納して通信します。
PlatformとFIDOキーは予めペアリングしておく必要があります。
FIDO Service UUID
0000fffd-0000-1000-8000-00805f9b34fb
FIDO Characteristic UUID
Characteristic Name | UUID | Property | memo |
---|---|---|---|
FIDO Control Point | F1D0FFF1-DEAA-ECEE-B42F-C9BA7ED623BB | Write | デバイスへ送信バッファ |
FIDO Status | F1D0FFF2-DEAA-ECEE-B42F-C9BA7ED623BB | Notify | デバイスからの応答バッファ |
FIDO Control Point Length | F1D0FFF3-DEAA-ECEE-B42F-C9BA7ED623BB | Read | 送信バッファの最大サイズ(2byte) |
FIDO Service Revision Bitfield | F1D0FFF4-DEAA-ECEE-B42F-C9BA7ED623BB | Read/Write | 通信プロトコルの種類(1byte) |
FIDO Service Revision | 00002A28-0000-1000-8000-00805F9B34FB | Read | リビジョン |
通信の手順としては、FIDOキーが発進するアドバタイズパケット
をPlatformがキャプチャし、Service UUID
で識別して接続します。その後、FIDO Control Point Characteristic
に接続して送信するとFIDO Status Characteristic
に応答が返ってきます。
通信でやりとりするPacketの中にCBORエンコードしたCTAPauthenticatorコマンドを格納します。
#WindowsにFIDOキーでサインインする試み
ここまでCTAPについて超ざっくり説明してきました。
このクソ複雑なCTAPを使うとどこでもFIDOキーとお話できるようになります。
- ブラウザ君だけが持っているWebAuthnに頼らなくても、FIDOのセキュアな仕組みを実装することができます。
- AndroidやiOSのネイティブアプリでFIDOキーを使うこともできるし、Windows7など古いOSや、ラズパイでもFIDOキー使えます。
- PythonでもCでもVB6でもFIDOできますたぶん。
というわけで、過去にFIDOキーを使った試みをやりました(※8)が、今回はWindowsにFIDOキーでサインインするサンプルを作ってみました。
(Windows Helloではありません)
※8 FIDO2セキュリティキーでリモートデスクトップをする試み,
FIDO2セキュリティキーで電子署名をする試み ,
ラズパイでYubikeyを光らせてみる
##デモ動画
###1.USBとPINでログイン
Yubikeyの青いやつを使っています。
CTAPデモ USB x PIN pic.twitter.com/7umhiYSuR6
— gebo (@gebogebogeboge) December 18, 2019
###2.USBと指紋でログイン
飛天のBioPassを使っています。
CTAPデモ USB x 指紋 pic.twitter.com/2PmYvBQjYv
— gebo (@gebogebogeboge) December 18, 2019
###3.NFCとPINでログイン
YubiKeyの黒いやつを使っています。ICカードリーダーは定番PaSoRiです。
CTAPデモ NFC x PIN pic.twitter.com/wbst00IyaS
— gebo (@gebogebogeboge) December 18, 2019
###4.BLEと指紋でログイン
FeitianのAllinPassです。最高にトゥーマッチなFIDOキーで大好きです。
CTAPデモ BLE x 指紋 pic.twitter.com/AhBN946rlN
— gebo (@gebogebogeboge) December 18, 2019
##解説
Windowsの認証レイヤ(ロック画面)はICredentialProvider
とICredentialProviderCredential
インタフェースを実装したカスタムCredentialProvider
を実装することでオーバライドできます(※8)。
今回はカスタムCredentialProvider
でCTAPを実装してWindowsにサインインさせるサンプルプログラムを作成しました。
仕組みはこんな感じ
- FIDOキーにWindowログイン情報を記録します。
- RPをWindowsサインインユーザー、ResidentKeyでPasswordを書き込んでおく。
- ログインのときは、カスタムCredentialProviderでResidentKeyされているPasswordを取り出してWindowsにサインインする。
- AttestationのVerifyはやってません。
※8 クソニッチな領域です。
ソースはGitHubで公開しています。
細かい解説は今後の投稿でやっていきたいと思います。
#おつかれさまでした
以上、FIDOのCTAPについてでした。
CTAPを使うとFIDOをWeb意外の領域でも実装できます。
レガシーなシステムやIoTの分野でセキュリティが求められる要件などにFIDOが活用できるのでは、とか思っています。
大切なこと
AttestationのVerifyはちゃんとやりましょう