# Copyright (c) 2020 Yubico AB
# All rights reserved.
#
#   Redistribution and use in source and binary forms, with or
#   without modification, are permitted provided that the following
#   conditions are met:
#
#    1. Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#    2. Redistributions in binary form must reproduce the above
#       copyright notice, this list of conditions and the following
#       disclaimer in the documentation and/or other materials provided
#       with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

from __future__ import annotations

import abc
from dataclasses import dataclass
from enum import Enum, unique
from typing import Any, Mapping, Sequence, cast

from .. import cbor
from ..utils import _JsonDataObject, sha256, websafe_encode
from ..webauthn import (
    AttestationObject,
    AuthenticatorSelectionCriteria,
    PublicKeyCredentialCreationOptions,
    PublicKeyCredentialDescriptor,
    PublicKeyCredentialRequestOptions,
    ResidentKeyRequirement,
    UserVerificationRequirement,
)
from .base import AssertionResponse, AttestationResponse, Ctap2
from .blob import LargeBlobs
from .pin import ClientPin, PinProtocol


class ExtensionProcessor(abc.ABC):
    """Base class for CTAP2 extension processing.

    See: :class:`RegistrationExtensionProcessor` and
    :class:`AuthenticationExtensionProcessor`.
    """

    def __init__(
        self,
        permissions: ClientPin.PERMISSION = ClientPin.PERMISSION(0),
        inputs: dict[str, Any] | None = None,
        outputs: dict[str, Any] | None = None,
    ):
        self.permissions = permissions
        self._inputs = inputs
        self._outputs = outputs


class RegistrationExtensionProcessor(ExtensionProcessor):
    """Processing state for a CTAP2 extension, for single use.

    The ExtensionProcessor holds state and logic for client processing of an extension,
    for a registration (MakeCredential) call.

    :param permissions: PinUvAuthToken permissions required by the extension.
    :param inputs: Default authenticator inputs, if prepare_inputs is not overridden.
    :param outputs: Default client outputs, if prepare_outputs is not overridden.
    """

    def prepare_inputs(self, pin_token: bytes | None) -> dict[str, Any] | None:
        "Prepare authenticator extension inputs, to be passed to the Authenenticator."
        return self._inputs

    def prepare_outputs(
        self,
        response: AttestationResponse,
        pin_token: bytes | None,
    ) -> dict[str, Any] | None:
        "Prepare client extension outputs, to be returned to the caller."
        return self._outputs


class AuthenticationExtensionProcessor(ExtensionProcessor):
    """Processing state for a CTAP2 extension, for single use.

    The ExtensionProcessor holds state and logic for client processing of an extension,
    for an authentication (GetAssertion) call.

    :param permissions: PinUvAuthToken permissions required by the extension.
    :param inputs: Default authenticator inputs, if prepare_inputs is not overridden.
    :param outputs: Default client outputs, if prepare_outputs is not overridden.
    """

    def prepare_inputs(
        self,
        selected: PublicKeyCredentialDescriptor | None,
        pin_token: bytes | None,
    ) -> dict[str, Any] | None:
        "Prepare authenticator extension inputs, to be passed to the Authenenticator."
        return self._inputs

    def prepare_outputs(
        self,
        response: AssertionResponse,
        pin_token: bytes | None,
    ) -> dict[str, Any] | None:
        "Prepare client extension outputs, to be returned to the caller."
        return self._outputs


class Ctap2Extension(abc.ABC):
    """Base class for CTAP2 extensions.

    As of python-fido2 1.2 these instances can be used for multiple requests and
    should be invoked via the make_credential and get_assertion methods.
    Subclasses are instantiated for a single request, if the Authenticator supports
    the extension.
    """

    @abc.abstractmethod
    def is_supported(self, ctap: Ctap2) -> bool:
        """Whether or not the extension is supported by the authenticator."""

    def make_credential(
        self,
        ctap: Ctap2,
        options: PublicKeyCredentialCreationOptions,
        pin_protocol: PinProtocol | None,
    ) -> RegistrationExtensionProcessor | None:
        """Start client extension processing for registration."""
        return None

    def get_assertion(
        self,
        ctap: Ctap2,
        options: PublicKeyCredentialRequestOptions,
        pin_protocol: PinProtocol | None,
    ) -> AuthenticationExtensionProcessor | None:
        """Start client extension processing for authentication."""
        return None


