import base64
import io
import json
import struct
from enum import Enum
from typing import (Any, Dict, List, Optional, Sequence, Set, Tuple, Type,
                    Union, cast)
from urllib.parse import urlparse

import cbor2

from webauthn_rp.errors import DecodingError, OriginError, ParserError
from webauthn_rp.types import (
    AndroidKeyAttestationStatement, AndroidSafetyNetAttestationStatement,
    AttestationObject, AttestationStatementFormatIdentifier,
    AttestedCredentialData, AuthenticationExtensionsClientOutputs,
    AuthenticatorAssertionResponse, AuthenticatorAttestationResponse,
    AuthenticatorData, AuthenticatorDataFlag, CollectedClientData, Coordinates,
    COSEAlgorithmIdentifier, COSEKeyOperation, COSEKeyType,
    CredentialPublicKey, EC2CredentialPublicKey, EC2Curve,
    FIDOU2FAttestationStatement, NoneAttestationStatement,
    OKPCredentialPublicKey, OKPCurve, Origin, PackedAttestationStatement,
    PackedECDAAAttestationStatement, PackedX509AttestationStatement,
    PublicKeyCredential, TokenBinding, TokenBindingStatus,
    TPMAttestationStatement, TPMECDAAAttestationStatement,
    TPMX509AttestationStatement)
from webauthn_rp.utils import curve_coordinate_byte_length
from webauthn_rp.validators import validate

__all__ = [
    'parse_origin',
    'parse_public_key_credential',
    'parse_okp_public_key',
    'parse_ec2_public_key',
    'parse_extensions',
    'parse_packed_attestation_statement',
    'parse_tpm_attestation_statement',
    'parse_android_key_attestation_statement',
    'parse_android_safetynet_attestation_statement',
    'parse_fido_u2f_attestation_statement',
    'parse_none_attestation_statement',
    'parse_client_data',
    'parse_cose_key',
    'parse_authenticator_data',
    'parse_attestation_object',
]

_SCHEME_DEFAULT_PORT_MAPPING = {
    'http': 80,
    'https': 443,
}


def parse_origin(origin: str) -> Origin:
    """Parse an origin string into the `Origin` tuple.

    Args:
      origin (str): An origin as defined in the HTML specification.

    Returns:
      An `Origin` tuple.
    
    References:
      * https://html.spec.whatwg.org/multipage/origin.html
    """
    if '://' not in origin:
        raise OriginError('Invalid origin, missing scheme')

    url = urlparse(origin)
    if url.path or url.params or url.query or url.fragment:
        raise OriginError('Invalid origin, cannot have path, params, query, '
                          'or fragment present')

    if not url.netloc:
        raise OriginError('Invalid origin, must provide a hostname')

    if url.scheme not in {'http', 'https'}:
        raise OriginError('Invalid origin, unsupported scheme {}'.format(
            url.scheme))

    scheme = url.scheme
    hostname = url.netloc
    if url.port is None:
        port = _SCHEME_DEFAULT_PORT_MAPPING[scheme]
    else:
        port = url.port

    return Origin(
        scheme=scheme,
        hostname=hostname,
        port=port,
    )


def _parse_dictionary_field(field_key: Any,
                            valid_types: Union[Type, Sequence[Type]],
                            dictionary: Dict,
                            required: bool = True) -> Any:
    valid_types_seq: Sequence[Type] = [valid_types] if (  # type: ignore
        type(valid_types) is type) else valid_types
    field = dictionary.get(field_key)
    if field is None:
        if not required: return
        raise ParserError(
            '{} is required in dictionary keys'.format(field_key))

    if type(field) not in valid_types_seq:
        raise ParserError('{} type must be one of {} not {}'.format(
            field_key, str(valid_types_seq), str(type(field))))

    return field


def _check_unsupported_keys(supported: Set[str], data: Dict):
    unsupported_keys = set(data.keys()).difference(supported)
    if unsupported_keys:
        raise ParserError(('Found unsupported keys in data {}').format(
            str(unsupported_keys)))


def _bytes_from_base64(s: str) -> bytes:
    try:
        return base64.b64decode(s)
    except Exception:
        raise DecodingError('Invalid base64 string')


