From 4b5a32577ba80da2a01d2f655b33ec41727edcee Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Thu, 17 Oct 2024 15:39:53 +0200 Subject: [PATCH] Pass options to extensions --- fido2/client.py | 15 +++++++---- fido2/ctap2/extensions.py | 57 ++++++++++++++++++--------------------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/fido2/client.py b/fido2/client.py index 167faaa..0049361 100644 --- a/fido2/client.py +++ b/fido2/client.py @@ -634,6 +634,9 @@ def do_make_credential( rk = selection.require_resident_key user_verification = selection.user_verification + on_keepalive = _user_keepalive(self.user_interaction) + + # Handle enterprise attestation enterprise_attestation = None if options.attestation == AttestationConveyancePreference.ENTERPRISE: if self.info.options.get("ep"): @@ -645,8 +648,6 @@ def do_make_credential( # Vendor facilitated enterprise_attestation = 1 - on_keepalive = _user_keepalive(self.user_interaction) - # Gather up permissions permissions = ClientPin.PERMISSION.MAKE_CREDENTIAL if exclude_list: @@ -658,6 +659,8 @@ def do_make_credential( used_extensions = [] client_inputs = extensions or {} for ext in extension_instances: + # TODO: Move options to the constructor instead + ext._create_options = options permissions |= ext.get_create_permissions(client_inputs) def _do_make(): @@ -775,6 +778,8 @@ def do_get_assertion( extension_instances = [cls(self.ctap2) for cls in self.extensions] client_inputs = extensions or {} for ext in extension_instances: + # TODO: Move options to get_get_permissions and process_get_input + ext._get_options = options permissions |= ext.get_get_permissions(client_inputs) def _do_auth(): @@ -798,9 +803,9 @@ def _do_auth(): used_extensions = [] try: for ext in extension_instances: - auth_input = ext._process_get_input_w_allow_list( - client_inputs, selected_cred - ) + # TODO: Move to process_get_input() + ext._selected = selected_cred + auth_input = ext.process_get_input(client_inputs) if auth_input is not None: used_extensions.append(ext) extension_inputs[ext.NAME] = auth_input diff --git a/fido2/ctap2/extensions.py b/fido2/ctap2/extensions.py index 7b8464a..6c4f13c 100644 --- a/fido2/ctap2/extensions.py +++ b/fido2/ctap2/extensions.py @@ -31,12 +31,15 @@ from .pin import ClientPin, PinProtocol from .blob import LargeBlobs from ..utils import sha256, websafe_encode -from ..webauthn import PublicKeyCredentialDescriptor +from ..webauthn import ( + PublicKeyCredentialDescriptor, + PublicKeyCredentialCreationOptions, + PublicKeyCredentialRequestOptions, +) from enum import Enum, unique from typing import Dict, Tuple, Any, Optional import abc import warnings -import inspect class Ctap2Extension(abc.ABC): @@ -49,6 +52,10 @@ class Ctap2Extension(abc.ABC): def __init__(self, ctap: Ctap2): self.ctap = ctap + # TODO: Pass options and selected to the various methods that need them instead + self._create_options: PublicKeyCredentialCreationOptions + self._get_options: PublicKeyCredentialRequestOptions + self._selected: Optional[PublicKeyCredentialDescriptor] def is_supported(self) -> bool: """Whether or not the extension is supported by the authenticator.""" @@ -84,33 +91,12 @@ def process_create_output( def get_get_permissions(self, inputs: Dict[str, Any]) -> ClientPin.PERMISSION: return ClientPin.PERMISSION(0) - def process_get_input( - self, - inputs: Dict[str, Any], - allow_credential: Optional[PublicKeyCredentialDescriptor] = None, - ) -> Any: + def process_get_input(self, inputs: Dict[str, Any]) -> Any: """Returns a value to include in the authenticator extension input, or None. """ return None - def _process_get_input_w_allow_list( - self, - inputs: Dict[str, Any], - allow_credential: Optional[PublicKeyCredentialDescriptor], - ) -> Any: - s = inspect.signature(self.process_get_input) - try: - s.bind(inputs, allow_credential) - except TypeError: - warnings.warn( - f"{type(self)}.process_get_input() does not take allow_credential, " - "which is deprecated.", - DeprecationWarning, - ) - return self.process_get_input(inputs) - return self.process_get_input(inputs, allow_credential) - def process_get_input_with_permissions( self, inputs: Dict[str, Any] ) -> Tuple[Any, ClientPin.PERMISSION]: @@ -162,21 +148,30 @@ def process_create_output(self, attestation_response, *args): else: return {"hmacCreateSecret": enabled} - def process_get_input(self, inputs, allow_credential=None): + def process_get_input(self, inputs): if not self.is_supported(): return data = inputs.get("prf") if data: + secrets = data.get("eval") by_creds = data.get("evalByCredential") if by_creds: - if not allow_credential: + # Make sure all keys are valid IDs from allow_credentials + allow_list = self._get_options.allow_credentials + if not allow_list: raise ValueError("evalByCredentials requires allowCredentials") - secrets = by_creds.get(websafe_encode(allow_credential.id)) - if not secrets: - raise ValueError("No matching credential ID found") - else: - secrets = data.get("eval") + ids = {websafe_encode(c.id) for c in allow_list} + if not ids.issuperset(by_creds): + raise ValueError("evalByCredentials contains invalid key") + if self._selected: + key = websafe_encode(self._selected.id) + if key in by_creds: + secrets = by_creds[key] + + if not secrets: + return + salts = ( _prf_salt(secrets["first"]), _prf_salt(secrets["second"]) if "second" in secrets else b"",