@dataclass(eq=False, frozen=True)
class HMACGetSecretInput(_JsonDataObject):
    """Client inputs for hmac-secret."""

    salt1: bytes
    salt2: bytes | None = None


@dataclass(eq=False, frozen=True)
class HMACGetSecretOutput(_JsonDataObject):
    """Client outputs for hmac-secret."""

    output1: bytes
    output2: bytes | None = None


def _prf_salt(secret):
    return sha256(b"WebAuthn PRF\0" + secret)


@dataclass(eq=False, frozen=True)
class AuthenticatorExtensionsPRFValues(_JsonDataObject):
    """Salt values for use with prf."""

    first: bytes
    second: bytes | None = None


@dataclass(eq=False, frozen=True)
class AuthenticatorExtensionsPRFInputs(_JsonDataObject):
    """Client inputs for prf."""

    eval: AuthenticatorExtensionsPRFValues | None = None
    eval_by_credential: Mapping[str, AuthenticatorExtensionsPRFValues] | None = None


@dataclass(eq=False, frozen=True)
class AuthenticatorExtensionsPRFOutputs(_JsonDataObject):
    """Client outputs for prf."""

    enabled: bool | None = None
    results: AuthenticatorExtensionsPRFValues | None = None


def _hmac_prepare_salts(allow_list, selected, prf, hmac):
    if prf:
        secrets = prf.eval
        by_creds = prf.eval_by_credential
        if by_creds:
            # Make sure all keys are valid IDs from allow_credentials
            if not allow_list:
                raise ValueError("evalByCredentials requires allowCredentials")
            ids = {websafe_encode(c.id) for c in allow_list}
            if not ids.issuperset(by_creds):
                raise ValueError("evalByCredentials contains invalid key")
            if selected:
                key = websafe_encode(selected.id)
                if key in by_creds:
                    secrets = by_creds[key]

        if not secrets:
            return

        salts = (
            _prf_salt(secrets.first),
            (_prf_salt(secrets.second) if secrets.second is not None else b""),
        )
    elif hmac:
        salts = hmac.salt1, hmac.salt2 or b""
    else:
        return

    if not (
        len(salts[0]) == HmacSecretExtension.SALT_LEN
        and (not salts[1] or len(salts[1]) == HmacSecretExtension.SALT_LEN)
    ):
        raise ValueError("Invalid salt length")

    return salts


def _hmac_format_outputs(enabled, decrypted, prf):
    output1 = decrypted[: HmacSecretExtension.SALT_LEN] if decrypted else None
    output2 = decrypted[HmacSecretExtension.SALT_LEN :] if decrypted else None

    if prf:
        result = AuthenticatorExtensionsPRFOutputs(
            enabled=enabled,
            results=(
                AuthenticatorExtensionsPRFValues(output1, output2) if output1 else None
            ),
        )
        # If result has no content, don't add an entry for it
        return {"prf": result} if result else None
    else:
        outputs = {}
        if enabled is not None:
            outputs["hmacCreateSecret"] = enabled
        if output1:
            outputs["hmacGetSecret"] = HMACGetSecretOutput(output1, output2)
        return outputs or None


class HmacSecretExtension(Ctap2Extension):
    """
    Implements the Pseudo-random function (prf) and the hmac-secret CTAP2 extensions.

    The hmac-secret extension is not directly available to clients by default, instead
    the prf extension is used.

    https://www.w3.org/TR/webauthn-3/#prf-extension

    https://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html#sctn-hmac-secret-extension

    :param allow_hmac_secret: Set to True to allow hmac-secret, in addition to prf.
    """

    NAME = "hmac-secret"
    MC_NAME = "hmac-secret-mc"
    SALT_LEN = 32

    def __init__(self, allow_hmac_secret=False):
        self._allow_hmac_secret = allow_hmac_secret

    def is_supported(self, ctap):
        return self.NAME in ctap.info.extensions

    def make_credential(self, ctap, options, pin_protocol):
        c_inputs = options.extensions or {}
        prf = c_inputs.get("prf") is not None
        hmac = self._allow_hmac_secret and c_inputs.get("hmacCreateSecret") is True
        if pin_protocol and self.is_supported(ctap) and (prf or hmac):
            inputs: dict[str, Any] = {HmacSecretExtension.NAME: True}
            shared_secret = None
            if self.MC_NAME in ctap.info.extensions:
                prf_salts = AuthenticatorExtensionsPRFInputs.from_dict(
                    cast(Mapping | None, c_inputs.get("prf"))
                )
                hmac_salts = (
                    HMACGetSecretInput.from_dict(
                        cast(Mapping | None, c_inputs.get("hmacGetSecret"))
                    )
                    if hmac
                    else None
                )
                salts = _hmac_prepare_salts(None, None, prf_salts, hmac_salts)
                if salts:
                    client_pin = ClientPin(ctap, pin_protocol)
                    key_agreement, shared_secret = client_pin._get_shared_secret()
                    salt_enc = pin_protocol.encrypt(shared_secret, salts[0] + salts[1])
                    salt_auth = pin_protocol.authenticate(shared_secret, salt_enc)
                    inputs[HmacSecretExtension.MC_NAME] = {
                        1: key_agreement,
                        2: salt_enc,
                        3: salt_auth,
                        4: pin_protocol.VERSION,
                    }

            class Processor(RegistrationExtensionProcessor):
                def prepare_inputs(self, pin_token):
                    return inputs

                def prepare_outputs(self, response, pin_token):
                    extensions = response.auth_data.extensions or {}
                    enabled = extensions.get(HmacSecretExtension.NAME, False)
                    value = extensions.get(HmacSecretExtension.MC_NAME)
                    decrypted = (
                        pin_protocol.decrypt(shared_secret, value)
                        if value and shared_secret
                        else None
                    )
                    return _hmac_format_outputs(enabled, decrypted, prf)

            return Processor()

    def get_assertion(self, ctap, options, pin_protocol):
        inputs = options.extensions or {}
        prf = AuthenticatorExtensionsPRFInputs.from_dict(
            cast(Mapping | None, inputs.get("prf"))
        )
        hmac = (
            HMACGetSecretInput.from_dict(
                cast(Mapping | None, inputs.get("hmacGetSecret"))
            )
            if self._allow_hmac_secret
            else None
        )

        if pin_protocol and self.is_supported(ctap) and (prf or hmac):
            client_pin = ClientPin(ctap, pin_protocol)
            key_agreement, shared_secret = client_pin._get_shared_secret()

            class Processing(AuthenticationExtensionProcessor):
                def prepare_inputs(self, selected, pin_token):
                    salts = _hmac_prepare_salts(
                        options.allow_credentials, selected, prf, hmac
                    )
                    if not salts:
                        return

                    salt_enc = pin_protocol.encrypt(shared_secret, salts[0] + salts[1])
                    salt_auth = pin_protocol.authenticate(shared_secret, salt_enc)

                    return {
                        HmacSecretExtension.NAME: {
                            1: key_agreement,
                            2: salt_enc,
                            3: salt_auth,
                            4: pin_protocol.VERSION,
                        }
                    }

                def prepare_outputs(self, response, pin_token):
                    extensions = response.auth_data.extensions or {}
                    value = extensions.get(HmacSecretExtension.NAME)
                    decrypted = (
                        pin_protocol.decrypt(shared_secret, value) if value else None
                    )
                    return _hmac_format_outputs(None, decrypted, prf)

            return Processing()


@dataclass(eq=False, frozen=True)
class AuthenticatorExtensionsLargeBlobInputs(_JsonDataObject):
    """Client inputs for largeBlob."""

    support: str | None = None
    read: bool | None = None
    write: bytes | None = None


@dataclass(eq=False, frozen=True)
class AuthenticatorExtensionsLargeBlobOutputs(_JsonDataObject):
    """Client outputs for largeBlob."""

    supported: bool | None = None
    blob: bytes | None = None
    written: bool | None = None


