marketplace.2019-06-04WebAuthnことはじめAuthこんにちは。MONTHの12日目の記事です。取扱WebAuthnのライブラリまとめはじめに人類は20年以上前からパスワードと戦ってきました。子供の誕生日をパスワードとして使うのをやめて乱数生成器に生成させたり、ノートにパスワードを書き込むのをやめて、1passwordを使ったりして進化してきました。最近ようやくパスワード自体が問題の根源だと理解し、パスワードを使わない認証技術に手を付けた。パスワードの代わりに、指紋・虹彩などの生体特徴や、セキュリティチップが組み込まれたUSBキー、Android携帯などの端末を利用して認証できる仕組みと、それを利用するWebサービスが増えてきました。Allianceが発表した標準仕様FIDO2の一部として策定され、ブラウザ経由でパスワードレス認証を可能にする仕組です。この仕組は、2019年3月にW3Cの標準化勧告にされていて、注目を集めています。各メジャーなブラウザもすでにWebAuthnをサポートしており、開発者にとって実装環境が整っています。メルカリでは常にこういった新しい技術に対するキャッチアップを行っており、技術検証(あとは単純に筆者の趣味)の文脈で、Go言語でWebAuthnの認証サーバを作ってみました。本記事では、WebAuthnの仕組みと実装を紹介したいと思います。WebAuthnについて概要WebAuthnは、パスワードレス認証を可能にするための仕組み(API)です。APIの拡張として策定されています。多くの認証システムは、サーバに送信されたID/パスワードの組み合わせの正しさをサーバが検証することで、認証を行ってますが、WebAuthnの認証システムでは、ID/パスワードの代わりに、公開鍵/秘密鍵の組み合わせを利用して認証を行います。認証の大まかな流れは以下になります。WebAuthnによる認証の大まかな流れID/パスワードを利用する認証システムと異なって、WebAuthnの認証システムを利用することで、サービス提供者にでもサービス利用者にでもいくつかメリットがあるかと思います。クレデンシャルとなる秘密鍵は認証器に保存されているため、ユーザはパスワードを暗記する必要がありません。そのため、パスワードを忘れたり他人に見られたりすることはありません。サービス提供者はパスワードの代わりに公開鍵を保存することになります。そのため、オペミスや外部からの攻撃によって、パスワードを漏洩することがなくなります。その際公開鍵が流出されることになり、パスワード漏洩と同じ深刻だと感じる人がいるかもしれませんが、サーバ管理者にとって秘密情報が含まれているパスワード(ハッシュ化済みのものにしても)を管理するに比べると、セキュリティリスクが低くなると言えるでしょう。ネットワーク経路上にパスワードが流れるとがなくなり、認証プロセスがより安全になります。WebAuthnの認証システムにも問題点があると考えています。記憶できるパスワードに比べると、物理の認証器はなくしやすいと感じる方がいるかもしれません。認証器を紛失した場合、パスワードの紛失と同様にセキュリティリスクが生じます。テキスト入力で完結できるパスワード認証に比べると、認証器を操作する必要があるため、認証のプロセスが煩雑と感じる人がいるかもしれませんし、認証器の携帯や保管が大変だと感じる人もいるかもしれません。システム構成WebAuthnを利用した認証システムは、以下の3つのパートで構成されています。Partyとも呼ばれてます)WebAuthnの標準仕様に従って実装されています。サポートする必要があります。いうプロトコルを利用して通信し、認証に必要な情報を提供してもらいます。etc..)はWebAuthnをサポートしています。秘密鍵、デジタル証明書が保存されている認証器ユーザはなにかしらの手段で自分の秘密鍵を保持する必要があります。異なるPC上で認証するニーズがあれば、秘密鍵を安全に保管できる、セキュリティチップなどが組み込まれたUSB型の認証キー(例えばyubikey)、あるいはスマートフォンを認証器として利用することが多いです。登録と認証パスワード認証と同様に、WebAuthnの認証システムも登録と認証の2つのステップで構成されます。個人情報だけでなく、認証のために認証器の公開鍵も登録します。認証器を利用してユーザ認証を行います。AttestationResponseの2つのエンドポイントを提供する必要があります。処理の流れは以下になります。ユーザが登録情報をRegisterエンドポイントに送信し、ユーザ登録を要求します。これによってユーザ/認証器登録が始まります。このエンドポイントは登録対象のユーザー情報を受け取ります。エンドポイントのリクエストの仕様やサーバの挙動はWebAuthnに定められていません。アカウント登録時に必要な情報をサーバに送信します。以下の例はログイン時に必要なusernameのみをリクエストに含めたものです。サービスの仕様によって、氏名や住所など必要なユーザ情報を一緒に送信します。auth.example.comusername=nerocrux送られた情報はあとで参照しますので、適切なバリデーションを行い、DBに一度保存します。サーバがフロントエンド(Javascript)に対して、レスポンスを返します。ユーザ情報を保存後、認証サーバはChallengeを生成しユーザ情報と一緒に保存しておきます。RelyingPartyの情報など、次のスッテップに必要な情報を含めたレスポンスを返します。このレスポンスのフォーマットは特に決まっていませんが、ステップ2のJavascriptプログラムが取扱しやすくするため、なるべくWebAuthnの仕様に合わせると良いでしょう。今回では以下の形のレスポンスを返します。"attestation":"direct"}各パラメータの説明を以下にまとめています。署名の正当性を検証するためのランダムな文字列。攻撃者に入手されると公開鍵が攻撃者のものに置換られる脆弱性が生じるので、サーバ上に生成し、類推されにくい文字列である必要があります。仕様では16文字以上の長さを持つランダム文字列であることを推奨しています。認証サーバの情報が含まれています。ユーザの登録情報が含まれています。Preferenceというもので、認証器からクレデンシャルをどのように認可サーバが受け取るかを示します。いうパラメータを通して直接に受け取るを意味します。ついて後ほど説明します。呼び出し、認証器に認証データを用意させます。Javascript側では、以上の情報の他にいくつかのパラメータをリクエストに加えて、navigator.credentials.create()を呼び出します。Creationに記載されています。今回では以下のリクエストを作っています。このパラメータは、認証器はどのような暗号アルゴリズムを使って鍵を作成するかを指定します。Algorithmに準していて、こちらから参照できます。そのほかにも以下のようなパラメータがあります。クロスプラットフォームの認証器であるyubikeyを使わせるか、それともプラットフォーム限定のTouchIDのみに限定させるか、など。3で説明するユーザと認証器のインタラクションのタイムアウト値。用意し、その他のパラメータに合わせて、ブラウザは認証器に対してauthenticatorMakeCredential()コマンドを呼び出します。Objectで、デコードすると以下のような情報が見れます。"type":"webauthn.create"}ブラウザの挙動やブラウザと認証器の間のやり取りは我々認証サーバの開発者は特に意識する必要がありませんが、興味があればCTAP2の仕様を覗いてみると楽しいです。認証器がユーザ確認を行い、公開鍵とAttestationを作成します。ブラウザが認証器を選択し、本人確認を行いなさいを促すポップアップを表示します。認証器選択ポップアップyubikeyを利用する場合、ユーザが本人の存在確認(光るボタンを指でタップする)を行います。認証器の仕様によって、指紋や虹彩等によって本人確認も行う場合もあります。本人確認が成功したら、認証器がこの登録セッションのために、新しい公開鍵とAttestationを作成します。Attestationとは、認証器の有効性や正当性を証明するためのパラメータです。認証器が作成された公開鍵とAttestation等の認証情報をattestationObjectとしてブラウザに返します。attestationObjectはCBORエンコードされた上でbase64urlエンコードされています。詳しくはAttestationについてパートにて説明します。Javascriptは、サーバのAttestationResponseエンドポイントに送信します。ブラウザがattestationObjectを取得後に、navigator.credentials.create()メソッドのレスポンスを用意してJavascriptに返します。attestationObjectのほかにいくつかのパラメータが追加されてます。具体的なレスポンスは以下の形になります。"type":"public-key"}各パラメータの意味は以下になります。サーバは認証情報を公開鍵を使って検証し、情報が正しければ登録を成功とします。認証サーバは受信した認証情報を検証します。検証することで、登録しようとする公開鍵は、登録に使われている正当性のある認証器から生成されたものであることを確認できます。成功レスポンスを返し、認証時に利用するために、公開鍵をDBに保存します。これでユーザ/認証器の登録が完了になります。AssertionResponseの2つのエンドポイントを提供する必要があります。処理の流れは以下になります。Loginエンドポイントに送信し、認証を要求します。登録と同様に、loginエンドポイントのリクエスト仕様もWebAuthnの仕様の範囲外です。通常、ユーザIDなど、ユーザを特定できるユニークな識別子を利用することが多いでしょう。サーバが登録済みチェックを行い、フロントエンド(Javascript)に対して、レスポンスを返します。登録に似たように、認証サーバがChallengeを生成し、DBに保存します。認証サーバはDBに保存されている認証器が作成した公開鍵の情報をallowCredentialsに格納して返します。こちらも登録と同様に、レスポンスの仕様が決まってないですが、ステップ2のJavascript側のプログラムを取扱しやすくするように、レスポンスはWebAuthnの仕様に合わせると良いでしょう。]}allowCredentialsに含まれている情報は以下です。challenge以外に、認証と同様に、認証器での本人確認のtimeoutを設定することも可能です。呼び出し、認証器に認証データを用意させます。登録と似たように、Javascriptはサーバから受け取った情報を利用し、navigator.credentials.get()を呼び出します。呼び出す際のリクエストは、1で示している認証サーバから受信したリクエストをそのまま利用してよいです。用意し、認証器に対してauthenticatorGetAssertion()コマンドを呼び出します。認証器がユーザ確認を行い、Assertionを作成します。認証器が署名した、ChallengeやRelyingPartyの情報が含まれたステートメントです。ChallengeやRelyingPartyの情報はclientDataJSONをハッシュ化した形(clientDataHashと命名されている)で認証器に渡されます。それに加え、認証器は自身の情報が含まれているauthenticatorDataを含めたステートメントを秘密鍵で署名します。signatureと呼ばれます。認証器が作成された認証情報をブラウザに返します。authenticatorDataをブラウザに返します。Javascriptが認証器から受信した認証情報やclientDataを含めたリクエストを用意し、AssertionResponseエンドポイントに送信します。エンドポイントに送信します。リクエストは以下の形になります。サーバは認証情報を公開鍵を使って検証し、情報が正しければ登録を成功とします。認証サーバはsignatureの有効性を検証します。セッションを発行しセッションIDをCookieにセットする等、一般的な認証サーバと同じように処理を継続します。ついてAttestationは、主に公開鍵をサーバが保存するときに、その公開鍵は攻撃者が置き換えたものではなく、ユーザが所持する認証器から生成されたものであることを保証するための仕組みです。AttestationResponseへのリクエストの中に、認証器の情報や公開鍵などの情報が含まれたAttestationObjectの有効性を認証サーバが検証することで、登録しようとする公開鍵は登録対象となる認証器による生成されたものかを検証できます。認証器の種類によってAttestationのフォーマットが異なります。SafetyNetのAttestationをサポートしなければなりません。Formatは以下の6種類があります。U2Fフォーマットを例として、Attestation検証を紹介します。Formatsとの互換性を担保したフォーマットで、よく見かけるyubikeyはこのフォーマットをサポートしています。Keyと呼ばれる秘密鍵が埋め込まれています。CAに署名されると思われます。CAに署名されています。Certificateと一緒にAttestationObjectに含まれ、認証サーバに送信されます。Certificateを使って署名を検証し、署名が有効であれば一緒に送られてきた公開鍵が有効である(登録しようとする、正当性のある認証器が作られた公開鍵である)ことを判定できます。この図にhash対象となるverificationDataを登場させると大変複雑になってしまうので、公開鍵/証明書の関係性を示すことにフォーカスしました。フォーマット前文で紹介したとおり、AttestationObjectはbase64urlでエンコードされているCBORです。AttestationObjectの構成は以下になります。実際CBORパース後のAttestationObjectは以下の形になります。"authData":h'49960DE5880...'}各パラメータの意味は以下になります。署名はどう表現されるか、認証器がattestation情報を表現するためのattStmtはどういったフォーマットになっているかを示すパラメータです。Signature(sig)などが含まれています。認証器に作られた、認証器の信頼性やセキュリティ等に関連する情報が含まれているバイナリデータです。authenticatorDataについて後ほど詳しく説明します。検証について説明します。procedureの部分に記載されてますが、全部紹介すると長くなりますので、ポイントと思うところをピックアップして紹介したいと思います。デコードまずAttestationResponseで受信した文字列のattestationObjectをデコードします。結果はAttestationObject構造体になります。(AttestationObject.AuthData)をパースします。認証器に作られた、認証器の信頼性やセキュリティ等に関連する情報が含まれているバイナリデータです。このパラメータを決められたbyte数でパースすることで、いくつかのパラメータを取得できます。これらのパラメータの意味はこちらを参照するとわかりやすいです。authenticatorDataの中に、attestedCredentialDataというパラメータがあり、認証器の情報や公開鍵の情報が含まれてます。このパラメータもバイナリで表現されており、利用時にauthenticatorDataと同様に、パースする必要があります。パース方法や各パラメータの意味はこちらから参照できます。以下のようにパースします。証明書の公開鍵に書き出します。COSEとはCBORを使って署名と暗号化を行うプロトコルで、JWTを使ったJOSEに似たようなものだと理解するとよいかと思います。以下のようにパースします。&v}そして、得られた構造体を使って、以下のように公開鍵(publicKeyU2F)を書き出します。準備ここに来ると、署名対象となるverificationDataを組み立てる準備ができました。以上のステップで得られたパラメータ(バイナリ)を組み合わせることで得られます。具体的は以下のパラメータを利用します。デコードし、SHA-256でハッシュ化することで得られます。sha256.Sum256(clientData)各パラメータはこんな感じで連結します。Chain)の中に、バイナリ形式(ASN.1データ構造)の証明書が含まれています。goでX.509証明書を取り扱うときに、ASN.1からx509.Certificate構造体に変換すると取扱しやすいです。U2FのAttestationで利用する証明書(attestnCert)は、必ず配列の1つ目の証明書を使うと仕様に決められています。署名検証これで署名検証のためのすべてのパラメータが揃えました。true}この検証が成功すれば、AttestationObjectが有効であり、Attestation成功となります。signature)の有効性を検証することで、認証器の有効性を検証する仕組みです。Attestationの図とは対称性がないですが、大体の流れはこちらの図で表現してみました。検証W3Cにちゃんと定義されているAssertionの検証手順があります。signature検証のところをピックアップして紹介したいと思います。Assertionの署名を検証するには、authenticatorDataとclientDataJSONを利用します。異なる点は、公開鍵が含まれていないところです。パースする必要があります。こちらもだいぶシンプルになってます。組み合わせることで得られます。デコードする必要があります。デコードし、SHA-256でハッシュ化することで得られます。各パラメータはこんな感じで連結します。公開鍵の取得と署名検証Assertionの署名検証はDBに保存されている公開鍵(COSE)を利用します。COSEをAttestation時と同様にパースし、以下の検証のロジックを通して、署名の有効性を検証します。nil}これで、署名検証が完了になります。取扱attestation時のattestationObjectやassertion時のauthenticatorDataはCBORになっています。開発時にデバッグするためにCBORをパースしてみたいという気持ちもあるはずです(Valueはだいたいバイナリでパースしても意味がないが、Keyの名前を確認したり、そもそも正しいCBORになっているかを確認するためにパースしたくなるはず)ので、そのときは以下のツールが役に立つと思います。パースするhttp://cbor.me/goでCBORをパースする際、以下のライブラリを利用しています。http://github.com/ugorji/go/codecWebAuthnのライブラリそもそもフルスクラッチでWebAuthnを実装するのが大変なので、なんかいい感じのライブラリがあるかな?と思う人がいると思います。僕は実装時に以下のライブラリを参考しました。https://github.com/duo-labs/webauthnGo言語以外のライブラリもあるはずですが、あまりリサーチしてなかったので紹介できません。まとめ今回は、WebAuthnを実装レベルで理解するため、WebAuthnの認証サーバを実装しながら標準仕様を読みました。完全理解までまだほど遠いですが、WebAuthnの基本的な概念や、登録・認証の流れ、Attestation/Assertionのやり方が大体理解出来ました。この学習プロセスの中で得た知見を本文にまとめてみましたが、ご一読いただき、WebAuthnに対して興味を持っていただけると幸いです。今後ですが、認可サーバの機能実装だけでなく、運用を含めてWebAuthnの認証サーバのあるべき姿を探究したいと考えています。認証器を紛失したときに、認可サーバはどのようなリカバリ方法を提供するか。漏洩された場合、どうやって問題のある認証器を特定しRevokeするか。Webチームの@urahiroshiです。MONTHの10日目の記事です。Functionsの関数コール権をIAMで管理するためのテクニックこんにちは。Calendar10日目の記事です。採用情報メルカリは常に優秀なソフトウェアエンジニアを募集しています。ご興味を持たれた方はぜひご応募ください。skills月別アーカイブスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。