Skip to content

Commit

Permalink
Refactor dataclass serialization
Browse files Browse the repository at this point in the history
  • Loading branch information
dainnilsson committed Oct 14, 2024
1 parent 4710f25 commit ef65f77
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 238 deletions.
52 changes: 24 additions & 28 deletions fido2/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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):
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion fido2/ctap2/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]):
Expand Down
18 changes: 10 additions & 8 deletions fido2/ctap2/bio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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,
)
Expand Down
29 changes: 10 additions & 19 deletions fido2/ctap2/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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("<B", sub_cmd)
+ (cbor.encode(params) if params else b"")
)
msg = b"\xff" * 32 + b"\x0d" + struct.pack("<B", sub_cmd)
if params is not None:
msg += cbor.encode(params)
pin_uv_protocol = self.pin_uv.protocol.VERSION
pin_uv_param = self.pin_uv.protocol.authenticate(self.pin_uv.token, msg)
else:
Expand Down Expand Up @@ -124,11 +117,9 @@ def set_min_pin_length(
:param force_change_pin: True if the Authenticator should enforce changing the
PIN before the next use.
"""
self._call(
Config.CMD.SET_MIN_PIN_LENGTH,
{
Config.PARAM.NEW_MIN_PIN_LENGTH: min_pin_length,
Config.PARAM.MIN_PIN_LENGTH_RPIDS: rp_ids,
Config.PARAM.FORCE_CHANGE_PIN: force_change_pin,
},
)
params: Dict[int, Any] = {Config.PARAM.FORCE_CHANGE_PIN: force_change_pin}
if min_pin_length is not None:
params[Config.PARAM.NEW_MIN_PIN_LENGTH] = min_pin_length
if rp_ids is not None:
params[Config.PARAM.MIN_PIN_LENGTH_RPIDS] = rp_ids
self._call(Config.CMD.SET_MIN_PIN_LENGTH, params)
17 changes: 12 additions & 5 deletions fido2/ctap2/credman.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@

from .. import cbor
from ..ctap import CtapError
from ..webauthn import PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity
from ..webauthn import (
PublicKeyCredentialDescriptor,
PublicKeyCredentialUserEntity,
_as_cbor,
)
from .base import Ctap2, Info
from .pin import PinProtocol, _PinUv

Expand Down Expand Up @@ -217,10 +221,11 @@ def delete_cred(self, cred_id: PublicKeyCredentialDescriptor) -> 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")

Expand All @@ -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")
Loading

0 comments on commit ef65f77

Please sign in to comment.