def parse_public_key_credential(data: Dict) -> PublicKeyCredential:
    """Parse a JSON public key credential into a `PublicKeyCredential`.

    Note that data that was originally bytes is expected to have been
    encoded using standard base64.

    Args:
      data (Dict): The JSON public key credential generated by a user agent.

    Returns:
      A `PublicKeyCredential` instance.

    Raises:
      ParserError: Could not correctly parse the data.
      DecodingError: Could not decode base64-encoded data.
    """
    _check_unsupported_keys({'id', 'rawId', 'response', 'type'}, data)
    id_ = _parse_dictionary_field('id', str, data)
    type_ = _parse_dictionary_field('type', str, data)
    raw_id = _bytes_from_base64(_parse_dictionary_field('rawId', str, data))

    response = _parse_dictionary_field('response', dict, data)
    client_data_JSON = _bytes_from_base64(
        _parse_dictionary_field('clientDataJSON', str, response))

    if 'attestationObject' in response:
        # Parse AuthenticatorAttestationResponse
        _check_unsupported_keys({'clientDataJSON', 'attestationObject'},
                                response)

        attestation_object = _bytes_from_base64(
            _parse_dictionary_field('attestationObject', str, response))

        return PublicKeyCredential(id=id_,
                                   type=type_,
                                   raw_id=raw_id,
                                   response=AuthenticatorAttestationResponse(
                                       client_data_JSON=client_data_JSON,
                                       attestation_object=attestation_object))
    else:
        # Parse AuthenticatorAssertionResponse
        _check_unsupported_keys(
            {'clientDataJSON', 'authenticatorData', 'signature', 'userHandle'},
            response)

        authenticator_data = _bytes_from_base64(
            _parse_dictionary_field('authenticatorData', str, response))
        signature = _bytes_from_base64(
            _parse_dictionary_field('signature', str, response))
        user_handle_b64s = _parse_dictionary_field('userHandle', str, response,
                                                   False)
        user_handle = None
        if user_handle_b64s is not None:
            user_handle = _bytes_from_base64(user_handle_b64s)

        return PublicKeyCredential(id=id_,
                                   type=type_,
                                   raw_id=raw_id,
                                   response=AuthenticatorAssertionResponse(
                                       client_data_JSON=client_data_JSON,
                                       authenticator_data=authenticator_data,
                                       signature=signature,
                                       user_handle=user_handle))


def _parse_credential_public_key_kty(
        credential_public_key: Dict
) -> Union[COSEKeyType.Name, COSEKeyType.Value]:
    kty_raw = _parse_dictionary_field(1, (int, str), credential_public_key)

    try:
        kty = cast(Union[COSEKeyType.Name, COSEKeyType.Value],
                   COSEKeyType(kty_raw))  # type: ignore
    except (KeyError, ValueError):
        raise ParserError(
            'Invalid credential public key type {}'.format(kty_raw))
    return kty


def _parse_credential_public_key_alg(
    credential_public_key: Dict
) -> Union[COSEAlgorithmIdentifier.Name, COSEAlgorithmIdentifier.Value]:
    alg_raw = _parse_dictionary_field(3, (int, str), credential_public_key)

    try:
        alg = cast(Union[COSEAlgorithmIdentifier.Name,
                         COSEAlgorithmIdentifier.Value],
                   COSEAlgorithmIdentifier(alg_raw))  # type: ignore
    except (KeyError, ValueError):
        raise ParserError(
            'Invalid credential public key alg type {}'.format(alg_raw))
    return alg


def _parse_credential_public_key_key_ops(
    credential_public_key: Dict
) -> Optional[Sequence[Union[COSEKeyOperation.Name, COSEKeyOperation.Value]]]:
    key_ops_raw = _parse_dictionary_field(4, (list, tuple),
                                          credential_public_key, False)

    if key_ops_raw is None: return None

    if len(key_ops_raw) < 1:
        raise ParserError(
            'Credential public key key_ops(4) must have at least 1 element')

    key_ops: List[Union[COSEKeyOperation.Name, COSEKeyOperation.Value]] = []
    for i, ko in enumerate(key_ops_raw):
        if type(ko) not in (str, int):
            raise ParserError(
                ('Credential public key key_ops(3) index {} should either be a'
                 ' text string or an integer').format(i))

        try:
            key_ops.append(COSEKeyOperation(ko))  # type: ignore
        except (KeyError, ValueError):
            raise ParserError('Invalid credential public key key op {}'.format(
                credential_public_key[4]))

    return key_ops


