View Source Wax (wax_ v0.6.7)
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:
Option | Type | Applies to | Default value | Notes |
---|---|---|---|---|
attestation | "none" or "direct" | registration | "none" | |
origin | String.t() | registration & authentication | Mandatory. Example: https://www.example.com | |
rp_id | String.t() or :auto | registration & authentication | If 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" | |
allow_credentials | [{Wax.AuthenticatorData.credential_id(), Wax.CoseKey.t()}] | authentication | [] | |
trusted_attestation_types | [t:Wax.Attestation.type/0] | registration | [:none, :basic, :uncertain, :attca, :anonca, :self] | |
verify_trust_root | boolean() | registration | true | Only 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 |
timeout | non_neg_integer() | registration & authentication | 20 * 60 | The validity duration of a challenge, in seconds |
android_key_allow_software_enforcement | boolean() | registration | false | When registration is a Android key, determines whether software enforcement is acceptable (true ) or only hardware enforcement is (false ) |
silent_authentication_enabled | boolean() | authentication | false | See 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
orself
attestation types, or disabling it forpacked
andu2f
formats by disabling it with theverify_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()} | {:allow_credentials, [{Wax.AuthenticatorData.credential_id(), Wax.CoseKey.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
authenticate(credential_id, auth_data_bin, sig, client_data_json_raw, challenge, credentials \\ [])
View Source@spec authenticate( Wax.CredentialId.t(), binary(), binary(), Wax.ClientData.raw_string(), Wax.Challenge.t(), [{Wax.AuthenticatorData.credential_id(), Wax.CoseKey.t()}] ) :: {:ok, Wax.AuthenticatorData.t()} | {:error, Exception.t()}
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 tonew_authentication_challenge/1
as it will be compared against the previously retrieved valid credential idsauth_data_bin
: the authenticator data returned by the WebAuthn javascript API. Must be the raw binary, not the base64 encoded formsig
: the signature returned by the WebAuthn javascript API. Must be the raw binary, not the base64 encoded formclient_data_json_raw
: the JSON string (and not the decoded JSON) of the client data JSON as returned by the WebAuthn javascript APIchallenge
: the challenge that was generated beforehand, and whose bytes has been sent to the browser and used as an input by the WebAuthn javascript APIcredentials
: 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:
- save the aaguid after registration along with the credential ID
- 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.
@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.
@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
}
register(attestation_object_cbor, client_data_json_raw, challenge)
View Source@spec register(binary(), Wax.ClientData.raw_string(), Wax.Challenge.t()) :: {:ok, {Wax.AuthenticatorData.t(), Wax.Attestation.result()}} | {:error, Exception.t()}
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 APIchallenge
: 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:
- Verify that no other user has the same credential id (and should fail otherwise)
- Store the new tuple
{credential id, cose key}
for the user