class LargeBlobExtension(Ctap2Extension):
    """
    Implements the Large Blob storage (largeBlob) WebAuthn extension.

    https://www.w3.org/TR/webauthn-3/#sctn-large-blob-extension
    """

    NAME = "largeBlobKey"

    def is_supported(self, ctap):
        return self.NAME in ctap.info.extensions and ctap.info.options.get(
            "largeBlobs", False
        )

    def make_credential(self, ctap, options, pin_protocol):
        inputs = options.extensions or {}
        data = AuthenticatorExtensionsLargeBlobInputs.from_dict(
            cast(Mapping | None, inputs.get("largeBlob"))
        )
        if data:
            if data.read or data.write:
                raise ValueError("Invalid set of parameters")
            if data.support == "required" and not self.is_supported(ctap):
                raise ValueError("Authenticator does not support large blob storage")

            class Processor(RegistrationExtensionProcessor):
                def prepare_inputs(self, pin_token):
                    return {LargeBlobExtension.NAME: True}

                def prepare_outputs(self, response, pin_token):
                    return {
                        "largeBlob": AuthenticatorExtensionsLargeBlobOutputs(
                            supported=response.large_blob_key is not None
                        )
                    }

            return Processor()

    def get_assertion(self, ctap, options, pin_protocol):
        inputs = options.extensions or {}
        data = AuthenticatorExtensionsLargeBlobInputs.from_dict(
            cast(Mapping | None, inputs.get("largeBlob"))
        )
        if data:
            if data.support or (data.read and data.write):
                raise ValueError("Invalid set of parameters")
            if not self.is_supported(ctap):
                raise ValueError("Authenticator does not support large blob storage")

            class Processor(AuthenticationExtensionProcessor):
                def prepare_outputs(self, response, pin_token):
                    assert data is not None  # noqa: S101 needed for mypy
                    blob_key = response.large_blob_key
                    if blob_key:
                        if data.read:
                            large_blobs = LargeBlobs(ctap)
                            blob = large_blobs.get_blob(blob_key)
                            return {
                                "largeBlob": AuthenticatorExtensionsLargeBlobOutputs(
                                    blob=blob
                                )
                            }
                        elif data.write:
                            large_blobs = LargeBlobs(ctap, pin_protocol, pin_token)
                            large_blobs.put_blob(blob_key, data.write)
                            return {
                                "largeBlob": AuthenticatorExtensionsLargeBlobOutputs(
                                    written=True
                                )
                            }

            return Processor(
                (
                    ClientPin.PERMISSION.LARGE_BLOB_WRITE
                    if data.write
                    else ClientPin.PERMISSION(0)
                ),
                inputs={LargeBlobExtension.NAME: True},
            )


class CredBlobExtension(Ctap2Extension):
    """
    Implements the Credential Blob (credBlob) CTAP2 extension.

    https://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html#sctn-credBlob-extension
    """

    NAME = "credBlob"

    def is_supported(self, ctap):
        return self.NAME in ctap.info.extensions

    def make_credential(self, ctap, options, pin_protocol):
        inputs = options.extensions or {}
        if self.is_supported(ctap):
            blob = inputs.get("credBlob")
            assert ctap.info.max_cred_blob_length is not None  # noqa: S101
            if blob and len(blob) <= ctap.info.max_cred_blob_length:
                return RegistrationExtensionProcessor(inputs={self.NAME: blob})

    def get_assertion(self, ctap, options, pin_protocol):
        inputs = options.extensions or {}
        if self.is_supported(ctap) and inputs.get("getCredBlob") is True:
            return AuthenticationExtensionProcessor(inputs={self.NAME: True})


class CredProtectExtension(Ctap2Extension):
    """
    Implements the Credential Protection CTAP2 extension.

    https://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html#sctn-credProtect-extension
    """

    @unique
    class POLICY(Enum):
        OPTIONAL = "userVerificationOptional"
        OPTIONAL_WITH_LIST = "userVerificationOptionalWithCredentialIDList"
        REQUIRED = "userVerificationRequired"

        @classmethod
        def str2int(cls, policy: str) -> int:
            return list(cls).index(cls(policy)) + 1

    NAME = "credProtect"

    def is_supported(self, ctap):
        return self.NAME in ctap.info.extensions

    def make_credential(self, ctap, options, pin_protocol):
        inputs = options.extensions or {}
        policy = inputs.get("credentialProtectionPolicy")
        if policy:
            index = CredProtectExtension.POLICY.str2int(policy)
            enforce = inputs.get("enforceCredentialProtectionPolicy", False)
            if enforce and not self.is_supported(ctap) and index > 1:
                raise ValueError("Authenticator does not support Credential Protection")

            return RegistrationExtensionProcessor(inputs={self.NAME: index})


class MinPinLengthExtension(Ctap2Extension):
    """
    Implements the Minimum PIN Length (minPinLength) CTAP2 extension.

    https://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html#sctn-minpinlength-extension
    """

    NAME = "minPinLength"

    def is_supported(self, ctap):
        # NB: There is no key in the extensions field.
        return "setMinPINLength" in ctap.info.options

    def make_credential(self, ctap, options, pin_protocol):
        inputs = options.extensions or {}
        if self.is_supported(ctap) and inputs.get(self.NAME) is True:
            return RegistrationExtensionProcessor(inputs={self.NAME: True})