def _parse_credential_public_key_kwargs(credential_public_key: Dict) -> dict:
    kty = _parse_credential_public_key_kty(credential_public_key)
    kid = _parse_dictionary_field(2, bytes, credential_public_key, False)
    alg = _parse_credential_public_key_alg(credential_public_key)
    key_ops = _parse_credential_public_key_key_ops(credential_public_key)
    base_IV = _parse_dictionary_field(5, bytes, credential_public_key, False)
    return {
        'kty': kty,
        'kid': kid,
        'alg': alg,
        'key_ops': key_ops,
        'base_IV': base_IV
    }


def _parse_ec2_public_key_crv(
        credential_public_key: Dict) -> Union[EC2Curve.Name, EC2Curve.Value]:
    crv_raw = _parse_dictionary_field(-1, (int, str), credential_public_key)
    try:
        return EC2Curve(crv_raw)  # type: ignore
    except (KeyError, ValueError):
        raise ParserError('Invalid EC2 curve {}'.format(crv_raw))


def _parse_okp_public_key_crv(
        credential_public_key: Dict) -> Union[OKPCurve.Name, OKPCurve.Value]:
    crv_raw = _parse_dictionary_field(-1, (int, str), credential_public_key)
    try:
        return OKPCurve(crv_raw)  # type: ignore
    except (KeyError, ValueError):
        raise ParserError('Invalid OKP curve {}'.format(crv_raw))


def parse_okp_public_key(
        credential_public_key: Dict) -> OKPCredentialPublicKey:
    """Parse a JSON OKP credential public key into an `OKPCredentialPublicKey`.

    Args:
      credential_public_key (Dict): The JSON OKP credential public key 
        generated by a user agent.

    Returns:
      An `OKPCredentialPublicKey` instance.

    Raises:
      ParserError: Could not correctly parse the data.
    
    References:
      * https://w3.org/TR/webauthn/#sec-attested-credential-data
      * https://cose-wg.github.io/cose-spec/#rfc.section.7
      * https://cose-wg.github.io/cose-spec/#rfc.section.7.1
      * https://cose-wg.github.io/cose-spec/#rfc.section.13.1
      * https://cose-wg.github.io/cose-spec/#rfc.section.13.2
    """
    x = _parse_dictionary_field(-2, bytes, credential_public_key)
    crv = _parse_okp_public_key_crv(credential_public_key)
    crv_len = curve_coordinate_byte_length(crv)
    if len(x) != crv_len:
        raise ParserError(
            'Packed credential public key x and y must be {} bytes'.format(
                crv_len))

    return OKPCredentialPublicKey(
        crv=crv,
        x=x,
        **_parse_credential_public_key_kwargs(credential_public_key),
    )


def parse_ec2_public_key(
        credential_public_key: Dict) -> EC2CredentialPublicKey:
    """Parse a JSON EC2 credential public key into an `EC2CredentialPublicKey`.

    Args:
      credential_public_key (Dict): The JSON EC2 credential public key 
        generated by a user agent.

    Returns:
      An `EC2CredentialPublicKey` instance.

    Raises:
      ParserError: Could not correctly parse the data.

    References:
      * https://w3.org/TR/webauthn/#sec-attested-credential-data
      * https://cose-wg.github.io/cose-spec/#rfc.section.7
      * https://cose-wg.github.io/cose-spec/#rfc.section.7.1
      * https://cose-wg.github.io/cose-spec/#rfc.section.13.1
      * https://cose-wg.github.io/cose-spec/#rfc.section.13.1.1
    """
    x = _parse_dictionary_field(-2, bytes, credential_public_key)
    y = _parse_dictionary_field(-3, bytes, credential_public_key)
    crv = _parse_ec2_public_key_crv(credential_public_key)
    crv_len = curve_coordinate_byte_length(crv)
    if len(x) != crv_len or len(y) != crv_len:
        raise ParserError(
            'Packed credential public key x and y must be {} bytes'.format(
                crv_len))

    return EC2CredentialPublicKey(
        x=x,
        y=y,
        crv=crv,
        **_parse_credential_public_key_kwargs(credential_public_key),
    )


