Web App Auth using Estonian ID (with Nginx and Flask)

I’m not from Estonia. I’ve never even been there, although I would like to go one day - it seems to be a very pretty place indeed. Despite all that, I have on my desk an Eesti Vabariik Digitaalne Isikutunnistus: a digital identity card issued by the Republic of Estonia.

Karula National Park

I got mine through the e-Residency programme, which is designed to let non-Estonians have a recognized digital identity. E-residents don’t have the rights and responsibilities real residents have, but they do get to establish companies, file taxes, open bank accounts, and digitally sign documents and contracts, among other things.

Becoming an e-Resident was pretty simple. The process goes something like this: Fill in an online form, pay the fee (about €50), and wait for the Politsei- ja Piirivalveamet to do a background check. When you get an email (in Estonian, Russian, and English) that your identity card is ready to be picked up, phone the embassy for an appointment to leave your fingerprints. And that’s it. You’ll get the ID card, a pair of PIN codes, a PUK code, and a tiny USB reader. I went for ice cream on the way back home, but that’s entirely optional.

From a developer’s point of view, the Estonian ID card is ‘just’ a PKCS#15 smartcard with a X.509 client certificate (there are some RSA keys on there as well, for signing). This means that anyone and everyone can use it to identify users. There’s no permission required, no red tape to cut through.

And that’s why I got one: to see how hard it would be to write an app that uses the eID for authentication. Not hard, as it turns out. As an example, let’s build an app that shows a friendly (personalized!) greeting to everyone who logs in.

Setting up nginx

There are instructions on the e-Residency website for using Apache/PHP, and the wiki also has instructions for IIS. I like nginx more than either, though, so here’s a (very) minimalistic nginx config:

events {}

http {
    server {
        listen 443 ssl;
        ssl_certificate /etc/nginx/ssl/server.crt;
        ssl_certificate_key /etc/nginx/ssl/server.key;
        ssl_client_certificate /etc/nginx/ssl/id.crt;
        ssl_verify_client on;
        ssl_verify_depth 3;

        location / {
            proxy_pass http://localhost:8080/;
            proxy_set_header X-Client $ssl_client_s_dn;
        }
    }
}

We tell nginx to listen for SSL (TLS) connections on the HTTPS port. The server.key and server.crt files are your usual SSL certificates: they serve to prove the server’s identity to the client. You would generate the first yourself, and get the second from a vendor. For our little example app, a self-signed certificate will do.

The ssl_client_certificate setting is more interesting. It points to a bundle of certificates; only if the client certificate nginx receives is signed by one of these certificates is the request allowed to go through. The certificates are published by SK, and to turn them into a bundle, you just have to concatenate them:

curl -Ls https://sk.ee/upload/files/JUUR-SK.PEM.cer \
         https://sk.ee/upload/files/EECCRCA.pem.cer \
         https://sk.ee/upload/files/ESTEID-SK%202007.PEM.cer \
         https://sk.ee/upload/files/ESTEID-SK%202011.pem.cer \
	 > id.crt

Next, we tell nginx that we want it to always require a client certificate by using the ssl_verify_client option, and that we want it to dig up to three levels deep to get the certificate validated.

Enabling SSL client certificate verification enables a handful of interesting nginx variables, including $ssl_client_cert, which contains the entire certificate, $ssl_client_verify, which should be SUCCESS, and $ssl_client_s_dn, which contains the subject’s distinguished name. For eID certificates, the distinguished name contains the person’s name. Which is exactly what our app needs!

Trying it out

The Flask app we’ve got waiting on port 8080 is quite simple. It reads the X-Client header we had nginx set, extracts the GN (given name) and SN (surname) from the DN (distinguished name), and shows a friendly greeting, just as we wanted.

import re
from flask import Flask, request

app = Flask(__name__)

@app.route("/")
def hello():
    dn = request.headers['X-Client']
    fields = dict(re.findall("/([^=]*)=([^/]*)", dn))
    doctype = "<!doctype html>"
    title = "<title>eID test</title>"
    style = "<style>body{text-align:center;font-family:sans-serif;}</style>"
    content = "<h2><br><br>HEY THERE, {GN} {SN}!</h2>"

    return doctype + title + style + content.format(**fields)

if __name__ == "__main__":
    app.run(port=8080)

And here (drumroll, please!) is the end result:

Screenshot

I’d say that’s pretty neat. Besides the first and last name, you get a serialNumber that’s guaranteed to be unique across all the cards - you could use that as a unique user identifier if you wanted.

(By the way, if you’re using Django instead of Flask, check out django-ssl-client-auth. It’s designed for, or at least tested with, Finnish government-issued ID cards, but from what I’ve seen eID cards should work as well.)

Conclusions

Our little example app isn’t really production-ready. For one thing, we don’t handle certificate revocation. When an ID card gets lost or stolen, the certificate that’s on the card gets revoked - but it will still be usable in our app.

Lists of certificates that have been revoked, called CRLs, are published by SK, and you could periodically fetch those. However, this is where things get a little more complicated. nginx has a setting for loading a CRL file, but not for multiple CRL files. Concatenating CRLs in the same way as CA certificates is something that should work, but in my case it didn’t. Your mileage may vary. An alternative to CRL files is OCSP, however, nginx does not support OCSP for client certificates, and there are no plans to add it.

 


Photo of Karula National Park by Amadvr, CC BY-SA 3.0 ee

Thanks for reading! If you have any questions, comments or corrections, feel free to shoot me an email.