class PinComplexityPolicyExtension(Ctap2Extension):
    """
    Implements the PIN Complexity Policy (pinComplexityPolicy) CTAP2 extension.

    https://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html#sctn-minpinlength-extension
    """

    NAME = "pinComplexityPolicy"

    def is_supported(self, ctap):
        return self.NAME in ctap.info.extensions

    def make_credential(self, ctap, options, pin_protocol):
        inputs = options.extensions or {}
        if self.is_supported(ctap) and inputs.get(self.NAME) is True:
            return RegistrationExtensionProcessor(inputs={self.NAME: True})


@dataclass(eq=False, frozen=True)
class CredentialPropertiesOutput(_JsonDataObject):
    """Client outputs for credProps."""

    rk: bool | None = None


class CredPropsExtension(Ctap2Extension):
    """
    Implements the Credential Properties (credProps) WebAuthn extension.

    https://www.w3.org/TR/webauthn-3/#sctn-authenticator-credential-properties-extension
    """

    NAME = "credProps"

    def is_supported(self, ctap):
        # NB: There is no key in the extensions field.
        return True

    def make_credential(self, ctap, options, pin_protocol):
        inputs = options.extensions or {}
        if inputs.get(self.NAME) is True:
            selection = (
                options.authenticator_selection or AuthenticatorSelectionCriteria()
            )
            rk = selection.resident_key == ResidentKeyRequirement.REQUIRED or (
                selection.resident_key == ResidentKeyRequirement.PREFERRED
                and ctap.info.options.get("rk")
            )

            return RegistrationExtensionProcessor(
                outputs={self.NAME: CredentialPropertiesOutput(rk=rk)}
            )


@dataclass(eq=False, frozen=True)
class PaymentCurrencyAmount(_JsonDataObject):
    currency: str
    value: str


@dataclass(eq=False, frozen=True)
class PaymentCredentialInstrument(_JsonDataObject):
    display_name: str
    icon: str
    icon_must_be_shown: bool = True


@dataclass(eq=False, frozen=True)
class AuthenticationExtensionsPaymentInputs(_JsonDataObject):
    """Client inputs for payment."""

    is_payment: bool | None = None
    rp_id: str | None = None
    top_origin: str | None = None
    payee_name: str | None = None
    payee_origin: str | None = None
    total: PaymentCurrencyAmount | None = None
    instrument: PaymentCredentialInstrument | None = None


class ThirdPartyPaymentExtension(Ctap2Extension):
    """
    Implements the Third Party Payment (thirdPartyPayment) CTAP2.2 extension.

    https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#sctn-thirdPartyPayment-extension

    Note that most of the processing for the WebAuthn extension needs to be done by the
    client, see:
    https://www.w3.org/TR/secure-payment-confirmation/#sctn-collectedclientpaymentdata-dictionary

    As such, this extension is not included in the default extensions list, and should
    not be used without a client that supports the WebAuthn payment extension.
    """

    NAME = "thirdPartyPayment"

    def is_supported(self, ctap):
        return self.NAME in ctap.info.extensions

    def make_credential(self, ctap, options, pin_protocol):
        inputs = options.extensions or {}
        data = AuthenticationExtensionsPaymentInputs.from_dict(
            cast(Mapping | None, inputs.get("payment"))
        )
        if self.is_supported(ctap) and data and data.is_payment:
            return RegistrationExtensionProcessor(inputs={self.NAME: True})

    def get_assertion(self, ctap, options, pin_protocol):
        inputs = options.extensions or {}
        data = AuthenticationExtensionsPaymentInputs.from_dict(
            cast(Mapping | None, inputs.get("payment"))
        )
        if self.is_supported(ctap) and data and data.is_payment:
            return AuthenticationExtensionProcessor(inputs={self.NAME: True})


@dataclass(eq=False, frozen=True)
class _SignGenerateKeyInputs(_JsonDataObject):
    algorithms: Sequence[int]


@dataclass(eq=False, frozen=True)
class _SignSignInputs(_JsonDataObject):
    key_handle: bytes
    tbs: bytes
    additional_args: bytes | None = None


@dataclass(eq=False, frozen=True)
class _SignInputs(_JsonDataObject):
    generate_key: _SignGenerateKeyInputs | None = None
    sign_by_credential: Mapping[str, _SignSignInputs] | None = None


