Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/improve-bytes-subclass-mypy-support #130

Merged
merged 4 commits into from
Apr 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
[mypy]
plugins = pydantic.mypy

python_version = 3.8

[pydantic-mypy]
init_typed=True

[mypy-asn1crypto.*]
ignore_missing_imports = True

Expand Down
3 changes: 1 addition & 2 deletions webauthn/authentication/verify_authentication_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
PublicKeyCredentialType,
TokenBindingStatus,
WebAuthnBaseModel,
BytesLike,
)


Expand All @@ -27,7 +26,7 @@ class VerifiedAuthentication(WebAuthnBaseModel):
Information about a verified authentication of which an RP can make use
"""

credential_id: BytesLike
credential_id: bytes
new_sign_count: int


Expand Down
74 changes: 35 additions & 39 deletions webauthn/helpers/structs.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from enum import Enum
from typing import List, Literal, Optional

from pydantic import BaseModel
from pydantic import BaseModel, validator
from pydantic.validators import strict_bytes_validator
from pydantic.fields import ModelField

from .bytes_to_base64url import bytes_to_base64url
from .cose import COSEAlgorithmIdentifier
Expand Down Expand Up @@ -30,35 +31,30 @@ class Config:
alias_generator = snake_case_to_camel_case
allow_population_by_field_name = True

@validator("*", pre=True, allow_reuse=True)
def _validate_bytes_fields(cls, v, field: ModelField):
"""
Allow for Pydantic models to define fields as `bytes`, but allow consuming projects to
specify bytes-adjacent values (bytes subclasses, memoryviews, etc...) that otherwise
function like `bytes`. Keeps the library Pythonic.
"""
if field.type_ != bytes:
return v

class BytesLike(bytes):
"""
Custom type to use as an annotation in Pydantic models. This helps us be a better dependency
and get along with other libraries like mongoengine that use "clever" bytes subclasses that
otherwise act like normal `bytes` type.

BaseModel properties with this type will accept either `bytes` OR a subclass of `bytes`.

See the following issues on GitHub for more context:

- https://github.com/duo-labs/py_webauthn/issues/110
- https://github.com/duo-labs/py_webauthn/issues/113
"""

@classmethod
def __get_validators__(cls):
yield cls.validate

@classmethod
def validate(cls, v):
if isinstance(v, bytes):
return v
"""
Return raw bytes from subclasses as well

`strict_bytes_validator()` performs a similar check to this, but it passes through the
subclass as-is and Pydantic then rejects it. Passing the subclass into `bytes()` lets us
return `bytes` and make Pydantic happy.
"""
return bytes(v)
elif isinstance(v, memoryview):
return v.tobytes()
else:
return strict_bytes_validator(v)


################
#
# Fundamental data structures
Expand Down Expand Up @@ -243,7 +239,7 @@ class PublicKeyCredentialUserEntity(WebAuthnBaseModel):
https://www.w3.org/TR/webauthn-2/#dictdef-publickeycredentialuserentity
"""

id: BytesLike
id: bytes
name: str
display_name: str

Expand Down Expand Up @@ -273,7 +269,7 @@ class PublicKeyCredentialDescriptor(WebAuthnBaseModel):
https://www.w3.org/TR/webauthn-2/#dictdef-publickeycredentialdescriptor
"""

id: BytesLike
id: bytes
type: Literal[
PublicKeyCredentialType.PUBLIC_KEY
] = PublicKeyCredentialType.PUBLIC_KEY
Expand Down Expand Up @@ -314,7 +310,7 @@ class CollectedClientData(WebAuthnBaseModel):
"""

type: ClientDataType
challenge: BytesLike
challenge: bytes
origin: str
cross_origin: Optional[bool] = None
token_binding: Optional[TokenBinding] = None
Expand Down Expand Up @@ -345,7 +341,7 @@ class PublicKeyCredentialCreationOptions(WebAuthnBaseModel):

rp: PublicKeyCredentialRpEntity
user: PublicKeyCredentialUserEntity
challenge: BytesLike
challenge: bytes
pub_key_cred_params: List[PublicKeyCredentialParameters]
timeout: Optional[int] = None
exclude_credentials: Optional[List[PublicKeyCredentialDescriptor]] = None
Expand All @@ -363,8 +359,8 @@ class AuthenticatorAttestationResponse(WebAuthnBaseModel):
https://www.w3.org/TR/webauthn-2/#authenticatorattestationresponse
"""

client_data_json: BytesLike
attestation_object: BytesLike
client_data_json: bytes
attestation_object: bytes


class RegistrationCredential(WebAuthnBaseModel):
Expand All @@ -381,7 +377,7 @@ class RegistrationCredential(WebAuthnBaseModel):
"""

id: str
raw_id: BytesLike
raw_id: bytes
response: AuthenticatorAttestationResponse
transports: Optional[List[AuthenticatorTransport]] = None
type: Literal[
Expand Down Expand Up @@ -436,9 +432,9 @@ class AttestedCredentialData(WebAuthnBaseModel):
https://www.w3.org/TR/webauthn-2/#attested-credential-data
"""

aaguid: BytesLike
credential_id: BytesLike
credential_public_key: BytesLike
aaguid: bytes
credential_id: bytes
credential_public_key: bytes


class AuthenticatorData(WebAuthnBaseModel):
Expand All @@ -455,7 +451,7 @@ class AuthenticatorData(WebAuthnBaseModel):
https://www.w3.org/TR/webauthn-2/#sctn-attested-credential-data
"""

rp_id_hash: BytesLike
rp_id_hash: bytes
flags: AuthenticatorDataFlags
sign_count: int
attested_credential_data: Optional[AttestedCredentialData] = None
Expand Down Expand Up @@ -498,7 +494,7 @@ class PublicKeyCredentialRequestOptions(WebAuthnBaseModel):
https://www.w3.org/TR/webauthn-2/#dictionary-assertion-options
"""

challenge: BytesLike
challenge: bytes
timeout: Optional[int] = None
rp_id: Optional[str] = None
allow_credentials: Optional[List[PublicKeyCredentialDescriptor]] = []
Expand All @@ -519,9 +515,9 @@ class AuthenticatorAssertionResponse(WebAuthnBaseModel):
https://www.w3.org/TR/webauthn-2/#authenticatorassertionresponse
"""

client_data_json: BytesLike
authenticator_data: BytesLike
signature: BytesLike
client_data_json: bytes
authenticator_data: bytes
signature: bytes
user_handle: Optional[bytes] = None


Expand All @@ -538,7 +534,7 @@ class AuthenticationCredential(WebAuthnBaseModel):
"""

id: str
raw_id: BytesLike
raw_id: bytes
response: AuthenticatorAssertionResponse
type: Literal[
PublicKeyCredentialType.PUBLIC_KEY
Expand Down
7 changes: 3 additions & 4 deletions webauthn/registration/verify_registration_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
RegistrationCredential,
TokenBindingStatus,
WebAuthnBaseModel,
BytesLike,
)
from .formats.android_key import verify_android_key
from .formats.android_safetynet import verify_android_safetynet
Expand All @@ -42,14 +41,14 @@ class VerifiedRegistration(WebAuthnBaseModel):
`attestation_object`: The raw attestation object for later scrutiny
"""

credential_id: BytesLike
credential_public_key: BytesLike
credential_id: bytes
credential_public_key: bytes
sign_count: int
aaguid: str
fmt: AttestationFormat
credential_type: PublicKeyCredentialType
user_verified: bool
attestation_object: BytesLike
attestation_object: bytes


expected_token_binding_statuses = [
Expand Down