Skip to content

Commit

Permalink
Make webauthn dataclasses kw_only
Browse files Browse the repository at this point in the history
  • Loading branch information
dainnilsson committed Jan 29, 2025
1 parent 3db8511 commit de03443
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 51 deletions.
25 changes: 14 additions & 11 deletions fido2/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,10 +229,10 @@ def get_response(self, index: int) -> AuthenticationResponse:
return AuthenticationResponse(
raw_id=assertion.credential["id"],
response=AuthenticatorAssertionResponse(
self._client_data,
assertion.auth_data,
assertion.signature,
assertion.user["id"] if assertion.user else None,
client_data=self._client_data,
authenticator_data=assertion.auth_data,
signature=assertion.signature,
user_handle=assertion.user["id"] if assertion.user else None,
),
authenticator_attachment=AuthenticatorAttachment.CROSS_PLATFORM,
client_extension_results=AuthenticationExtensionsClientOutputs(
Expand Down Expand Up @@ -413,7 +413,9 @@ def do_make_credential(

return RegistrationResponse(
raw_id=credential.credential_id,
response=AuthenticatorAttestationResponse(client_data, att_obj),
response=AuthenticatorAttestationResponse(
client_data=client_data, attestation_object=att_obj
),
authenticator_attachment=AuthenticatorAttachment.CROSS_PLATFORM,
client_extension_results=AuthenticationExtensionsClientOutputs({}),
type=PublicKeyCredentialType.PUBLIC_KEY,
Expand Down Expand Up @@ -502,13 +504,10 @@ def __init__(
extensions: Sequence[Ctap2Extension],
):
self.ctap2 = Ctap2(device)
self.info = self.ctap2.info
self._extensions = extensions
self.user_interaction = user_interaction

@property
def info(self):
return self.ctap2.info

def _filter_creds(
self, rp_id, cred_list, pin_protocol, pin_token, event, on_keepalive
):
Expand Down Expand Up @@ -845,7 +844,9 @@ def _do_make():

return RegistrationResponse(
raw_id=credential.credential_id,
response=AuthenticatorAttestationResponse(client_data, att_obj),
response=AuthenticatorAttestationResponse(
client_data=client_data, attestation_object=att_obj
),
authenticator_attachment=AuthenticatorAttachment.CROSS_PLATFORM,
client_extension_results=AuthenticationExtensionsClientOutputs(
extension_outputs
Expand Down Expand Up @@ -926,7 +927,9 @@ def _do_auth():
if allow_list and not selected_cred:
# We still need to send a dummy value if there was an allow_list
# but no matches were found:
selected_cred = PublicKeyCredentialDescriptor(allow_list[0].type, b"\0")
selected_cred = PublicKeyCredentialDescriptor(
type=allow_list[0].type, id=b"\0"
)

# Perform get assertion
assertions = self.ctap2.get_assertions(
Expand Down
19 changes: 14 additions & 5 deletions fido2/client/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,9 @@ def make_credential(self, options, event=None):

return RegistrationResponse(
raw_id=credential.credential_id,
response=AuthenticatorAttestationResponse(client_data, att_obj),
response=AuthenticatorAttestationResponse(
client_data=client_data, attestation_object=att_obj
),
authenticator_attachment=AuthenticatorAttachment.CROSS_PLATFORM,
client_extension_results=AuthenticationExtensionsClientOutputs(
{k: _wrap_ext(k, v) for k, v in extension_outputs.items()}
Expand All @@ -334,7 +336,16 @@ def get_assertion(self, options, event=None):
CollectedClientData.TYPE.GET, options.challenge
)

selection = options.authenticator_selection or AuthenticatorSelectionCriteria()
attachment = WebAuthNAuthenticatorAttachment.ANY
for hint in options.hints or []:
match hint:
case "security-key":
attachment = WebAuthNAuthenticatorAttachment.CROSS_PLATFORM
case "client-device":
attachment = WebAuthNAuthenticatorAttachment.PLATFORM
case _:
continue
break

flags = 0
large_blob = None
Expand Down Expand Up @@ -400,9 +411,7 @@ def get_assertion(self, options, event=None):
ctypes.byref(
WebAuthNGetAssertionOptions(
options.timeout or 0,
WebAuthNAuthenticatorAttachment.from_string(
selection.authenticator_attachment or "any"
),
attachment,
WebAuthNUserVerificationRequirement.from_string(
options.user_verification or "discouraged"
),
Expand Down
48 changes: 26 additions & 22 deletions fido2/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ def to_descriptor(
:rtype: PublicKeyCredentialDescriptor
"""
return PublicKeyCredentialDescriptor(
PublicKeyCredentialType.PUBLIC_KEY, credential.credential_id, transports
type=PublicKeyCredentialType.PUBLIC_KEY,
id=credential.credential_id,
transports=transports,
)


Expand Down Expand Up @@ -145,7 +147,9 @@ def __init__(
self.timeout = None
self.attestation = AttestationConveyancePreference(attestation)
self.allowed_algorithms = [
PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, alg)
PublicKeyCredentialParameters(
type=PublicKeyCredentialType.PUBLIC_KEY, alg=alg
)
for alg in CoseKey.supported_algorithms()
]
self._verify_attestation = verify_attestation or _ignore_attestation
Expand Down Expand Up @@ -190,18 +194,18 @@ def register_begin(

return (
CredentialCreationOptions(
PublicKeyCredentialCreationOptions(
self.rp,
PublicKeyCredentialUserEntity.from_dict(user),
challenge,
self.allowed_algorithms,
self.timeout,
descriptors,
(
public_key=PublicKeyCredentialCreationOptions(
rp=self.rp,
user=PublicKeyCredentialUserEntity.from_dict(user),
challenge=challenge,
pub_key_cred_params=self.allowed_algorithms,
timeout=self.timeout,
exclude_credentials=descriptors,
authenticator_selection=(
AuthenticatorSelectionCriteria(
authenticator_attachment,
resident_key_requirement,
user_verification,
authenticator_attachment=authenticator_attachment,
resident_key=resident_key_requirement,
user_verification=user_verification,
)
if any(
(
Expand All @@ -212,8 +216,8 @@ def register_begin(
)
else None
),
self.attestation,
extensions,
attestation=self.attestation,
extensions=extensions,
)
),
state,
Expand Down Expand Up @@ -305,13 +309,13 @@ def authenticate_begin(

return (
CredentialRequestOptions(
PublicKeyCredentialRequestOptions(
challenge,
self.timeout,
self.rp.id,
descriptors,
user_verification,
extensions,
public_key=PublicKeyCredentialRequestOptions(
challenge=challenge,
timeout=self.timeout,
rp_id=self.rp.id,
allow_credentials=descriptors,
user_verification=user_verification,
extensions=extensions,
)
),
state,
Expand Down
26 changes: 13 additions & 13 deletions fido2/webauthn.py
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ def _as_cbor(data: _JsonDataObject) -> Mapping[str, Any]:
return {k: super(_JsonDataObject, data).__getitem__(k) for k in data}


@dataclass(eq=False, frozen=True)
@dataclass(eq=False, frozen=True, kw_only=True)
class PublicKeyCredentialRpEntity(_JsonDataObject):
name: str
id: str | None = None
Expand All @@ -472,27 +472,27 @@ def id_hash(self) -> bytes | None:
return sha256(self.id.encode("utf8")) if self.id else None


@dataclass(eq=False, frozen=True)
@dataclass(eq=False, frozen=True, kw_only=True)
class PublicKeyCredentialUserEntity(_JsonDataObject):
name: str
id: bytes
display_name: str | None = None


@dataclass(eq=False, frozen=True)
@dataclass(eq=False, frozen=True, kw_only=True)
class PublicKeyCredentialParameters(_JsonDataObject):
type: PublicKeyCredentialType
alg: int


@dataclass(eq=False, frozen=True)
@dataclass(eq=False, frozen=True, kw_only=True)
class PublicKeyCredentialDescriptor(_JsonDataObject):
type: PublicKeyCredentialType
id: bytes
transports: Sequence[AuthenticatorTransport] | None = None


@dataclass(eq=False, frozen=True)
@dataclass(eq=False, frozen=True, kw_only=True)
class AuthenticatorSelectionCriteria(_JsonDataObject):
authenticator_attachment: AuthenticatorAttachment | None = None
resident_key: ResidentKeyRequirement | None = None
Expand All @@ -519,7 +519,7 @@ def __post_init__(self):
)


@dataclass(eq=False, frozen=True)
@dataclass(eq=False, frozen=True, kw_only=True)
class PublicKeyCredentialCreationOptions(_JsonDataObject):
rp: PublicKeyCredentialRpEntity
user: PublicKeyCredentialUserEntity
Expand All @@ -534,7 +534,7 @@ class PublicKeyCredentialCreationOptions(_JsonDataObject):
extensions: Mapping[str, Any] | None = None


@dataclass(eq=False, frozen=True)
@dataclass(eq=False, frozen=True, kw_only=True)
class PublicKeyCredentialRequestOptions(_JsonDataObject):
challenge: bytes
timeout: int | None = None
Expand All @@ -545,13 +545,13 @@ class PublicKeyCredentialRequestOptions(_JsonDataObject):
extensions: Mapping[str, Any] | None = None


@dataclass(eq=False, frozen=True)
@dataclass(eq=False, frozen=True, kw_only=True)
class AuthenticatorAttestationResponse(_JsonDataObject):
client_data: CollectedClientData = field(metadata=dict(name="clientDataJSON"))
attestation_object: AttestationObject


@dataclass(eq=False, frozen=True)
@dataclass(eq=False, frozen=True, kw_only=True)
class AuthenticatorAssertionResponse(_JsonDataObject):
client_data: CollectedClientData = field(metadata=dict(name="clientDataJSON"))
authenticator_data: AuthenticatorData
Expand Down Expand Up @@ -594,7 +594,7 @@ def __repr__(self):
return repr(dict(self))


@dataclass(eq=False, frozen=True)
@dataclass(eq=False, frozen=True, kw_only=True)
class RegistrationResponse(_JsonDataObject):
id: str = field(init=False)
raw_id: bytes
Expand Down Expand Up @@ -626,7 +626,7 @@ def from_dict(cls, data):
return super().from_dict(data)


@dataclass(eq=False, frozen=True)
@dataclass(eq=False, frozen=True, kw_only=True)
class AuthenticationResponse(_JsonDataObject):
id: str = field(init=False)
raw_id: bytes
Expand Down Expand Up @@ -658,11 +658,11 @@ def from_dict(cls, data):
return super().from_dict(data)


@dataclass(eq=False, frozen=True)
@dataclass(eq=False, frozen=True, kw_only=True)
class CredentialCreationOptions(_JsonDataObject):
public_key: PublicKeyCredentialCreationOptions


@dataclass(eq=False, frozen=True)
@dataclass(eq=False, frozen=True, kw_only=True)
class CredentialRequestOptions(_JsonDataObject):
public_key: PublicKeyCredentialRequestOptions
57 changes: 57 additions & 0 deletions tests/device/test_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from fido2.webauthn import Aaguid


def assert_list_of(typ, value):
assert isinstance(value, list)
for v in value:
assert isinstance(v, typ)


def assert_dict_of(k_type, v_type, value):
assert isinstance(value, dict)
for k, v in value.items():
assert isinstance(k, k_type)
assert isinstance(v, v_type)


def assert_unique(value):
assert len(set(value)) == len(value)


def test_get_info_fields(ctap2):
info = ctap2.get_info()

assert_list_of(str, info.versions)
assert len(info.versions) > 0

assert_list_of(str, info.extensions)
assert isinstance(info.aaguid, Aaguid)
assert_dict_of(str, bool | None, info.options)
assert isinstance(info.max_msg_size, int)
assert_list_of(int, info.pin_uv_protocols)
assert_unique(info.pin_uv_protocols)
assert isinstance(info.max_creds_in_list, int)
assert isinstance(info.max_cred_id_length, int)
assert_list_of(str, info.transports)
assert_unique(info.transports)

assert_list_of(dict, info.algorithms)
assert isinstance(info.max_large_blob, int)
assert isinstance(info.force_pin_change, bool)
assert isinstance(info.min_pin_length, int)
assert info.min_pin_length >= 4
assert isinstance(info.firmware_version, int)
assert isinstance(info.max_cred_blob_length, int)
assert isinstance(info.max_rpids_for_min_pin, int)
assert isinstance(info.preferred_platform_uv_attempts, int)
assert isinstance(info.uv_modality, int)
assert_dict_of(str, int, info.certifications)

assert isinstance(info.remaining_disc_creds, int | None)
assert_list_of(int, info.vendor_prototype_config_commands)
assert_list_of(str, info.attestation_formats)
assert_unique(info.attestation_formats)
assert len(info.attestation_formats) > 0

assert isinstance(info.uv_count_since_pin, int | None)
assert isinstance(info.long_touch_for_reset, bool)

0 comments on commit de03443

Please sign in to comment.