diff --git a/examples/cred_blob.py b/examples/cred_blob.py index 212912d..e0e3040 100644 --- a/examples/cred_blob.py +++ b/examples/cred_blob.py @@ -41,7 +41,7 @@ # Prefer UV token if supported uv = "discouraged" -if info.options.get("uv") or info.options.get("bioEnroll"): +if info and (info.options.get("uv") or info.options.get("bioEnroll")): uv = "preferred" print("Authenticator is configured for User Verification") diff --git a/examples/credential.py b/examples/credential.py index 9d66047..54b6f64 100644 --- a/examples/credential.py +++ b/examples/credential.py @@ -39,7 +39,7 @@ # Prefer UV if supported and configured -if info and info.options.get("uv") or info.options.get("bioEnroll"): +if info and (info.options.get("uv") or info.options.get("bioEnroll")): uv = "preferred" print("Authenticator supports User Verification") else: diff --git a/examples/hmac_secret.py b/examples/hmac_secret.py index 236207a..4ef8575 100644 --- a/examples/hmac_secret.py +++ b/examples/hmac_secret.py @@ -34,32 +34,28 @@ is now allowed in a browser setting. See also prf.py for an example which uses the PRF extension which is enabled by default. """ -from fido2.hid import CtapHidDevice from fido2.server import Fido2Server -from fido2.client import Fido2Client, WindowsClient +from fido2.client import Fido2Client from fido2.ctap2.extensions import HmacSecretExtension -from exampleutils import CliInteraction +from exampleutils import CliInteraction, enumerate_devices import ctypes import sys import os +# Use the Windows WebAuthn API if available, and we're not running as admin try: - from fido2.pcsc import CtapPcscDevice -except ImportError: - CtapPcscDevice = None - + from fido2.client.windows import WindowsClient -def enumerate_devices(): - for dev in CtapHidDevice.list_devices(): - yield dev - if CtapPcscDevice: - for dev in CtapPcscDevice.list_devices(): - yield dev + use_winclient = ( + WindowsClient.is_available() and not ctypes.windll.shell32.IsUserAnAdmin() + ) +except ImportError: + use_winclient = False uv = "discouraged" -if WindowsClient.is_available() and not ctypes.windll.shell32.IsUserAnAdmin(): +if use_winclient: # Use the Windows WebAuthn API if available, and we're not running as admin # By default only the PRF extension is allowed, we need to explicitly # configure the client to allow hmac-secret @@ -105,12 +101,15 @@ def enumerate_devices(): credentials = [auth_data.credential_data] # HmacSecret result: -if not result.client_extension_results.get("hmacCreateSecret"): - print("Failed to create credential with HmacSecret") - sys.exit(1) +if result.client_extension_results.get("hmacCreateSecret"): + print("New credential created, with HmacSecret") +else: + # This fails on Windows, but we might still be able to use hmac-secret even if + # the credential wasn't made with it, so keep going + print("Failed to create credential with HmacSecret, it might not work") + credential = auth_data.credential_data -print("New credential created, with the HmacSecret extension.") # Prepare parameters for getAssertion allow_list = [{"type": "public-key", "id": credential.credential_id}] diff --git a/examples/prf.py b/examples/prf.py index 553e40d..d70a7fd 100644 --- a/examples/prf.py +++ b/examples/prf.py @@ -33,7 +33,6 @@ from fido2.server import Fido2Server from fido2.utils import websafe_encode from exampleutils import get_client -import sys import os @@ -66,9 +65,12 @@ credential = auth_data.credential_data # PRF result: -if not result.client_extension_results.get("prf", {}).get("enabled"): - print("Failed to create credential with PRF", result.client_extension_results) - sys.exit(1) +if result.client_extension_results.get("prf", {}).get("enabled"): + print("New credential created, with PRF") +else: + # This fails on Windows, but we might still be able to use prf even if + # the credential wasn't made with it, so keep going + print("Failed to create credential with PRF, it might not work") print("New credential created, with the PRF extension.") diff --git a/fido2/client/win_api.py b/fido2/client/win_api.py index 82e2f8e..18ba854 100644 --- a/fido2/client/win_api.py +++ b/fido2/client/win_api.py @@ -41,6 +41,11 @@ from ..utils import websafe_decode from ..webauthn import AttestationObject, AuthenticatorData, ResidentKeyRequirement +from ..ctap2.extensions import ( + AuthenticatorExtensionsPRFInputs, + HMACGetSecretInput, + AuthenticatorExtensionsLargeBlobInputs, +) from enum import IntEnum, unique from ctypes.wintypes import BOOL, DWORD, LONG, LPCWSTR, HWND, WORD @@ -291,7 +296,7 @@ class WebAuthNCredWithHmacSecretSalt(ctypes.Structure): def __init__(self, cred_id, salt): self.cred_id = cred_id - self.salt = ctypes.pointer(salt) + self.pHmacSecretSalt = ctypes.pointer(salt) class WebAuthNHmacSecretSaltValues(ctypes.Structure): @@ -1129,30 +1134,37 @@ def get_assertion( u2f_appid = extensions["appid"] if extensions.get("getCredBlob"): win_extensions.append(WebAuthNExtension("credBlob", BOOL(True))) - if "largeBlob" in extensions: - if extensions["largeBlob"].get("read", False): + large_blob = AuthenticatorExtensionsLargeBlobInputs.from_dict( + extensions.get("largeBlob") + ) + if large_blob: + if large_blob.read: large_blob_operation = WebAuthNLargeBlobOperation.GET else: - large_blob = extensions["largeBlob"]["write"] + large_blob = large_blob.write large_blob_operation = WebAuthNLargeBlobOperation.SET - if "prf" in extensions: - global_salts = extensions["prf"].get("eval") - cred_salts = extensions["prf"].get("evalByCredential", {}) + prf = AuthenticatorExtensionsPRFInputs.from_dict(extensions.get("prf")) + if prf: + cred_salts = prf.eval_by_credential or {} hmac_secret_salts = WebAuthNHmacSecretSaltValues( - WebAuthNHmacSecretSalt(**global_salts) if global_salts else None, + ( + WebAuthNHmacSecretSalt(prf.eval.first, prf.eval.second) + if prf.eval + else None + ), [ WebAuthNCredWithHmacSecretSalt( websafe_decode(cred_id), - WebAuthNHmacSecretSalt(**salts), + WebAuthNHmacSecretSalt(salts.first, salts.second), ) for cred_id, salts in cred_salts.items() ], ) elif "hmacGetSecret" in extensions and self._allow_hmac_secret: flags |= 0x00100000 - salts = extensions["hmacGetSecret"] + salts = HMACGetSecretInput.from_dict(extensions["hmacGetSecret"]) hmac_secret_salts = WebAuthNHmacSecretSaltValues( - WebAuthNHmacSecretSalt(salts["salt1"], salts.get("salt2")) + WebAuthNHmacSecretSalt(salts.salt1, salts.salt2) ) if event: diff --git a/fido2/client/windows.py b/fido2/client/windows.py index 5e2578b..8101141 100644 --- a/fido2/client/windows.py +++ b/fido2/client/windows.py @@ -27,7 +27,7 @@ from __future__ import annotations -from . import WebAuthnClient, _BaseClient, AssertionSelection, ClientError +from . import WebAuthnClient, _BaseClient, AssertionSelection, ClientError, _cbor_list from .win_api import ( WinAPI, WebAuthNAuthenticatorAttachment, @@ -35,7 +35,6 @@ WebAuthNAttestationConveyancePreference, WebAuthNEnterpriseAttestation, ) -from ..ctap2 import AssertionResponse from ..rpid import verify_rp_id from ..webauthn import ( CollectedClientData, @@ -49,7 +48,16 @@ ResidentKeyRequirement, AuthenticatorAttachment, PublicKeyCredentialType, + _as_cbor, +) +from ..ctap2 import AssertionResponse +from ..ctap2.extensions import ( + HMACGetSecretOutput, + AuthenticatorExtensionsPRFOutputs, + AuthenticatorExtensionsLargeBlobOutputs, + CredentialPropertiesOutput, ) +from ..utils import _JsonDataObject from typing import Callable, Sequence import sys @@ -57,6 +65,19 @@ logger = logging.getLogger(__name__) +_extension_output_types: dict[str, type[_JsonDataObject]] = { + "hmacGetSecret": HMACGetSecretOutput, + "prf": AuthenticatorExtensionsPRFOutputs, + "largeBlob": AuthenticatorExtensionsLargeBlobOutputs, + "credProps": CredentialPropertiesOutput, +} + + +def _wrap_ext(key, value): + if key in _extension_output_types: + return _extension_output_types[key].from_dict(value) + return value + class WindowsClient(WebAuthnClient, _BaseClient): """Fido2Client-like class using the Windows WebAuthn API. @@ -130,9 +151,9 @@ def make_credential(self, options, event=None): try: att_obj, extensions = self.api.make_credential( - options.rp, - options.user, - options.pub_key_cred_params, + _as_cbor(options.rp), + _as_cbor(options.user), + _cbor_list(options.pub_key_cred_params), client_data, options.timeout or 0, selection.resident_key or ResidentKeyRequirement.DISCOURAGED, @@ -143,7 +164,7 @@ def make_credential(self, options, event=None): selection.user_verification or "discouraged" ), attestation, - options.exclude_credentials, + _cbor_list(options.exclude_credentials), options.extensions, event, enterprise_attestation, @@ -160,7 +181,9 @@ def make_credential(self, options, event=None): raw_id=credential.credential_id, response=AuthenticatorAttestationResponse(client_data, att_obj), authenticator_attachment=AuthenticatorAttachment.CROSS_PLATFORM, - client_extension_results=AuthenticationExtensionsClientOutputs(extensions), + client_extension_results=AuthenticationExtensionsClientOutputs( + {k: _wrap_ext(k, v) for k, v in extensions.items()} + ), type=PublicKeyCredentialType.PUBLIC_KEY, ) @@ -191,7 +214,7 @@ def get_assertion(self, options, event=None): WebAuthNUserVerificationRequirement.from_string( options.user_verification or "discouraged" ), - options.allow_credentials, + _cbor_list(options.allow_credentials), options.extensions, event, ) @@ -210,5 +233,5 @@ def get_assertion(self, options, event=None): user=user, ) ], - extensions, + {k: _wrap_ext(k, v) for k, v in extensions.items()}, )