WebAuthnライブラリ調査めも(PyWebAuthn)
WebAuthnライブラリ調査めも(PyWebAuthn)
はじめに
そろそろ自分でもWebAuthnのライブラリを作りたいと思い、その前に既存の
WebAuthnライブラリを調査した際のメモ。
今回調査したのは以下のpy_webauthnというpythonのライブラリ。
結論から言うと、py_webauthnを利用することで以下のことができた。
- navigator.credentials.create()で必要な引数の生成
- attestationの検証
- navigator.credentials.get()で必要な引数の生成
- assertionの検証
検証環境
色々試してるときは以下の環境で行なっていました。
Python 3.7.1 pip 18.1 Google Chrome 71.0.3578.98
インストールはこんな感じでできます。
pip install webauthn
今回はversion0.4で試しました。
もちろんversionが更新され次第モジュールの中身はが変わる思いますので、詳細は公式のリポジトリやドキュメントを参照いただければ幸いです。
クラス
PyWebAuthnでFIDO2なサーバーを実装する際、主に以下の5つのクラスを利用します。
- WebAuthnMakeCredentialOptions
- WebAuthnRegistrationResponse
- WebAuthnUser
- WebAuthnAssertionOptions
- WebAuthnAssertionResponse
WebAuthnMakeCredentialOptions
navigator.credentials.create()のオプションを生成。
make_credential_options = webauthn.WebAuthnMakeCredentialOptions( challenge, rp_name, rp_id, user_id, username, display_name, icon_url) return jsonify(make_credential_options.registration_dict)
IN
名前 | 説明 | サンプル |
---|---|---|
challenge | 乱数 | eUh0QEZmFFIB9liCconUwrIInAg1wr5F |
rp_name | RPの名前 | localhost |
rp_id | RPの識別子 | localhost |
user_id | ユーザーの識別子 | mboMB0WRtZtXwLDfv8gp |
username | ユーザー名 | hoge |
display_name | ユーザーの表示名 | hoge |
icon_url | ユーザーのアイコン | 'https://excample.com' |
ここで引数として指定しないnavigator.credentials.create()のオプションはモジュールが内部で持っており、指定できなさそう。
OUT
名前 | 説明 | サンプル |
---|---|---|
make_credential_options | オブジェクト | |
make_credential_options.registration_dict | navigator.credentials.create() で利用する引数 | {'challenge': 'MwBz8Jfjy9DzkwL8OfLYL8i0NxXFtC3t', 'rp': {'name': 'localhost', 'id': 'localhost'}, 'user': {'id': 'nlCeE9NB4OjUjNth5RnU', 'name': 'fuga', 'displayName': 'fuga', 'icon': 'https://example.com'}, 'pubKeyCredParams': [{'alg': -7, 'type': 'public-key'}, {'alg': -257, 'type': 'public-key'}, {'alg': -37, 'type': 'public-key'}], 'timeout': 60000, 'excludeCredentials': [], 'attestation': 'direct', 'extensions': {'webauthn.loc': True}} |
WebAuthnAssertionOptions
navigator.credentials.get()のオプションを生成。
webauthn_assertion_options = webauthn.WebAuthnAssertionOptions( webauthn_user, challenge)
IN
名前 | 説明 | サンプル |
---|---|---|
webauthn_user | オブジェクト | nlCeE9NB4OjUjNth5RnU (fuga, fuga, 161) |
challenge | 乱数 | eUh0QEZmFFIB9liCconUwrIInAg1wr5F |
WebAuthnUser
webauthn_user = webauthn.WebAuthnUser(
user.id,
user.username,
user.display_name,
user.icon_url,
user.credential_id,
user.pub_key,
user.sign_count,
user.rp_id)
IN
名前 | 説明 | サンプル |
---|---|---|
id | ユーザーの識別子 | nlCeE9NB4OjUjNth5RnU |
username | ユーザー名 | fuga |
display_name | ユーザーの表示名 | fuga |
icon_url | ユーザーのアイコン | https://example.com |
credential_id | 公開鍵に紐づく識別子 | NlMC9-6s84VC0Hl1_3o4Z6LewYNMMtGfhzWZRMZQO9nln7A7Lk4K1in9L4iqYE-a498HW3IoEAyQoYl3sRiY0A |
pub_key | 公開鍵 | b'pQECAyYgASFYIPBdJY7nJ6pQF4JjUwRjaq3_G5LofeGqfNUlZOhg0LgeIlggCHgIXCtgr--H4Kn2mfwfQ-JDeBhn1zKMu8leMAseTrw' |
sign_count | カウンタ | 161 |
rp_id | RPの識別子 | localhost |
OUT
名前 | 説明 | サンプル |
---|---|---|
webauthn_user | オブジェクト | nlCeE9NB4OjUjNth5RnU (fuga, fuga, 161) |
WebAuthnRegistrationResponse
Attestationの検証に必要なパラメーター(主にnavigator.credentials.createの戻り)を渡してオブジェクトを生成。 verifyメソッドを呼び出すことで検証を行う。
webauthn_registration_response = webauthn.WebAuthnRegistrationResponse( RP_ID, ORIGIN, registration_response, challenge, trust_anchor_dir, trusted_attestation_cert_required, self_attestation_permitted, none_attestation_permitted, uv_required=False) # User Verification try: webauthn_credential = webauthn_registration_response.verify() except Exception as e: return jsonify({'fail': 'Registration failed. Error: {}'.format(e)})
IN
名前 | 説明 | サンプル |
---|---|---|
RP_ID | RPの識別子 | localhost |
ORIGIN | RPのオリジン | localhost |
registration_response | navigator.credentials.create()の戻り値を含むImmutableMultiDictオブジェクト | ImmutableMultiDict([('id', 'NlMC9-6s84VC0Hl1_3o4Z6LewYNMMtGfhzWZRMZQO9nln7A7Lk4K1in9L4iqYE-a498HW3IoEAyQoYl3sRiY0A'), ('rawId', 'NlMC9-6s84VC0Hl1_3o4Z6LewYNMMtGfhzWZRMZQO9nln7A7Lk4K1in9L4iqYE-a498HW3IoEAyQoYl3sRiY0A'), ('type', 'public-key'), ('attObj', 'o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEgwRgIhAPcJ8rNHotiTW3KjTYxXwttQDV0hh3yIwy_nnlSz16hXAiEAjVR0qqjZ7PClJ7UAh3TmrdHIlg_3y3aVhSb_o4DEtZJjeDVjgVkCwjCCAr4wggGmoAMCAQICBHSG_cIwDQYJKoZIhvcNAQELBQAwLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMG8xCzAJBgNVBAYTAlNFMRIwEAYDVQQKDAlZdWJpY28gQUIxIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDE5NTUwMDM4NDIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASVXfOt9yR9MXXv_ZzE8xpOh4664YEJVmFQ-ziLLl9lJ79XQJqlgaUNCsUvGERcChNUihNTyKTlmnBOUjvATevto2wwajAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuMTATBgsrBgEEAYLlHAIBAQQEAwIFIDAhBgsrBgEEAYLlHAEBBAQSBBD4oBHzjApNFYAGFxEfntx9MAwGA1UdEwEB_wQCMAAwDQYJKoZIhvcNAQELBQADggEBADFcSIDmmlJ-OGaJvWn9CqhvSeueToVFQVVvqtALOgCKHdwB-Wx29mg2GpHiMsgQp5xjB0ybbnpG6x212FxESJ-GinZD0ipchi7APwPlhIvjgH16zVX44a4e4hOsc6tLIOP71SaMsHuHgCcdH0vg5d2sc006WJe9TXO6fzV-ogjJnYpNKQLmCXoAXE3JBNwKGBIOCvfQDPyWmiiG5bGxYfPty8Z3pnjX-1MDnM2hhr40ulMxlSNDnX_ZSnDyMGIbk8TOQmjTF02UO8auP8k3wt5D1rROIRU9-FCSX5WQYi68RuDrGMZB8P5-byoJqbKQdxn2LmE1oZAyohPAmLcoPO5oYXV0aERhdGFYxEmWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjQQAAAKH4oBHzjApNFYAGFxEfntx9AEA2UwL37qzzhULQeXX_ejhnot7Bg0wy0Z-HNZlExlA72eWfsDsuTgrWKf0viKpgT5rj3wdbcigQDJChiXexGJjQpQECAyYgASFYIPBdJY7nJ6pQF4JjUwRjaq3_G5LofeGqfNUlZOhg0LgeIlggCHgIXCtgr--H4Kn2mfwfQ-JDeBhn1zKMu8leMAseTrw'), ('clientData', 'eyJjaGFsbGVuZ2UiOiJNd0J6OEpmank5RHprd0w4T2ZMWUw4aTBOeFhGdEMzdCIsIm9yaWdpbiI6Imh0dHBzOi8vbG9jYWxob3N0OjUwMDAiLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0'), ('registrationClientExtensions', '{}')]) |
challenge | 乱数 | eUh0QEZmFFIB9liCconUwrIInAg1wr5F |
trust_anchor_dir | 証明書を配置するディレクトリ | /py_webauthn/flask_demo/trusted_attestation_roots |
trusted_attestation_cert_required | 信頼されたattestation certを要求するか | True |
self_attestation_permitted | self attestationを許可するか | False |
none_attestation_permitted, | attestationの検証をするかどうか | True |
uv_required | UserVerificationを要求するか | False |
registration_responseはImmutableMultiDictオブジェクトである必要がある。以下のようにフロントエンドから送信することでImmutableMultiDictオブジェクトとして扱うことができるようだ。
const newAttestationForServer = { id: credential.id, rawId: btoa(rawId), type: credential.type, attObj: btoa(attObj), clientData: btoa(clientDataJSON), registrationClientExtensions: JSON.stringify(registrationClientExtensions) } const formData = new FormData(); Object.entries(newAttestationForServer).forEach(([key, value]) => { formData.set(key, value); }); return fetch('/attestation/result', { method: 'POST', body: formData })
WebAuthnRegistrationResponse.verify()
Attestationの検証を行う。
OUT
名前 | 説明 | サンプル |
---|---|---|
webauthn_credential | rp_id, origin, cred_id, credential_public_key, sign_countなどを含むWebAuthnCredentialオブジェクト | b'NlMC9-6s84VC0Hl1_3o4Z6LewYNMMtGfhzWZRMZQO9nln7A7Lk4K1in9L4iqYE-a498HW3IoEAyQoYl3sRiY0A' (localhost, https://localhost:5000, 161) |
WebAuthnAssertionResponse
Assertionの検証に必要なパラメーター(主にnavigator.credentials.getの戻り)を渡してオブジェクトを生成。 verifyメソッドを呼び出すことで検証を行う。
webauthn_user = webauthn.WebAuthnUser( user.ukey, user.username, user.display_name, user.icon_url, user.credential_id, user.pub_key, user.sign_count, user.rp_id) webauthn_assertion_response = webauthn.WebAuthnAssertionResponse( webauthn_user, assertion_response, challenge, origin, uv_required=False) # User Verification try: sign_count = webauthn_assertion_response.verify() except Exception as e: return jsonify({'fail': 'Assertion failed. Error: {}'.format(e)}) # Update counter. user.sign_count = sign_count
IN
名前 | 説明 | サンプル |
---|---|---|
webauthn_user | オブジェクト | nlCeE9NB4OjUjNth5RnU (fuga, fuga, 161) |
assertion_response | navigator.credentials.get()の戻り値を含むImmutableMultiDictオブジェクト | ImmutableMultiDict([('id', 'NlMC9-6s84VC0Hl1_3o4Z6LewYNMMtGfhzWZRMZQO9nln7A7Lk4K1in9L4iqYE-a498HW3IoEAyQoYl3sRiY0A'), ('rawId', 'NlMC9-6s84VC0Hl1_3o4Z6LewYNMMtGfhzWZRMZQO9nln7A7Lk4K1in9L4iqYE-a498HW3IoEAyQoYl3sRiY0A'), ('type', 'public-key'), ('authData', 'SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAAog=='), ('clientData', 'eyJjaGFsbGVuZ2UiOiJiRmRpYmI3ZnQ1UVB2Zk9RT2ZDc2ZUbDJNdUhKYXJBUCIsIm9yaWdpbiI6Imh0dHBzOi8vbG9jYWxob3N0OjUwMDAiLCJ0eXBlIjoid2ViYXV0aG4uZ2V0In0='), ('signature', '30450220439462e37bfeed9243789c91b3cfdcf95f5d4f9c1c08d725dae7892cee8bd687022100c528f5e70954213c55bfbbe8ccc201d68f06d83da69ce6c68c8092e04a1f08f3'), ('assertionClientExtensions', '{}')]) |
challenge | 乱数 | eUh0QEZmFFIB9liCconUwrIInAg1wr5F |
origin | RPのオリジン | localhost |
uv_required | User Verificationを要求するか | False |
WebAuthnAssertionResponse.verify()
Assertionの検証を行う。
OUT
名前 | 説明 | サンプル |
---|---|---|
sign_count | カウンタ | 162 |
動作確認
py_webauthnのリポジトリにはデモも用意してあり,簡単にパラメーターを変えたりして試すことができる。 github.com
以下は色々試していたときの画面。YubiKeyによる登録,認証はできた。