class _CredentialPublicKeyParser(Enum):
    OKP = parse_okp_public_key
    EC2 = parse_ec2_public_key


def parse_extensions(
        extensions: Dict) -> AuthenticationExtensionsClientOutputs:
    """Parse an authenticator's JSON extension outputs.

    Args:
      extensions (Dict): The JSON extension client outputs generated by a user
        agent and the user's authenticator.

    Returns:
      An `AuthenticationExtensionsClientOutputs` instance.

    Raises:
      ParserError: Could not correctly parse the extension data.

    References:
      * https://w3.org/TR/webauthn/#dictdef-authenticationextensionsclientoutputs
    """
    supported_extensions = {
        'appid', 'txAuthSimple', 'txAuthGeneric', 'authnSel', 'exts', 'uvi',
        'loc', 'uvm', 'biometricPerfBounds'
    }

    unsupported_extensions = set(
        extensions.keys()).difference(supported_extensions)

    if unsupported_extensions:
        raise ParserError('Found unsupported extensions {}'.format(
            str(unsupported_extensions)))

    appid = extensions.get('appid')
    tx_auth_simple = extensions.get('txAuthSimple')
    tx_auth_generic = extensions.get('txAuthGeneric')
    authn_sel = extensions.get('authnSel')
    exts = extensions.get('exts')
    uvi = extensions.get('uvi')
    loc = extensions.get('loc')
    uvm = extensions.get('uvm')
    biometric_perf_bounds = extensions.get('biometricPerfBounds')

    if appid is not None:
        if type(appid) is not bool:
            raise ParserError('appid extension client output should be a bool')

    if tx_auth_simple is not None:
        if type(tx_auth_simple) is not str:
            raise ParserError(
                'tx_auth_simple extension client output should be a str')

    if tx_auth_generic is not None:
        if type(tx_auth_generic) is not bytes:
            raise ParserError(
                'tx_auth_generic extension client output should be bytes')

    if authn_sel is not None:
        if type(authn_sel) is not bool:
            raise ParserError(
                'authn_sel extension client output should be bool')

    if exts is not None:
        if type(exts) not in (list, tuple):
            raise ParserError('exts extension client output should be list')

        for i, e in enumerate(exts):
            if type(e) is not str:
                raise ParserError(
                    'exts[{0}] extension client output should be str'.format(
                        i))

    if uvi is not None:
        if type(uvi) is not bytes:
            raise ParserError('uvi extension client output should be bytes')

    if loc is not None:
        if type(loc) is not dict:
            raise ParserError('loc extension client output should be dict')

        if any(type(x) not in (int, float) for x in loc.values()):
            raise ParserError(
                'Coordinate value in loc extension must be float')

        supported_cvalues = {
            'latitude', 'longitude', 'altitude', 'accuracy',
            'altitudeAccuracy', 'heading', 'speed'
        }

        unsupported_cvalues = set(loc.keys()).difference(supported_cvalues)
        if unsupported_cvalues:
            raise ParserError('Found unsupported loc key values {}'.format(
                str(unsupported_cvalues)))

        loc = Coordinates(latitude=loc.get('latitude'),
                          longitude=loc.get('longitude'),
                          altitude=loc.get('altitude'),
                          accuracy=loc.get('accuracy'),
                          altitude_accuracy=loc.get('altitudeAccuracy'),
                          heading=loc.get('heading'),
                          speed=loc.get('speed'))

    if uvm is not None:
        if type(uvm) is not list:
            raise ParserError('uvm extension client output should be list')

        for i, uvm_entry in enumerate(uvm):
            if type(uvm_entry) is not list:
                raise ParserError(
                    'uvm[{0}] extension client output should be list'.format(
                        i))

            for j, v in enumerate(uvm_entry):
                if type(v) is not int:
                    raise ParserError(
                        'uvm[{0}][{1}] extension client output should be str'.
                        format(i, j))

    if biometric_perf_bounds is not None:
        if type(biometric_perf_bounds) is not bool:
            raise ParserError(
                'biometric_perf_bounds extension client output should be bool')

    return AuthenticationExtensionsClientOutputs(
        appid=appid,
        tx_auth_simple=tx_auth_simple,
        tx_auth_generic=tx_auth_generic,
        authn_sel=authn_sel,
        exts=exts,
        uvi=uvi,
        loc=loc,
        uvm=uvm,
        biometric_perf_bounds=biometric_perf_bounds,
    )


