From de8197a7e328e97c1fceec943f6e824d8d84b06b Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 9 Oct 2024 13:15:18 +0200 Subject: [PATCH] Refactor dataclass serialization --- fido2/client.py | 52 ++++++------ fido2/ctap2/base.py | 2 +- fido2/ctap2/bio.py | 18 ++-- fido2/ctap2/config.py | 29 +++---- fido2/ctap2/credman.py | 17 ++-- fido2/mds3.py | 32 ++++---- fido2/server.py | 2 +- fido2/utils.py | 182 ++++++++++++++++++++--------------------- fido2/webauthn.py | 120 ++++++++++++--------------- 9 files changed, 216 insertions(+), 238 deletions(-) diff --git a/fido2/client.py b/fido2/client.py index 12c4e8b..3ed65de 100644 --- a/fido2/client.py +++ b/fido2/client.py @@ -44,13 +44,14 @@ AuthenticatorAttestationResponse, AuthenticatorAssertionResponse, AttestationConveyancePreference, + _as_cbor, ) from .cose import ES256 from .rpid import verify_rp_id -from .utils import sha256, _DataClassMapping +from .utils import sha256 from enum import IntEnum, unique from urllib.parse import urlparse -from dataclasses import replace, asdict +from dataclasses import replace from threading import Timer, Event from typing import ( Type, @@ -69,17 +70,6 @@ logger = logging.getLogger(__name__) -def _as_cbor(data): - if data is None: - return None - if isinstance(data, Sequence): - return [_as_cbor(d) for d in data] - if isinstance(data, _DataClassMapping): - # Remove empty values and do not serialize value - return {k: v for k, v in asdict(data).items() if v is not None} # type: ignore - return data - - class ClientError(Exception): @unique class ERR(IntEnum): @@ -348,16 +338,16 @@ def do_make_credential( if ( rk or user_verification == UserVerificationRequirement.REQUIRED - or ES256.ALGORITHM not in [p["alg"] for p in key_params] + or ES256.ALGORITHM not in [p.alg for p in key_params] or enterprise_attestation ): raise CtapError(CtapError.ERR.UNSUPPORTED_OPTION) - app_param = sha256(rp["id"].encode()) + app_param = sha256(rp.id.encode()) dummy_param = b"\0" * 32 for cred in exclude_list or []: - key_handle = cred["id"] + key_handle = cred.id try: self.ctap1.authenticate(dummy_param, app_param, key_handle, True) raise ClientError.ERR.OTHER_ERROR() # Shouldn't happen @@ -413,7 +403,7 @@ def do_get_assertion( self.ctap1.authenticate, client_param, app_param, - cred["id"], + cred.id, ) assertions = [AssertionResponse.from_ctap1(app_param, cred, auth_resp)] return AssertionSelection(client_data, assertions) @@ -452,6 +442,12 @@ def _get_extension_results(self, assertion): return extension_outputs +def _cbor_list(values): + if not values: + return None + return [_as_cbor(v) for v in values] + + class _Ctap2ClientBackend(_ClientBackend): def __init__( self, @@ -611,7 +607,7 @@ def do_make_credential( # Handle auth pin_protocol, pin_token, pin_auth, internal_uv = self._get_auth_params( - client_data, rp["id"], user_verification, permissions, event, on_keepalive + client_data, rp.id, user_verification, permissions, event, on_keepalive ) if not (rk or internal_uv): @@ -625,10 +621,10 @@ def do_make_credential( att_obj = self.ctap2.make_credential( client_data.hash, - rp, - user, - key_params, - exclude_list or None, + _as_cbor(rp), + _as_cbor(user), + _cbor_list(key_params), + _cbor_list(exclude_list), extension_inputs or None, options, pin_auth, @@ -706,7 +702,7 @@ def do_get_assertion( assertions = self.ctap2.get_assertions( rp_id, client_data.hash, - allow_list or None, + _cbor_list(allow_list), extension_inputs or None, options, pin_auth, @@ -814,10 +810,10 @@ def make_credential( try: return self._backend.do_make_credential( client_data, - _as_cbor(rp), - _as_cbor(options.user), - _as_cbor(options.pub_key_cred_params), - _as_cbor(options.exclude_credentials), + rp, + options.user, + options.pub_key_cred_params, + options.exclude_credentials, options.extensions, selection.require_resident_key, selection.user_verification, @@ -859,7 +855,7 @@ def get_assertion( return self._backend.do_get_assertion( client_data, options.rp_id, - _as_cbor(options.allow_credentials), + options.allow_credentials, options.extensions, options.user_verification, event, diff --git a/fido2/ctap2/base.py b/fido2/ctap2/base.py index fd51000..4fd1390 100644 --- a/fido2/ctap2/base.py +++ b/fido2/ctap2/base.py @@ -51,7 +51,7 @@ def args(*params) -> Dict[int, Any]: :param params: Arguments, in order, to add to the command. :return: The input parameters as a dict. """ - return dict((i, v) for i, v in enumerate(params, 1) if v is not None) + return {i: v for i, v in enumerate(params, 1) if v is not None} class _CborDataObject(_DataClassMapping[int]): diff --git a/fido2/ctap2/bio.py b/fido2/ctap2/bio.py index b2b204b..e822da8 100644 --- a/fido2/ctap2/bio.py +++ b/fido2/ctap2/bio.py @@ -34,7 +34,7 @@ from enum import IntEnum, unique from threading import Event -from typing import Optional, Callable, Mapping, Any, Tuple +from typing import Optional, Callable, Mapping, Any, Tuple, Dict import struct import logging @@ -203,8 +203,6 @@ def __init__(self, ctap: Ctap2, pin_uv_protocol: PinProtocol, pin_uv_token: byte self.pin_uv_token = pin_uv_token def _call(self, sub_cmd, params=None, auth=True, event=None, on_keepalive=None): - if params is not None: - params = {k: v for k, v in params.items() if v is not None} kwargs = { "modality": self.modality, "sub_cmd": sub_cmd, @@ -247,7 +245,11 @@ def enroll_begin( logger.debug(f"Starting fingerprint enrollment (timeout={timeout})") result = self._call( FPBioEnrollment.CMD.ENROLL_BEGIN, - {FPBioEnrollment.PARAM.TIMEOUT_MS: timeout}, + ( + {FPBioEnrollment.PARAM.TIMEOUT_MS: timeout} + if timeout is not None + else None + ), event=event, on_keepalive=on_keepalive, ) @@ -277,12 +279,12 @@ def enroll_capture_next( remaining to complete the enrollment. """ logger.debug(f"Capturing next sample with (timeout={timeout})") + params: Dict[int, Any] = {FPBioEnrollment.PARAM.TEMPLATE_ID: template_id} + if timeout is not None: + params[FPBioEnrollment.PARAM.TIMEOUT_MS] = timeout result = self._call( FPBioEnrollment.CMD.ENROLL_CAPTURE_NEXT, - { - FPBioEnrollment.PARAM.TEMPLATE_ID: template_id, - FPBioEnrollment.PARAM.TIMEOUT_MS: timeout, - }, + params, event=event, on_keepalive=on_keepalive, ) diff --git a/fido2/ctap2/config.py b/fido2/ctap2/config.py index 3279ed6..019a3f3 100644 --- a/fido2/ctap2/config.py +++ b/fido2/ctap2/config.py @@ -31,7 +31,7 @@ from .base import Ctap2, Info from .pin import PinProtocol, _PinUv -from typing import Optional, List +from typing import Optional, List, Dict, Any from enum import IntEnum, unique import struct @@ -78,17 +78,10 @@ def __init__( ) def _call(self, sub_cmd, params=None): - if params: - params = {k: v for k, v in params.items() if v is not None} - else: - params = None if self.pin_uv: - msg = ( - b"\xff" * 32 - + b"\x0d" - + struct.pack(" None: :param cred_id: The PublicKeyCredentialDescriptor of the credential to delete. """ - logger.debug(f"Deleting credential with ID: {cred_id['id'].hex()}") + cred_id = PublicKeyCredentialDescriptor.from_dict(cred_id) + logger.debug(f"Deleting credential with ID: {cred_id}") self._call( CredentialManagement.CMD.DELETE_CREDENTIAL, - {CredentialManagement.PARAM.CREDENTIAL_ID: cred_id}, + {CredentialManagement.PARAM.CREDENTIAL_ID: _as_cbor(cred_id)}, ) logger.info("Credential deleted") @@ -237,12 +242,14 @@ def update_user_info( if not CredentialManagement.is_update_supported(self.ctap.info): raise ValueError("Authenticator does not support update_user_info") + cred_id = PublicKeyCredentialDescriptor.from_dict(cred_id) + user_info = PublicKeyCredentialUserEntity.from_dict(user_info) logger.debug(f"Updating credential: {cred_id} with user info: {user_info}") self._call( CredentialManagement.CMD.UPDATE_USER_INFO, { - CredentialManagement.PARAM.CREDENTIAL_ID: cred_id, - CredentialManagement.PARAM.USER: user_info, + CredentialManagement.PARAM.CREDENTIAL_ID: _as_cbor(cred_id), + CredentialManagement.PARAM.USER: _as_cbor(user_info), }, ) logger.info("Credential user info updated") diff --git a/fido2/mds3.py b/fido2/mds3.py index 7c89745..19bbd22 100644 --- a/fido2/mds3.py +++ b/fido2/mds3.py @@ -34,7 +34,7 @@ verify_x509_chain, AttestationVerifier, ) -from .utils import websafe_decode, _CamelCaseDataObject +from .utils import websafe_decode, _JsonDataObject from .cose import CoseKey from cryptography import x509 @@ -53,19 +53,19 @@ @dataclass(eq=False, frozen=True) -class Version(_CamelCaseDataObject): +class Version(_JsonDataObject): major: int minor: int @dataclass(eq=False, frozen=True) -class RogueListEntry(_CamelCaseDataObject): +class RogueListEntry(_JsonDataObject): sk: bytes date: int @dataclass(eq=False, frozen=True) -class BiometricStatusReport(_CamelCaseDataObject): +class BiometricStatusReport(_JsonDataObject): cert_level: int modality: str effective_date: int @@ -76,7 +76,7 @@ class BiometricStatusReport(_CamelCaseDataObject): @dataclass(eq=False, frozen=True) -class CodeAccuracyDescriptor(_CamelCaseDataObject): +class CodeAccuracyDescriptor(_JsonDataObject): base: int min_length: int max_retries: Optional[int] = None @@ -84,7 +84,7 @@ class CodeAccuracyDescriptor(_CamelCaseDataObject): @dataclass(eq=False, frozen=True) -class BiometricAccuracyDescriptor(_CamelCaseDataObject): +class BiometricAccuracyDescriptor(_JsonDataObject): self_attested_frr: Optional[float] = field( default=None, metadata=dict(name="selfAttestedFRR") ) @@ -97,14 +97,14 @@ class BiometricAccuracyDescriptor(_CamelCaseDataObject): @dataclass(eq=False, frozen=True) -class PatternAccuracyDescriptor(_CamelCaseDataObject): +class PatternAccuracyDescriptor(_JsonDataObject): min_complexity: int max_retries: Optional[int] = None block_slowdown: Optional[int] = None @dataclass(eq=False, frozen=True) -class VerificationMethodDescriptor(_CamelCaseDataObject): +class VerificationMethodDescriptor(_JsonDataObject): user_verification_method: Optional[str] = None ca_desc: Optional[CodeAccuracyDescriptor] = None ba_desc: Optional[BiometricAccuracyDescriptor] = None @@ -112,14 +112,14 @@ class VerificationMethodDescriptor(_CamelCaseDataObject): @dataclass(eq=False, frozen=True) -class RgbPaletteEntry(_CamelCaseDataObject): +class RgbPaletteEntry(_JsonDataObject): r: int g: int b: int @dataclass(eq=False, frozen=True) -class DisplayPngCharacteristicsDescriptor(_CamelCaseDataObject): +class DisplayPngCharacteristicsDescriptor(_JsonDataObject): width: int height: int bit_depth: int @@ -131,7 +131,7 @@ class DisplayPngCharacteristicsDescriptor(_CamelCaseDataObject): @dataclass(eq=False, frozen=True) -class EcdaaTrustAnchor(_CamelCaseDataObject): +class EcdaaTrustAnchor(_JsonDataObject): x: str = field(metadata=dict(name="X")) y: str = field(metadata=dict(name="Y")) c: str @@ -160,7 +160,7 @@ class AuthenticatorStatus(str, Enum): @dataclass(eq=False, frozen=True) -class StatusReport(_CamelCaseDataObject): +class StatusReport(_JsonDataObject): status: AuthenticatorStatus effective_date: Optional[date] = field( metadata=dict( @@ -182,7 +182,7 @@ class StatusReport(_CamelCaseDataObject): @dataclass(eq=False, frozen=True) -class ExtensionDescriptor(_CamelCaseDataObject): +class ExtensionDescriptor(_JsonDataObject): fail_if_unknown: bool = field(metadata=dict(name="fail_if_unknown")) id: str tag: Optional[int] = None @@ -190,7 +190,7 @@ class ExtensionDescriptor(_CamelCaseDataObject): @dataclass(eq=False, frozen=True) -class MetadataStatement(_CamelCaseDataObject): +class MetadataStatement(_JsonDataObject): description: str authenticator_version: int schema: int @@ -247,7 +247,7 @@ class MetadataStatement(_CamelCaseDataObject): @dataclass(eq=False, frozen=True) -class MetadataBlobPayloadEntry(_CamelCaseDataObject): +class MetadataBlobPayloadEntry(_JsonDataObject): status_reports: Sequence[StatusReport] time_of_last_status_change: date = field( metadata=dict( @@ -285,7 +285,7 @@ class MetadataBlobPayloadEntry(_CamelCaseDataObject): @dataclass(eq=False, frozen=True) -class MetadataBlobPayload(_CamelCaseDataObject): +class MetadataBlobPayload(_JsonDataObject): legal_header: str no: int next_update: date = field( diff --git a/fido2/server.py b/fido2/server.py index 14c025f..bd30f16 100644 --- a/fido2/server.py +++ b/fido2/server.py @@ -195,7 +195,7 @@ def register_begin( CredentialCreationOptions( PublicKeyCredentialCreationOptions( self.rp, - user, + PublicKeyCredentialUserEntity.from_dict(user), challenge, self.allowed_algorithms, self.timeout, diff --git a/fido2/utils.py b/fido2/utils.py index 5e42869..fd962bd 100644 --- a/fido2/utils.py +++ b/fido2/utils.py @@ -43,6 +43,7 @@ Optional, Sequence, Mapping, + Dict, Any, TypeVar, Hashable, @@ -166,59 +167,6 @@ def read(self, size: Optional[int] = -1) -> bytes: return data -def _snake2camel(name: str) -> str: - parts = name.split("_") - return parts[0] + "".join(p.title() for p in parts[1:]) - - -def _parse_value(t, value, from_dict): - if value is None: - return None - - if Optional[t] == t: # Optional, get the type - t = t.__args__[0] - - # Handle list of values - if issubclass(getattr(t, "__origin__", object), Sequence): - t = t.__args__[0] - return [_parse_value(t, v, from_dict) for v in value] - - # Handle Mappings - if issubclass(getattr(t, "__origin__", object), Mapping) and isinstance( - value, Mapping - ): - return value - - # Check if type is already correct - try: - if isinstance(value, t): - return value - except TypeError: - pass - - # If called from "from_dict", recurse using the same - if from_dict and hasattr(t, "from_dict"): - return t.from_dict(value) - - # Check for subclass of _DataClassMapping - try: - is_dataclass = issubclass(t, _DataClassMapping) - except TypeError: - is_dataclass = False - - if is_dataclass: - # Recursively call constructor for nested _DataClassMappings - kwargs = {} - for f in fields(t): # type: ignore - key = t._get_field_key(f) - if key in value: - kwargs[f.name] = value[key] - return t(**kwargs) - - # Convert to enum values, other wrappers - return t(value) - - _T = TypeVar("_T", bound=Hashable) @@ -228,49 +176,81 @@ class _DataClassMapping(Mapping[_T, Any]): def __post_init__(self): hints = get_type_hints(type(self)) + self._field_keys: Dict[_T, Field[Any]] + object.__setattr__(self, "_field_keys", {}) + for f in fields(self): # type: ignore + self._field_keys[self._get_field_key(f)] = f value = getattr(self, f.name) - if value is None: - continue - try: - value = _parse_value(hints[f.name], value, False) - except (TypeError, KeyError, ValueError): - raise ValueError( - f"Error parsing field {f.name} for {self.__class__.__name__}" - ) - object.__setattr__(self, f.name, value) + if value is not None: + try: + value = self._parse_value(hints[f.name], value) + object.__setattr__(self, f.name, value) + except (TypeError, KeyError, ValueError): + raise ValueError( + f"Error parsing field {f.name} for {self.__class__.__name__}" + ) @classmethod @abstractmethod def _get_field_key(cls, field: Field) -> _T: raise NotImplementedError() - def __getitem__(self, key): - for f in fields(self): # type: ignore - if key == self._get_field_key(f): - value = getattr(self, f.name) - serialize = f.metadata.get("serialize") - if serialize: - return serialize(value) - if isinstance(value, _DataClassMapping): - return dict(value) - if isinstance(value, Sequence) and all( - isinstance(x, _DataClassMapping) for x in value - ): - return [dict(x) for x in value] - return value - raise KeyError(key) - def __iter__(self): return ( - self._get_field_key(f) - for f in fields(self) # type: ignore - if getattr(self, f.name) is not None + k for k, f in self._field_keys.items() if getattr(self, f.name) is not None ) def __len__(self): return len(list(iter(self))) + def __getitem__(self, key): + f = self._field_keys[key] + value = getattr(self, f.name) + if value is None: + raise KeyError(key) + serialize = f.metadata.get("serialize") + if serialize: + return serialize(value) + if isinstance(value, _DataClassMapping): + return dict(value) + if isinstance(value, list) and all( + isinstance(v, _DataClassMapping) for v in value + ): + return [dict(v) for v in value] + return value + + @classmethod + def _parse_value(cls, t, value): + if Optional[t] == t: # Optional, get the type + t = t.__args__[0] + + # Check if type is already correct + try: + if isinstance(value, t): + return value + except TypeError: + pass + + # Handle list of values + if issubclass(getattr(t, "__origin__", object), Sequence): + t = t.__args__[0] + return [cls._parse_value(t, v) for v in value] + + # Handle Mappings + elif issubclass(getattr(t, "__origin__", object), Mapping) and isinstance( + value, Mapping + ): + # Note: We are not recursively parsing members of the mapping + return value + + # Check if type has from_dict + if hasattr(t, "from_dict"): + return t.from_dict(value) + + # Convert to enum values, other wrappers + return t(value) + @classmethod def from_dict(cls, data: Optional[Mapping[_T, Any]]): if data is None: @@ -287,19 +267,37 @@ def from_dict(cls, data: Optional[Mapping[_T, Any]]): hints = get_type_hints(cls) for f in fields(cls): # type: ignore key = cls._get_field_key(f) - if key in data: - value = data[key] - if value is not None: - deserialize = f.metadata.get("deserialize") - if deserialize: - value = deserialize(value) - else: - value = _parse_value(hints[f.name], value, True) - kwargs[f.name] = value + value = data.get(key) + if value is None: + continue + deserialize = f.metadata.get("deserialize") + if deserialize: + value = deserialize(value) + else: + t = hints[f.name] + value = cls._parse_value(t, value) + + kwargs[f.name] = value return cls(**kwargs) -class _CamelCaseDataObject(_DataClassMapping[str]): +class _JsonDataObject(_DataClassMapping[str]): @classmethod def _get_field_key(cls, field: Field) -> str: - return field.metadata.get("name", _snake2camel(field.name)) + name = field.metadata.get("name") + if name: + return name + parts = field.name.split("_") + return parts[0] + "".join(p.title() for p in parts[1:]) + + def __getitem__(self, key): + value = super().__getitem__(key) + if isinstance(value, bytes): + return websafe_encode(value) + return value + + @classmethod + def _parse_value(cls, t, value): + if isinstance(t, type) and issubclass(t, bytes) and isinstance(value, str): + return websafe_decode(value) + return super()._parse_value(t, value) diff --git a/fido2/webauthn.py b/fido2/webauthn.py index 9297cd9..33eedc8 100644 --- a/fido2/webauthn.py +++ b/fido2/webauthn.py @@ -34,7 +34,7 @@ websafe_decode, websafe_encode, ByteBuffer, - _CamelCaseDataObject, + _JsonDataObject, ) from .features import webauthn_json_mapping from enum import Enum, EnumMeta, unique, IntFlag @@ -409,12 +409,6 @@ class _StringEnum(str, Enum, metaclass=_StringEnumMeta): """ -_b64_metadata = dict( - serialize=lambda x: websafe_encode(x) if webauthn_json_mapping.enabled else x, - deserialize=lambda x: websafe_decode(x) if webauthn_json_mapping.enabled else x, -) - - @unique class AttestationConveyancePreference(_StringEnum): NONE = "none" @@ -457,8 +451,30 @@ class PublicKeyCredentialType(_StringEnum): PUBLIC_KEY = "public-key" +class _WebAuthnDataObject(_JsonDataObject): + def __getitem__(self, key): + if webauthn_json_mapping.enabled: + return super().__getitem__(key) + return super(_JsonDataObject, self).__getitem__(key) + + @classmethod + def _parse_value(cls, t, value): + if webauthn_json_mapping.enabled: + return super()._parse_value(t, value) + return super(_JsonDataObject, cls)._parse_value(t, value) + + @classmethod + def from_dict(cls, data: Optional[Mapping[str, Any]]): + webauthn_json_mapping.warn() + return super().from_dict(data) + + +def _as_cbor(data: _WebAuthnDataObject) -> Mapping[str, Any]: + return {k: super(_JsonDataObject, data).__getitem__(k) for k in data} + + @dataclass(eq=False, frozen=True) -class PublicKeyCredentialRpEntity(_CamelCaseDataObject): +class PublicKeyCredentialRpEntity(_WebAuthnDataObject): name: str id: Optional[str] = None @@ -469,41 +485,27 @@ def id_hash(self) -> Optional[bytes]: @dataclass(eq=False, frozen=True) -class PublicKeyCredentialUserEntity(_CamelCaseDataObject): +class PublicKeyCredentialUserEntity(_WebAuthnDataObject): name: str - id: bytes = field(metadata=_b64_metadata) + id: bytes display_name: Optional[str] = None @dataclass(eq=False, frozen=True) -class PublicKeyCredentialParameters(_CamelCaseDataObject): +class PublicKeyCredentialParameters(_WebAuthnDataObject): type: PublicKeyCredentialType alg: int - @classmethod - def _deserialize_list(cls, value): - if value is None: - return None - items = [cls.from_dict(e) for e in value] - return [e for e in items if e.type is not None] - @dataclass(eq=False, frozen=True) -class PublicKeyCredentialDescriptor(_CamelCaseDataObject): +class PublicKeyCredentialDescriptor(_WebAuthnDataObject): type: PublicKeyCredentialType - id: bytes = field(metadata=_b64_metadata) + id: bytes transports: Optional[Sequence[AuthenticatorTransport]] = None - @classmethod - def _deserialize_list(cls, value): - if value is None: - return None - items = [cls.from_dict(e) for e in value] - return [e for e in items if e.type is not None] - @dataclass(eq=False, frozen=True) -class AuthenticatorSelectionCriteria(_CamelCaseDataObject): +class AuthenticatorSelectionCriteria(_WebAuthnDataObject): authenticator_attachment: Optional[AuthenticatorAttachment] = None resident_key: Optional[ResidentKeyRequirement] = None user_verification: Optional[UserVerificationRequirement] = None @@ -530,45 +532,32 @@ def __post_init__(self): @dataclass(eq=False, frozen=True) -class PublicKeyCredentialCreationOptions(_CamelCaseDataObject): +class PublicKeyCredentialCreationOptions(_WebAuthnDataObject): rp: PublicKeyCredentialRpEntity user: PublicKeyCredentialUserEntity - challenge: bytes = field(metadata=_b64_metadata) - pub_key_cred_params: Sequence[PublicKeyCredentialParameters] = field( - metadata=dict(deserialize=PublicKeyCredentialParameters._deserialize_list), - ) + challenge: bytes + pub_key_cred_params: Sequence[PublicKeyCredentialParameters] timeout: Optional[int] = None - exclude_credentials: Optional[Sequence[PublicKeyCredentialDescriptor]] = field( - default=None, - metadata=dict(deserialize=PublicKeyCredentialDescriptor._deserialize_list), - ) + exclude_credentials: Optional[Sequence[PublicKeyCredentialDescriptor]] = None authenticator_selection: Optional[AuthenticatorSelectionCriteria] = None attestation: Optional[AttestationConveyancePreference] = None extensions: Optional[Mapping[str, Any]] = None @dataclass(eq=False, frozen=True) -class PublicKeyCredentialRequestOptions(_CamelCaseDataObject): - challenge: bytes = field(metadata=_b64_metadata) +class PublicKeyCredentialRequestOptions(_WebAuthnDataObject): + challenge: bytes timeout: Optional[int] = None rp_id: Optional[str] = None - allow_credentials: Optional[Sequence[PublicKeyCredentialDescriptor]] = field( - default=None, - metadata=dict(deserialize=PublicKeyCredentialDescriptor._deserialize_list), - ) + allow_credentials: Optional[Sequence[PublicKeyCredentialDescriptor]] = None user_verification: Optional[UserVerificationRequirement] = None extensions: Optional[Mapping[str, Any]] = None @dataclass(eq=False, frozen=True) -class AuthenticatorAttestationResponse(_CamelCaseDataObject): - client_data: CollectedClientData = field( - metadata=dict( - _b64_metadata, - name="clientDataJSON", - ) - ) - attestation_object: AttestationObject = field(metadata=_b64_metadata) +class AuthenticatorAttestationResponse(_WebAuthnDataObject): + client_data: CollectedClientData = field(metadata=dict(name="clientDataJSON")) + attestation_object: AttestationObject extension_results: Optional[Mapping[str, Any]] = None def __getitem__(self, key): @@ -586,17 +575,12 @@ def from_dict(cls, data: Optional[Mapping[str, Any]]): @dataclass(eq=False, frozen=True) -class AuthenticatorAssertionResponse(_CamelCaseDataObject): - client_data: CollectedClientData = field( - metadata=dict( - _b64_metadata, - name="clientDataJSON", - ) - ) - authenticator_data: AuthenticatorData = field(metadata=_b64_metadata) - signature: bytes = field(metadata=_b64_metadata) - user_handle: Optional[bytes] = field(metadata=_b64_metadata, default=None) - credential_id: Optional[bytes] = field(metadata=_b64_metadata, default=None) +class AuthenticatorAssertionResponse(_WebAuthnDataObject): + client_data: CollectedClientData = field(metadata=dict(name="clientDataJSON")) + authenticator_data: AuthenticatorData + signature: bytes + user_handle: Optional[bytes] = None + credential_id: Optional[bytes] = None extension_results: Optional[Mapping[str, Any]] = None def __getitem__(self, key): @@ -614,8 +598,8 @@ def from_dict(cls, data: Optional[Mapping[str, Any]]): @dataclass(eq=False, frozen=True) -class RegistrationResponse(_CamelCaseDataObject): - id: bytes = field(metadata=_b64_metadata) +class RegistrationResponse(_WebAuthnDataObject): + id: bytes response: AuthenticatorAttestationResponse authenticator_attachment: Optional[AuthenticatorAttachment] = None client_extension_results: Optional[Mapping] = None @@ -627,8 +611,8 @@ def __post_init__(self): @dataclass(eq=False, frozen=True) -class AuthenticationResponse(_CamelCaseDataObject): - id: bytes = field(metadata=_b64_metadata) +class AuthenticationResponse(_WebAuthnDataObject): + id: bytes response: AuthenticatorAssertionResponse authenticator_attachment: Optional[AuthenticatorAttachment] = None client_extension_results: Optional[Mapping] = None @@ -640,10 +624,10 @@ def __post_init__(self): @dataclass(eq=False, frozen=True) -class CredentialCreationOptions(_CamelCaseDataObject): +class CredentialCreationOptions(_WebAuthnDataObject): public_key: PublicKeyCredentialCreationOptions @dataclass(eq=False, frozen=True) -class CredentialRequestOptions(_CamelCaseDataObject): +class CredentialRequestOptions(_WebAuthnDataObject): public_key: PublicKeyCredentialRequestOptions