Exemplo n.º 1
0
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
Exemplo n.º 2
0
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
Exemplo n.º 3
0
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),
    )
Exemplo n.º 4
0
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))
Exemplo n.º 5
0
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
Exemplo n.º 6
0
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
Exemplo n.º 7
0
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)
Exemplo n.º 8
0
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
Exemplo n.º 9
0
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
Exemplo n.º 10
0
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
Exemplo n.º 11
0
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,
    )
Exemplo n.º 12
0
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)
Exemplo n.º 13
0
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,
    )
Exemplo n.º 14
0
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)))