def _parse_attestation_statement_alg(
    att_stmt: Dict
) -> Union[COSEAlgorithmIdentifier.Name, COSEAlgorithmIdentifier.Value]:
    alg = _parse_dictionary_field('alg', (int, str), att_stmt)

    try:
        alg = COSEAlgorithmIdentifier(alg)  # type: ignore
    except (KeyError, ValueError):
        raise ParserError('Invalid algorithm identifier {}'.format(alg))
    return alg


def _parse_attestation_statement_x5c(att_stmt: Dict) -> Sequence[bytes]:
    x5c = _parse_dictionary_field('x5c', (list, tuple), att_stmt)

    for i, e in enumerate(x5c):
        if type(e) is not bytes:
            raise ParserError('x5c[{}] must be a byte string'.format(i))
    return x5c


def parse_packed_attestation_statement(
        att_stmt: Dict) -> PackedAttestationStatement:
    """Parse a JSON packed attestation statement.

    Args:
      att_stmt (Dict): The JSON attestation statement.

    Returns:
      A `PackedAttestationStatement` instance.

    Raises:
      ParserError: Could not correctly parse the attestation statement.

    References:
      * https://w3.org/TR/webauthn/#packed-attestation
    """
    supported_keys = {'alg', 'sig'}
    alg = _parse_attestation_statement_alg(att_stmt)
    sig = _parse_dictionary_field('sig', bytes, att_stmt)

    if 'x5c' in att_stmt:
        supported_keys.add('x5c')
        _check_unsupported_keys(supported_keys, att_stmt)
        x5c = _parse_attestation_statement_x5c(att_stmt)
        return PackedX509AttestationStatement(alg=alg, sig=sig, x5c=x5c)

    if 'ecdaaKeyId' in att_stmt:
        supported_keys.add('ecdaaKeyId')
        _check_unsupported_keys(supported_keys, att_stmt)
        ecdaa_key_id = _parse_dictionary_field('ecdaaKeyId', bytes, att_stmt)
        return PackedECDAAAttestationStatement(alg=alg,
                                               sig=sig,
                                               ecdaa_key_id=ecdaa_key_id)

    _check_unsupported_keys(supported_keys, att_stmt)
    return PackedAttestationStatement(alg=alg, sig=sig)


def parse_tpm_attestation_statement(att_stmt: Dict) -> TPMAttestationStatement:
    """Parse a JSON TPM attestation statement.

    Args:
      att_stmt (Dict): The JSON attestation statement.

    Returns:
      A `TPMAttestationStatement` instance.

    Raises:
      ParserError: Could not correctly parse the attestation statement.

    References:
      * https://w3.org/TR/webauthn/#tpm-attestation
    """
    supported_keys = {'alg', 'sig', 'ver', 'certInfo', 'pubArea'}
    alg = _parse_attestation_statement_alg(att_stmt)
    sig = _parse_dictionary_field('sig', bytes, att_stmt)
    ver = _parse_dictionary_field('ver', str, att_stmt)
    cert_info = _parse_dictionary_field('certInfo', bytes, att_stmt)
    pub_area = _parse_dictionary_field('pubArea', bytes, att_stmt)

    if 'x5c' in att_stmt:
        supported_keys.add('x5c')
        _check_unsupported_keys(supported_keys, att_stmt)
        x5c = _parse_attestation_statement_x5c(att_stmt)
        return TPMX509AttestationStatement(alg=alg,
                                           sig=sig,
                                           ver=ver,
                                           cert_info=cert_info,
                                           pub_area=pub_area,
                                           x5c=x5c)

    if 'ecdaaKeyId' in att_stmt:
        supported_keys.add('ecdaaKeyId')
        _check_unsupported_keys(supported_keys, att_stmt)
        ecdaa_key_id = _parse_dictionary_field('ecdaaKeyId', bytes, att_stmt)
        return TPMECDAAAttestationStatement(alg=alg,
                                            sig=sig,
                                            ver=ver,
                                            cert_info=cert_info,
                                            pub_area=pub_area,
                                            ecdaa_key_id=ecdaa_key_id)

    _check_unsupported_keys(supported_keys, att_stmt)
    return TPMAttestationStatement(alg=alg,
                                   sig=sig,
                                   ver=ver,
                                   cert_info=cert_info,
                                   pub_area=pub_area)


