Go straight to the code samples/instructions

Let's face it, if you are using passwords on your web site or application, you are part of the problem. It doesn't matter if you're using bcrypt or scrypt, or all the salt in the world, you're still perpetuating the password problems and pain.

A previous post talked about the many authentication problems with enterprise Windows networks, but the world of web applications is at least as big of an authentication hairball, and many of the same password problems apply:

They can be guessed – Regardless of complexity requirements, users will find and use the simplest pattern allowed, and attackers routinely take advantage. Everybody reuses them – Even if you tell your users not to, it will still happen. Once your users have had to memorize a complicated password, they’ll use it when they need to sign up for RockYou. Then when RockYou’s passwords get hacked, the bad guys will have the passwords to your site. They are hard to remember – which annoys your users, drains their energy, and causes a lot of the above problems as well as writing passwords down on sticky notes for random visitors to see or to end up on office photographs on instagram. Social engineering – Your users will type their passwords into a webpage that looks like your site, but was really just from a spoofed email. Your users will give them up to somebody who sounds like tech support on the phone. This happens all the time. Unfortunately, users will frequently fail the security tasks given to them. They are easier to steal with MITM attacks – If you use SSL for all your traffic, but do not use client certificates, an attacker who can intercept your traffic just needs to get, steal, or crack one certificate from any of over 150 certificate authorities to intercept all of your users' passwords. This has happened to many people before. If you don't use SSL at all times, it is easy for an attacker who can observe or MITM your traffic to steal passwords. Hash dumps and offline cracking – The hashes of all passwords are stored forever in your database. An attacker who finds a SQL injection or database backup or otherwise compromises your site at one point in time can brute-force the creds offline at high speed (millions or billions of tries per second) without triggering lockouts or getting logged and crack many passwords. Even if you use bcrypt or scrypt or salt and make it slow, many users will still get their passwords stolen. Easy lockouts – With no credentials but a list of usernames, an attacker can cause great pain and anguish by locking out everyone’s account. Online brute force – Especially if lockouts are disabled, but even if not, attackers can brute force network logins “online” against all your users and probably find plenty of good passwords. People save their passwords, email them to themselves, etc. – even if you tell them not to. Compromised server password sniffing – An attacker who compromises the server at one point in time can obtain all passwords of all users who log in by saving those passwords as they are entered. The attacker can then use those later or on other sites without leaving any malicious code or signs of compromise on the server. This is related to the next point... Painful post-attack cleanup – If an intruder got access to the web server, when cleaning up after the intrusion, you must reset every password in addition to all your other tasks. This usually causes great anguish and lost work time, and even can hit other websites. Security does not just consist of how to prevent an attack, but also how to limit the damage of one and pain of recovering from one.

The strongest way to handle client authentication would be with hardware like smart cards that will stop attackers from stealing users' credentials even if their home computers are compromised, but unlike enterprise networks, this is way too expensive for most public-facing web applications.

But thankfully, using "soft" client certificates is almost as good, and it is surprisingly easy:

Private keys cannot be guessed – Because users didn't choose them! It does not matter if anybody reuses them – Because websites should never have the private key, only the public key. You do not have to remember them – Since they are stored on your devices. Social engineering is a lot harder – Since a private key is never sent to servers, if a website asked your users to export their private key and send it to them, it would be very complicated, suspicious, and not look like your website at all. Your users won't be able to give them up to somebody who sounds like tech support on the phone. They cannot be stolen with MITM attacks – If you use SSL for all your traffic, and use client certificates, an attacker who can intercept your traffic, even if the attacker has received, stolen, or cracked a server certificate from any CA, cannot obtain any client private keys or intercept any users' traffic unless the attacker also has a certificate with a private key of each user the attacker wants to monitor. (and not from any CA, but your CA specifically) Obtaining every user's certificates from one application CA is a vastly harder problem than obtaining a valid certificate for only a single server from any CA. Two-way authentication is inherently more secure than one-way authentication. Hash dumps and offline cracking – Will not happen since stored 2048-bit public keys are probably not going to be cracked in any of our lifetimes. Client lockouts – Can be disabled for certificate logins since... Online brute force – will never work. The private keys are cryptographically securely randomly generated, not chosen by users. It is harder to leave keys lying around or in email – Since they are generated and stored automatically within your system. Compromised server password sniffing – Will not happen since the client's private key is never sent to the server or in server memory. Of course any data on the server would be lost if the server was compromised. Easier post-attack cleanup – if an intruder got access to the web server, when cleaning up after the intrusion, you don't have to reset anyone's key, since you only held the public key, not the private key.

