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.
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:
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.
First a compact summary of the registration process:
PublicKeyCredentialCreationOptions
including a
challenge, relying party info and user info.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 partyargs.publickey.user
contains user informationargs.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 fallbacksargs.publickey.timeout
specifies the maximum number of milliseconds the application
is willing to wait for the user or the HSM to respondargs.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.
First a compact summary of the authentication process:
PublicKeyCredentialRequestOptions
including a
challenge.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 millisecondsargs.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 */
});
This part will be added in near future.
This article is tagged:
Dipl.-Ing. Thomas Spielauer, Wien (webcomplains389t48957@tspi.at)
This webpage is also available via TOR at http://rh6v563nt2dnxd5h2vhhqkudmyvjaevgiv77c62xflas52d5omtkxuid.onion/