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: