def test_device_challenge_webauthn(self): """Test webauthn""" request = get_request("/") request.user = self.user webauthn_device = WebAuthnDevice.objects.create( user=self.user, public_key=bytes_to_base64url(b"qwerqwerqre"), credential_id=bytes_to_base64url(b"foobarbaz"), sign_count=0, rp_id="foo", ) challenge = get_challenge_for_device(request, webauthn_device) del challenge["challenge"] self.assertEqual( challenge, { "allowCredentials": [{ "id": "Zm9vYmFyYmF6", "type": "public-key", }], "rpId": "testserver", "timeout": 60000, "userVerification": "preferred", }, ) with self.assertRaises(ValidationError): validate_challenge_webauthn({}, request, self.user)
def test_decode_uncompressed_ec2_public_key(self) -> None: decoded = decode_credential_public_key( base64url_to_bytes( "BBaxKZueVyr5ICDfosygxwRflSdPUcNheZhThXCeTFTNo0EM9dj0V+xJ1JwpE2XZ/8NRIt5KVvr71Zl0rB8BWOs=" )) assert isinstance(decoded, DecodedEC2PublicKey) assert decoded.kty == COSEKTY.EC2 assert decoded.alg == COSEAlgorithmIdentifier.ECDSA_SHA_256 assert decoded.crv == 1 assert (decoded.x and bytes_to_base64url(decoded.x) == "FrEpm55XKvkgIN-izKDHBF-VJ09Rw2F5mFOFcJ5MVM0") assert (decoded.y and bytes_to_base64url(decoded.y) == "o0EM9dj0V-xJ1JwpE2XZ_8NRIt5KVvr71Zl0rB8BWOs")
def test_decodes_ec2_public_key(self) -> None: decoded = decode_credential_public_key( base64url_to_bytes( "pQECAyYgASFYIDDHBDxTqWP4yZZnAa524L6uPuwhireUwRD5sXY6U2gxIlggxuwbECbDdNfTTegnc174oYdusZiMmJgct0yI_ulrJGI" )) assert isinstance(decoded, DecodedEC2PublicKey) assert decoded.kty == COSEKTY.EC2 assert decoded.alg == COSEAlgorithmIdentifier.ECDSA_SHA_256 assert decoded.crv == 1 assert (decoded.x and bytes_to_base64url(decoded.x) == "MMcEPFOpY_jJlmcBrnbgvq4-7CGKt5TBEPmxdjpTaDE") assert (decoded.y and bytes_to_base64url(decoded.y) == "xuwbECbDdNfTTegnc174oYdusZiMmJgct0yI_ulrJGI")
def test_decode_rsa_public_key(self) -> None: decoded = decode_credential_public_key( base64url_to_bytes( "pAEDAzkBACBZAQDxfpXrj0ba_AH30JJ_-W7BHSOPugOD8aEDdNBKc1gjB9AmV3FPl2aL0fwiOMKtM_byI24qXb2FzcyjC7HUVkHRtzkAQnahXckI4wY_01koaY6iwXuIE3Ya0Zjs2iZyz6u4G_abGnWdObqa_kHxc3CHR7Xy5MDkAkKyX6TqU0tgHZcEhDd_Lb5ONJDwg4wvKlZBtZYElfMuZ6lonoRZ7qR_81rGkDZyFaxp6RlyvzEbo4ijeIaHQylqCz-oFm03ifZMOfRHYuF4uTjJDRH-g4BW1f3rdi7DTHk1hJnIw1IyL_VFIQ9NifkAguYjNCySCUNpYli2eMrPhAu5dYJFFjINIUMBAAE" )) assert isinstance(decoded, DecodedRSAPublicKey) assert decoded.kty == COSEKTY.RSA assert decoded.alg == COSEAlgorithmIdentifier.RSASSA_PKCS1_v1_5_SHA_256 assert decoded.e and bytes_to_base64url(decoded.e) == "AQAB" assert ( decoded.n and bytes_to_base64url(decoded.n) == "8X6V649G2vwB99CSf_luwR0jj7oDg_GhA3TQSnNYIwfQJldxT5dmi9H8IjjCrTP28iNuKl29hc3Mowux1FZB0bc5AEJ2oV3JCOMGP9NZKGmOosF7iBN2GtGY7Nomcs-ruBv2mxp1nTm6mv5B8XNwh0e18uTA5AJCsl-k6lNLYB2XBIQ3fy2-TjSQ8IOMLypWQbWWBJXzLmepaJ6EWe6kf_NaxpA2chWsaekZcr8xG6OIo3iGh0Mpags_qBZtN4n2TDn0R2LheLk4yQ0R_oOAVtX963Yuw0x5NYSZyMNSMi_1RSEPTYn5AILmIzQskglDaWJYtnjKz4QLuXWCRRYyDQ" )
def test_raises_exception_on_unsupported_attestation_type(self) -> None: cred_json = { "id": "FsWBrFcw8yRjxV8z18Egh91o1AScNRYkIuUoY6wIlIhslDpP7eydKi1q5s9g1ugDP9mqBlPDDFPRbH6YLwHbtg", "rawId": "FsWBrFcw8yRjxV8z18Egh91o1AScNRYkIuUoY6wIlIhslDpP7eydKi1q5s9g1ugDP9mqBlPDDFPRbH6YLwHbtg", "response": { "attestationObject": "o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIgRpuZ6hdaLAgWgCFTIo4BGSTBAxwwqk4u3s1-JAzv_H4CIQCZnfoic34aOwlac1A09eflEtb0V1kO7yGhHOw5P5wVWmN4NWOBWQLBMIICvTCCAaWgAwIBAgIEKudiYzANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbjELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEnMCUGA1UEAwweWXViaWNvIFUyRiBFRSBTZXJpYWwgNzE5ODA3MDc1MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKgOGXmBD2Z4R_xCqJVRXhL8Jr45rHjsyFykhb1USGozZENOZ3cdovf5Ke8fj2rxi5tJGn_VnW4_6iQzKdIaeP6NsMGowIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjEwEwYLKwYBBAGC5RwCAQEEBAMCBDAwIQYLKwYBBAGC5RwBAQQEEgQQbUS6m_bsLkm5MAyP6SDLczAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQByV9A83MPhFWmEkNb4DvlbUwcjc9nmRzJjKxHc3HeK7GvVkm0H4XucVDB4jeMvTke0WHb_jFUiApvpOHh5VyMx5ydwFoKKcRs5x0_WwSWL0eTZ5WbVcHkDR9pSNcA_D_5AsUKOBcbpF5nkdVRxaQHuuIuwV4k1iK2IqtMNcU8vL6w21U261xCcWwJ6sMq4zzVO8QCKCQhsoIaWrwz828GDmPzfAjFsJiLJXuYivdHACkeJ5KHMt0mjVLpfJ2BCML7_rgbmvwL7wBW80VHfNdcKmKjkLcpEiPzwcQQhiN_qHV90t-p4iyr5xRSpurlP5zic2hlRkLKxMH2_kRjhqSn4aGF1dGhEYXRhWMRJlg3liA6MaHQ0Fw9kdmBbj-SuuaKGMseZXPO6gx2XY0UAAAAqbUS6m_bsLkm5MAyP6SDLcwBAFsWBrFcw8yRjxV8z18Egh91o1AScNRYkIuUoY6wIlIhslDpP7eydKi1q5s9g1ugDP9mqBlPDDFPRbH6YLwHbtqUBAgMmIAEhWCAq3y0RWh8nLzanBZQwTA7yAbUy9KEDAM0b3N9Elrb0VCJYIJrX7ygtpyInb5mXBE7g9YEow6xWrJ400HhL2r4q5tzV", "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoicERSbWtkZHVBaS1BVTJ4Nm8tRnFxaEkzWEsybmxWbHNDU3IwNHpXa050djg0SndyTUh0RWxSSEhVV0xFRGhrckVhUThCMWxCY0lIX1ZTUnFwX1JBQXciLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2V9", }, "type": "public-key", "clientExtensionResults": {}, "transports": ["nfc", "usb"], } # Take the otherwise legitimate credential and mangle its attestationObject's # "fmt" to something it could never actually be parsed_atte_obj = cbor2.loads(base64url_to_bytes(cred_json["response"]["attestationObject"])) # type: ignore parsed_atte_obj["fmt"] = "not_real_fmt" cred_json["response"]["attestationObject"] = bytes_to_base64url(cbor2.dumps(parsed_atte_obj)) # type: ignore credential = RegistrationCredential.parse_raw(json.dumps(cred_json)) challenge = base64url_to_bytes( "pDRmkdduAi-AU2x6o-FqqhI3XK2nlVlsCSr04zWkNtv84JwrMHtElRHHUWLEDhkrEaQ8B1lBcIH_VSRqp_RAAw" ) rp_id = "localhost" expected_origin = "http://localhost:5000" with self.assertRaisesRegex( Exception, "value is not a valid enumeration member" ): verify_registration_response( credential=credential, expected_challenge=challenge, expected_origin=expected_origin, expected_rp_id=rp_id, )
def webauthn_authentication_validate(request): if request.authenticated_userid is not None: return {"fail": {"errors": ["Already authenticated"]}} try: two_factor_data = _get_two_factor_data(request) except TokenException: request.session.flash( request._("Invalid or expired two factor login."), queue="error") return { "fail": { "errors": [request._("Invalid or expired two factor login.")] } } redirect_to = two_factor_data.get("redirect_to") userid = two_factor_data.get("userid") user_service = request.find_service(IUserService, context=None) form = WebAuthnAuthenticationForm( **request.POST, request=request, user_id=userid, user_service=user_service, challenge=request.session.get_webauthn_challenge(), origin=request.host_url, rp_id=request.domain, ) request.session.clear_webauthn_challenge() if form.validate(): webauthn = user_service.get_webauthn_by_credential_id( userid, bytes_to_base64url(form.validated_credential.credential_id)) webauthn.sign_count = form.validated_credential.new_sign_count _login_user( request, userid, two_factor_method="webauthn", two_factor_label=webauthn.label, ) request.response.set_cookie( USER_ID_INSECURE_COOKIE, hashlib.blake2b(str(userid).encode("ascii"), person=b"warehouse.userid").hexdigest().lower(), ) if not request.user.has_recovery_codes: send_recovery_code_reminder_email(request, request.user) return { "success": request._("Successful WebAuthn assertion"), "redirect_to": redirect_to, } errors = [str(error) for error in form.credential.errors] return {"fail": {"errors": errors}}
def test_verify_webauthn_credential_already_in_use(self, user_service, monkeypatch): user = UserFactory.create() user_service.add_webauthn( user.id, label="test_label", credential_id=bytes_to_base64url(b"foo"), public_key=b"bar", sign_count=1, ) fake_validated_credential = VerifiedRegistration( credential_id=b"foo", credential_public_key=b"bar", sign_count=0, aaguid="wutang", fmt=AttestationFormat.NONE, credential_type=PublicKeyCredentialType.PUBLIC_KEY, user_verified=False, attestation_object=b"foobar", ) verify_registration_response = pretend.call_recorder( lambda *a, **kw: fake_validated_credential) monkeypatch.setattr(webauthn, "verify_registration_response", verify_registration_response) with pytest.raises(webauthn.RegistrationRejectedError): user_service.verify_webauthn_credential( pretend.stub(), challenge=pretend.stub(), rp_id=pretend.stub(), origin=pretend.stub(), )
def test_get_webauthn_credential_options(self, user_service): user = UserFactory.create() options = user_service.get_webauthn_credential_options( user.id, challenge=b"fake_challenge", rp_name="fake_rp_name", rp_id="fake_rp_id", ) assert options["user"]["id"] == bytes_to_base64url( str(user.id).encode()) assert options["user"]["name"] == user.username assert options["user"]["displayName"] == user.name assert options["challenge"] == bytes_to_base64url(b"fake_challenge") assert options["rp"]["name"] == "fake_rp_name" assert options["rp"]["id"] == "fake_rp_id" assert "icon" not in options["user"]
def test_verify_assertion_response(monkeypatch): fake_verified_authentication = VerifiedAuthentication( credential_id=b"a credential id", new_sign_count=69, ) mock_verify_authentication_response = pretend.call_recorder( lambda *a, **kw: fake_verified_authentication ) monkeypatch.setattr( pywebauthn, "verify_authentication_response", mock_verify_authentication_response, ) not_a_real_user = pretend.stub( webauthn=[ pretend.stub( public_key=bytes_to_base64url(b"fake public key"), sign_count=68 ) ] ) resp = webauthn.verify_assertion_response( ( '{"id": "foo", "rawId": "foo", "response": ' '{"authenticatorData": "foo", "clientDataJSON": "bar", ' '"signature": "wutang"}}' ), challenge=b"not_a_real_challenge", user=not_a_real_user, origin="fake_origin", rp_id="fake_rp_id", ) assert mock_verify_authentication_response.calls == [ pretend.call( credential=AuthenticationCredential( id="foo", raw_id=b"~\x8a", response=AuthenticatorAssertionResponse( client_data_json=b"m\xaa", authenticator_data=b"~\x8a", signature=b"\xc2\xebZ\x9e", user_handle=None, ), type=PublicKeyCredentialType.PUBLIC_KEY, ), expected_challenge=b"bm90X2FfcmVhbF9jaGFsbGVuZ2U", expected_rp_id="fake_rp_id", expected_origin="fake_origin", credential_public_key=b"fake public key", credential_current_sign_count=68, require_user_verification=False, ) ] assert resp == fake_verified_authentication
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: # Webauthn Challenge has already been validated webauthn_credential: VerifiedRegistration = response.validated_data[ "response"] existing_device = WebAuthnDevice.objects.filter( credential_id=bytes_to_base64url( webauthn_credential.credential_id)).first() if not existing_device: WebAuthnDevice.objects.create( user=self.get_pending_user(), public_key=bytes_to_base64url( webauthn_credential.credential_public_key), credential_id=bytes_to_base64url( webauthn_credential.credential_id), sign_count=webauthn_credential.sign_count, rp_id=get_rp_id(self.request), name="WebAuthn Device", ) else: return self.executor.stage_invalid( "Device with Credential ID already exists.") return self.executor.stage_ok()
def test_requires_origin(self): client_data_str = json.dumps({ "type": "webauthn.create", "challenge": bytes_to_base64url(b"challenge"), }) client_data_bytes = client_data_str.encode("utf-8") with self.assertRaisesRegex( InvalidClientDataJSONStructure, 'missing required property "origin"', ): parse_client_data_json(client_data_bytes)
def test_omit_token_binding_if_not_present(self): client_data_str = json.dumps({ "type": "webauthn.create", "challenge": bytes_to_base64url(b"challenge"), "origin": "http://localhost:5000", }) client_data_bytes = client_data_str.encode("utf-8") output = parse_client_data_json(client_data_bytes) assert output.token_binding is None
def test_requires_type(self): client_data_str = json.dumps({ "challenge": bytes_to_base64url(b"challenge"), "origin": "http://localhost:5000", }) client_data_bytes = client_data_str.encode("utf-8") with self.assertRaisesRegex( InvalidClientDataJSONStructure, 'missing required property "type"', ): parse_client_data_json(client_data_bytes)
def test_verify_registration_response(monkeypatch): fake_verified_registration = VerifiedRegistration( credential_id=b"foo", credential_public_key=b"bar", sign_count=0, aaguid="wutang", fmt=AttestationFormat.NONE, credential_type=PublicKeyCredentialType.PUBLIC_KEY, user_verified=False, attestation_object=b"foobar", ) mock_verify_registration_response = pretend.call_recorder( lambda *a, **kw: fake_verified_registration ) monkeypatch.setattr( pywebauthn, "verify_registration_response", mock_verify_registration_response ) resp = webauthn.verify_registration_response( ( '{"id": "foo", "rawId": "foo", "response": ' '{"attestationObject": "foo", "clientDataJSON": "bar"}}' ), b"not_a_real_challenge", rp_id="fake_rp_id", origin="fake_origin", ) assert mock_verify_registration_response.calls == [ pretend.call( credential=RegistrationCredential( id="foo", raw_id=b"~\x8a", response=AuthenticatorAttestationResponse( client_data_json=b"m\xaa", attestation_object=b"~\x8a" ), transports=None, type=PublicKeyCredentialType.PUBLIC_KEY, ), expected_challenge=bytes_to_base64url(b"not_a_real_challenge").encode(), expected_rp_id="fake_rp_id", expected_origin="fake_origin", require_user_verification=False, ) ] assert resp == fake_verified_registration
def test_get_webauthn_assertion_options(self, user_service): user = UserFactory.create() user_service.add_webauthn( user.id, label="test_label", credential_id="foo", public_key="bar", sign_count=1, ) options = user_service.get_webauthn_assertion_options( user.id, challenge=b"fake_challenge", rp_id="fake_rp_id") assert options["challenge"] == bytes_to_base64url(b"fake_challenge") assert options["rpId"] == "fake_rp_id" assert options["allowCredentials"][0]["id"] == user.webauthn[ 0].credential_id
def test_omit_id_when_missing_from_token_binding(self): client_data_str = json.dumps({ "type": "webauthn.create", "challenge": bytes_to_base64url(b"challenge"), "origin": "http://localhost:5000", "tokenBinding": { "status": "present", }, }) client_data_bytes = client_data_str.encode("utf-8") output = parse_client_data_json(client_data_bytes) assert output.token_binding assert output.token_binding.id is None
def test_require_status_in_token_binding_when_present(self): client_data_str = json.dumps({ "type": "webauthn.create", "challenge": bytes_to_base64url(b"challenge"), "origin": "http://localhost:5000", "tokenBinding": { "id": "someidhere" }, }) client_data_bytes = client_data_str.encode("utf-8") with self.assertRaises(InvalidClientDataJSONStructure) as context: parse_client_data_json(client_data_bytes) assert 'missing required property "status"' in str(context.exception)
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_include_token_binding_when_present(self): client_data_str = json.dumps({ "type": "webauthn.create", "challenge": bytes_to_base64url(b"challenge"), "origin": "http://localhost:5000", "tokenBinding": { "status": "present", "id": "someidhere" }, }) client_data_bytes = client_data_str.encode("utf-8") output = parse_client_data_json(client_data_bytes) assert output.token_binding assert output.token_binding.status == TokenBindingStatus.PRESENT assert output.token_binding.id == "someidhere"
def verify_webauthn_credential(self, credential, *, challenge, rp_id, origin): """ Checks whether the given credential is valid, i.e. suitable for generating assertions during authentication. Returns the validated credential on success, raises webauthn.RegistrationRejectedError on failure. """ validated_credential = webauthn.verify_registration_response( credential, challenge=challenge, rp_id=rp_id, origin=origin) webauthn_cred = (self.db.query(WebAuthn).filter_by( credential_id=bytes_to_base64url( validated_credential.credential_id)).first()) if webauthn_cred is not None: raise webauthn.RegistrationRejectedError( "Credential ID already in use") return validated_credential
def validate_response(self, response: dict) -> dict: """Validate webauthn challenge response""" challenge = self.request.session["challenge"] try: registration: VerifiedRegistration = verify_registration_response( credential=RegistrationCredential.parse_raw(dumps(response)), expected_challenge=challenge, expected_rp_id=get_rp_id(self.request), expected_origin=get_origin(self.request), ) except InvalidRegistrationResponse as exc: LOGGER.warning("registration failed", exc=exc) raise ValidationError(f"Registration failed. Error: {exc}") credential_id_exists = WebAuthnDevice.objects.filter( credential_id=bytes_to_base64url( registration.credential_id)).first() if credential_id_exists: raise ValidationError("Credential ID already exists.") return registration
def get(self, request): """ :param request: The current request :type request: ~django.http.HttpResponse :return: The mfa challenge as JSON :rtype: ~django.http.JsonResponse """ if "mfa_user_id" not in request.session: return JsonResponse( { "success": False, "error": _("You need to log in first") }, status=403) if request.user.is_authenticated: return JsonResponse({ "success": False, "error": _("You are already logged in.") }) user = get_user_model().objects.get(id=request.session["mfa_user_id"]) webauthn_challenge = generate_authentication_options( rp_id=settings.HOSTNAME, allow_credentials=[ PublicKeyCredentialDescriptor(id=key.key_id) for key in user.mfa_keys.all() ], ) request.session["challenge"] = bytes_to_base64url( webauthn_challenge.challenge) # pylint: disable=http-response-with-content-type-json return HttpResponse(options_to_json(webauthn_challenge), content_type="application/json")
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, )
def test_generate_webauthn_challenge(): challenge = webauthn.generate_webauthn_challenge() assert isinstance(challenge, bytes) assert challenge == base64url_to_bytes(bytes_to_base64url(challenge))
def verify_registration_response( *, credential: RegistrationCredential, expected_challenge: bytes, expected_rp_id: str, expected_origin: Union[str, List[str]], require_user_verification: bool = False, supported_pub_key_algs: List[ COSEAlgorithmIdentifier] = default_supported_pub_key_algs, pem_root_certs_bytes_by_fmt: Optional[Mapping[AttestationFormat, List[bytes]]] = None, ) -> VerifiedRegistration: """Verify an authenticator's response to navigator.credentials.create() Args: `credential`: The value returned from `navigator.credentials.create()`. `expected_challenge`: The challenge passed to the authenticator within the preceding registration options. `expected_rp_id`: The Relying Party's unique identifier as specified in the precending registration options. `expected_origin`: The domain, with HTTP protocol (e.g. "https://domain.here"), on which the registration should have occurred. Can also be a list of expected origins. (optional) `require_user_verification`: Whether or not to require that the authenticator verified the user. (optional) `supported_pub_key_algs`: A list of public key algorithm IDs the RP chooses to restrict support to. Defaults to all supported algorithm IDs. Returns: Information about the authenticator and registration Raises: `helpers.exceptions.InvalidRegistrationResponse` if the response cannot be verified """ verified = False # FIDO-specific check if bytes_to_base64url(credential.raw_id) != credential.id: raise InvalidRegistrationResponse("id and raw_id were not equivalent") # FIDO-specific check if credential.type != PublicKeyCredentialType.PUBLIC_KEY: raise InvalidRegistrationResponse( 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_CREATE: raise InvalidRegistrationResponse( f'Unexpected client data type "{client_data.type}", expected "{ClientDataType.WEBAUTHN_CREATE}"' ) if expected_challenge != client_data.challenge: raise InvalidRegistrationResponse( "Client data challenge was not expected challenge") if isinstance(expected_origin, str): if expected_origin != client_data.origin: raise InvalidRegistrationResponse( f'Unexpected client data origin "{client_data.origin}", expected "{expected_origin}"' ) else: try: expected_origin.index(client_data.origin) except ValueError: raise InvalidRegistrationResponse( 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 InvalidRegistrationResponse( f'Unexpected token_binding status of "{status}", expected one of "{",".join(expected_token_binding_statuses)}"' ) attestation_object = parse_attestation_object(response.attestation_object) auth_data = attestation_object.auth_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 InvalidRegistrationResponse("Unexpected RP ID hash") if not auth_data.flags.up: raise InvalidRegistrationResponse( "User was not present during attestation") if require_user_verification and not auth_data.flags.uv: raise InvalidRegistrationResponse( "User verification is required but user was not verified during attestation" ) if not auth_data.attested_credential_data: raise InvalidRegistrationResponse( "Authenticator did not provide attested credential data") attested_credential_data = auth_data.attested_credential_data if not attested_credential_data.credential_id: raise InvalidRegistrationResponse( "Authenticator did not provide a credential ID") if not attested_credential_data.credential_public_key: raise InvalidRegistrationResponse( "Authenticator did not provide a credential public key") if not attested_credential_data.aaguid: raise InvalidRegistrationResponse( "Authenticator did not provide an AAGUID") decoded_credential_public_key = decode_credential_public_key( attested_credential_data.credential_public_key) if decoded_credential_public_key.alg not in supported_pub_key_algs: raise InvalidRegistrationResponse( f'Unsupported credential public key alg "{decoded_credential_public_key.alg}", expected one of: {supported_pub_key_algs}' ) # Prepare a list of possible root certificates for certificate chain validation pem_root_certs_bytes: List[bytes] = [] if pem_root_certs_bytes_by_fmt: custom_certs = pem_root_certs_bytes_by_fmt.get(attestation_object.fmt) if custom_certs: # Load any provided custom root certs pem_root_certs_bytes.extend(custom_certs) if attestation_object.fmt == AttestationFormat.NONE: # A "none" attestation should not contain _anything_ in its attestation # statement num_att_stmt_fields_set = [ val for _, val in asdict(attestation_object.att_stmt).items() if val is not None ] if len(num_att_stmt_fields_set) > 0: raise InvalidRegistrationResponse( "None attestation had unexpected attestation statement") # There's nothing else to verify, so mark the verification successful verified = True elif attestation_object.fmt == AttestationFormat.FIDO_U2F: verified = verify_fido_u2f( attestation_statement=attestation_object.att_stmt, client_data_json=response.client_data_json, rp_id_hash=auth_data.rp_id_hash, credential_id=attested_credential_data.credential_id, credential_public_key=attested_credential_data. credential_public_key, aaguid=attested_credential_data.aaguid, pem_root_certs_bytes=pem_root_certs_bytes, ) elif attestation_object.fmt == AttestationFormat.PACKED: verified = verify_packed( attestation_statement=attestation_object.att_stmt, attestation_object=response.attestation_object, client_data_json=response.client_data_json, credential_public_key=attested_credential_data. credential_public_key, pem_root_certs_bytes=pem_root_certs_bytes, ) elif attestation_object.fmt == AttestationFormat.TPM: verified = verify_tpm( attestation_statement=attestation_object.att_stmt, attestation_object=response.attestation_object, client_data_json=response.client_data_json, credential_public_key=attested_credential_data. credential_public_key, pem_root_certs_bytes=pem_root_certs_bytes, ) elif attestation_object.fmt == AttestationFormat.APPLE: verified = verify_apple( attestation_statement=attestation_object.att_stmt, attestation_object=response.attestation_object, client_data_json=response.client_data_json, credential_public_key=attested_credential_data. credential_public_key, pem_root_certs_bytes=pem_root_certs_bytes, ) elif attestation_object.fmt == AttestationFormat.ANDROID_SAFETYNET: verified = verify_android_safetynet( attestation_statement=attestation_object.att_stmt, attestation_object=response.attestation_object, client_data_json=response.client_data_json, pem_root_certs_bytes=pem_root_certs_bytes, ) elif attestation_object.fmt == AttestationFormat.ANDROID_KEY: verified = verify_android_key( attestation_statement=attestation_object.att_stmt, attestation_object=response.attestation_object, client_data_json=response.client_data_json, credential_public_key=attested_credential_data. credential_public_key, pem_root_certs_bytes=pem_root_certs_bytes, ) else: # Raise exception on an attestation format we're not prepared to verify raise InvalidRegistrationResponse( f'Unsupported attestation type "{attestation_object.fmt}"') # If we got this far and still couldn't verify things then raise an error instead # of simply returning False if not verified: raise InvalidRegistrationResponse( "Attestation statement could not be verified") return VerifiedRegistration( credential_id=attested_credential_data.credential_id, credential_public_key=attested_credential_data.credential_public_key, sign_count=auth_data.sign_count, aaguid=aaguid_to_string(attested_credential_data.aaguid), fmt=attestation_object.fmt, credential_type=credential.type, user_verified=auth_data.flags.uv, attestation_object=response.attestation_object, )