Adding Easy SSL Client Authentication To Any Webapp


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:

  1. They can be guessed – Regardless of complexity requirements, users will find and use the simplest pattern allowed, and attackers routinely take advantage.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. Easy lockouts – With no credentials but a list of usernames, an attacker can cause great pain and anguish by locking out everyone’s account.
  8. 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.
  9. People save their passwords, email them to themselves, etc. – even if you tell them not to.
  10. 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...
  11. 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:

  1. Private keys cannot be guessed – Because users didn't choose them!
  2. It does not matter if anybody reuses them – Because websites should never have the private key, only the public key.
  3. You do not have to remember them – Since they are stored on your devices.
  4. 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.
  5. 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.
  6. 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.
  7. Client lockouts – Can be disabled for certificate logins since...
  8. Online brute force – will never work. The private keys are cryptographically securely randomly generated, not chosen by users.
  9. It is harder to leave keys lying around or in email – Since they are generated and stored automatically within your system.
  10. 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.
  11. 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\n\r\0\x0B"), '', $key);
  $keyreq .= "\nCN=".$username;
  $keyreq .= "\nemailAddress=".$CAmail;
  $keyreq .= "\n0.OU=".$CAorg." client certificate";
  $keyreq .= "\norganizationName=".$CAorg;
  $keyreq .= "\ncountryName=".$CAcountry;
  $keyreq .= "\nstateOrProvinceName=".$CAstate;
  $keyreq .= "\nlocalityName=".$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.

