Easy, Password-Free, Cryptographic Authentication for Web Applications

See also the cryptographic authentication page.

Update. The demo code mentioned below has been updated to fix bugs. If you find any additional bugs please report them through the contact form or by posting to the PJCL forum. (The PJCL user forum has been discontinued as of May 27, 2018.) The date of the latest update will be shown in the PJCL page. Please see also the blog post Cryptographic Authentication Is Not That Easy After All.

For years there has been consensus that passwords have to go. To the many reasons for not using password authentication, the European GDPR will add, when it goes into effect on May 25, stringent requirements to notify users and regulators when passwords are compromised, backed by substantial fines. And yet, passwords are still the dominant authentication technology for web applications. This is because the alternatives that have been proposed and tried so far are complicated and expensive to implement. But there is a simple alternative that you can implement yourself, if you are a web application developer: cryptographic authentication with a digital-signature key pair stored in the browser.

At last week’s Internet Identity Workshop (IIW) we showed how easy it is to implement this alternative. We gave a demo of a sample web application, exercising the user interface and looking at the code. The sample application was implemented in Node.js and used the Pomcor JavaScript cryptographic library (PJCL) on the client and server sides. The code of the sample application, which we will refer to as the demo code, can be found in the PJCL page of the Pomcor site (subsequently modified as explained below to accommodate Internet Explorer).

Node.js is convenient because it allows full stack developers to use the same programming language in the client and the server. PJCL is convenient because it can be used in Node.js exactly as in the browser. But these convenience benefits are not essential. Cryptographic authentication can replace passwords in web applications that use any programming language on the server side, using different cryptographic libraries in the client side and the server side. Here I will discuss cryptographic authentication in more general, language-independent terms.

A key pair as a plug-in replacement for a password

Authentication with a key pair can replace authentication with a password in a web application with little else changing.

The registration and login forms for two-party cryptographic authentication with a key pair as demonstrated at IIW are like those for password authentication, except that they do not ask for a password.

The registration form may ask for a username and user information such as first and last name and/or email address. When the user submits the registration form, the browser generates a random key pair, stores the key pair in the browser’s local storage (the localStorage of the Web Storage API), and sends the username, the user info and the public key to the server over a TLS connection, after the TLS handshake where the server authenticates with a TLS server certificate. For reasons explained below, the browser also proves possession (i.e. knowledge) of the private key by signing a challenge received from the server and sending the signature to the server along with the username, the user info and the public key. The server verifies the mathematical validity of the public key (in a way that depends on the digital signature scheme being used), uses the public key to verify the signature on the challenge, and creates a user record containing the username, the user info and the public key. It may also create a session record containing a random session ID and a reference to the user record, and set a cookie containing the session ID in the browser, so that the user is logged in after registering.

The login form only asks for the username. When the user submits the form, the browser retrieves the key pair from local storage, uses the private key to sign a fresh challenge received from the server, and sends the username and the signature to the server, again over TLS with server authentication. The server locates the user record referenced by the username, verifies the signature on the challenge found in the record, creates a session record with a random session ID, and sets a cookie containing the session ID in the browser. (In the demo code the browser also sends the public key and the server verifies that the received public key agrees with the one found in the record before making the computational effort of verifying the signature.)

Conveying the challenge from the server to the browser

One interesting issue that remains to be explained is how the server conveys the challenge to the browser during the registration and login protocols. In both protocols, since the browser initiates the exchange but needs a fresh challenge for the signature, two roundtrips between the browser and the server are required. In a single-page application, it is straightforward to implement those two roundtrips as two XMLHttpRequests. (They may also be implemented over a single WebSockets connection.) In a traditional multi-page application like the one shown at IIW, on the other hand, it is more straightforward to use an HTTP request and response for each roundtrip; this is what the demo code does. In each protocol, the actions taken by the browser and the server are then interleaved as follows.

In the registration protocol, the browser sends the username and user info in the first HTTP request, over TLS with server authentication. The server generates the random challenge and creates a user record containing the username, the user info and the challenge. (The user record is incomplete at this stage, and will be deleted by a garbage collection process, not implemented in the demo code, if it is not completed within a reasonable time.) The server then sends a JavaScript redirect response, by which I mean an HTTP 200 response with JavaScript code but no HTML content that sends the second HTTP request. The JavaScript code includes the username and the challenge as JavaScript literals assigned to JavaScript variables. When executed on the browser, the JavaScript code generates the key pair, stores it in local storage, and signs the challenge with the private key. Then, in the second HTTP request, the browser sends to the server the username, the public key and the signature, again over TLS with server authentication. When the server receives the second HTTP request, it uses the username to locate the user record, which contains the challenge, it verifies the mathematical validity of the public key, and it uses the public key to verify the signature on the challenge. Then it creates the session record, sets the session cookie, and sends an HTTP 303 response that redirects the browser to a welcome page.

In the login protocol, the browser sends the username in the first HTTP request (over TLS with server authentication). The server uses the username to locate the user record, generates the fresh random challenge, stores it in the user record, and sends a JavaScript redirection response, where the JavaScript code includes the username and the challenge as JavaScript literals assigned to JavaScript variables. When executed on the browser the JavaScript code retrieves the key pair from local storage, uses the private key to sign the challenge, and sends the username and the signature in the second HTTP request (again over TLS with server authentication). (In the demo code, the browser also sends the public key.) When the server receives the request, it uses the username to locate the user record, it uses the public key in the user record to verify the signature on the challenge also found in the record, it creates the session record, it sets the cookie, and it redirects the browser to the welcome page. (In the demo code the server checks that the public key received from the browser agrees with the one found in the record before verifying the signature, as in the registration protocol.)

