WebAuthnライブラリ調査めも(PyWebAuthn)

WebAuthnライブラリ調査めも(PyWebAuthn)

はじめに

そろそろ自分でもWebAuthnのライブラリを作りたいと思い、その前に既存の WebAuthnライブラリを調査した際のメモ。
今回調査したのは以下のpy_webauthnというpythonのライブラリ。

github.com

結論から言うと、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による登録,認証はできた。

f:id:kent056-n:20190120183432p:plain

f:id:kent056-n:20190120183519p:plain

f:id:kent056-n:20190120183536p:plain

f:id:kent056-n:20190120183600p:plain

参考