, , , , , , , , , , ,

  1. #1 by Dror Harari on December 1, 2013 - 12:35 am

    Nice writeup Matt. Just wanted to mention a site called startssl.com which can produce personal certs for free based on your email that are signed by a CA that is accepted by most clients (browses)…

  2. #2 by Pete on December 1, 2013 - 9:55 am

    I’d be interested in hearing strategies for handling access from more than 1 device.

    • #3 by scriptjunkie on December 2, 2013 - 4:00 pm

      Issue multiple certs; don’t make your users fumble around with certificate export/import. I would treat multiple devices kind of like adding an application-specific gmail password. You would log in from your desktop with first certificate, then request a new device registration. It would give you a one-use code you enter from your other device and it issues that device a certificate. And you’re back to no passwords on all your devices. There’s nothing saying a user can’t be issued multiple certificates.

  3. #4 by Chris on December 1, 2013 - 8:18 pm

    Was talking to a friend who does Web App virtualization for Citrix, he brought up a good point about how many devices people use to access the same web app (ie. your home and work Desktops, Laptop, iPad, iPhone, etc.).. Is there an an easy way to propagate the same private key to multiple devices?

    • #5 by scriptjunkie on December 2, 2013 - 4:33 pm

      See reply to Pete. Allow cert-authenticated users to add device, and issue cert to that device given one-time-use-code. Of course, you could add in all kinds of other features; allow a user to see all device certificates which have been issued, revoke those you don’t need anymore. Just like account recovery, you could add additional security verification steps before issuing a new certificate; requiring confirmation via text or email.

  4. #6 by Certificate Bob on December 2, 2013 - 8:28 am

    Nice one! I wish that the world would use Client Authenticated TLS more. You have written a nice tutorial!

    It is actually trivial for a passive observer to obtain both ‘certificates’; Client Authenticated TLS passes the certificates (which are the “public key” component of the keypair) in the clear.
    Authentication of both sides is occuring based on the possession of the private key; verified by the other party with a signature from each other on a value supplied by each other.

    What is non-trivial is obtaining BOTH the client AND the server private keys. It is not possible to MiTM client authenticated TLS (without both pkeys) because each side asserts a signed message which cannot be spoofed.

    Active MiTM requires the substitution of a server certificate (and private key) and thus ability to spoof a signature from the value supplied by the client.

    Windows DOES support PEM or DER; a PEM file can be renamed .crt or .cer and the Crypto Shell extensions will correctly parse and import it.
    I strongly suggest people dont use DER format at all — a vast majority of crytography engines (CAPI, CNG, NSS, jce, apache/nginx, etc) support the PEM human readable format and it easily transfers over the clipboard to a remote shell. Cant think of a non-PEM supporting client i have seen in years. Some online references say keytool (java) has a problem but i have never found this to be the case.

    There is a graphical CSR generator built into windows — One just has to generate a Custom Certificiate request from the Certificates MMC SnapIn. Generating a CSR will leave the pending request in a pending container, and this will automatically “upgrade” to a certificate+key when the users imports the signed certificate.

    • #7 by scriptjunkie on December 2, 2013 - 3:50 pm

      Bob, what you are saying about certs is correct. Sometimes I cheat a little on precise language to try not to confuse anybody.

      Having been told you can do client SSL certificate generation in IE javascript with some activeX controls, I found some open-source code to do that and now added it to the bottom of the article. I have not used it yet, but that’s where I would start if I needed to support IE users.

  5. #8 by Blaise Pabon on December 2, 2013 - 11:12 pm

    Bravo!
    Thank you, this article fills a great need *detailed* instructions on how to make web apps fundamentally more secure.
    Nice touch to include the part about IE and DER format.
    Have you considered doing a similar article on implementing TLS forward secrecy? Say, using ephemeral Diffie-Hellman (TLS-ECDHE)?
    (Disclosure: I work for a company that uses a more elaborate version of this approach)

    • #9 by scriptjunkie on December 4, 2013 - 4:57 pm

      Thank you! I was not planning on a TLS forward secrecy one since it just takes modifying one conf file and there are example configurations already out there. What I do is simply add the following lines to your apache SSL configuration right after where you list your SSLCACertificateFile, etc.:
      SSLProtocol all -SSLv2
      SSLHonorCipherOrder On
      SSLCipherSuite EECDH+AES128:EDH+AES128:-SHA1:EECDH+AES256:EDH+AES256:EECDH+3DES:EDH+3DES:AES256:3DES:!aNULL:!eNULL:!EXP:!LOW:!MD5

  6. #10 by James on April 1, 2014 - 7:39 pm

    Great article! I have one question. How did you do to the make your site request a particular certificate? E.g. the popup in Firefox says: This has requested that you identify yourself with a certificate: http://www.scriptjunkie.us:443…, and the generated cert was picked up by Firefox? (I have more than 1 client certs stored in the browser).

    Thanks,

    • #11 by scriptjunkie on April 2, 2014 - 2:11 am

      Honestly, I didn’t do anything to request my cert specifically. I think Firefox must just be smart enough to remember this site is where it got it from, or last used it.

  7. #12 by ken on May 2, 2014 - 5:30 pm

    I tried the sample client cert generator. Shortly after that I visited the BBC website to view their Heartbleed story and it prompted me for the cert I got from the test page. What am I to make of that?

    Here’s the story I visited that prompted for the cert:

    http://www.bbc.com/news/technology-26954540

    • #13 by scriptjunkie on May 8, 2014 - 2:36 am

      Interesting! I would guess one of the resources in the BBC page was requesting a client certificate. That’s very uncommon; I rarely see it, but who knows, maybe someone is taking our advice.

  8. #14 by PAC on September 2, 2014 - 12:59 am

    Did not work with iPad.

    • #15 by scriptjunkie on September 2, 2014 - 1:22 am

      What browser? I don’t have an iPad, but I tested with Google Chrome and it worked on my android, but not with the stock browser.

  9. #16 by rajiv on December 6, 2014 - 8:19 pm

    I’m not so sure this is a great idea.

    I just got a cert in Chrome (on Windows) and successfully authenticated. Then I went to verifycert in Internet Explorer, and it also used the same certificate to authenticate.

    This seems to imply that _every process_ running in my user space will have unfettered access to every site that has issued me a client certificate.

    • #17 by scriptjunkie on December 7, 2014 - 1:29 am

      rajiv, every process already does. It’s trivial for any process running in your user space to read your saved passwords directly from the Internet Explorer or Google Chrome password managers, or keylog them as you type them. This is not a defect of certs.

  10. #18 by Problem on January 10, 2015 - 10:57 am

    I’ve tried to implement this on my side and i got problem. I succesefully issue a client certificate and i get the message that i’m already authenticated as blabla on getcert.php but when i go on /auth/ i get certificate-based authentication failed. However the certificates i issue i can use on your site to authenticate.

    • #19 by scriptjunkie on January 10, 2015 - 3:27 pm

      Just to be clear; you’re not getting a cert from my site and then trying it on yours, right? You’re getting it from yours and trying to re-use on yours. My guess is; I would check to make sure the CA that you stood up to issue certs is trusted by your apache for client cert auth. If it’s trusted in one URL, but not another on the same server, maybe one of them is set up with “SSLVerifyCLient optional” and the other as “SSLVerifyClient optional_no_ca”? (the later one will accept all certs)

  11. #20 by Larry Irwin on March 18, 2015 - 2:58 am

    Apache 2.4, when running getacert.php, it is running it as user www-data. When getacert.php executes the openssl shell command, it hasn’t got permission to read the CA private key…
    changing directory and file permissions for www-data to read the private key probably isn’t a good idea…
    I added +x to the folder and +r to the key for “other” for testing and the key is created in the newcerts folder, but I still don’t get it delivered to the browser… Tips appreciated on how to properly allow www-data to read the private key and how to debug why the cert isn’t being delivered…

  12. #21 by Larry Irwin on March 18, 2015 - 9:15 pm

    Got it working.
    Some debugging info I inserted was messing it up.
    I’m still concerned about what the ownership and permissions should be on /etc/apache2/ssl.crt and the keys held within.
    Could you post that here?
    Thanks!

  13. #23 by scriptjunkie on March 21, 2015 - 4:46 pm

    Larry,
    In this implementation, www-data needs to be able to read the private key of the CA to issue the certificates. One alternative would be to add a new user and create a setuid program to do that for you.

    Another would be to use a similar system that does not trust the CA, and verifies users’ public keys directly. This is what I implemented in EasyAuth. (https://www.scriptjunkie.us/2014/10/replacing-passwords-with-easyauth/) In EasyAuth, it’s kind of counter-intuitive, but we’re not actually trusting the CA. It’s just a fluke of the fact that an x509 certificate needs to be signed by something. In EasyAuth, we track users by their specific public key instead of trusting a certificate authority. This makes it more like SSH does public/private key verification without a PKI. So we could even post that private key publicly.

  14. #24 by Barry on March 27, 2015 - 10:03 am

    Your getacert page failed for me on Chrome (41.0.2272.89 m). I think it’s because Chrome no longer accepts PEM format certificates as application/x-x509-user-cert. In my own implementation, I had to convert them to DER to get it to accept them. Hopefully you can fix that because it’s a great demo page.

    • #25 by scriptjunkie on April 6, 2015 - 2:27 am

      I am unable to reproduce with the latest Chrome.
      https://imgur.com/rSIn0Yn
      Screenshot
      Also, according to the openssl CA documentation “-out filename” refers to “the output file to output certificates to. The default is standard output. The certificate details will also be printed out to this file in PEM format (except that -spkac outputs DER format)” since I’m using -spkac, it should be DER format.

Comments are closed.