LoginSignup
29
19

More than 3 years have passed since last update.

CTAP超ざっくりまとめ&WindowsにFIDOキーでサインインする試み

Last updated at Posted at 2019-12-21

はじめに

本記事は認証認可技術 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が動いていて、BrowserWebAuthnを実装していて、そのWebAuthnの内部ではAuthenticator(FIDOキー)CTAPで通信します。

CTAP.png

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)です。
CBORCBORです。細かいことは知りません。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 パンクしたらどうなるのか、やってみましょう。

参考
CTAP2 お勉強メモ#3 - 登録(Create)

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を変更したりはsetPINchangePINで行います。
重要なのがgetKeyAgreementで、ECDHという超謎な方法でPINを暗号化する共通鍵(SharedSecret)をジェネレートするためのKeyAgreementというパラメータを取得します(※6)。
そんで、setPINchangePINgetPINTokenはパラメータには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

参考
CTAP2 お勉強メモ#5 - PIN

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_INITCID(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コマンドを送信し、応答を受信します。

詳細
CTAP2 お勉強メモ#7 - NFC

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コマンドを格納します。

参考
CTAP2 お勉強メモ#8 - BLE

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の青いやつを使っています。

2.USBと指紋でログイン

飛天のBioPassを使っています。

3.NFCとPINでログイン

YubiKeyの黒いやつを使っています。ICカードリーダーは定番PaSoRiです。

4.BLEと指紋でログイン

FeitianのAllinPassです。最高にトゥーマッチなFIDOキーで大好きです。

解説

Windowsの認証レイヤ(ロック画面)はICredentialProviderICredentialProviderCredentialインタフェースを実装したカスタム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はちゃんとやりましょう

29
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
19