HTTP et l'authentification par certificat

Pour protéger un site, il est facile de mettre en place un formulaire d'authentification basique, qui ressemble à ça (sans contenu en fond, en théorie) :

C'est pas beau, et sur le long terme c'est assez énervant : le navigateur ne supporte pas toujours l'autocomplétion. Cela dit, l'apport en sécurité est assez bon: un bruteforce se détecte (et se bloque) facilement, par exemple avec Fail2ban.

Mais ne peut-on pas faire mieux ? Ne pas avoir à taper un nom d'utilisateur/mot de passe à chaque fois, voire même pourquoi ne pas utiliser des clés privées comme avec SSH ? (C'est nul comme introduction, mais ça me permet de faire une analogie avec les certificats SSH, donc chut !)

Par chance, HTTPS propose déjà des certificats SSL qui identifient le serveur, et il est possible d'avoir un certificat côté client aussi ! Et oui, un fichier sur votre ordinateur qui vous identifie automatiquement au site. Trop fort !

Remarque importante : Il est nécessaire d'avoir un certificat SSL (ce qui explique sans doute que ce soit relativement peu répandu, mais espérons que ça change !). LetsEncrypt <3

Remarque moins importante : Avec clients Windows, il est possible que ce soit plus difficile à configurer. Mais bon, si vous utilisez OSX et/ou Linux (c'est super simple avec curl ;D), tout va bien se passer, promis.

Dans une architecture basée sur des certificats, il faut une autorité et plusieurs certificats. Dans la plupart des tutoriels, on vous apprend à générer un certificat autorité et un client. Pour innover, nous allons générer non pas un mais DEUX certificats clients. Et tout ça pour le même prix.

Génération du CA (Certificat Autorité)

C'est l'étape la plus importante. Si vous perdez ce fichier (ce qui n'arrivera pas), vous êtes bons pour tout recommencer.


$ openssl req -out ca.pem -new -x509
Generating a 2048 bit RSA private key
..........+++
.......................+++
writing new private key to 'privkey.pem'
Enter PEM pass phrase: PunkyPa$$
Verifying - Enter PEM pass phrase: PunkyPa$$
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]: FR
State or Province Name (full name) [Some-State]: Paris
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]: UnGeek
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []: *.ungeek.fr
Email Address []:

Cette opération va vous donner deux fichiers : ca.pem, qui contient la clé publique et privkey.pem la clé ... Bravo ! Privée.
On autorise le certificat sur *.ungeek.fr. C'est surtout utile au client, afin qu'il sache à qui envoyer le certificat.

C'est tout :D


Génération du certificat pour Bob


$ openssl genrsa -out bob_client.key 1024
Generating RSA private key, 1024 bit long modulus
............++++++
..............++++++
e is 65537 (0x10001)

bob_client.key contient la clé privée de Bob.


$ openssl req -key bob_client.key -new -out bob_client.crt
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]: FR
State or Province Name (full name) [Some-State]: Paris
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]: UnGeek
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []: Bob Le Ponge
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

On génère une demande de certificat au nom de Bob Le Ponge, qui est dans bob_client.csr (csr comme Certificate Signing Request ;D).


$ openssl x509 -req -days 365 -in bob_client.csr -CA ca.pem -CAkey privkey.pem -set_serial 01 -out bob_client.csr
Signature ok
subject=/C=FR/ST=Paris/O=UnGeek/CN=Bob Le Ponge
Getting CA Private Key
Enter pass phrase for privkey.pem: PunkyPa$$

Cool, maintenant on a une clé privée et une preuve que Bob est bien autorisé par notre CA. Vu qu'on est gentil, on donne à Bob son certificat au format .p12 qui est reconnu par tous les OS (sauf les mauvais :D).


$ openssl pkcs12 -export -clcerts -inkey bob_client.key -in bob_client.csr -out Bob.p12 -name "Bob Le Ponge"
Enter Export Password: LeMotDePasseDeBob
Verifying - Enter Export Password: LeMotDePasseDeBob

Hop, vous transmettez le fichier Bob.p12 à Bob et il peut l'importer.

Importation du certificat de Bob

Avec Windows

Sur Windows, ça ressemble à ça :
(oui le nom est différent, désolé Bob). C'est ici que Bob rentre son mot de passe LeMotDePasseDeBob.

