Проблема подписи с webauthn на django с djoser

В данный момент я работаю над реализацией webauthn на одном проекте. Суть в том, чтобы дать возможность пользователю использовать FaceId или скан отпечатка пальца на своем мобильном на сайте.

Я попробовал djoser версию webauthn, но я хотел дать возможность пользователю, у которого уже есть аккаунт, поэтому я взял реализацию webauthn из djoser и обновил ее, чтобы она работала с уже созданным аккаунтом.

Я могу запросить запрос на регистрацию токена webauthn и создать токен webauthn с помощью фронта (Angular), где я использую @simplewebauthn/browser ("@simplewebauthn/browser": "^6.3.0-alpha.1") . Там все работает нормально.

Я использую последнюю версию djoser, используя git, и версия webauthn 0.4.7 связана с djoser.

djoser @git+https://github.com/sunscrapers/djoser.git@abdf622f95dfa2c6278c4bd6d50dfe69559d90c0
webauthn==0.4.7

Но когда я отправляю обратно на бэкэнд результат регистрации, у меня возникает ошибка:

Authentication rejected. Error: Invalid signature received..

Вот SignUpView:

    permission_classes = (AllowAny,)

    def post(self, request, ukey):
        co = get_object_or_404(CredentialOptions, ukey=ukey)

        webauthn_registration_response = WebAuthnRegistrationResponse(
            rp_id=settings.DJOSER["WEBAUTHN"]["RP_ID"],
            origin=settings.DJOSER["WEBAUTHN"]["ORIGIN"],
            registration_response=request.data,
            challenge=co.challenge,
            none_attestation_permitted=True,
        )
        try:
            webauthn_credential = webauthn_registration_response.verify()
        except RegistrationRejectedException as e:
            return Response(
                {api_settings.NON_FIELD_ERRORS_KEY: format(e)},
                status=status.HTTP_400_BAD_REQUEST,
            )
        user = User.objects.get(username=request.data["username"])
        user_serializer = CustomUserSerializer(user)
        co.challenge = ""
        co.user = user
        co.sign_count = webauthn_credential.sign_count
        co.credential_id = webauthn_credential.credential_id.decode()
        co.public_key = webauthn_credential.public_key.decode()
        co.save()


        return Response(user_serializer.data, status=status.HTTP_201_CREATED)

И я основал свою работу на https://github.com/sunscrapers/djoser/blob/abdf622f95dfa2c6278c4bd6d50dfe69559d90c0/djoser/webauthn/views.py#L53

Вот также SignUpRequesrtView, где я отредактировал некоторые мелочи, чтобы заставить его работать так, как я хочу:

class SignupRequestView(APIView):
    permission_classes = (AllowAny,)

    def post(self, request):
        CredentialOptions.objects.filter(username=request.data["username"]).delete()

        serializer = WebauthnSignupSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        co = serializer.save()

        credential_registration_dict = WebAuthnMakeCredentialOptions(
            challenge=co.challenge,
            rp_name=settings.DJOSER["WEBAUTHN"]["RP_NAME"],
            rp_id=settings.DJOSER["WEBAUTHN"]["RP_ID"],
            user_id=co.ukey,
            username=co.username,
            display_name=co.display_name,
            icon_url="",
        )

        return Response(credential_registration_dict.registration_dict)```

And I also updated the WebAuthnSignupSerializer to retrieve an check if there's an account with the username given and if yes, create the CredentialOptions:

class WebauthnSignupSerializer(serializers.ModelSerializer): class Meta: model = CredentialOptions поля = ("username", "display_name")

def create(self, validated_data):
    validated_data.update(
        {
            "challenge": create_challenge(
                length=settings.DJOSER["WEBAUTHN"]["CHALLENGE_LENGTH"]
            ),
            "ukey": create_ukey(length=settings.DJOSER["WEBAUTHN"]["UKEY_LENGTH"]),
        }
    )
    return super().create(validated_data)

def validate_username(self, username):
    if User.objects.filter(username=username).exists():
        return username
    else:
        raise serializers.ValidationError(f"User {username} does not exist.")```

Похоже, возникла проблема с подписью токена WebAuthn. Исключение RegistrationRejectedException указывает на то, что подпись недействительна. Это может быть вызвано различными причинами, такими как проблема с ключами, используемыми для подписи токена, или проблема со способом проверки подписи на стороне сервера.

Можно попробовать перепроверить, что ключи, используемые на стороне клиента (в коде Angular), совпадают с ключами, используемыми на стороне сервера для проверки. Вы также можете попробовать зарегистрировать подпись на стороне клиента и сервера и сравнить значения, чтобы увидеть, есть ли какие-либо расхождения.

Еще одна вещь, которую необходимо проверить - это конфигурация настроек WebAuthn в проекте Django. Убедитесь, что RP_ID, ORIGIN и другие соответствующие параметры установлены правильно в настройках Django.

Если у вас продолжаются проблемы, может быть полезно обратиться к документации по библиотеке @simplewebauthn/browser и библиотеке django-webauthn, чтобы узнать, есть ли какие-либо известные проблемы или шаги по устранению неполадок, которые вы можете выполнить.

Похоже, что вы столкнулись с ошибкой при попытке проверить ответ регистрации WebAuthn на бэкенде. Это сообщение об ошибке указывает на то, что подпись, включенная в ответ, недействительна.

Есть несколько возможных причин, почему это может произойти. Одна из возможных причин заключается в том, что данные, включенные в ответ, были каким-то образом подделаны или повреждены, что привело к недействительной подписи. Другая возможность заключается в том, что существует проблема с парой ключей, которая использовалась для подписания ответа, например, неправильно настроенный закрытый ключ или проблема с криптографическим алгоритмом, использованным для генерации подписи.

Для устранения этой проблемы я бы рекомендовал попробовать следующие шаги:

Убедитесь, что объект webauthn_registration_response, передаваемый в webauthn_registration_response.verify(), построен правильно и содержит все необходимые данные. Это можно сделать, распечатав объект webauthn_registration_response в консоли и проверив правильность его свойств.

Убедитесь, что объект request.data, переданный в SignUpView, содержит правильные данные. Это можно сделать, распечатав объект request.data на консоли и убедившись, что он содержит ожидаемые данные.

Убедитесь, что пара ключей, используемая для подписания ответа, правильно настроена и что для создания подписи используется закрытый ключ. Это можно сделать, проверив конфигурацию пары ключей и убедившись, что для подписи ответа используется правильный закрытый ключ.

Если все вышеперечисленные шаги были проверены, а проблема остается, может потребоваться отладка метода webauthn_registration_response.verify(), чтобы понять, почему подпись отклоняется. Это можно сделать с помощью отладчика, чтобы пройти по коду и проверить промежуточные значения, используемые для проверки подписи.

Надеюсь, это поможет! Дайте мне знать, если у вас возникнут дополнительные вопросы.

Ну, это не совсем ответ, а скорее небольшая "помощь".

Вы можете проверить, действительна ли подпись, используя этот небольшой инструмент (внизу страницы): https://webauthn.passwordless.id/demos/playground.html

По крайней мере, используя это, вы будете знать, корректны ли ваши данные или что-то было сохранено неправильно. Существует так много преобразований из байтов в base64url и обратно, что это не всегда легко отследить. Возможно, это проблема формата данных/конвертации? Например, не конвертируется в байты или случайно происходит двойное кодирование в base64url.

Наконец, хранимый открытый ключ имеет разный формат в зависимости от алгоритма. Либо "raw", либо обернутый "ASN.1", на случай, если у вас возникнут проблемы с самим ключом.

Удачи!

Я полный новичок, поэтому не стесняйтесь пропустить этот комментарий или сказать мне, что я тупой за то, что я говорю.

Сообщение об ошибке "Аутентификация отклонена. Ошибка: Получена недействительная подпись" указывает на то, что подпись регистрационного ответа WebAuthn недействительна или не соответствует ожидаемому значению.

В вашем коде, похоже, используется метод verify() объекта webauthn_registration_response для проверки подписи регистрационного ответа. Этот метод сверяет подпись ответа с вызовом, который был отправлен клиенту, и если подпись недействительна или не соответствует ожидаемому значению, то возникает исключение RegistrationRejectedException.

Чтобы решить эту проблему, вы можете попробовать выполнить следующие действия:

  1. Проверьте значение атрибута challenge объекта объекта webauthn_registration_response, и убедитесь, что оно соответствует challenge, который был отправлен клиенту в запросе на регистрацию.

  2. Проверьте значение атрибута registration_response объекта объекта webauthn_registration_response, и убедитесь, что это действительный регистрационный ответ, который был получен от клиента.

  3. Проверьте значение атрибута none_attestation_permitted объекта объекта webauthn_registration_response, и убедитесь, что он установлен в значение True, если клиент использует тип аттестации "none".

  4. Если ошибка сохраняется, попробуйте добавить дополнительные протоколы или отладочные чтобы понять, почему метод verify() отвергает регистрационный ответ. отклоняет ответ регистрации.

Метод verify() ожидает объект RegistrationResponse в качестве аргумента, но вы передаете ему все данные запроса. Вам нужно извлечь поле registrationResponse из данных запроса и передать его в метод verify().

Изменить это:

webauthn_registration_response = WebAuthnRegistrationResponse(
rp_id=settings.DJOSER["WEBAUTHN"]["RP_ID"],
origin=settings.DJOSER["WEBAUTHN"]["ORIGIN"],
registration_response=request.data,  # <-- update this line
challenge=co.challenge,
none_attestation_permitted=True,)

К этому:

webauthn_registration_response = WebAuthnRegistrationResponse(
rp_id=settings.DJOSER["WEBAUTHN"]["RP_ID"],
origin=settings.DJOSER["WEBAUTHN"]["ORIGIN"],
registration_response=request.data['registrationResponse'],  # <-- update this line
challenge=co.challenge,
none_attestation_permitted=True,)
Вернуться на верх