예제 #1
0
    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)
예제 #2
0
    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")
예제 #3
0
    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")
예제 #4
0
    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"
        )
예제 #5
0
    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,
            )
예제 #6
0
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}}
예제 #7
0
    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(),
            )
예제 #8
0
    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"]
예제 #9
0
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
예제 #10
0
 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)
예제 #14
0
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
예제 #15
0
    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"
예제 #20
0
    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
예제 #21
0
    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
예제 #22
0
    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")
예제 #23
0
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,
    )
예제 #24
0
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,
    )