(Screen inutile. M'enfin). Et c'est tout ! :D

Avec Linux

Aucune idée. Pas testé, et je n'ai pas l'environnement de test nécessaire. :-(

Avec OS X

Pas de temps à perdre, à l'ouverture du fichier .p12, on tombe directement sur la demande de mot de passe (LeMotDePasseDeBob)
Et ... c'est tout.
Vous pouvez vérifier qu'il est bien importé dans le Trousseau de Clé, login (en haut), Certificats (en bas).

Avec Firefox

WTF ? Pourquoi une section juste pour Firefox ?!
Tout simple: Firefox ne semble pas utiliser les certificats du système (c'est pas plus mal, dans un sens ... #Lenovo). Il faut aller dans Préférences, Avancé, Certificats puis dans Afficher les Certificats et enfin aller dans l'onglet Vos Certificats. La route est longue, mais ça en vaut la peine !


Génération du certificat pour Alice

Je suis sûr que la génération du certificat de Bob s'est mal passée. C'est obligatoire. Réussir du premier coup, ça n'arrive que dans un monde imaginaire, ou au pays des merveilles. Du coup, un peu moins de commentaires pour Alice. Tout va bien se passer.


$ openssl genrsa -out alice_client.key 1024
Generating RSA private key, 1024 bit long modulus
............++++++
..............++++++
e is 65537 (0x10001)
$ openssl req -key alice_client.key -new -out alice_client.crt
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]: FR
State or Province Name (full name) [Some-State]: Paris
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]: UnGeek
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []: Alice Wonderworld
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
$ openssl x509 -req -days 365 -in alice_client.csr -CA ca.pem -CAkey privkey.pem -set_serial 01 -out alice_client.csr
Signature ok
subject=/C=FR/ST=Paris/O=UnGeek/CN=Alice Wonderland
Getting CA Private Key
Enter pass phrase for privkey.pem: PunkyPa$$
$ openssl pkcs12 -export -clcerts -inkey alice_client.key -in alice_client.csr -out Alice.p12 -name "Alice Wonderland"
Enter Export Password: LeMotDePasseD'Alice
Verifying - Enter Export Password: LeMotDePasseD'Alice

Et l'importation, vous savez faire. Et voilà. (Pour tout vous dire, je devais réaliser cette partie de manière non interactive, mais ça ne fonctionnait pas. J'éditerais quand j'aurais réussi)


Mise en place du certificat côté serveur.

J'utilise nginx et Debian8. Ne vous amusez pas à copier la configuration dans Apache, ;D
On commence par copier ca.pem dans un dossier accessible par nginx (par exemple, /etc/nginx/certs/) et on adapte notre hôte virtuel.

server {  
    listen        4430;
    ssl on;
    server_name ca.ungeek.fr;

    error_log /var/log/nginx/testconfg.log debug;
    ssl_certificate      /etc/mon_certificat_ssl.crt;
    ssl_certificate_key  /etc/mon_certificat_ssl.key;
    ssl_client_certificate /etc/nginx/certs/ca.pem;
    ssl_verify_client on;

    location / {
        root           /var/www/;
        return 200 $ssl_client_s_dn;
    }
}

Dans mon cas, je listen sur ca.ungeek.fr:4430 (pour le plaisir), j'active le log d'erreur au niveau debug et je fausse l'index.
ssl_verify_client : on précise que le certificat est requis, optional le rend optionnel et optional_no_ca accepte tout certificat, même s'il ne provient pas de vous.

Si vous utilisez Apache2, tentez ceci (non, ce n'est pas plus court, c'est juste que je ne donne pas le fichier entier)

SSLVerifyClient require  
SSLCACertificateFile /etc/nginx/certs/ca.pem  

(Et oui, je troll en mettant le CA dans le dossier nginx. C'est gratuit.)

Et voilà ! Désormais, lorsque vous naviguerez sur une page protégée par cette méthode pour la première fois, une fenêtre vous demandera de confirmer. Et après, vous serez automatiquement identifié.

Have fun ! :-)

Bonus Avec curl, c'est tout simple : curl --key bob_client.key --cert bob_client.csr "https://ca.ungeek.fr/page/secrete/" Et étant donné que ça fonctionne assurément, vous pouvez l'utiliser pour tester et produire des logs utiles (pour savoir si un problème vient du client ou de votre serveur)