Skip to content

Commit

Permalink
Updated doc
Browse files Browse the repository at this point in the history
  • Loading branch information
tanguilp committed Feb 9, 2019
1 parent 5bc4991 commit a3eee1c
Show file tree
Hide file tree
Showing 22 changed files with 226 additions and 43 deletions.
199 changes: 175 additions & 24 deletions lib/wax.ex
Original file line number Diff line number Diff line change
Expand Up @@ -132,32 +132,87 @@ defmodule Wax do
## Example:
```elixir
iex> Wax.new_registration_challenge("Georges", [trusted_attestation_types: [:basic, :attca]])
iex> Wax.new_registration_challenge(trusted_attestation_types: [:basic, :attca])
%Wax.Challenge{
allow_credentials: [],
bytes: <<107, 108, 196, 138, 218, 172, 248, 168, 167, 89, 174, 213, 32, 60,
236, 116, 180, 47, 11, 3, 233, 16, 210, 225, 146, 231, 219, 168, 251, 51,
228, 224>>,
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>>,
exp: nil,
origin: "http://localhost:4000",
rp_id: "localhost",
token_binding_status: nil,
trusted_attestation_types: [:basic, :attca],
user: "Georges",
user_verified_required: false,
verify_trust_root: true
}
```
"""

@spec new_registration_challenge(Wax.User.t(), opts()) :: Wax.Challenge.t()
@spec new_registration_challenge(opts()) :: Wax.Challenge.t()

def new_registration_challenge(user, opts) do
def new_registration_challenge(opts) do
opts = set_opts(opts)

Wax.Challenge.new(user, opts)
Wax.Challenge.new(opts)
end

@doc """
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:
`{cose_key, {attestation_type, trust_path, metadata_statement}}`.
See `t: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:
```elixir
case Wax.register(attestation_object, client_data_json_raw, challenge) do
{:ok, {cose_key, {_, _, metadata_statement}}} ->
# tee is for "trusted execution platform"
if :key_protection_tee in metadata_statement.key_protection or
:key_protection_secure_element in metadata_statement.key_protection
do
register_key(user, credential_id, 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
- a cose key: returned by this function, 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 they do
user 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
"""

@spec register(binary(), Wax.ClientData.raw_string(), Wax.Challenge.t())
:: {:ok, {Wax.CoseKey.t(), Wax.Attestation.result()}} | {:error, atom()}

Expand Down Expand Up @@ -195,15 +250,123 @@ defmodule Wax do
end
end

@spec new_authentication_challenge(Wax.User.t(), [{Wax.CredentialId.t(), Wax.CoseKey.t()}], opts())
@doc """
Generates a new challenge for authentication
The first argument is a list of (credential id, cose key) which were previsouly
registered (after successful `register/3`) for a user. This can be retrieved from
a user database for instance.
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:
```elixir
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(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_verified_required: false,
verify_trust_root: true
}
```
"""

@spec new_authentication_challenge([{Wax.CredentialId.t(), Wax.CoseKey.t()}], opts())
:: Wax.Challenge.t()

def new_authentication_challenge(user, allow_credentials, opts) do
def new_authentication_challenge(allow_credentials, opts) do
opts = set_opts(opts)

Wax.Challenge.new(user,allow_credentials, opts)
Wax.Challenge.new(allow_credentials, opts)
end

@doc """
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/2` 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
The call returns `{:ok, sign_count}` in case of success, or `{:error, :reason}` otherwise.
The `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](https://www.w3.org/TR/2019/PR-webauthn-20190117/#verifying-assertion)
for more details.
"""
@spec authenticate(Wax.CredentialId.t(),
binary(),
binary(),
Expand Down Expand Up @@ -259,7 +422,7 @@ defmodule Wax do

@spec valid_challenge?(Wax.ClientData.t(), Wax.Challenge.t()) :: :ok | {:error, any()}

def valid_challenge?(client_data, challenge) do
defp valid_challenge?(client_data, challenge) do
if client_data.challenge == challenge.bytes do
:ok
else
Expand Down Expand Up @@ -324,7 +487,6 @@ defmodule Wax do
:: :ok | {:error, any()}

defp attestation_trustworthy?({type, _, _}, %Wax.Challenge{trusted_attestation_types: tatl})
when is_list(tatl)
do
if type in tatl do
:ok
Expand All @@ -333,17 +495,6 @@ defmodule Wax do
end
end

defp attestation_trustworthy?(attestation_result,
%Wax.Challenge{trusted_attestation_types: tatf})
when is_function(tatf, 1)
do
if tatf.(attestation_result) do
:ok
else
{:error, :untrusted_attestation_type}
end
end

@spec cose_key_from_credential_id(Wax.CredentialId.t(), Wax.Challenge.t())
:: {:ok, Wax.CoseKey.t()} | {:error, any()}

Expand Down
2 changes: 2 additions & 0 deletions lib/wax/attestation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ defmodule Wax.Attestation do
{:ok, __MODULE__.result()} | {:error, any()}
)

@doc false

@spec statement_verify_fun(binary()) ::
{:ok, attestation_statement_format_verify_fun} | {:error, any()}

Expand Down
2 changes: 2 additions & 0 deletions lib/wax/attestation_statement_format/android_key.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ defmodule Wax.AttestationStatementFormat.AndroidKey do
#FIXME: test cert can be found at https://fidoalliance.org/wp-content/uploads/Hardware-backed_Keystore_White_Paper_June2018.pdf
require Logger

@moduledoc false

@asn_output_dir 'android_key/asn_generated'

# from https://github.com/NuclearAndroidProject1/android_hardware_libhardware/blob/master/include/hardware/keymaster_defs.h
Expand Down
2 changes: 2 additions & 0 deletions lib/wax/attestation_statement_format/android_safetynet.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule Wax.AttestationStatementFormat.AndroidSafetynet do
require Logger

@moduledoc false

@behaviour Wax.AttestationStatementFormat

# GSR2 root certificate
Expand Down
2 changes: 2 additions & 0 deletions lib/wax/attestation_statement_format/fido_u2f.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule Wax.AttestationStatementFormat.FIDOU2F do
require Logger

@moduledoc false

@behaviour Wax.AttestationStatementFormat

@impl Wax.AttestationStatementFormat
Expand Down
2 changes: 2 additions & 0 deletions lib/wax/attestation_statement_format/none.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
defmodule Wax.AttestationStatementFormat.None do
@moduledoc false

@behaviour Wax.AttestationStatementFormat

@impl Wax.AttestationStatementFormat
Expand Down
2 changes: 2 additions & 0 deletions lib/wax/attestation_statement_format/packed.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule Wax.AttestationStatementFormat.Packed do
require Logger

@moduledoc false

@behaviour Wax.AttestationStatementFormat

# from https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements
Expand Down
2 changes: 2 additions & 0 deletions lib/wax/attestation_statement_format/tpm.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule Wax.AttestationStatementFormat.TPM do
require Logger

@moduledoc false

#structures described in http://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf

@behaviour Wax.AttestationStatementFormat
Expand Down
18 changes: 3 additions & 15 deletions lib/wax/attested_credential_data.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,8 @@ defmodule Wax.AttestedCredentialData do
credential_public_key: Wax.CoseKey.t()
}

def new(aaguid, credential_id, credential_public_key) do
%__MODULE__{
aaguid: aaguid,
credential_id: credential_id,
credential_public_key: credential_public_key
}
end

@doc """
Decode attested credential data
@doc false

In addition, the number of bytes read is returned so that extensions can be further parsed
(and only when `with_appended_extensions` is `true`
"""
@spec decode(binary(), boolean())
:: {:ok, t() | {t(), non_neg_integer()}} | {:error, any()}

Expand Down Expand Up @@ -72,11 +60,11 @@ defmodule Wax.AttestedCredentialData do
@spec cbor_decode_binary_unknown_length(binary, non_neg_integer(), non_neg_integer())
:: {any(), non_neg_integer()}

def cbor_decode_binary_unknown_length(_bin, nb, max) when nb > max do
defp cbor_decode_binary_unknown_length(_bin, nb, max) when nb > max do
raise "#{__MODULE__}: invalid CBOR decode error"
end

def cbor_decode_binary_unknown_length(bin, nb, max) do
defp cbor_decode_binary_unknown_length(bin, nb, max) do
try do
%{} = cpk = :cbor.decode(<<bin::binary-size(nb)>>)

Expand Down
2 changes: 2 additions & 0 deletions lib/wax/authenticator_data.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
defmodule Wax.AuthenticatorData do
@moduledoc false

@enforce_keys [
:rp_id_hash,
:flag_user_present,
Expand Down
4 changes: 3 additions & 1 deletion lib/wax/challenge.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ defmodule Wax.Challenge do
verify_trust_root: boolean()
}

@doc false

@spec new(Wax.parsed_opts()) :: t()
def new(allow_credentials \\ [],
%{origin: origin,
Expand All @@ -46,7 +48,7 @@ defmodule Wax.Challenge do
end

@spec random_bytes() :: binary
def random_bytes() do
defp random_bytes() do
:crypto.strong_rand_bytes(32)
end
end
3 changes: 3 additions & 0 deletions lib/wax/client_data.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ defmodule Wax.ClientData do

@type raw_string :: String.t()

@doc false

@spec parse_raw_json(raw_string()) :: {:ok, t()} | {:error, any()}

def parse_raw_json(client_data_json_raw) do
#FIXME: implement https://encoding.spec.whatwg.org/#utf-8-decode ?

Expand Down
Loading

0 comments on commit a3eee1c

Please sign in to comment.