2015年11月3日火曜日

いまさらながらのFIDO U2F

よーし、今更ながらFIDO U2Fについて書いちゃうぞー
そろそろFIDO 2.0の仕様も公開されるんじゃないかっていうこのご時世にな!
思い立って書き始めたのに大した理由はないけど、先日買ったスマホが初めてのUAF対応のやつだったし、じゃあついでにU2Fも、くらいの勢いで。

そういえばブログ書くの2年半ぶりくらいで、ちょうどその2年半前の投稿でFIDOのことを触れていました。そのときは仕様もまだ公開前で全く盛り上がってもいなかったなあと思いつつも、今も盛り上がってるのは一部だけだったりするようなしないような。

とは言え、11/10(火)のOpenID Summit Tokyo 2015ではFIDO 2.0のセッションがあったり、11/20(金)には第2回 FIDOアライアンス東京セミナーがあったりと、盛り上がってる感はありますね。仕事がどうなるか見えないところですが、意地でも両方ともなんとかして参加したいところです。前者は思わず個人スポンサーにもなってしまったし。

FIDO概要

FIDOとはFast IDentity Onlineの略でFIDO Allianceという団体で仕様策定されており、UAFとU2Fという2つの仕様を現在公開しています。現在公開されているこれらの仕様は総称してFIDO 1.0とか呼ばれたりします。

これらの仕様のうちこの記事で触れるU2FとはUniversal 2nd Factorの略で、その名の通り二要素認証における第二要素を標準化しようというものです。そう第一要素は典型的なユーザ識別子とパスワードによる認証を使うというのが前提です。ただプロトコル上に明に第一要素だとかパスワードが現れてくるわけではないです。しかし実装上は第一要素としてパスワードはともかくユーザ識別子は必要となるかと思います(後述)。

もう一つの仕様であるUAF (Universal Authntication Framework)はパスワードレスを目指しているのでそこが大きな違いです。そしてFIDOはどうしてもパスワードレスの文脈で語られることが多いので、UAFの方が注目されちゃいますね。普及度ではU2Fの方な気がしますけど、どっちもそれほど普及しているわけでもないですねw

まあ普及に関してはそろそろ仕様公開されるんじゃないかというFIDO 2.0に期待です。SAMLもOpenIDもOAuthも広く普及したのは2.0ですし。UAFとU2Fに別れてしまったFIDO 1.0とは異なり、FIDO 2.0ではこういう区別はなくなるらしいのもいいですね。1.0からまとめとけよという気がしますが標準化には色々あるんでしょうね。そういう視点で見るとUAFを推している会社とU2Fを推している会社は綺麗に別れているのが各仕様のドキュメント群の著者からも見て取れます。前者がNok Nok LabsとPayPal、後者がYubicoとGoogleのようですね。そしてFIDO 2.0はMicrosoftの存在が大きくなるんだと思われます。

FIDO U2F仕様

シーケンス

じゃあ早速U2F仕様をシーケンス面から見てみましょう。UAFと違ってU2Fは全般的にそっけない記述なのでシーケンスすら仕様には載ってませんが、大体次の図のようになるでしょう。
FIDO U2F 基本シーケンス例
特に難しいところはないですね。普通の二要素認証とか二段階認証と呼ばれるやつの典型的なシーケンスです。

なおU2F TokenはU2F Deviceと言われたりもします。またこれはどっちかというとUAF側の用語の色が濃いですがまれにU2FでもAuthenticatorと呼ばれる場合もあります。UAFとU2F間で用語がまちまちなのはもちろん、U2F内だけでもまちまちです。フィーリングで読んでください。またU2Fのドキュメント一式の中にFIDO Technical Glossaryという用語集がありますが、これは全体的になんとなくUAF寄りの説明になっているように見えます。このドキュメントは更新のタイミングで多少差分が出る場合がありますが、基本的にUAFと同一のものとなっており、なぜUAF寄りに見えるかというと、著者の所属を見ると…もうおわかりですね?

