def test_parses_uv_false(self) -> None: auth_data = _generate_auth_data()[0] output = parse_authenticator_data(auth_data) assert output.flags.up is True assert output.flags.uv is False
def test_parses_uv_false(self) -> None: auth_data = _generate_auth_data()[0] output = parse_authenticator_data(auth_data) self.assertTrue(output.flags.up) self.assertFalse(output.flags.uv)
def test_correctly_parses_simple(self) -> None: (auth_data, rp_id_hash, sign_count, _, _, _) = _generate_auth_data(10, up=True, uv=True) output = parse_authenticator_data(auth_data) assert output.rp_id_hash == rp_id_hash assert output.flags.up is True assert output.flags.uv is True assert output.flags.at is False assert output.flags.ed is False assert output.sign_count == sign_count
def test_correctly_parses_attested_credential_data(self) -> None: ( auth_data, _, _, aaguid, credential_id, credential_public_key, ) = _generate_auth_data(10, up=True, uv=True, at=True) output = parse_authenticator_data(auth_data) cred_data = output.attested_credential_data assert cred_data assert cred_data.aaguid == aaguid assert cred_data.credential_id == credential_id assert cred_data.credential_public_key == credential_public_key
def test_parses_only_extension_data(self) -> None: # Pulled from Conformance Testing suite auth_data = base64url_to_bytes( "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2OBAAAAjaFxZXhhbXBsZS5leHRlbnNpb254dlRoaXMgaXMgYW4gZXhhbXBsZSBleHRlbnNpb24hIElmIHlvdSByZWFkIHRoaXMgbWVzc2FnZSwgeW91IHByb2JhYmx5IHN1Y2Nlc3NmdWxseSBwYXNzaW5nIGNvbmZvcm1hbmNlIHRlc3RzLiBHb29kIGpvYiE" ) output = parse_authenticator_data(auth_data) extensions = output.extensions self.assertIsNotNone(extensions) assert extensions # Make mypy happy parsed_extensions = cbor2.loads(extensions) self.assertEqual( parsed_extensions, { 'example.extension': 'This is an example extension! If you read this message, you probably successfully passing conformance tests. Good job!', })
def test_parses_attested_credential_data_and_extension_data(self) -> None: auth_data = bytes.fromhex( "50569158be61d7a1ba084f80e45e938fd326e0a8dff07b37036e6c82303ae26bc1000004377b3024675546afcb92e4495c8a1e193f00dca30058b8d74f6bd74de90baeb34afb51e3578e1ac4ca9f79a7f88473d8254d5762ca82d68f3bf63f49e9b284caab4d45d6f9bb468d0c1b7f0f727378c1db8adb4802cb7c5ad9c5eb905bf0ba03f79bd1f04d63765452d49c4087acfad340516dc892eafd87d498ae9e6fd6f06a3f423108ebdc032d93e82fdd6deacc1b638fd56838a482f01232ad01e266e016a50b8121816997a167f41139900fe46094b8ef30aad14ee08cc457366a033bb4a0554dcf9c9589f9622d4f84481541014c870291c87d7a3bbe3d8b07eb02509de5721e3f728aa5eac41e9c5af02869a4010103272006215820e613b86a8d4ebae24e84a0270b6773f7bb30d1d59f5ec379910ebe7c87714274a16b6372656450726f7465637401" ) output = parse_authenticator_data(auth_data) cred_data = output.attested_credential_data self.assertIsNotNone(cred_data) assert cred_data # Make mypy happy self.assertEqual( bytes_to_base64url(cred_data.credential_public_key), "pAEBAycgBiFYIOYTuGqNTrriToSgJwtnc_e7MNHVn17DeZEOvnyHcUJ0") extensions = output.extensions self.assertIsNotNone(extensions) assert extensions # Make mypy happy parsed_extensions = cbor2.loads(extensions) self.assertEqual(parsed_extensions, {'credProtect': 1})
def test_correctly_parses_attested_credential_data(self) -> None: auth_data = base64url_to_bytes( "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NBAAAAJch83ZdWwUm4niTLNjZU81AAIHa7Ksm5br3hAh3UjxP9-4rqu8BEsD-7SZ2xWe1_yHv6pAEDAzkBACBZAQDcxA7Ehs9goWB2Hbl6e9v-aUub9rvy2M7Hkvf-iCzMGE63e3sCEW5Ru33KNy4um46s9jalcBHtZgtEnyeRoQvszis-ws5o4Da0vQfuzlpBmjWT1dV6LuP-vs9wrfObW4jlA5bKEIhv63-jAxOtdXGVzo75PxBlqxrmrr5IR9n8Fw7clwRsDkjgRHaNcQVbwq_qdNwU5H3hZKu9szTwBS5NGRq01EaDF2014YSTFjwtAmZ3PU1tcO_QD2U2zg6eB5grfWDeAJtRE8cbndDWc8aLL0aeC37Q36-TVsGe6AhBgHEw6eO3I3NW5r9v_26CqMPBDwmEundeq1iGyKfMloobIUMBAAE" ) output = parse_authenticator_data(auth_data) cred_data = output.attested_credential_data self.assertIsNotNone(cred_data) assert cred_data # Make mypy happy self.assertEqual( cred_data.aaguid, base64url_to_bytes("yHzdl1bBSbieJMs2NlTzUA"), ) self.assertEqual( cred_data.credential_id, base64url_to_bytes("drsqybluveECHdSPE_37iuq7wESwP7tJnbFZ7X_Ie_o"), ) self.assertEqual( cred_data.credential_public_key, base64url_to_bytes( "pAEDAzkBACBZAQDcxA7Ehs9goWB2Hbl6e9v-aUub9rvy2M7Hkvf-iCzMGE63e3sCEW5Ru33KNy4um46s9jalcBHtZgtEnyeRoQvszis-ws5o4Da0vQfuzlpBmjWT1dV6LuP-vs9wrfObW4jlA5bKEIhv63-jAxOtdXGVzo75PxBlqxrmrr5IR9n8Fw7clwRsDkjgRHaNcQVbwq_qdNwU5H3hZKu9szTwBS5NGRq01EaDF2014YSTFjwtAmZ3PU1tcO_QD2U2zg6eB5grfWDeAJtRE8cbndDWc8aLL0aeC37Q36-TVsGe6AhBgHEw6eO3I3NW5r9v_26CqMPBDwmEundeq1iGyKfMloobIUMBAAE" ), )
def verify_authentication_response( *, credential: 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()`. `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 precending 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 """ # FIDO-specific check if bytes_to_base64url(credential.raw_id) != credential.id: raise InvalidAuthenticationResponse( "id and raw_id were not equivalent") # FIDO-specific check if credential.type != PublicKeyCredentialType.PUBLIC_KEY: raise InvalidAuthenticationResponse( f'Unexpected credential type "{credential.type}", expected "public-key"' ) response = credential.response client_data = parse_client_data_json(response.client_data_json) 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(response.authenticator_data) # Generate a hash of the expected RP ID for comparison 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: # Require the sign count to have been incremented over what was reported by the # authenticator the last time this credential was used, otherwise this might be # a replay attack 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(response.client_data_json) client_data_hash_bytes = client_data_hash.digest() signature_base = response.authenticator_data + 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=response.signature, data=signature_base, ) except InvalidSignature: raise InvalidAuthenticationResponse( "Could not verify authentication signature") return VerifiedAuthentication( credential_id=credential.raw_id, new_sign_count=auth_data.sign_count, )