defmodule WebAuthnEx.AuthAssertionResponse do @moduledoc """ Validates assertion """ alias WebAuthnEx.AuthenticatorResponse alias __MODULE__ @enforce_keys [:credential_id, :auth_data_bytes, :signature] defstruct [ :credential_id, :auth_data_bytes, :signature, :allowed_credentials, :valid_authenticator, :valid_assertion_statement, :valid_credential, :valid_signature ] def new( credential_id, auth_data_bytes, signature, challenge, original_origin, allowed_credentials, rp_id, client_data_json ) do %AuthAssertionResponse{ credential_id: credential_id, auth_data_bytes: auth_data_bytes, signature: signature, allowed_credentials: allowed_credentials, valid_authenticator: nil, valid_assertion_statement: nil, valid_credential: nil, valid_signature: nil } |> valid?(challenge, original_origin, rp_id, client_data_json) |> result() end def result( %AuthAssertionResponse{ valid_authenticator: valid_authenticator, valid_assertion_statement: valid_assertion_statement, valid_credential: valid_credential, valid_signature: valid_signature } = auth_assertion_response ) do cond do valid_authenticator == false -> {:error, "Validation of authenticator failed!"} valid_assertion_statement == false -> {:error, "Validation of assertion statement failed!"} valid_credential == false -> {:error, "Validation of credential failed!"} valid_signature == false -> {:error, "Validation of signature failed!"} true -> {:ok, auth_assertion_response} end end def valid?( %AuthAssertionResponse{} = auth_assertion_response, original_challenge, original_origin, rp_id, client_data_json ) do auth_assertion_response |> valid_authenticator_response?( original_challenge, original_origin, auth_assertion_response.auth_data_bytes, rp_id, client_data_json ) |> valid_credential?() |> valid_signature?( credential_public_key( auth_assertion_response.allowed_credentials, auth_assertion_response.credential_id ), auth_assertion_response.signature, client_data_json, auth_assertion_response.auth_data_bytes ) end def valid_authenticator_response?( auth_assertion_response, original_challenge, original_origin, auth_data_bytes, rp_id, client_data_json ) do case AuthenticatorResponse.valid?( original_challenge, original_origin, auth_data_bytes, rp_id, client_data_json ) do true -> %AuthAssertionResponse{auth_assertion_response | valid_authenticator: true} false -> %AuthAssertionResponse{auth_assertion_response | valid_authenticator: false} end end def valid_credential?(%AuthAssertionResponse{} = auth_assertion_response) do credential_valid = auth_assertion_response.allowed_credentials |> Enum.map(fn c -> c[:id] end) |> Enum.member?(auth_assertion_response.credential_id) case credential_valid do true -> %AuthAssertionResponse{auth_assertion_response | valid_credential: true} false -> %AuthAssertionResponse{auth_assertion_response | valid_credential: false} end end def valid_signature?( auth_assertion_response, public_key_bytes, signature, client_data_json, authenticator_data_bytes ) do if auth_assertion_response.valid_credential do client_data_hash = :crypto.hash(:sha256, client_data_json) public_key = {{:ECPoint, public_key_bytes}, {:namedCurve, :prime256v1}} signature_valid = :public_key.verify( authenticator_data_bytes <> client_data_hash, :sha256, signature, public_key ) case signature_valid do true -> %AuthAssertionResponse{auth_assertion_response | valid_signature: true} false -> %AuthAssertionResponse{auth_assertion_response | valid_signature: false} end else %AuthAssertionResponse{auth_assertion_response | valid_signature: false} end end def credential_public_key(allowed_credentials, credential_id) do matched_credential = Enum.find(allowed_credentials, fn x -> x[:id] == credential_id end) matched_credential[:public_key] end end