| from dataclasses import dataclass |
| import hashlib |
| from typing import List, Union |
|
|
| from cryptography.exceptions import InvalidSignature |
|
|
| from webauthn.helpers import ( |
| bytes_to_base64url, |
| byteslike_to_bytes, |
| decode_credential_public_key, |
| decoded_public_key_to_cryptography, |
| parse_authenticator_data, |
| parse_backup_flags, |
| parse_client_data_json, |
| parse_authentication_credential_json, |
| verify_signature, |
| ) |
| from webauthn.helpers.exceptions import InvalidAuthenticationResponse |
| from webauthn.helpers.structs import ( |
| AuthenticationCredential, |
| ClientDataType, |
| CredentialDeviceType, |
| PublicKeyCredentialType, |
| TokenBindingStatus, |
| ) |
|
|
|
|
| @dataclass |
| class VerifiedAuthentication: |
| """ |
| Information about a verified authentication of which an RP can make use |
| """ |
|
|
| credential_id: bytes |
| new_sign_count: int |
| credential_device_type: CredentialDeviceType |
| credential_backed_up: bool |
|
|
|
|
| expected_token_binding_statuses = [ |
| TokenBindingStatus.SUPPORTED, |
| TokenBindingStatus.PRESENT, |
| ] |
|
|
|
|
| def verify_authentication_response( |
| *, |
| credential: Union[str, dict, AuthenticationCredential], |
| expected_challenge: bytes, |
| expected_rp_id: str, |
| expected_origin: Union[str, List[str]], |
| credential_public_key: bytes, |
| credential_current_sign_count: int, |
| require_user_verification: bool = False, |
| ) -> VerifiedAuthentication: |
| """Verify a response from navigator.credentials.get() |
| |
| Args: |
| - `credential`: The value returned from `navigator.credentials.get()`. Can be either a |
| stringified JSON object, a plain dict, or an instance of RegistrationCredential |
| - `expected_challenge`: The challenge passed to the authenticator within the preceding |
| authentication options. |
| - `expected_rp_id`: The Relying Party's unique identifier as specified in the preceding |
| authentication options. |
| - `expected_origin`: The domain, with HTTP protocol (e.g. "https://domain.here"), on which |
| the authentication ceremony should have occurred. |
| - `credential_public_key`: The public key for the credential's ID as provided in a |
| preceding authenticator registration ceremony. |
| - `credential_current_sign_count`: The current known number of times the authenticator was |
| used. |
| - (optional) `require_user_verification`: Whether or not to require that the authenticator |
| verified the user. |
| |
| Returns: |
| Information about the authenticator |
| |
| Raises: |
| `helpers.exceptions.InvalidAuthenticationResponse` if the response cannot be verified |
| """ |
| if isinstance(credential, str) or isinstance(credential, dict): |
| credential = parse_authentication_credential_json(credential) |
|
|
| |
| if bytes_to_base64url(credential.raw_id) != credential.id: |
| raise InvalidAuthenticationResponse("id and raw_id were not equivalent") |
|
|
| |
| if credential.type != PublicKeyCredentialType.PUBLIC_KEY: |
| raise InvalidAuthenticationResponse( |
| f'Unexpected credential type "{credential.type}", expected "public-key"' |
| ) |
|
|
| response = credential.response |
|
|
| client_data_bytes = byteslike_to_bytes(response.client_data_json) |
| authenticator_data_bytes = byteslike_to_bytes(response.authenticator_data) |
| signature_bytes = byteslike_to_bytes(response.signature) |
|
|
| client_data = parse_client_data_json(client_data_bytes) |
|
|
| if client_data.type != ClientDataType.WEBAUTHN_GET: |
| raise InvalidAuthenticationResponse( |
| f'Unexpected client data type "{client_data.type}", expected "{ClientDataType.WEBAUTHN_GET}"' |
| ) |
|
|
| if expected_challenge != client_data.challenge: |
| raise InvalidAuthenticationResponse("Client data challenge was not expected challenge") |
|
|
| if isinstance(expected_origin, str): |
| if expected_origin != client_data.origin: |
| raise InvalidAuthenticationResponse( |
| f'Unexpected client data origin "{client_data.origin}", expected "{expected_origin}"' |
| ) |
| else: |
| try: |
| expected_origin.index(client_data.origin) |
| except ValueError: |
| raise InvalidAuthenticationResponse( |
| f'Unexpected client data origin "{client_data.origin}", expected one of {expected_origin}' |
| ) |
|
|
| if client_data.token_binding: |
| status = client_data.token_binding.status |
| if status not in expected_token_binding_statuses: |
| raise InvalidAuthenticationResponse( |
| f'Unexpected token_binding status of "{status}", expected one of "{",".join(expected_token_binding_statuses)}"' |
| ) |
|
|
| auth_data = parse_authenticator_data(authenticator_data_bytes) |
|
|
| |
| expected_rp_id_hash = hashlib.sha256() |
| expected_rp_id_hash.update(expected_rp_id.encode("utf-8")) |
| expected_rp_id_hash_bytes = expected_rp_id_hash.digest() |
|
|
| if auth_data.rp_id_hash != expected_rp_id_hash_bytes: |
| raise InvalidAuthenticationResponse("Unexpected RP ID hash") |
|
|
| if not auth_data.flags.up: |
| raise InvalidAuthenticationResponse("User was not present during authentication") |
|
|
| if require_user_verification and not auth_data.flags.uv: |
| raise InvalidAuthenticationResponse( |
| "User verification is required but user was not verified during authentication" |
| ) |
|
|
| if ( |
| auth_data.sign_count > 0 or credential_current_sign_count > 0 |
| ) and auth_data.sign_count <= credential_current_sign_count: |
| |
| |
| |
| raise InvalidAuthenticationResponse( |
| f"Response sign count of {auth_data.sign_count} was not greater than current count of {credential_current_sign_count}" |
| ) |
|
|
| client_data_hash = hashlib.sha256() |
| client_data_hash.update(client_data_bytes) |
| client_data_hash_bytes = client_data_hash.digest() |
|
|
| signature_base = authenticator_data_bytes + client_data_hash_bytes |
|
|
| try: |
| decoded_public_key = decode_credential_public_key(credential_public_key) |
| crypto_public_key = decoded_public_key_to_cryptography(decoded_public_key) |
|
|
| verify_signature( |
| public_key=crypto_public_key, |
| signature_alg=decoded_public_key.alg, |
| signature=signature_bytes, |
| data=signature_base, |
| ) |
| 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, |
| ) |
|
|