View Source Wax (wax_ v0.6.5)

Functions for FIDO2 registration and authentication

options

Options

The options are set when generating the challenge (for both registration and authentication). Options can be configured either globally in the configuration file or when generating the challenge. Some also have default values.

Option values set during challenge generation take precedence over globally configured options, which takes precedence over default values.

These options are:

OptionTypeApplies toDefault valueNotes
attestation"none" or "direct"registration"none"
originString.t()registration & authenticationMandatory. Example: https://www.example.com
rp_idString.t() or :autoregistration & authenticationIf set to :auto, automatically determined from the origin (set to the host)With :auto, it defaults to the full host (e.g.: www.example.com). This option allow you to set the rp_id to another valid value (e.g.: example.com)
user_verification"discouraged", "preferred" or "required"registration & authentication"preferred"
trusted_attestation_types[t:Wax.Attestation.type/0]registration[:none, :basic, :uncertain, :attca, :anonca, :self]
verify_trust_rootboolean()registrationtrueOnly for u2f and packed attestation. tpm attestation format is always checked against metadata
acceptable_authenticator_statuses[String.t()]registration["FIDO_CERTIFIED", "FIDO_CERTIFIED_L1", "FIDO_CERTIFIED_L1plus", "FIDO_CERTIFIED_L2", "FIDO_CERTIFIED_L2plus", "FIDO_CERTIFIED_L3", "FIDO_CERTIFIED_L3plus"]The "UPDATE_AVAILABLE" status is not whitelisted by default
timeoutnon_neg_integer()registration & authentication20 * 60The validity duration of a challenge, in seconds
android_key_allow_software_enforcementboolean()registrationfalseWhen registration is a Android key, determines whether software enforcement is acceptable (true) or only hardware enforcement is (false)
silent_authentication_enabledboolean()authenticationfalseSee https://github.com/fido-alliance/conformance-tools-issues/issues/434

fido2-metadata

FIDO2 Metadata

If you use attestation, you need to enabled metadata.

configuring-mdsv3-metadata

Configuring MDSv3 metadata

This is the official metadata service of the FIDO foundation.

Set the :update_metadata environment variable to true and metadata will load automatically through HTTP from https://mds3.fidoalliance.org/.

loading-fido2-metadata-from-a-directory

Loading FIDO2 metadata from a directory

In addition to the FIDO2 metadata service, it is possible to load metadata from a directory. To do so, the :metadata_dir application environment variable must be set to one of:

  • a String.t(): the path to the directory containing the metadata files
  • an atom(): in this case, the files are loaded from the "fido2_metadata" directory of the private ("priv/") directory of the application (whose name is the atom)

In both case, Wax tries to load all files (even directories and other special files).

Example configuration

config :wax_,
  origin: "http://localhost:4000",
  rp_id: :auto,
  metadata_dir: :my_application

will try to load all files of the "priv/fido2_metadata/" of the :my_application as FIDO2 metadata statements. On failure, a warning is emitted.

security-considerations

Security considerations

  • Make sure to understand the implications of not using attested credentials before accepting none or self attestation types, or disabling it for packed and u2f formats by disabling it with the verify_trust_root option
  • This library has not be reviewed by independent security / FIDO2 specialists - use it at your own risks or blindly trust its author! If you're knowledgeable about FIDO2 and willing to help reviewing it, please contact the author

Link to this section Summary

Functions

Verifies a authentication response from the client WebAuthn javascript call

Generates a new challenge for authentication

Generates a new challenge for registration

Verifies a registration response from the client WebAuthn javascript call

Link to this section Types

@type opt() ::
  {:attestation, String.t()}
  | {:origin, String.t()}
  | {:rp_id, String.t() | :auto}
  | {:user_verification, String.t()}
  | {:trusted_attestation_types, [Wax.Attestation.type()]}
  | {:verify_trust_root, boolean()}
  | {:acceptable_authenticator_statuses, [String.t()]}
  | {:issued_at, integer()}
  | {:timeout, non_neg_integer()}
  | {:android_key_allow_software_enforcement, boolean()}
  | {:silent_authentication_enabled, boolean()}
@type opts() :: [opt()]

Link to this section Functions

Link to this function

authenticate(credential_id, auth_data_bin, sig, client_data_json_raw, challenge, credentials \\ [])

View Source

Verifies a authentication response from the client WebAuthn javascript call

The input params are:

  • credential_id: the credential id returned by the WebAuthn javascript API. Must be of the same form as the one passed to new_authentication_challenge/1 as it will be compared against the previously retrieved valid credential ids
  • auth_data_bin: the authenticator data returned by the WebAuthn javascript API. Must be the raw binary, not the base64 encoded form
  • sig: the signature returned by the WebAuthn javascript API. Must be the raw binary, not the base64 encoded form
  • client_data_json_raw: the JSON string (and not the decoded JSON) of the client data JSON as returned by the WebAuthn javascript API
  • challenge: the challenge that was generated beforehand, and whose bytes has been sent to the browser and used as an input by the WebAuthn javascript API
  • credentials: see Self-discoverable credentials below