Accommodating browser peculiarities

The demo code implements workarounds for an issue with Firefox and an issue with Internet Explorer.

The issue with Firefox is related to the behaviour of the browser's back button. The sample web app has a home page containing the registration and login forms, and a welcome page reached after registering or logging in. Going from the home page to the welcome page takes a JavaScript redirection followed by an HTTP redirection, as explained above. What happens if the user clicks the back button after reaching the welcome page? Chrome, Internet Explorer, Edge and Safari do they right thing: they go back to the home page without any interaction with the server. Firefox, unfortunately, replays the Javascript response of the first redirection, which should be viewed as a bug. As a workaround, the server looks for this buggy behavior when it receives the second HTTP request. If the behavior is detected, it refrains from creating a fresh login session and redirects to the home page rather than the welcome page. (At registration, the behavior causes the key pair to be replaced with a new one, but that is not a problem, as the public key is also replaced in the server.) This workaround was already implemented in the sample web app we demoed at IIW.

The issue with Internet Explorer, which we've discovered after the workshop, is that it still uses an “ms” prefix to “crypto”, and thus refers to the getRandomValues() method for obtaining browser entropy as

msCrypto.getRandomValues()

rather than

crypto.getRandomValues().

(The sample web app uses the deterministic random bit generator of PJCL, implemented as specified by NIST, to combine browser entropy with server entropy, and the browser entropy is obtained by means of getRandomValues(). I will explain this in more detail in a future blog post.) The traditional fix for this is to define

var cryptoObject = window.crypto || window.msCrypto;

In the ancillary file browserEntropy.js included along with PJCL in the zip archive downloadable from the PJCL page we define instead

var cryptoObject = self.crypto || self.msCrypto;

because the window global object is not available in web workers. (IE and Safari do not support getRandomValues() in web worker scope, but Chrome, Firefox and Edge do.)

Necessary and unnecessary precautions

You may be wondering why the registration protocol described above includes signing a challenge with the private key corresponding to the public key being registered. This is necessary for two reasons.

First, the signature shows to the server that the party submitting the second HTTP request of the registration process knows the challenge that was sent in response to the first request. Without the signature, an attacker who were able to suppress the victim’s second request could submit his/her own second request, thus registering his/her own public key and gaining access to the user info submitted by the victim in the first request.

Second, although there is no reason for the public key to be exposed to outside parties, and it should therefore be treated as a secret, implementors may not make much effort to protect a key that is called “public”. If an attacker captured the public key of a victim user, and no proof of possession of the private key were required to register a public key, the attacker could use the victim’s public key when creating his/her own account. (Both accounts would have the same public key.) Then if the victim visited a page of a web site controlled by the attacker (unrelated to to the web application), JavaScript code in the attacker's web page could submit a login form (invisibly to the victim, as in the JavaScript redirection of the above protocols) that logged the victim into the attacker’s account. This attack could cause the same severe damage as the closely related login CSRF attack.

On the other hand, it is unnecessary to protect the two-party cryptogaphic authentication method described above against a man-in-the-middle attack akin to the mafia fraud attack or the chess grandmaster problem (see Desmedt Y., Goutier C., Bengio S., Special Uses and Abuses of the Fiat-Shamir Passport Protocol, Crypto 1987, Section 3.2.2, and Bruce Schneier, Applied Cryptography, 1996, Section 5.2). Such man-in-the-middle attacks are often described in the context of zero-knowledge proofs of identity, but can also be mounted against authentication with a third-party cryptographic credential, such as an X.509 certificate and its associated private key. In the latter case, the attacker authenticates to a service provider by enticing the victim to authenticate to the attacker and relaying messages to the service provider: the victim sends its certificate to the attacker, who forwards it to the service provider; the service provider replies with a challenge, that the attacker forwards to the victim; the victim signs the challenge with the private key and sends the signature to the attacker, who forwards it to the service provider.

This attack can be thwarted by requiring the signature to be computed over both the challenge and the identity of the service provider. This is what TLS-with-mutual-authentication does, where the third-party cryptographic credential is a TLS client certificate sent during the TLS handshake. At the end of the handshake, the browser proves possession of the private key by signing a hash of the messages exchanged earlier in the handshake, which include a message where the server sends its identity to the browser, included in the TLS server certificate.

But this precaution is unnecessary in the above login protocol for two-party authentication to a web application (playing the role of the service provider) with a key pair, because the credential (the user name and key pair) is specific to the web application and is protected by the same origin policy of the web, enforced by the browser. An attacker could entice a user of the web application to log in to a web site controlled by the attacker that uses the same login protocol, but the user’s browser would have different key pairs for the attacker’s web site and the web application. Even though both key pairs would be stored in the browser’s local storage, JavaScript code from the attacker’s site could not retrieve the key pair pertaining to the web application, and could not use it to sign a challenge issued by the web application.

Leave a Reply

Your email address will not be published.