Add support for WebAuthn data class JSON serialization.

See https://github.com/w3c/webauthn/issues/1683 for details.
This commit is contained in:
Dain Nilsson 2022-08-10 15:41:27 +02:00
parent f7e8c59649
commit 8debc41942
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
2 changed files with 159 additions and 12 deletions

67
fido2/features.py Normal file
View File

@ -0,0 +1,67 @@
from typing import Optional
import warnings
class FeatureNotEnabledError(Exception):
pass
class _Feature:
def __init__(self, name: str, desc: str):
self._enabled: Optional[bool] = None
self._name = name
self._desc = desc
@property
def enabled(self) -> bool:
self.warn()
return self._enabled is True
@enabled.setter
def enabled(self, value: bool) -> None:
if self._enabled is not None:
raise ValueError(
f"{self._name} has already been configured with {self._enabled}"
)
self._enabled = value
def require(self, state=True) -> None:
if self._enabled != state:
self.warn()
raise FeatureNotEnabledError(
f"Usage requires {self._name}.enabled = {state}"
)
def warn(self) -> None:
if self._enabled is None:
warnings.warn(
f"""Deprecated use of {self._name}.
You are using deprecated functionality which will change in the next major version of
python-fido2. You can opt-in to use the new functionality now by adding the following
to your code somewhere where it gets executed prior to using the affected functionality:
import fido2.features
fido2.features.{self._name}.enabled = True
To silence this warning but retain the current behavior, instead set enabled to False:
fido2.features.{self._name}.enabled = False
{self._desc}
""",
DeprecationWarning,
)
webauthn_json_mapping = _Feature(
"webauthn_json_mapping",
"""JSON values for WebAuthn data class Mapping interface.
This changes the keys and values used by the webauthn data classes when accessed using
the Mapping (dict) interface (eg. user_entity["id"] and the from_dict() methods) to be
JSON-friendly and align with the current draft of the next WebAuthn Level specification.
For the most part, this means that binary values (bytes) are represented as URL-safe
base64 encoded strings instead.
""",
)

View File

@ -36,6 +36,7 @@ from .utils import (
ByteBuffer,
_CamelCaseDataObject,
)
from .features import webauthn_json_mapping
from enum import Enum, EnumMeta, unique, IntFlag
from dataclasses import dataclass, field
from typing import Any, Mapping, Optional, Sequence, Tuple, Union, cast
@ -406,6 +407,12 @@ 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"
@ -461,7 +468,7 @@ class PublicKeyCredentialRpEntity(_CamelCaseDataObject):
@dataclass(eq=False, frozen=True)
class PublicKeyCredentialUserEntity(_CamelCaseDataObject):
name: str
id: bytes
id: bytes = field(metadata=_b64_metadata)
display_name: Optional[str] = None
@ -481,7 +488,7 @@ class PublicKeyCredentialParameters(_CamelCaseDataObject):
@dataclass(eq=False, frozen=True)
class PublicKeyCredentialDescriptor(_CamelCaseDataObject):
type: PublicKeyCredentialType
id: bytes
id: bytes = field(metadata=_b64_metadata)
transports: Optional[Sequence[AuthenticatorTransport]] = None
@classmethod
@ -521,7 +528,7 @@ class AuthenticatorSelectionCriteria(_CamelCaseDataObject):
class PublicKeyCredentialCreationOptions(_CamelCaseDataObject):
rp: PublicKeyCredentialRpEntity
user: PublicKeyCredentialUserEntity
challenge: bytes
challenge: bytes = field(metadata=_b64_metadata)
pub_key_cred_params: Sequence[PublicKeyCredentialParameters] = field(
metadata=dict(deserialize=PublicKeyCredentialParameters._deserialize_list),
)
@ -537,12 +544,12 @@ class PublicKeyCredentialCreationOptions(_CamelCaseDataObject):
@dataclass(eq=False, frozen=True)
class PublicKeyCredentialRequestOptions(_CamelCaseDataObject):
challenge: bytes
challenge: bytes = field(metadata=_b64_metadata)
timeout: Optional[int] = None
rp_id: Optional[str] = None
allow_credentials: Optional[Sequence[PublicKeyCredentialDescriptor]] = field(
default=None,
metadata={"deserialize": PublicKeyCredentialDescriptor._deserialize_list},
metadata=dict(deserialize=PublicKeyCredentialDescriptor._deserialize_list),
)
user_verification: Optional[UserVerificationRequirement] = None
extensions: Optional[Mapping[str, Any]] = None
@ -550,16 +557,89 @@ class PublicKeyCredentialRequestOptions(_CamelCaseDataObject):
@dataclass(eq=False, frozen=True)
class AuthenticatorAttestationResponse(_CamelCaseDataObject):
client_data: bytes
attestation_object: AttestationObject
client_data: CollectedClientData = field(
metadata=dict(
_b64_metadata,
name="clientDataJSON",
)
)
attestation_object: AttestationObject = field(metadata=_b64_metadata)
extension_results: Optional[Mapping[str, Any]] = None
def __getitem__(self, key):
if key == "clientData" and not webauthn_json_mapping.enabled:
return self.client_data
return super().__getitem__(key)
@classmethod
def from_dict(cls, data: Optional[Mapping[str, Any]]):
if data is not None and not webauthn_json_mapping.enabled:
value = dict(data)
value["clientDataJSON"] = value.pop("clientData", None)
data = value
return super().from_dict(data)
@dataclass(eq=False, frozen=True)
class AuthenticatorAssertionResponse(_CamelCaseDataObject):
client_data: bytes
authenticator_data: AuthenticatorData
signature: bytes
user_handle: bytes
credential_id: bytes
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)
extension_results: Optional[Mapping[str, Any]] = None
def __getitem__(self, key):
if key == "clientData" and not webauthn_json_mapping.enabled:
return self.client_data
return super().__getitem__(key)
@classmethod
def from_dict(cls, data: Optional[Mapping[str, Any]]):
if data is not None and not webauthn_json_mapping.enabled:
value = dict(data)
value["clientDataJSON"] = value.pop("clientData", None)
data = value
print("parse assertion from", data)
return super().from_dict(data)
@dataclass(eq=False, frozen=True)
class RegistrationResponse(_CamelCaseDataObject):
id: bytes = field(metadata=_b64_metadata)
response: AuthenticatorAttestationResponse
authenticator_attachment: Optional[AuthenticatorAttachment] = None
client_extension_results: Optional[Mapping] = None
type: Optional[PublicKeyCredentialType] = None
def __post_init__(self):
webauthn_json_mapping.require()
super().__post_init__()
@dataclass(eq=False, frozen=True)
class AuthenticationResponse(_CamelCaseDataObject):
id: bytes = field(metadata=_b64_metadata)
response: AuthenticatorAssertionResponse
authenticator_attachment: Optional[AuthenticatorAttachment] = None
client_extension_results: Optional[Mapping] = None
type: Optional[PublicKeyCredentialType] = None
def __post_init__(self):
webauthn_json_mapping.require()
super().__post_init__()
@dataclass(eq=False, frozen=True)
class CredentialCreationOptions(_CamelCaseDataObject):
public_key: PublicKeyCredentialCreationOptions
@dataclass(eq=False, frozen=True)
class CredentialRequestOptions(_CamelCaseDataObject):
public_key: PublicKeyCredentialRequestOptions