The call returns {:ok, authenticator_data} in case of success, or {:error, e} otherwise.

The auth_data.sign_count is the number of signature performed by this authenticator for this credential id, and can be used to detect cloning of authenticator. See point 17 of the 7.2. Verifying an Authentication Assertion for more details.

When using attestation, it's a good practice to check that the authenticator is not revoked after authentication. To do so:

  1. save the aaguid after registration along with the credential ID
  2. check the authenticator is not revoked after authentication using the aaguid saved in step 1. and Wax.Metadata.get_by_aaguid/2

self-discoverable-credentials-resident-keys

Self-discoverable credentials (resident keys)

It is possible to call the WebAuthn API without :allow_credentials when the FIDO2 device is capable of storing self-discoverable credentials. In this case, the device prompts the user for an account to use, and authenticates the user.

The WebAuthn API then returns a userHandle parameter, which is the user id which was used during registration.

In this flow, it is not possible to know the allowed credentials in advance. Instead, one need to use the user handle to retrieve the user keys after the WebAuthn API returns, and before calling this function, with the retrieved keys passed as the :credentials parameter.

Link to this function

new_authentication_challenge(opts \\ [])

View Source
@spec new_authentication_challenge(opts()) :: Wax.Challenge.t()

Generates a new challenge for authentication

The returned structure:

  • Contains the challenge bytes under the bytes key (e.g.: challenge.bytes). This is a random value that must be used by the javascript WebAuthn call
  • Must be passed backed to authenticate/5

Typically, this structure is stored in the session (cookie...) for the time the WebAuthn authentication process is performed on the client side.

example

Example:

iex> cred_ids_and_associated_keys = UserDatabase.load_cred_id("Georges")
[
  {"vwoRFklWfHJe1Fqjv7wY6exTyh23PjIBC4tTc4meXCeZQFEMwYorp3uYToGo8rVwxoU7c+C8eFuFOuF+unJQ8g==",
   %{
     -3 => <<121, 21, 84, 106, 84, 48, 91, 21, 161, 78, 176, 199, 224, 86, 196,
       226, 116, 207, 221, 200, 26, 202, 214, 78, 95, 112, 140, 236, 190, 183,
       177, 223>>,
     -2 => <<195, 105, 55, 252, 13, 134, 94, 208, 83, 115, 8, 235, 190, 173,
       107, 78, 247, 125, 65, 216, 252, 232, 41, 13, 39, 104, 231, 65, 200, 149,
       172, 118>>,
     -1 => 1,
     1 => 2,
     3 => -7
   }},
  {"E0YtUWEPcRLyW1wd4v3KuHqlW1DRQmF2VgNhhR1FumtMYPUEu/d3RO+WC4T4XIa0PZ6Pjw+IBNQDn/It5UjWmw==",
   %{
     -3 => <<113, 34, 76, 107, 120, 21, 246, 189, 21, 167, 119, 39, 245, 140,
       143, 133, 209, 19, 63, 196, 145, 52, 43, 2, 193, 208, 200, 103, 3, 51,
       37, 123>>,
     -2 => <<199, 68, 146, 57, 216, 62, 11, 98, 8, 108, 9, 229, 40, 97, 201,
       127, 47, 240, 50, 126, 138, 205, 37, 148, 172, 240, 65, 125, 70, 81, 213,
       152>>,
     -1 => 1,
     1 => 2,
     3 => -7
   }}
]
iex> Wax.new_authentication_challenge(allow_credentials: cred_ids_and_associated_keys)
%Wax.Challenge{
  allow_credentials: [
    {"vwoRFklWfHJe1Fqjv7wY6exTyh23PjIBC4tTc4meXCeZQFEMwYorp3uYToGo8rVwxoU7c+C8eFuFOuF+unJQ8g==",
     %{
       -3 => <<121, 21, 84, 106, 84, 48, 91, 21, 161, 78, 176, 199, 224, 86,
         196, 226, 116, 207, 221, 200, 26, 202, 214, 78, 95, 112, 140, 236, 190,
         183, 177, 223>>,
       -2 => <<195, 105, 55, 252, 13, 134, 94, 208, 83, 115, 8, 235, 190, 173,
         107, 78, 247, 125, 65, 216, 252, 232, 41, 13, 39, 104, 231, 65, 200,
         149, 172, 118>>,
       -1 => 1,
       1 => 2,
       3 => -7
     }},
    {"E0YtUWEPcRLyW1wd4v3KuHqlW1DRQmF2VgNhhR1FumtMYPUEu/d3RO+WC4T4XIa0PZ6Pjw+IBNQDn/It5UjWmw==",
     %{
       -3 => <<113, 34, 76, 107, 120, 21, 246, 189, 21, 167, 119, 39, 245, 140,
         143, 133, 209, 19, 63, 196, 145, 52, 43, 2, 193, 208, 200, 103, 3, 51,
         37, 123>>,
       -2 => <<199, 68, 146, 57, 216, 62, 11, 98, 8, 108, 9, 229, 40, 97, 201,
         127, 47, 240, 50, 126, 138, 205, 37, 148, 172, 240, 65, 125, 70, 81,
         213, 152>>,
       -1 => 1,
       1 => 2,
       3 => -7
     }}
  ],
  bytes: <<130, 70, 153, 38, 189, 145, 193, 3, 132, 158, 170, 216, 8, 93, 221,
    46, 206, 156, 104, 24, 78, 167, 182, 5, 6, 128, 194, 201, 196, 246, 243,
    194>>,
  exp: nil,
  origin: "http://localhost:4000",
  rp_id: "localhost",
  token_binding_status: nil,
  trusted_attestation_types: [:none, :basic, :uncertain, :attca, :self],
  user_verification: "preferred",
  verify_trust_root: true
}