Sadly, client certificate authentication is not frequently taught, and there are very few code samples or instructions. Curiously, even the security community rarely suggests it, and even after countless failures of password authentication, we often debate the merits of one hash or salting algorithm vs. another that do not solve most of the problems with password authentication. As a result, enabling client certificate authentication may seem to be very difficult or require complicated client-side configuration and installation, especially since certificate authentication is often wrapped into a giant all-consuming PKI deployment.

But certificate authentication and even issuance is actually easy with modern browsers. Want to see how easy it can be? Go to https://www.scriptjunkie.us/getacert and get yourself a cert, then go to https://www.scriptjunkie.us/auth/verifycert to test your new cert.

But what about account recovery?

You might be wondering about what to do if a user loses their certificate, gets a new device, or needs to log in from somewhere else. Account recovery is often the weakest link of any authentication scheme, and it is not the purpose of this post. But I highly recommend ensuring your account recovery process is strong and not easy to fake with publicly available information, like many account recovery questions are. Examples include family names, phone numbers, addresses, SSN's. Don't just rely on email either, although it can play a part. Text-message based authentication with your cell phone and/or a friend's cell phone as a second factor is a good idea. One idea would be to require some number of your Facebook friends or people you follow on Twitter to vouch for your account recovery. You could require a small charge from a credit card coming from an account with the user's name on it. All of these would be difficult for an attacker to do without attracting attention. If you are performing an account recovery, disable the old certificates! Also, remember to have an appropriate level of security; if you lose a forum key it shouldn't be as hard to reset as a bank key.

Instructions

This uses the HTML5 <keygen> element which has support from all modern browsers. This does not include IE unfortunately, but IE can also be supported with a server-generated certificate Edit: or using javascript without much difficulty. (see instructions at bottom) You can replicate this on your own server in five easy steps using just the below instructions. You can also download some of the code and files here.

If you are using another web platform look into installing plugins to enable client certificate authentication, like this WordPress plugin, or this phpBB modification, or this MediaWiki extension.

Here are the steps:

0. I started by installing Ubuntu Server, selecting the LAMP option, and otherwise using defaults. Your server may have slightly different configuration file paths. Switch to root since apache configuration requires root privileges.

sudo bash

1. Create a root CA for your application

mkdir /etc/apache2/ssl.crt/ cd /etc/apache2/ssl.crt/ openssl genrsa -out rootCA.key 2048 openssl req -x509 -new -nodes -key rootCA.key -days 7300 -out rootCA.pem

Fill in appropriate values as prompted.

cp rootCA.pem ca-bundle.crt

2. Enable SSL on Apache

cd /etc/apache2/sites-enabled/ ln -s ../sites-available/default-ssl.conf a2enmod ssl

If you purchased a cert, you could install that now.

Then edit /etc/apache2/sites-available/default-ssl.conf with your favorite editor and uncomment the line "SSLCACertificateFile /etc/apache2/ssl.crt/ca-bundle.crt" which tells the web server to respect your CA.

sed -i.bak 's/#SSLCACertificateFile/SSLCACertificateFile/' /etc/apache2/sites-available/default-ssl.conf

Make sure there is a line "SSLOptions +StdEnvVars" (should be there by default, add if necessary)

And since we also want to allow use of .htaccess files, (although you could put all the directives in the apache conf files instead of .htaccess)

sed -i.bak 's/AllowOverride None/AllowOverride All/g' /etc/apache2/apache2.conf service apache2 restart

Now you can go visit https://1.2.3.4/ or whatever your server's IP is to verify it works

3. Set up client auth on a directory

mkdir /var/www/auth cd /var/www/auth echo '<?php phpinfo();' > index.php echo SSLVerifyClient optional > .htaccess echo SSLVerifyDepth 1 >> .htaccess

Now go visit https://1.2.3.4/auth or whatever your server's IP is, and in the

"Apache Environment" section you should see SSL_CLIENT_VERIFY None

4. Create an openssl CA configuration file and CA directory. To keep our web app more

self-contained, we'll create this as an inaccessible subdirectory of it.

4.1 Create the directory

mkdir /var/www/auth/ca/ cd /var/www/auth/ca/ touch index.txt mkdir newcerts echo 1000 > serial echo Deny from all > .htaccess chown -R www-data .

4.2 Save this file as /var/www/auth/ca/ca.conf

[ ca ] default_ca = CA_default [ CA_default ] dir = /var/www/auth/ca/ database = $dir/index.txt new_certs_dir = $dir/newcerts certificate = /etc/apache2/ssl.crt/rootCA.pem serial = $dir/serial private_key = /etc/apache2/ssl.crt/rootCA.key RANDFILE = $dir/private/.rand default_days = 3650 default_crl_days= 60 default_md = sha1 policy = policy_any email_in_dn = yes name_opt = ca_default cert_opt = ca_default copy_extensions = none [ policy_any ] countryName = supplied stateOrProvinceName = optional organizationName = optional organizationalUnitName = optional commonName = supplied emailAddress = optional