def parse_android_key_attestation_statement(
        att_stmt: Dict) -> AndroidKeyAttestationStatement:
    """Parse a JSON Android key attestation statement.

    Args:
      att_stmt (Dict): The JSON attestation statement.

    Returns:
      An `AndroidKeyAttestationStatement` instance.

    Raises:
      ParserError: Could not correctly parse the attestation statement.

    References:
      * https://w3.org/TR/webauthn/#android-key-attestation
    """
    supported_keys = {'alg', 'sig', 'x5c'}
    alg = _parse_attestation_statement_alg(att_stmt)
    sig = _parse_dictionary_field('sig', bytes, att_stmt)
    x5c = _parse_attestation_statement_x5c(att_stmt)
    _check_unsupported_keys(supported_keys, att_stmt)
    return AndroidKeyAttestationStatement(alg=alg, sig=sig, x5c=x5c)


def parse_android_safetynet_attestation_statement(
        att_stmt: Dict) -> AndroidSafetyNetAttestationStatement:
    """Parse a JSON Android SafetyNet attestation statement.

    Args:
      att_stmt (Dict): The JSON attestation statement.

    Returns:
      An `AndroidSafetyNetAttestationStatement` instance.

    Raises:
      ParserError: Could not correctly parse the attestation statement.

    References:
      * https://w3.org/TR/webauthn/#android-safetynet-attestation
    """
    supported_keys = {'ver', 'response'}
    ver = _parse_dictionary_field('ver', str, att_stmt)
    response = _parse_dictionary_field('response', bytes, att_stmt)
    _check_unsupported_keys(supported_keys, att_stmt)
    return AndroidSafetyNetAttestationStatement(ver=ver, response=response)


def parse_fido_u2f_attestation_statement(
        att_stmt: Dict) -> FIDOU2FAttestationStatement:
    """Parse a JSON FIDO U2F attestation statement.

    Args:
      att_stmt (Dict): The JSON attestation statement.

    Returns:
      A `FIDOU2FAttestationStatement` instance.

    Raises:
      ParserError: Could not correctly parse the attestation statement.

    References:
      * https://www.w3.org/TR/webauthn/#fido-u2f-attestation
    """
    supported_keys = {'sig', 'x5c'}
    sig = _parse_dictionary_field('sig', bytes, att_stmt)
    x5c = _parse_attestation_statement_x5c(att_stmt)
    _check_unsupported_keys(supported_keys, att_stmt)
    return FIDOU2FAttestationStatement(sig=sig, x5c=x5c)


def parse_none_attestation_statement(
        att_stmt: Dict) -> NoneAttestationStatement:
    """Parse a JSON NONE attestation statement.

    Args:
      att_stmt (Dict): The JSON attestation statement.

    Returns:
      A `NoneAttestationStatement` instance.

    Raises:
      ParserError: Could not correctly parse the attestation statement.

    References:
      * https://w3.org/TR/webauthn/#none-attestation
    """
    _check_unsupported_keys(set(), att_stmt)
    return NoneAttestationStatement()


class _AttestationStatementParser(Enum):
    PACKED = parse_packed_attestation_statement
    TPM = parse_tpm_attestation_statement
    ANDROID_KEY = parse_android_key_attestation_statement
    ANDROID_SAFETYNET = parse_android_safetynet_attestation_statement
    FIDO_U2F = parse_fido_u2f_attestation_statement
    NONE = parse_none_attestation_statement