When self-discoverable credentials (so-called resident keys) are to be used, omit the :allow_credentials parameter and use instead the :credentials parameter of authenticate/6 when verifying.

Link to this function

new_registration_challenge(opts \\ [])

View Source
@spec new_registration_challenge(opts()) :: Wax.Challenge.t()

Generates a new challenge for registration

The returned structure:

  • Contains the challenge bytes under the bytes key (e.g.: challenge.bytes). This is a random value that must be used by the javascript WebAuthn call
  • Must be passed backed to register/3

Typically, this structure is stored in the session (cookie...) for the time the WebAuthn process is performed on the client side.

example

Example:

iex> Wax.new_registration_challenge(trusted_attestation_types: [:basic, :attca])
%Wax.Challenge{
  allow_credentials: [],
  bytes: <<192, 64, 240, 166, 163, 188, 76, 255, 108, 227, 18, 33, 123, 19, 61,
    3, 166, 195, 190, 157, 24, 207, 210, 179, 180, 136, 10, 135, 82, 172, 134,
    17>>,
  origin: "http://localhost:4000",
  rp_id: "localhost",
  token_binding_status: nil,
  trusted_attestation_types: [:basic, :attca],
  user_verification: "preferred",
  verify_trust_root: true
}
Link to this function

register(attestation_object_cbor, client_data_json_raw, challenge)

View Source

Verifies a registration response from the client WebAuthn javascript call

The input params are:

  • attestation_object_cbor: the raw binary response from the WebAuthn javascript API. When transmitting it back from the browser to the server, it will probably be base64 encoded. Make sure to decode it before.
  • client_data_json_raw: the JSON string (and not the decoded JSON) of the client data JSON as returned by the WebAuthn javascript API
  • challenge: the challenge that was generated beforehand, and whose bytes has been sent to the browser and used as an input by the WebAuthn javascript API

The success return value is of the form: {authenticator_data, {attestation_type, trust_path, metadata_statement}}. One can access the credential public key i nthe authenticator data structure:

auth_data.attested_credential_data.credential_public_key

Regarding the attestation processes' result, see Wax.Attestation.result/0 for more details. Note, however, that you can use the returned metadata statement (if any) to further check the authenticator capabilites. For example, the following conditions will only allow attestation generated by hardware protected attestation keys:

case Wax.register(attestation_object, client_data_json_raw, challenge) do
  {:ok, {authenticator_data, {_, _, metadata_statement}}} ->
    # tee is for "trusted execution platform"
    if "tee" in metadata_statement["keyProtection"] or
       "secure_element" in metadata_statement["keyProtection"]
    do
      register_key(user, credential_id, authenticator_data.attested_credential_data.cose_key)

      :ok
    else
      {:error, :not_hardware_protected}
    end

  {:error, _} = error ->
    error
end

When performing registration, the server has the 3 following pieces of data:

  • user id: specific to the server implementation. Can be a email, login name, or an opaque user identifier
  • credential id: an ID returned by the WebAuthn javascript. It is a handle to further authenticate the user. It is also available in the authenticator data in binary form, and can be accessed by typing: auth_data.attested_credential_data.credential_id
  • the COSE key: available in the authenticator data (auth_data.attested_credential_data.credential_public_key) under the form of a map containing a public key use for further authentication

A credential id is related to a cose key, and vice-versa.

Note that a user can have several {credential id, cose key} pairs, for example if the user uses different authenticators. The unique key (for storage, etc.) is therefore the tuple {user id, credential id}.

In the success case, and after calling register/3, a server shall:

  1. Verify that no other user has the same credential id (and should fail otherwise)
  2. Store the new tuple {credential id, cose key} for the user