5. Create a certificate generation page. It must display a keygen form, receive submitted certificate requests, then generate and send the client certificate back. Save this example page as /var/www/getacert.php:

<?php //Should not happen since this should be in a directory that does not ask for client certificates if($_SERVER['SSL_CLIENT_S_DN_CN']) die("You are already authenticated as ".$_SERVER["SSL_CLIENT_S_DN_CN"]); date_default_timezone_set('UTC'); $CAorg = 'MyApp'; $CAcountry = 'US'; $CAstate = 'CA'; $CAcity = 'Sacramento'; $confpath = '/var/www/auth/ca/ca.conf'; $cadb = '/var/www/auth/ca/index.txt'; //will need to be reset $days = 3650; if($_SERVER['REQUEST_METHOD'] == 'POST'){ $f = fopen($cadb, 'w'); //reset CA DB fclose($f); $uniqpath = tempnam('/tmp/','certreq'); $username = $_POST['username']; //Validate this first! $CAmail = "test@example.com"; //This too! Make sure that's their email. //If they're submitting a key, first save it to an spkac file $key = $_POST['pubkey']; if (preg_match('/\s/',$username) || preg_match('/\s/',$CAmail)) die("Must not have whitespace in username or email!"); $keyreq = "SPKAC=".str_replace(str_split(" \t

\r\0\x0B"), '', $key); $keyreq .= "

CN=".$username; $keyreq .= "

emailAddress=".$CAmail; $keyreq .= "

0.OU=".$CAorg." client certificate"; $keyreq .= "

organizationName=".$CAorg; $keyreq .= "

countryName=".$CAcountry; $keyreq .= "

stateOrProvinceName=".$CAstate; $keyreq .= "

localityName=".$CAcity; file_put_contents($uniqpath.".spkac",$keyreq); //Now sign the file $command = "openssl ca -config ".$confpath." -days ".$days." -notext -batch -spkac ".$uniqpath.".spkac -out ".$uniqpath.".out 2>&1"; $output = shell_exec($command); //And send it back to the user $length = filesize($uniqpath); header('Last-Modified: '.date('r+b')); header('Accept-Ranges: bytes'); header('Content-Length: '.$length); header('Content-Type: application/x-x509-user-cert'); readfile($uniqpath.".out"); unlink($uniqpath.".out"); unlink($uniqpath.".spkac"); unlink($uniqpath); exit; } ?> <!DOCTYPE html> <html> <h1>Let's generate you a cert so you don't have to use a password!</h1> Hit the Generate button and then install the certificate it gives you in your browser. All modern browsers (except for Internet Explorer) should be compatible. <form method="post"> <keygen name="pubkey" challenge="randomchars"> The username I want: <input type="text" name="username" value="Alice"> <input type="submit" name="createcert" value="Generate"> </form> <strong>Wait a minute, then refresh this page over HTTPS to see your new cert in action!</strong> </html>

Credit for some of this code and general configuration go to http://lists.whatwg.org/pipermail/whatwg-whatwg.org/attachments/20080714/07ea5534/attachment.txt

PS. IE support

IE does not provide client-side certificate generation, so your server script will need to generate the private key and then send it in an IE-compatible form to your clients.

Edit: IE can generate certificates in Javascript

With some ActiveX magic, IE can in fact generate client certificates. I found this open-source code (IEKeygen.js) from the clerezza project which uses the ActiveX X509Enrollment class to generate certificates: http://svn.apache.org/viewvc/incubator/clerezza/issues/CLEREZZA-243/org.apache.clerezza.platform.accountcontrolpanel/org.apache.clerezza.platform.accountcontrolpanel.core/src/main/resources/org/apache/clerezza/platform/accountcontrolpanel/profile-staticweb/scripts/IEKeygen.js?view=markup&pathrev=1029869

Update: broken link. See explorer-keygen.js reproduced from https://raw.githubusercontent.com/bennomadic/django-webid-auth/1f0ca4ab3f019c3ffc273ea9427d53bf9e2f58fd/examples/example_webid_auth/media/js/explorer-keygen.js

If you want to use server-side generated certificates; quoting my source at http://www.garex.net/apache/#CCuconv you need to:

a) Generate user key

openssl genrsa -des3 -out garex.key 1024

b) Create user certificate request

openssl req -new -key garex.KEY -out garex.CSR

Then sign it (already shown above)

d) Convert user certificate and import it in your browser

Once again Microsoft's Internet Explorer has its own standards: it only accepts certificates of the type DER. Therefore we have to convert our user certificate and the root CA certificate:

openssl x509 -inform PEM -in garex.CRT -outform DER -out garex.CRT.der openssl x509 -inform PEM -in garexCA.CRT -outform DER -out garexCA.CRT.der

Import these two certificates via IE and you are finished.