def parse_client_data(client_data_JSON: bytes) -> CollectedClientData:
    """Parse the raw UTF-8-encoded client data JSON.

    Args:
      client_data_JSON (bytes): The UTF-8-encoded client data JSON.

    Returns:
      A `CollectedClientData` instance.

    Raises:
      ParserError: Could not correctly parse the client data JSON.

    References:
      * https://w3.org/TR/webauthn/#dictdef-collectedclientdata
    """
    try:
        client_data_text = client_data_JSON.decode('utf-8')
        client_data = json.loads(client_data_text)
    except (UnicodeDecodeError, json.JSONDecodeError):
        raise DecodingError('Could not decode the client data JSON')

    if type(client_data) is not dict:
        raise ParserError('Client data JSON must be a dictionary')

    type_ = client_data.get('type')
    challenge = client_data.get('challenge')
    origin = client_data.get('origin')

    if not all(isinstance(x, str) for x in (type_, challenge, origin)):
        raise ParserError('Invalid client data parsed')

    token_binding_data = client_data.get('tokenBinding')
    token_binding = None
    if token_binding_data is not None:
        if type(token_binding_data) is not dict:
            raise ParserError('Token Binding data must be a dictionary')

        token_binding_status = token_binding_data.get('status')
        token_binding_id = token_binding_data.get('id')

        if token_binding_status is None:
            raise ParserError('Token Binding status must be present')

        try:
            token_binding_status_enum = TokenBindingStatus(
                token_binding_status)
        except ValueError:
            raise ParserError(
                'Invalid Token Binding status {}'.format(token_binding_status))

        if token_binding_status_enum == TokenBindingStatus.PRESENT and (
                token_binding_id is None):
            raise ParserError(
                'Token Binding must contain an id if status is {}'.format(
                    TokenBindingStatus.PRESENT))

        token_binding = TokenBinding(status=token_binding_status_enum,
                                     id=token_binding_id)

    return CollectedClientData(type=type_,
                               challenge=challenge,
                               origin=origin,
                               token_binding=token_binding)


def parse_cose_key(
        credential_public_key: Union[Dict, bytes]) -> CredentialPublicKey:
    """Parse the CBOR-encoded, or decoded, credential public key.

    Args:
      credential_public_key (Union[Dict, bytes]): A CBOR-encoded public key
        or a CBOR-decoded dictionary of a credential public key.

    Returns:
      An instance of `CredentialPublicKey`.

    Raises:
      ParserError: Could not correctly parse the credential public key.
      DecodingError: Could not decode the raw CBOR data.

    References:
      * https://w3.org/TR/webauthn/#sec-attested-credential-data
      * https://cose-wg.github.io/cose-spec/#rfc.section.7
      * https://cose-wg.github.io/cose-spec/#rfc.section.7.1
    """
    if type(credential_public_key) is bytes:
        try:
            credential_public_key = cbor2.loads(credential_public_key)
        except cbor2.CBORDecodeError:
            raise DecodingError('Could not decode credential public key CBOR')

        if type(credential_public_key) is not dict:
            raise ParserError(
                'Credential public key CBOR must be a dictionary')
    try:
        cose_key_type = COSEKeyType(credential_public_key[1])  # type: ignore
    except (KeyError, ValueError):
        raise ParserError('Invalid or missing COSE key type encountered')

    try:
        cpk_parser = getattr(_CredentialPublicKeyParser,
                             cose_key_type.name)  # type: ignore
    except AttributeError:
        raise ParserError('Parser not supported for key type {}'.format(
            cose_key_type.name))  # type: ignore

    return cpk_parser(credential_public_key)


def _read_bytes(bio: io.BytesIO, n: int) -> bytes:
    x = bio.read1(n)
    if len(x) != n:
        raise EOFError('Unexpected number of bytes read')
    return x


