How to use the Webauthn API on a webpage to implement 2FA using YubiKey or TPM
01 Jul 2020 - tsp
Last update 05 Jul 2020
19 mins
Note: Currently this article only covers the client side. The
part covering the server side will be added in near future.
What is webauthn (in a nutshell)?
So what is webauthn
anyways? Webauthn is an API that allows JavaScript applications
inside web-browsers to access public key based authentication methods. Basically this
sounds like already existing stuff (like SSL client certificates using an PKCS11 module)
and it really serves a similar purpose.
With Client SSL certificates one could configure an certificate for a client or using
PKCS11 use a smartcard to authenticate with a webservice during every SSL connection buildup.
This works pretty well but browser support for HSMs and PKCS11 smartcards is pretty low.
Also the user experience is rather bad with most implementations and since these certificates
are built around X.509 certificates an unique user identification is provided to
webservices because the same DN is used for every request. On the other hand some popular
browsers like Chromium donāt support PKCS11 to use
external smartcards at all - though they allow the usage of SSL client certificates
stored in files on the users computer. Another reason why SSL client certificates
are not really favored by many people is the tedious procedure for generating them
when not being supplied with a company or government issued smartcard. Support
to use SSL certificates on the server side is easy when one either pins the
distinguished name as well as the certificate signature to a given user account
or even trusts the certificate authority that signed the certificates and uses
the distinguished name as user identifier directly.
Other approaches such as the Austrian citizen card
used an local webservice that was capable of signing individual requests. They also
allowed pinning the signature to a given government certified ID - but only to
applications that have been authorized by an government agency to do so. This
worked less flawless than PKCS11 smartcards since it required installation of
a local webservice, to prevent certificate warnings even the installation of
a new governmental issued root certificate (people who know much about cryptography
will shudder when reading this) and the local software was not easy to install
and of course had portability issues despite being written in Java - for an
experienced system administrator it took about 30 minutes to get the system
up and running on unsupported platforms like FreeBSD. On the other hand the
big advantage of that system is that itās not only useful to authenticate
a user against a service but also to directly sign specific actions - the software
also supported class 3 card terminals so one could use them to display a summary
of the authorized action and get back a legal signature that a given user
authorized a given action. Because of this the system has also been used by
banks and governmental institutions. Unfortunately this system has been deemed
to be phased out due to the migration of many services to authenticate against
SMS tan (people knowing cryptography or infosec will shudder now again
even more).
To solve that problem another solution has been invented (see XKCD
for a short reference on how great that usually works). Since webautn
has been driven by the W3C and some major players who formed the FIDO alliance
such as Google, Blackberry, NXP, Samsung, Visa, Netflix, etc. this turned out to
be supported rather well after initial release. Unfortunately webauthn
is only
available to browsers running JavaScript inside webpages (which is in my opinion
the major drawback of this solution since JavaScript inside webpages poses a major
security risk especially on low end consumer computers not using ECC memory or
smartphones since attacks like rowhammer are unpreventable there and the attach
surface of a JavaScript interpreter or JIT compiler is pretty huge). Webauthn
supports the usage of a variety of hardware security modules such as trusted
platform modules (non roaming authenticator) or roaming authenticators such
as USB or NFC tokens such as the YubiKey.
All hardware that implements the client to authenticator protocol
(CTAP)
or universal 2nd factor
(U2F) protocol can be used.
Web applications can perform two basic operations:
Registration
of a user. During registration the user selects one of the
authenticators that match the requirements by the relying party and uses this
authenticator to sign their own public key together with a challenge supplied
by the service. This allows the application to register a public key used
by a given user.
Authentication
works similar - the relying party simply issues a challenge
that gets signed by the authenticator. The signature gets returned and the
server validates if the signature has been made with the private key matching
one of the users public keys that have been previously registered.
Hardware implementing webauthn normally even provides more security measures.
Since most roaming tokens support a method of presence indication or even
entering a pin code automated attacks by malware are restricted to the number
of signatures that users are actively generating. Itās not possible for malware
to issue a whole bunch of signatures automatically. This is of course not true
when using a TPM or vTPM on compromised hardware though.
Many hardware devices also offer an key store implementation that derives
an own key for each and every service based on the services domain - that ensures
that different services see different public keys and are not capable of correlating
the users who use the same hardware key which is especially interesting in case
people use aliases for different services. Also itās not easily possible to
determine which of the tokens has been used for which service or for which user
account so a stolen key poses less risk than a stolen smartcard containing an X.509
certificate.
Another advantage of using a roaming key via USB or NFC is that these interfaces
are well supported on hardware like a PC or smartphone even without complex
configuration - Chromium and Firefox on PCs support USB tokens out of the box,
one might have to configure udev/devd rules on Unices though; Browsers on
Android phones support using the keys via NFC out of the box. Because of this
the keys are also easily usable on untrusted hardware - in case the hardware
sniffs passwords an attacker is incapable of using the same credentials without
having access to the hardware security module as long as the user hasnāt issued
enough signatures to change settings inside their webservice - this is for example
pretty useful when logging into a mail account from work or untrusted places (for
example it wouldāve solved a hack at a school during which students used stolen
passwords to access exam papers and change marks inside the web based grading
systems even when theyād have stolen the passwords).
For the web application itās possible to decide:
- If they require a local or a roaming module (i.e. a module bound to the
platform like a TPM/vTPM or a USB/NFC token like a YubiKey or a smartcard).
- They can choose if they require attestation. Attestation might be used to
certify that the device that gets registers conforms to different characteristics
in a certified way or originates from a genuine device. This will most likely
be required when one implements the usage of roaming tokens in high security
facilities that are required to legally proof authentication steps and keep
an audit trail. It wonāt really be required for some normal web applications
although itās recommended to save the raw attestation statement after every
credential creation in an audit trail (attestation also allows the server
side to determine the type and an image of the device that has been used for
example).
- One can request user verification instead of presence indication. This
would require a user to enter a password or pin code. On many currently rolled
out devices the hardware only supports presence indication that just limits
the amount of signatures generated by a device to the number of actions
that have been authorized by the user. Verification on the other hand is only
implemented directly on other devices and requires software based pin entry
on other devices (like the YubiKey or TPMs).
- It can be decided if a resident key is required. In this case the client
is required to create a new key-pair for the given user identity. If a key is
not residential the HSMs simply derive the key used during authorization
using the credential ID used during the request - i.e. they normally do not
store information about the credential IDs on the device but re-generate
the key again from the information about the relying party ID, the credential
ID and the domain of the service. In case residential keys are required they
have to store the information required to derive that key as some encrypted
state on the device which will be accessible using a PIN code or similar
authentication method later on again. The main advantage though using resident
keys is that the user.id is stored on the device too so the user doesnāt
have to enter a username again during the next authentication event - using
non resident keys the username has to be entered manually again or be available
from the service in some way (i.e. currently logged in user, etc.).
Note that each user has to be able to register at least two security keys
per account (primary and backup) to be capable of logging in after loss of
the primary key - and they have to be able to assign identifiers/labels to the
keys. Last usage and registration time should be visible to users by specification
and they have to be capable to remove keys from their accounts. Other recovery
options should be considered and supported too.
Operations on the client
Registration
First a compact summary of the registration process:
- The server sends the
PublicKeyCredentialCreationOptions
including a
challenge, relying party info and user info.
- The browser issued an creation request using the webauthn API to the
HSM, the HSM creates the user keypair and attestation if the user indicates
oneās presence or enters the a password or pin.
- The attestation object and public key and JSON encoded client data
is returned to the server. This might be done using AJAX and performing
some client side actions or during a reload of the webpage (note these
are both ArrayBuffer and not Base64 encoded objects in JS!)
Now the detailed process:
First one has to register the used hardware security module like a YubiKey or TPM.
This allows one to bind the user account to the module. This is done by creating
a challenge on the server side that gets passed together with information about
the user and the relying party to the HSM. The hardware security module then either
creates or uses a public key that got selected based on the user and relying party
information and uses this key to sign an Credential
object. The parameters
passed to the navigator.credentials.cerate
function has the following properties:
args.publickey.rp
contains information about the relying party
args.publickey.user
contains user information
args.publickey.challenge
is the random server side generated challenge that
will get signed.
args.publickey.pubKeyCredParams
specifies which type of key should be generated.
There can be multiple key parameters to provide fallbacks
args.publickey.timeout
specifies the maximum number of milliseconds the application
is willing to wait for the user or the HSM to respond
args.publickey.authenticatorSelection
allows one to specify which type of HSMs one
would like to use (internal or external TPMs, etc.)
args.publickey.attestation
specifies if one requires identity attestation by a CA
or if a random key and identity is acceptable. The latter one is sufficient for most
web applications ("none"
).
Before being capable of performing the registration procedure the client script has to
request a random challenge for a given user ID. This might be done using Ajax or
an HTTP webpage reloading roundtrip. One might imagine that being a simple function call
like
requestChallengeFromServer(username)
.then(function(challenge) {
/*
One might also pass more than the challenge like key types and authenticator settings.
The website may also pass IDs of authenticators that should be excluded so in case a
user offers multiple authenticators already registered ones should not be supplied.
Note that it might be a good idea to let users enter a description for an authenticator
on the server side when adding them.
*/
}).catch(function(errorinfo) {
/*
In case of an error ...
*/
});
Then one builds the request parameter object:
var reqparam = {
publicKey : {
/* Required information about the relying party. Optionally an "icon" can be specified */
rp : { id : "testservice.example.com", name : "Test host example" },
/* Information about the user; optional "icon" can be specified */
user : { displayName : "TestUser", id : new Uint8Array(32), name : "Testing testuser" },
/* Server generated challenge that will be signed */
challenge : new Uint8Array(32),
/* Selection of allowed key types */
pubKeyCredParams : [
{ type : "public-key", alg : -7 }, /* ECDSAwithSHA256 */
{ type : "public-key", alg : -37 } /* RSA */
],
/* Timeout we're willing to wait */
timeout : 5 * 60 * 1000,
/* Authenticator selection */
authenticatorSelection : {
authenticatorAttachment : "cross-platform",
// requireResidentKey : true, /* If this is set an PIN or password is required */
userVerification : "preferred"
},
/* We do not need attestation */
attestation : "none"
}
};
And now is capable of creating a registration signature:
navigator.credentials.create(reqparam)
.then(function(newCredentials) {
/*
Now we got our new credentials. We'll have to pass them
to the server side again - either with AJAX or HTTP roundtrip
*/
}).catch(function(err)) {
/* Error handling for timeout or aborted request */
});
The result of the creation is a Credential
object - in case of
public key authentication itās a PublicKeyCredential
instance. The following
properties are defined:
Credential.id
is a read only String that contains an (opaque) identifier
for the credential (UUID, usernames, etc.)
Cretential.type
specifies the type, public-key
for the PublicKeyCredential
PublicKeyCredential.rawId
is an ArrayBuffer
that holds an globally unique
identifier for this credential. It might be used during the get
operation later on.
PublicKeyCredential.response
is the main response object. For create it is
an instance of AuthenticatorAttestationResponse
:
PublicKeyCredential.response.clientDataJSON
contains the client data that was
passed to the create function in form of an ArrayBuffer
. The signature has been
calculated over that Array.
PublicKeyCredential.response.attestationObject
is an ArrayBuffer
instance
containing the attestation object.
PublicKeyCredential.response.getTransports()
returns an array of the transport
methods supported by the authenticator (for example usb
, nfc
, etc.). This
might also be empty.
Note that the AuthenticatorAttestationResponse
returns the input data as well as the attestation
Object as ArrayBuffer despite them being mainly serialized JSON. This is done so the exact
binary layout is preserved to prevent errors during hash and therefore signature calculation.
Authentication
First a compact summary of the authentication process:
- The server sends the
PublicKeyCredentialRequestOptions
including a
challenge.
- The browser issues a get request using the webauthn API to the
HSM, the HSM creates the user keypair and attestation if the user indicates
oneās presence or enters the a password or pin.
- The JSON encoded client data is returned to the server. This might be done
using AJAX and performing some client side actions or during a reload of the
webpage (note these are both ArrayBuffer and not Base64 encoded objects in JS!)
The server then validates the signature against the known public keys.
Now the detailed process:
Authentication is done in a similar way. It uses a challenge response mechanism.
The argument to navigator.credentials.get
works similar as the above one
with fewer arguments:
args.publicKey.challenge
contains the cryptographic challenge that will
be signed by the HSM. This is the only required argument.
args.publicKey.timeout
is again a timeout in milliseconds
args.publicKey.rpId
is a string that contains the ID of the relying party
that has to match the registration. If not specified this is the domain of
the current page. It has to be a subdomain of the current page.
args.publicKey.allowCredentials
might optionally limit the allowed credentials.
Note that one shouldnāt use this to limit the request only to registered credentials
i.e. there should be no way to determine which HSMs have been registered by
the request. If one wants to do this each of the credentials has a transport
like internal
or usb
, an type of public-key
and an Uint8Array thats
specifying the id
of the credential.
args.publicKey.userVerification
qualifies how the user authentication should
be part of the process.
args.mediation
specifies if the log on at every website visit should be silent
, optional
or required
.
args.unmediated
might be specified as true
if there should be no user interaction
which might be interesting for TPMs or similar devices.
Before being capable of performing the authentication procedure the client script has to
request a random challenge for a given user ID. This might be done using Ajax or
an HTTP webpage reloading roundtrip. One might imagine that being a simple function call
like
requestAuthChallengeFromServer(username)
.then(function(challenge) {
/*
One might also pass more than the challenge like key types and authenticator settings.
The website may also pass IDs of authenticators that should be used.
*/
}).catch(function(errorinfo) {
/*
In case of an error ...
*/
});
Then one builds the request parameter object:
var req = {
publicKey : {
challenge : Uint8Array(),
rpId : "testhost.example.com",
userVerification: "preferred",
timeout : 300000
}
}
and now request signature of the challenge:
navigator.credentials.get(req)
.then(function(responseCredentials) {
/*
Now we got our new credentials. We'll have to pass them
to the server side again - either with AJAX or HTTP roundtrip
*/
}).catch(function(err)) {
/* Error handling for timeout or aborted request */
});
Operations on the server side
This part will be added in near future.
This article is tagged: