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
Reading time 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:

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:

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:

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:

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:

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:

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:

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:


Data protection policy

Dipl.-Ing. Thomas Spielauer, Wien (webcomplains389t48957@tspi.at)

This webpage is also available via TOR at http://rh6v563nt2dnxd5h2vhhqkudmyvjaevgiv77c62xflas52d5omtkxuid.onion/

Valid HTML 4.01 Strict Powered by FreeBSD IPv6 support