def parse_authenticator_data(auth_data: bytes) -> AuthenticatorData:
    """Parse the raw authenticator data.

    Args:
      auth_data (bytes): The raw authenticator data bytes.

    Returns:
      An instance of `AuthenticatorData`.

    Raises:
      ParserError: Could not correctly parse the authenticator data.
      DecodingError: Could not decode raw CBOR data.

    References:
      * https://w3.org/TR/webauthn/#authenticator-data
    """
    if len(auth_data) < 37:
        raise ParserError('Attestation auth data must be at least 35 bytes')

    rp_id_hash = auth_data[:32]
    flags = auth_data[32]
    signature_counter_bytes = auth_data[33:37]
    signature_counter_uint32, = struct.unpack('>I', signature_counter_bytes)

    attested_credential_data_included = bool(flags
                                             & AuthenticatorDataFlag.AT.value)
    extension_data_included = bool(flags & AuthenticatorDataFlag.ED.value)

    remaining_bytes_io = io.BytesIO(auth_data[37:])

    attested_credential_data = None
    aeci = None

    if attested_credential_data_included:
        try:
            aaguid = _read_bytes(remaining_bytes_io, 16)
            credential_id_length_bytes = _read_bytes(remaining_bytes_io, 2)
            credential_id_length_uint16, = struct.unpack(
                '>H', credential_id_length_bytes)
            credential_id = _read_bytes(remaining_bytes_io,
                                        credential_id_length_uint16)

            try:
                credential_public_key = cbor2.load(remaining_bytes_io)
            except cbor2.CBORDecodeError:
                raise DecodingError(
                    'Could not decode the credential public key CBOR')

            if type(credential_public_key) is not dict:
                raise ParserError('Credential public key must be a dictionary')

            cpk = parse_cose_key(credential_public_key)
            validate(cpk)

            attested_credential_data = AttestedCredentialData(
                aaguid=aaguid,
                credential_id_length=credential_id_length_uint16,
                credential_id=credential_id,
                credential_public_key=cpk,
            )
        except EOFError:
            raise ParserError(
                'Could not read the included attested credential data')

    if extension_data_included:
        try:
            try:
                extensions = cbor2.load(remaining_bytes_io)
            except cbor2.CBORDecodeError:
                raise DecodingError('Could not decode the extensions CBOR')

            if type(extensions) is not dict:
                raise ParserError('Extension data CBOR must be a dictionary')

            aeci = parse_extensions(extensions)
        except EOFError:
            raise ParserError('Could not read the included extension data')

    if remaining_bytes_io.read1(1) != b'':
        raise ParserError(
            'The authenticator data has unexpected leftover bytes')

    return AuthenticatorData(
        rp_id_hash=rp_id_hash,
        flags=flags,
        sign_count=signature_counter_uint32,
        attested_credential_data=attested_credential_data,
        extensions=aeci,
    )


def parse_attestation_object(
        attestation_object: bytes) -> Tuple[AttestationObject, Dict]:
    """Parse the raw CBOR-encoded attestation object.

    Args:
      attestation_object (bytes): The raw authenticator data bytes.

    Returns:
      A 2-tuple where the first element is the parsed `AttestationObject`
      instance and the second element is its raw CBOR-decoded dictionary.

    Raises:
      ParserError: Could not correctly parse the authenticator data.
      DecodingError: Could not decode raw CBOR data.

    References:
      * https://w3.org/TR/webauthn/#attestation-object
    """
    try:
        attestation_object_data = cbor2.loads(attestation_object)
    except cbor2.CBORDecodeError:
        raise DecodingError('Could not decode the attestation object CBOR')

    if type(attestation_object_data) is not dict:
        raise ParserError('Attestation object CBOR must be a dictionary')

    try:
        auth_data = attestation_object_data['authData']
        fmt = attestation_object_data['fmt']
        att_stmt = attestation_object_data['attStmt']

        if type(fmt) is not str:
            raise ParserError('fmt must be a text string')

        try:
            asfi = AttestationStatementFormatIdentifier(fmt)
        except ValueError:
            raise ParserError(
                'Invalid attestation statement format identifier')

        if type(auth_data) is not bytes:
            raise ParserError('Attestation auth data should be bytes')

        authenticator_data = parse_authenticator_data(auth_data)
    except KeyError as e:
        raise ParserError('Missing key in attestation ({})'.format(str(e)))

    if type(att_stmt) is not dict:
        raise ParserError('attStmt must be a dictionary')

    try:
        as_parser = getattr(_AttestationStatementParser, asfi.name)
        attestation_statement = as_parser(att_stmt)
    except AttributeError:
        raise ParserError('Unsupported attestation statement {}'.format(
            asfi.name))

    return AttestationObject(
        auth_data=authenticator_data,
        fmt=asfi,
        att_stmt=attestation_statement,
    ), attestation_object_data
