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/backup-state #136

Merged
merged 5 commits into from
Jul 13, 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
18 changes: 18 additions & 0 deletions tests/test_parse_authenticator_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ def _generate_auth_data(
sign_count: int = 0,
up: bool = True,
uv: bool = False,
be: bool = False,
bs: bool = False,
at: bool = False,
ed: bool = False,
) -> Tuple[bytes, bytes, int, Optional[bytes], Optional[bytes], Optional[bytes]]:
Expand All @@ -20,6 +22,8 @@ def _generate_auth_data(
`sign_count`: How many times the authenticator has been used
`up`: Whether user was present
`uv`: Whether user was verified
`be`: Whether credential can be backed up
`bs`: Whether credential has been backed up
`at`: Whether attested credential data is present
`ed`: Whether extension data is present

Expand All @@ -39,6 +43,10 @@ def _generate_auth_data(
flags = flags | 1 << 0
if uv is True:
flags = flags | 1 << 2
if be is True:
flags = flags | 1 << 3
if bs is True:
flags = flags | 1 << 4
if at is True:
flags = flags | 1 << 6
if ed is True:
Expand Down Expand Up @@ -90,6 +98,8 @@ def test_correctly_parses_simple(self) -> None:
assert output.rp_id_hash == rp_id_hash
assert output.flags.up is True
assert output.flags.uv is True
assert output.flags.be is False
assert output.flags.be is False
assert output.flags.at is False
assert output.flags.ed is False
assert output.sign_count == sign_count
Expand Down Expand Up @@ -164,3 +174,11 @@ def test_parses_only_extension_data(self) -> None:
'example.extension': 'This is an example extension! If you read this message, you probably successfully passing conformance tests. Good job!',
}
)

def test_parses_backup_state_flags(self) -> None:
(auth_data, _, _, _, _, _) = _generate_auth_data(be=True, bs=True)

output = parse_authenticator_data(auth_data)

assert output.flags.be is True
assert output.flags.be is True
56 changes: 56 additions & 0 deletions tests/test_parse_backup_flags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from unittest import TestCase

from webauthn.helpers import parse_backup_flags
from webauthn.helpers.structs import AuthenticatorDataFlags
from webauthn.helpers.exceptions import InvalidBackupFlags


class TestParseBackupFlags(TestCase):
flags: AuthenticatorDataFlags

def setUp(self) -> None:
self.flags = AuthenticatorDataFlags(
up=True,
uv=False,
be=False,
bs=False,
at=False,
ed=False,
)

def test_returns_single_device_not_backed_up(self) -> None:
self.flags.be = False
self.flags.bs = False

parsed = parse_backup_flags(self.flags)

self.assertEqual(parsed.credential_device_type, 'single_device')
self.assertEqual(parsed.credential_backed_up, False)

def test_returns_multi_device_not_backed_up(self) -> None:
self.flags.be = True
self.flags.bs = False

parsed = parse_backup_flags(self.flags)

self.assertEqual(parsed.credential_device_type, 'multi_device')
self.assertEqual(parsed.credential_backed_up, False)

def test_returns_multi_device_backed_up(self) -> None:
self.flags.be = True
self.flags.bs = True

parsed = parse_backup_flags(self.flags)

self.assertEqual(parsed.credential_device_type, 'multi_device')
self.assertEqual(parsed.credential_backed_up, True)

def test_raises_on_invalid_backup_state_flags(self) -> None:
self.flags.be = False
self.flags.bs = True

with self.assertRaisesRegex(
InvalidBackupFlags,
"impossible",
):
parse_backup_flags(self.flags)
2 changes: 2 additions & 0 deletions tests/test_verify_authentication_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ def test_verify_authentication_response_with_EC2_public_key(self):
"EDx9FfAbp4obx6oll2oC4-CZuDidRVV4gZhxC529ytlnqHyqCStDUwfNdm1SNHAe3X5KvueWQdAX3x9R1a2b9Q"
)
assert verification.new_sign_count == 78
assert verification.credential_backed_up == False
assert verification.credential_device_type == 'single_device'

def test_verify_authentication_response_with_RSA_public_key(self):
credential = AuthenticationCredential.parse_raw(
Expand Down
2 changes: 2 additions & 0 deletions tests/test_verify_registration_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ def test_verifies_none_attestation_response(self) -> None:
)
assert verification.credential_type == PublicKeyCredentialType.PUBLIC_KEY
assert verification.sign_count == 23
assert verification.credential_backed_up == False
assert verification.credential_device_type == 'single_device'

def test_raises_exception_on_unsupported_attestation_type(self) -> None:
cred_json = {
Expand Down
8 changes: 8 additions & 0 deletions webauthn/authentication/verify_authentication_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
decode_credential_public_key,
decoded_public_key_to_cryptography,
parse_authenticator_data,
parse_backup_flags,
parse_client_data_json,
verify_signature,
)
from webauthn.helpers.exceptions import InvalidAuthenticationResponse
from webauthn.helpers.structs import (
AuthenticationCredential,
ClientDataType,
CredentialDeviceType,
PublicKeyCredentialType,
TokenBindingStatus,
WebAuthnBaseModel,
Expand All @@ -28,6 +30,8 @@ class VerifiedAuthentication(WebAuthnBaseModel):

credential_id: bytes
new_sign_count: int
credential_device_type: CredentialDeviceType
credential_backed_up: bool


expected_token_binding_statuses = [
Expand Down Expand Up @@ -157,7 +161,11 @@ def verify_authentication_response(
except InvalidSignature:
raise InvalidAuthenticationResponse("Could not verify authentication signature")

parsed_backup_flags = parse_backup_flags(auth_data.flags)

return VerifiedAuthentication(
credential_id=credential.raw_id,
new_sign_count=auth_data.sign_count,
credential_device_type=parsed_backup_flags.credential_device_type,
credential_backed_up=parsed_backup_flags.credential_backed_up,
)
1 change: 1 addition & 0 deletions webauthn/helpers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .options_to_json import options_to_json # noqa: F401
from .parse_attestation_object import parse_attestation_object # noqa: F401
from .parse_authenticator_data import parse_authenticator_data # noqa: F401
from .parse_backup_flags import parse_backup_flags
from .parse_client_data_json import parse_client_data_json # noqa: F401
from .validate_certificate_chain import validate_certificate_chain # noqa: F401
from .verify_safetynet_timestamp import verify_safetynet_timestamp # noqa: F401
Expand Down
4 changes: 4 additions & 0 deletions webauthn/helpers/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,7 @@ class InvalidTPMCertInfoStructure(Exception):

class InvalidCertificateChain(Exception):
pass


class InvalidBackupFlags(Exception):
pass
2 changes: 2 additions & 0 deletions webauthn/helpers/parse_authenticator_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ def parse_authenticator_data(val: bytes) -> AuthenticatorData:
flags = AuthenticatorDataFlags(
up=flags_bytes & (1 << 0) != 0,
uv=flags_bytes & (1 << 2) != 0,
be=flags_bytes & (1 << 3) != 0,
bs=flags_bytes & (1 << 4) != 0,
at=flags_bytes & (1 << 6) != 0,
ed=flags_bytes & (1 << 7) != 0,
)
Expand Down
33 changes: 33 additions & 0 deletions webauthn/helpers/parse_backup_flags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from enum import Enum
from pydantic import BaseModel

from .structs import AuthenticatorDataFlags, CredentialDeviceType
from .exceptions import InvalidBackupFlags


class ParsedBackupFlags(BaseModel):
credential_device_type: CredentialDeviceType
credential_backed_up: bool


def parse_backup_flags(flags: AuthenticatorDataFlags) -> ParsedBackupFlags:
"""Convert backup eligibility and backup state flags into more useful representations

Raises:
`helpers.exceptions.InvalidBackupFlags` if an invalid backup state is detected
"""
credential_device_type = CredentialDeviceType.SINGLE_DEVICE

# A credential that can be backed up can typically be used on multiple devices
if flags.be:
credential_device_type = CredentialDeviceType.MULTI_DEVICE

if credential_device_type == CredentialDeviceType.SINGLE_DEVICE and flags.bs:
raise InvalidBackupFlags(
"Single-device credential indicated that it was backed up, which should be impossible."
)

return ParsedBackupFlags(
credential_device_type=credential_device_type,
credential_backed_up=flags.bs,
)
27 changes: 27 additions & 0 deletions webauthn/helpers/structs.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def _validate_bytes_fields(cls, v, field: ModelField):
# values
return v


################
#
# Fundamental data structures
Expand Down Expand Up @@ -411,13 +412,18 @@ class AuthenticatorDataFlags(WebAuthnBaseModel):
Attributes:
`up`: [U]ser was [P]resent
`uv`: [U]ser was [V]erified
`be`: [B]ackup [E]ligible
`bs`: [B]ackup [S]tate
`at`: [AT]tested credential is included
`ed`: [E]xtension [D]ata is included

https://www.w3.org/TR/webauthn-2/#flags
"""

up: bool
uv: bool
be: bool
bs: bool
at: bool
ed: bool

Expand Down Expand Up @@ -540,3 +546,24 @@ class AuthenticationCredential(WebAuthnBaseModel):
type: Literal[
PublicKeyCredentialType.PUBLIC_KEY
] = PublicKeyCredentialType.PUBLIC_KEY


################
#
# Credential Backup State
#
################


class CredentialDeviceType(str, Enum):
"""A determination of the number of devices a credential can be used from

Members:
`SINGLE_DEVICE`: A credential that is bound to a single device
`MULTI_DEVICE`: A credential that can be used from multiple devices (e.g. passkeys)

https://w3c.github.io/webauthn/#sctn-credential-backup (L3 Draft)
"""

SINGLE_DEVICE = "single_device"
MULTI_DEVICE = "multi_device"
8 changes: 8 additions & 0 deletions webauthn/registration/verify_registration_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
decode_credential_public_key,
parse_attestation_object,
parse_client_data_json,
parse_backup_flags,
)
from webauthn.helpers.cose import COSEAlgorithmIdentifier
from webauthn.helpers.exceptions import InvalidRegistrationResponse
from webauthn.helpers.structs import (
AttestationFormat,
ClientDataType,
CredentialDeviceType,
PublicKeyCredentialType,
RegistrationCredential,
TokenBindingStatus,
Expand Down Expand Up @@ -49,6 +51,8 @@ class VerifiedRegistration(WebAuthnBaseModel):
credential_type: PublicKeyCredentialType
user_verified: bool
attestation_object: bytes
credential_device_type: CredentialDeviceType
credential_backed_up: bool


expected_token_binding_statuses = [
Expand Down Expand Up @@ -262,6 +266,8 @@ def verify_registration_response(
if not verified:
raise InvalidRegistrationResponse("Attestation statement could not be verified")

parsed_backup_flags = parse_backup_flags(auth_data.flags)

return VerifiedRegistration(
credential_id=attested_credential_data.credential_id,
credential_public_key=attested_credential_data.credential_public_key,
Expand All @@ -271,4 +277,6 @@ def verify_registration_response(
credential_type=credential.type,
user_verified=auth_data.flags.uv,
attestation_object=response.attestation_object,
credential_device_type=parsed_backup_flags.credential_device_type,
credential_backed_up=parsed_backup_flags.credential_backed_up,
)