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.
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.
Accommodating browser peculiarities
The demo code implements workarounds for an issue with Firefox and an issue with Internet Explorer.
The issue with Internet Explorer, which we've discovered after the
workshop, is that it still uses an “
crypto”, and thus refers to the
getRandomValues() method for obtaining browser entropy as
(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
var cryptoObject = self.crypto || self.msCrypto;
window global object is not available in web
workers. (IE and Safari do not support
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 a proof of possession of the private key by the browser as the public key is registered with the server. This is not necessary in theory, but is very much necessary as a best practice.
In two-party cryptographic authentication to a web application with a key pair, where the user authenticates merely as a repeat visitor, without relying on a third-party credential, there is no reason for the public key to be exposed to outside parties. Therefore the public key should be treated as a shared secret between the user's browser and the application. If it is indeed a shared secret, there is no need for the browser to prove possession of the pivate key when registering the public key.
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.