インタフェース概要

前述のシーケンスの要求と応答と書いてあるところをインタフェースに注目してもう少し分解すると次の図のような感じになります。

FIDO U2F インタフェース

(1)から(4)が要求、(5)から(8)が応答です。

Relying PartyはClient-SideとServer-Sideに分けて書いてみました。前者は要はウェブブラウザ上で動作するJavaScriptです。そして(1)と(8)とRelying Party (Server-Side)を点線にしたのはU2Fの仕様上はここは仕様範囲外だからです。なので仕様上、UAFとは違ってU2F Serverというのは登場しないはずです。まあもちろん○○ Serverと言っても、TCPとかでのServerである必要はないので、Relying Party (特に上図でいうClient-Side) をU2F Serverと定義してもおかしくはないですが、U2F仕様上はRelying Partyで統一されています。といいつつ極一部の本編以外のドキュメントにU2F Serverという記述が出てくることもありますが、前述のようにフィーリングで(ry

(2)と(7)のHigh-level JavaScript APIと、(3)と(6)のLow-level MessagePort APIは、FIDO U2F Javascript APIというドキュメントに仕様が記載されています。後者はU2F Clientが必ず実装しないといけないAPIですが、前者はオプションです。なので上図ではRelying PartyがHigh-level JavaScript APIを叩いていますが、直接Low-level MessagePort APIを叩く場合もあります。

(4)と(5)はU2F ClientとU2F Tokenの間のインタフェースになりますが、ここはRaw Messageと呼ばれるデータをデバイス(U2F Token)に合わせて様々なトランスポートに載せてやりとりをすることになります。現在トランスポートとして、USB-HIDBluetoothNFCが定義されています。

図中に「raw」と書かれた箱がいくつか出てきますが、これがRaw Messageです。FIDO U2F Raw Message Formatsというドキュメントに仕様が記載されています。本来はU2F ClientとU2F Tokenの間のためのデータ書式と言えますが、署名とかがある関係上、当然署名検証を行うRelying Partyでも意識する必要が有ります。そしてRaw Messageは計算資源が非常に限られることも多いU2F Tokenに合わせて設計されていると思われるため、みんな大好きバイナリフォーマットです。そんなに複雑ではないですがちょっと面倒ですね。

インタフェース詳細

ではもう少し中身を見てみましょう。
その前にこれまでの図では区別してませんでしたが、実はシーケンスの要求-応答のところは、High-level JavaScript APIやLow-level MessagePort APIの名称でいうと、registerとsignの2種類のタイプがあります。前者がU2F Tokenを登録するときで、後者がそれ以降の認証のときです。

なお、以降で出てくるWebIDLで記述されたinterfaceやdictionary、Raw Messageの構成図はいずれもFIDO U2F仕様のドキュメントからの引用です。

また以降でいくつかJSON形式のデータの具体例が出てきますが、読みやすいように適宜、改行や空白を入れています。

登録時の(2)

で、まずは登録時の(2)のところですが、WebIDLで記述されたインタフェースは下記のようになります。


interface u2f {
    void register (RegisterRequest[] registerRequests, SignRequest[] signRequests, function(RegisterResponse or Error) callback, optional int? opt_timeoutSeconds);
    void sign (SignRequest[] signRequests, function(SignResponse or Error) callback, optional int? opt_timeoutSeconds);
};

登録に関係あるのは、u2f.registerの方で、RegisterRequest[]が登録に関するメインのデータですね。詳細な仕様を載せるとわかりにくくなるのでそれはU2Fのドキュメントを見てもらうとして、その具体例で示すと下記のような感じになります。

[
    {
        "version": "U2F_V2",
        "challenge": "aubUD228hfCtApQ6DDpD9srlidsusUegcWrAcyFIMfU",
        "appId": "https://centos6.toke.jp"
    }
]

appIdの詳細はFIDO AppID and Facet Specificationを見ていただくとして、とりあえずWebアプリケーションの場合はオリジンと思ってください。challengeやversionはまんまですね。

登録時の(3)

こちらもそのまま引用します。Low-level MessagePort APIでは下記のようなRequestというdictionaryが定義されています。dictionaryはJavaScriptに束縛するとオブジェクトになって、dictionaryのメンバがオブジェクトのプロパティになります。このオブジェクトをpostMessaageでウェブブラウザのU2F機能に送りつけることになります。

dictionary Request {
    DOMString          type;
    SignRequest[]      signRequests;
    RegisterRequest[]? registerRequests;
    int?               timeoutSeconds;
    optional int?      requestId;
};

これも具体例を見てみましょう。基本的にu2f.registerで指定されたパラメータを結合して、typeをつけたようなレベルですね。このRequest dictionaryは登録時も認証時も用いるので、typeでそれを区別しています。"u2f_register_request" か "u2f_sign_request" が指定されます。

{
    "type": "u2f_register_request",
    "signRequests": [],
    "registerRequests": [
        {
            "version": "U2F_V2",
            "challenge": "aubUD228hfCtApQ6DDpD9srlidsusUegcWrAcyFIMfU",
            "appId": "https://centos6.toke.jp"
        }
    ],
    "timeoutSeconds": 30,
    "requestId": 1
}

登録時の(4)

ここは前述のようにRaw Messageを様々なトランスポートに載せてU2F Client (Low-level) とU2F Tokenの間で要求-応答の通信を行います。Relying Partyを作る上ではあまり意識する必要はないところですね。ただRaw Message自体は前述のようにRelying Partyでも意識する必要があるので簡単に触れておきます。ここでやり取りされるRaw Registration Request Messageは下記のようなデータになっています。

Raw Registration Request Message

challenge parameterはその名の通りRelying Partyから送られてきたチャレンジに相当しますが、実際には下記のClient Dataと呼ばれるもののSHA-256ハッシュ値になります。


dictionary ClientData {
    DOMString             typ;
    DOMString             challenge;
    DOMString             origin;
    (DOMString or JwkKey) cid_pubkey;
};

Client Dataの具体例は下記のような感じになります。

{
    "typ": "navigator.id.finishEnrollment",
    "challenge": "aubUD228hfCtApQ6DDpD9srlidsusUegcWrAcyFIMfU",
    "origin": "https://centos6.toke.jp",
    "cid_pubkey": ""
}
typは登録時は 'navigator.id.finishEnrollment' になります。cid_pubkeyはTLSチャネルIDをブラウザやサポートしているときのみ使用されます。

登録時の(5)

(4)のあとU2F Token内で鍵ペアが生成されて、下記のRaw Registration Response MessageがU2F Clientに返されます。このRaw MessageがこのあとRelying Partyまで伝えられ、署名検証されることになります。ここで署名検証に用いられる公開鍵は図中のattestation certificateに含まれているもので、user public keyはここでは単に伝送されるだけの単なるデータの一部です。user public keyは登録時ではなく認証時の署名検証に用いられます。key handleはこのuser public keyに対応する鍵ペアを識別する値で、これも認証時に用いられます。

Raw Registration Response Message

このattestation certificateはX.509のDER形式の証明書になります。自分の使っているYubicoのU2F Tokenの場合、Issuer, Validity, Subjectは下記のような感じになっています。

Issuer: CN=Yubico U2F Root CA Serial 457200631
Validity
    Not Before: Aug  1 00:00:00 2014 GMT
    Not After : Sep  4 00:00:00 2050 GMT
Subject: CN=Yubico U2F EE Serial nnnnnnnn

Subjectの方のnnnnnnnnはU2F Tokenのシリアル番号と思われます。user public keyはRelying Party毎に生成されることになるかと思いますが、このattestation certificateはU2F Token製造時に発行されて格納されて、多分その後ずっと共通のものが使用されると思われるので、この情報を使って実質的にRelying Party横断の名寄せに使えちゃいそうですね。具体例を全部載せていないのはそういうことですw

登録時の(6)

ここは登録時の(3)に対する応答ということで、Responseというdictionaryが定義されています。また登録時の応答用にRegisterResponseというdictionaryも定義されています。


dictionary Response {
    DOMString                                   type;
    (Error or RegisterResponse or SignResponse) responseData;
    int?                                        requestId;
};


dictionary RegisterResponse {
    DOMString registrationData;
    DOMString clientData;
};

これも具体例を見てみましょう。
{
    "data": {
        "type": "u2f_register_response",
        "requestId": 1,
        "responseData": {
            "registrationData": "【websafe-base64エンコーディングされたRaw MessageのRegistration Response Message】",
            "version": "U2F_V2",
            "challenge": "aubUD228hfCtApQ6DDpD9srlidsusUegcWrAcyFIMfU",
            "appId": "https://centos6.toke.jp",
            "clientData": "eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZmluaXNoRW5yb2xsbWVudCIsImNoYWxsZW5nZSI6ImF1YlVEMjI4aGZDdEFwUTZERHBEOXNybGlkc3VzVWVnY1dyQWN5RklNZlUiLCJvcmlnaW4iOiJodHRwczovL2NlbnRvczYudG9rZS5qcCIsImNpZF9wdWJrZXkiOiIifQ"
        }
    }
}

clientDataのところで出てきましたねeyJ。もちろんここはみなさん迷わずbase64デコードするところだと思いますが、これの中身は既に登録時の(4)のところで見ましたね。ちなみにU2Fのドキュメントではwebase-base64が使われていると書かれていますが、このwebsafe-base64とはJSON Webなんとかでおなじみのいわゆるbase64urlのことです。'+'の代わりに'-'、'/'の代わりに'_'を使うアレです。

registrationDataは元データがJSONじゃないのでeyJしてませんが、同様にwebsafe-base64エンコードされています。元データは登録時の(5)で出てきたRaw Registration Response Messageです。

登録時の(7)

いよいよ最後です。とは言っても登録時の(2)で指定したcallback関数に登録時の(6)で出てきた responseDataが渡されるくるだけです。

認証時の(2)

interfaceは登録時の(2)で認証時に用いるu2f.signも載せましたね。認証時の場合は、SignRequest[]がメインのデータですね。その具体例で示すと下記のような感じになります。

[
    {
        "version": "U2F_V2",
        "challenge": "CAkFgYNpVySxIduggCxxB06_0Da47LUvpUWPH3_--JE",
        "keyHandle": "ZPWBYmapVxKnk4oKWotihGT2TPsXV-_w9FREk3vhfx7wCGjzRRWlyT1m9borfU6ZpXqbiR36CzIF30ybhI-_-w",
        "appId": "https://centos6.toke.jp"
    }
]

ここでRelying Partyから認証要求するときにkeyHandleが指定されています。このkeyHandleは登録時にユーザの所持するU2F Token内で生成された鍵ペアを識別する値でしたね。これが何を意味するかというと、Rerying PartyがFIDO U2Fの認証要求を行うときは既に、そのユーザを認証もしくは最低限でも識別をしている必要があることになるわけです。識別をしたユーザに対応するkeyHandleをRelying Partyの登録ユーザDB等から取り出して認証要求に埋め込む必要があるわけなので。めんどくさい仕様だなあと思いますけどUniversal 2nd Factorですからね。

認証時の(3)

前述の通り登録時の(3)と同じRequest dictionaryを用います。具体例は下記です。typeがu2f_sign_requestになっている以外、特筆するところはなさそうですね。

{
    "type": "u2f_sign_request",
    "signRequests": [
        {
            "version": "U2F_V2",
            "challenge": "CAkFgYNpVySxIduggCxxB06_0Da47LUvpUWPH3_--JE",
            "keyHandle": "ZPWBYmapVxKnk4oKWotihGT2TPsXV-_w9FREk3vhfx7wCGjzRRWlyT1m9borfU6ZpXqbiR36CzIF30ybhI-_-w",
            "appId": "https://centos6.toke.jp"
        }
    ],
    "timeoutSeconds": 30,
    "requestId": 1
}

認証時の(4)

ここでやりとりされるRaw MessageであるAuthentication Request Messageは下記のようなデータになっています。あまり見るべきところはないですが、control byteの値によって指定されたkey handleが登録済みかどうかをチェックするモードか、通常の認証処理を行うモードかを指定できます。

Raw Authentication Request Message

認証時の(5)

下記の図で表されるRaw Authentication Response Messageが返却されます。

Raw Authentication Response Message

前述のように認証時のsignatureはkey handleで識別される鍵ペアの秘密鍵で生成された署名です。この鍵ペアの公開鍵に対応するのが登録時の(5)で出てきたuser public keyです。

user presenceは、その名の通りユーザが存在していたかを示します。ユーザの存在はU2F Tokenについているボタンにユーザがタッチしたかとかで確認するのだと思います。いずれにせよ存在せずに成功させることは現バージョンでは想定していないと思うのでここのバイトは0x01固定になります。

counterはU2F Tokenの複製を検出するために使われるカウンタ値を示します。U2F Tokenは内部で認証のたびにインクリメントされるカウンタを持っていて、その値をこのcounterに埋め込みます。Relying Party側では前回の認証時のカウンタ値を保持しておき、このcounterで示されるカウンタ値が前回のカウンタ値と同じだったり、小さかったりしたら、何かがおかしいということでカウンタ検証エラーとするわけです。現実的な脅威に対してどの程度有効なのかはよくわかりません。なおカウンタはU2F Token内で要求のapplication parameter (≒Relying Party)毎に持つか、application parameter横断でグローバルに持つかは計算資源等を考慮して実装に任されています。自分の持っているYubicoの製品はグローバルカウンタでした。

認証時の(6)

登録時の(6)と同じResponse dictionaryが返されます。その中に含まれるSignResponse dictionaryの定義は下記になります。


dictionary SignResponse {
    DOMString keyHandle;
    DOMString signatureData;
    DOMString clientData;
};

具体例で示すと下記のような感じになります。

{
    "data": {
        "type": "u2f_sign_response",
        "requestId": 1,
        "responseData": {
            "keyHandle": "ZPWBYmapVxKnk4oKWotihGT2TPsXV-_w9FREk3vhfx7wCGjzRRWlyT1m9borfU6ZpXqbiR36CzIF30ybhI-_-w",
            "clientData": "eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZ2V0QXNzZXJ0aW9uIiwiY2hhbGxlbmdlIjoiQ0FrRmdZTnBWeVN4SWR1Z2dDeHhCMDZfMERhNDdMVXZwVVdQSDNfLS1KRSIsIm9yaWdpbiI6Imh0dHBzOi8vY2VudG9zNi50b2tlLmpwIiwiY2lkX3B1YmtleSI6IiJ9",
            "signatureData": "AQAAAHcwRAIgK2Gu2C57S3V8iNQFmUGZrOrI0a5gCntFMqibdKJVL1oCIG121eeePR18fQLxkGpIcX0fyAsQFnPADYa3LieZWoYj"
        }
    }
}

このsignatureDataは認証時の(5)のRaw Authentication Response Messageをwebsafe-base64エンコードしたものです。

認証時の(7)

いよいよ最後です。とは言っても認証時の(2)で指定したcallback関数に認証時の(6)で出てきた responseDataが渡されるくるだけです。

あー疲れたw

FIDO U2F実装

Client

Linuxとかのログインに使えるpam-u2fのようなのもあるけど、基本的なユースケースはWebブラウザだろうし、実質的にはGoogle Chromeのみといった感じでしょうか。

Google Chromeではバージョン38以降で使用できますが、使えるのはLow-level MessagePort APIだけです(後述)。なおFIDO U2F (Universal 2nd Factor) extensionを使った例をサンプルプログラムとかでよく見かけたんですが、このextension相当の機能がChrome本体に入っているのでこのextenstionをインストールする必要はありません。このextensionを使っている場合は、クライアントサイドのコード中にextention IDとして「pfboblefjcgdjicmnffhdgionmgcdmne」が出てくることになります。Chrome本体の同等の機能は「CryptoToken Component Extension」と呼ばれているようで、「kmendfapggjehodndflmmgagdbamhnfd」というextension IDが使われています。余談ながら、このComponet Extensionと呼ばれる(?)拡張機能相当の機能は通常はChromeのextention(拡張機能)一覧に見えませんが、例えばMacの場合は下記のようにオプションをつけて起動すれば存在を確認することができます。

$ open "/Applications/Google Chrome.app" --args --show-component-extension-options

CryptoToken Component Extension

しかしどういうわけか、このChrome本体に取り込まれているU2F機能はLow-level MessagePort APIの実装部分だけで、High-level JavaScript APIの実装部分は前述のFIDO U2F extensionには含まれているのに、Chrome本体には含まれていません。なのでHigh-level JavaScript APIを使いたいRelying Partyは、このFIDO U2F extensionのHigh-level JavaScript APIのファイルを自サーバに配置することになるでしょう。その際、同ファイル内に出てくるextension IDの「pfboblefjcgdjicmnffhdgionmgcdmne」を「kmendfapggjehodndflmmgagdbamhnfd」に書き換えることをお忘れなく。

Firefoxもずっと前から実装しよう話は出てたのですが、ついに先日extensionが出ましたね。Mac版 Firefox 41.0.2 / U2F Support Add-on 0.0.2 / Yubico FIDO U2F SPECIAL SECURITY KEY の組み合わせで問題なく動いているように見えました。High-level JavaScript APIも動いています。

Token

一番メジャーなのはYubicoの製品群だと思いますが、FIDO AllianceのサイトFIDO Certifiedのページを見ると他にも色々あるようです。自分もYubicoの青いやつ (FIDO U2F SPECIAL SECURITY KEY) を使ってます。
Yubicoの青いやつ

写真の真ん中に金色の部分がありますが、登録・認証時はこの部分の鍵マークが青く光り、その部分に指でタッチするとuser presenceが確認できたということになります。

Relying Party

メジャーどころで知っているのは下記の3つです。自分も一応3つとも設定していますが、普段使っているのはいずれもソフトウェアトークン(TOTP)による二段階認証です。U2F Token持ち歩くの面倒だし、なくしそうだしw


上記の内、DropboxとGitHubはHigh-level JavaScript APIを使っていますが、GoogleはLow-level MessagePort APIを使っています。Googleは余計なものは要らないってポリシーでChrome本体にも前者のAPIが含まれていないんですかね。

FIDO 2.0が控えている状況ではこれ以上Relying Partyはそう増えないかもですね。あの多要素認証の方式を貪欲に増やしていくLastPassですら対応してくれないしw でもFirefoxでもU2F使えるようになってきたし、この対応しない理由を見るとLastPassもそのうち対応するんですかね。

貪欲

自分のサイトをFIDO U2F対応Relying Partyにするには?

YubicoとかGoogleがRelying Party向け(だけではないけど)ライブラリとかの実装をサンプルとともに公開しているのでそれ使ってください。上で色々仕様書いてきたけど、こんなこと知らなくてもあっさり作れます。超簡単。
結局のところ、結論は、

             /)
           ///)
          /,.=゙''"/
   /     i f ,.r='"-‐'つ____   こまけぇこたぁいいんだよ!!
  /      /   _,.-‐'~/⌒  ⌒\
    /   ,i   ,二ニ⊃( ●). (●)\
   /    ノ    il゙フ::::::⌒(__人__)⌒::::: \
      ,イ「ト、  ,!,!|     |r┬-|     |
     / iトヾヽ_/ィ"\      `ー'´     /

0 件のコメント:

コメントを投稿