@dataclass(eq=False, frozen=True)
class _SignGeneratedKey(_JsonDataObject):
    key_handle: bytes
    public_key: bytes
    algorithm: int
    attestation_object: AttestationObject


@dataclass(eq=False, frozen=True)
class _SignOutputs(_JsonDataObject):
    generated_key: _SignGeneratedKey | None = None
    signature: bytes | None = None


class PreviewSignExtension(Ctap2Extension):
    """
    WARNING: This extension is currently in DRAFT.
    It is to be considered experimental, and NOT part of the stable API of this library.

    Implements version 4-SNAPSHOT-2025-08-21T15:00 of the sign CTAP2 extension.

    https://github.com/yubicolabs/webauthn-sign-extension
    """

    NAME = "previewSign"

    def is_supported(self, ctap):
        return self.NAME in ctap.info.extensions

    def make_credential(self, ctap, options, pin_protocol):
        inputs = options.extensions or {}
        data = _SignInputs.from_dict(inputs.get(self.NAME))
        if not data or not self.is_supported(ctap):
            return

        if data.sign_by_credential or not data.generate_key:
            raise ValueError("Invalid inputs")

        class Processor(RegistrationExtensionProcessor):
            def prepare_inputs(self, pin_token):
                gk = data.generate_key

                selection = (
                    options.authenticator_selection or AuthenticatorSelectionCriteria()
                )
                flags = (
                    0b101
                    if selection.user_verification
                    == UserVerificationRequirement.REQUIRED
                    else 0b001
                )
                outputs = {3: gk.algorithms, 4: flags}

                return {PreviewSignExtension.NAME: outputs}

            def prepare_outputs(self, response, pin_token):
                extensions = response.auth_data.extensions or {}
                data = extensions.get(PreviewSignExtension.NAME)
                if not data:
                    return None
                att_obj_bytes = response.unsigned_extension_outputs[
                    PreviewSignExtension.NAME
                ][7]
                att_resp = AttestationResponse.from_dict(
                    cast(Mapping, cbor.decode(att_obj_bytes))
                )
                att_obj = AttestationObject.create(
                    att_resp.fmt, att_resp.auth_data, att_resp.att_stmt
                )
                cred_data = att_obj.auth_data.credential_data
                assert cred_data is not None  # noqa: S101

                return {
                    PreviewSignExtension.NAME: _SignOutputs(
                        generated_key=_SignGeneratedKey(
                            key_handle=cred_data.credential_id,
                            public_key=cbor.encode(cred_data.public_key),
                            algorithm=data.get(3),
                            attestation_object=att_obj,
                        ),
                    )
                }

        return Processor()

    def get_assertion(self, ctap, options, pin_protocol):
        inputs = options.extensions or {}
        data = _SignInputs.from_dict(inputs.get(self.NAME))
        if not data or not self.is_supported(ctap):
            return

        if not data.sign_by_credential or data.generate_key:
            raise ValueError("Invalid inputs")

        class Processor(AuthenticationExtensionProcessor):
            def prepare_inputs(self, selected, pin_token):
                by_creds = data.sign_by_credential

                # Make sure all keys are valid IDs from allow_credentials
                allow_list = options.allow_credentials
                if not allow_list or not selected:
                    raise ValueError("sign requires allowCredentials")
                ids = {websafe_encode(c.id) for c in allow_list}
                if ids.difference(by_creds):
                    raise ValueError("keyHandleByCredential is not valid")
                cred_inputs = by_creds[websafe_encode(selected.id)]

                authenticator_input = {
                    2: cred_inputs.key_handle,
                    6: cred_inputs.tbs,
                }
                if cred_inputs.additional_args:
                    authenticator_input[7] = cred_inputs.additional_args

                return {PreviewSignExtension.NAME: authenticator_input}

            def prepare_outputs(self, response, pin_token):
                extensions = response.auth_data.extensions or {}
                data = extensions.get(PreviewSignExtension.NAME)
                if data:
                    return {PreviewSignExtension.NAME: _SignOutputs(signature=data[6])}

        return Processor()


_DEFAULT_EXTENSIONS = [
    HmacSecretExtension(),
    LargeBlobExtension(),
    CredBlobExtension(),
    CredProtectExtension(),
    MinPinLengthExtension(),
    CredPropsExtension(),
]
