LetsEncrypt, .well-known et NGINX

Il y a quelques jours, j’ai eu la bonne surprise de voir ma demande a la beta de Let’s Encrypt être acceptée. A moi l’argent, la gloire et la crypto !

letsencrypt
Pour rappel, letsencrypt est une autorité de certification (CA) qui ne demande aucune validation humaine de leur part. Pas d’envoie de papiers d’identité ou autre preuves. Pour prouver que vous etes bien possesseur du domaine, il suffit que, lors de la génération du certificat, vous placiez un fichier json accessible a une url « mondomaine.com/.well-known/… ».

La procédure de génération est donnée (et risque d’évoluer d’ici la fin de la beta) mais en résumé:

Make sure your web server displays the following content at                                                                                           
http://mart-e.be/.well-known/acme-challenge/abcdef... before continuing:

{"header": ...}

Content-Type header MUST be set to application/jose+json.

If you don't have HTTP server configured, you can run the following
command on the target server (as root):

mkdir -p /tmp/letsencrypt/public_html/.well-known/acme-challenge
cd /tmp/letsencrypt/public_html
echo -n '{"header": ...}' > .well-known/acme-challenge/abcdef...
# run only once per server:
$(command -v python2 || command -v python2.7 || command -v python2.6) -c \
"import BaseHTTPServer, SimpleHTTPServer; \
SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map = {'': 'application/jose+json'}; \
s = BaseHTTPServer.HTTPServer(('', 80), SimpleHTTPServer.SimpleHTTPRequestHandler); \
s.serve_forever()"
Press ENTER to continue

Super, seulement wordpress ne me laisse pas créer des dossiers arbitraires dans mon dossier contenant le code (et c’est une bonne nouvelle en soit). Pour résoudre cela, un petit bricolage nginx s’imposait.

HTTPS sur un blog, c'est comme un éléphant-mitrailleuse. Ça ne sert pas a grand chose mais c'est cool!
HTTPS sur un blog, c’est comme un éléphant-mitrailleuse. Ça ne sert pas a grand chose mais c’est cool!

Dans la config nginx du blog, ajoutez un bloc location:

Mise à jour: Michel propose une solution plus simple sans s’ennuyer avec un serveur local en python!

server {
       listen 80;
       server_name mart-e.be;
       location ~ /.well-known {
            proxy_pass  http://127.0.0.1:7890;
       }
       
       # reste de la config pour wordpress
}

Où 7890 est un numero de port totalement arbitraire.
Ensuite, suivant +/- les conseils du wizard de letsencrypt

$ mkdir -p /tmp/letsencrypt/public_html/.well-known/acme-challenge
$ cd /tmp/letsencrypt/public_html
$ echo -n '{"header":...}  > .well-known/acme-challenge/abcdef...
$ python -c "import BaseHTTPServer, SimpleHTTPServer; SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map = {'': 'application/jose+json'}; s = BaseHTTPServer.HTTPServer(('', 7890), SimpleHTTPServer.SimpleHTTPRequestHandler); s.serve_forever()"

Notez que j’ai changé le numéro de port de 80 a 7890 dans les paramètres de HTTPServer.

On relance NGINX (nginx -s reload), continue l’exécution du wizard (press ENTER) et victoire !

Press ENTER to continue

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at
   /etc/letsencrypt/live/mart-e.be/fullchain.pem. Your cert will
   expire on 2016-01-26. To obtain a new version of the certificate in
   the future, simply run Let's Encrypt again.

Vous pouvez tuer la commande python une fois validé et retirer la config nginx (ou commentez, le certificat beta ne vaut que 90 jours).

En bonus, la config nginx qui va bien:

ssl_certificate /etc/letsencrypt/live/mart-e.be/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mart-e.be/privkey.pem;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK';
ssl_protocols TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/nginx/dhparams.pem;

Avec le fichier dhparams.pem servant à la génération d’une clef diffie-helman, via la commande openssl dhparam -out dhparam.pem 2048

Mozilla a un super générateur de config. J’ai prit la config moderne, désolé utilisateurs de vieux soft, je décroche ainsi un beau A sur le CryptCheck un peu nazi de Aeris !

CrptCheck

Vous pouvez maintenant accéder a mart-e.be en https !

SMailArchiver : script de backup sécurisé d’email

Mise à jour: le script a été mit à jour, les possibilités et commandes ont légèrement changé, lisez le README pour plus de détail ou suivez mes annonces avec le tag smailarchiver.

Faisant un peu le nettoyage dans mes boites à emails, j’ai voulu sauver tous mes vieux messages avant suppression. Cependant, je voulais un moyen de faire cela de manière automatisée (possible d’utiliser cron), incrémental (ne pas tout retélécharger à chaque fois), supportant la compression et surtout garder une certaine confidentialité (chiffrement).

J’ai trouvé des bouts de codes pour faire chaque partie séparément (ici et ici par exemple) mais pas de programme permettant de faire tout cela ensemble. A que cela ne tienne, j’ai donc créé un petit script python contentant mes besoins. Ainsi est né SMailArchiver (avec S pour Secured ou Suicide, question de point de vue) que je partage ici.

Ce petit bout de code (approchant des 400 lignes de python) permet facilement de faire des backup de plusieurs comptes emails avec toute une série d’option comme la compression des mails avant le chiffrement (une compression après le chiffrement pourrait également être utile mais est moins efficace) ou le choix d’une clef constituée uniquement de bytes aléatoires ou renforcée par un mot de passe de votre choix. Le chiffrement se fait avec AES 256 avec une signature HMAC-SHA256 histoire d’empêcher les manipulations. C’est pas Truecrypt mais ça suffit pour cacher les conversations avec votre maitresse des yeux indiscrets de madame votre femme (ou inversement).

big mess
Avant d’utiliser SMailArchiver, ma gestion des emails ressemblait à ça… et mes cheveux étaient secs et cassants.

Le programme peut recevoir les paramètres nécessaires par la console, via des arguments ou via un fichier de config (regardez le fichier d’exemple dans le dépot).

$ python smailarchiver.py 
Enter your email username: foo@bar.com
Enter your imap server: imap.bar.com
Enter your password: 
Enc key: 6MIiWqNiJ4h1qDmp5Z4OpWKRLst7eAbirWOPHIm9zqk=
Sig key: 9JmR0593CWwtnJhRWqdLHk7tXvX/h6l6A2GLKA4iVq4=
$ python smailarchiver.py --user foo@bar.com --imap imap.bar.com --passwd monkey1 --key foo@bar.com.keys --promp --compress
Enter your encryption/signature password: 
Enc salt: KNwhv7kuNs0/iCZZTUyd05IuOylzG/n2oBALj1UMJ0c=
Sig salt: A99CxfzwRbU4P4lse6eN5O+g2wethRzL4gMH7xqFkgE=
Hash: oNcX7GAyHsS+bNOT8UCYAYM/ltCmx34E9Gmmpky02AE=
$ python2 smailarchiver.py -c config.json
Enter your encryption/signature password: 
Enc salt: m87TeE9fmr5XPf7yvN0w3PYOv3ivlNKpKGp35hC+N/k=
Sig salt: 2k9Wu5HBEo7G0J0EzlglOqV0Zko1LmhfiGnxD2QEzhE=
Hash: FA3TdkhWNZqy9BjGOk2UVZ6AeqgHSrparH3ynHdvj38=
$ python2 smailarchiver.py --decrypt foo@bar.com/ --key foo@bar.com.keys --promp
Enter your encryption/signature password: 
Decrypting 16.gz.mbox
Decrypting 11.gz.mbox
Decrypting 45.gz.mbox
...

Code évidement open source que vous pouvez trouver le code sur mon dépot gitorious. C’est compatible python 2.7 et plus (python 3 c’est l’avenir !) et nécessite pycrypto (qui est compatible python 2.1 et 3.2, j’imagine même pas le bordel que ça doit être). Mes tests très scientifiques type àvuedenezçapasse montrent que le programme fonctionne mais on est jamais à une erreur prêt, n’hésitez pas à me communiquer vos idées d’améliorations ou corrections.

PS: fait amusant concernant AES 256, saviez vous que l’énergie contenue dans une supernova ne suffirait pas rien que pour énumérer les 2²⁵⁶ états possibles (calcul non compris donc). A moins de trouver une faille de sécurité, il est donc impossible, avec les notions de la physique actuelle, de faire un brute force sur une clef AES 256. Source: Applied Cryptography, Bruce Schneier (extrait en question).

Emails : ne soyez pas considérés comme un spam

Un des problèmes avec la gestion des mails maison, c’est qu’on a vite fait d’être considéré comme un spam par un filtre un peu trop difficile. Très gênant si vous pensiez gérer votre adresse professionnelle. Heureusement il existe des solutions. J’ai mis à jour la page de mon wiki concernant la gestion des mails avec reverse DNS, SPF et DKIM. Voici un résumé :

Les filtres

Le principe des filtres anti-spam (dont SpamAssassin est le plus connu) est très simple. Vous partez d’un score de zéro. Lorsque le filtre détecte quelque chose de suspect votre score augmente, lorsque vous avez un comportement que n’aurait pas un spammeur, vous perdez des points. Arrivé à une certaine limite, vous devenez un spam.

Évidement toute la difficulté réside à bien pondérer et trouver les bons critères. Le fait d’utiliser des mots comme « viagra » va vous faire gagner des points. Les spammeurs l’ayant comprit, ils utiliseront v1agra par exemple. Les critères faisant perdre des points ne doivent pas non plus être simples sinon un spammeur les utilisera pour passer inaperçu.

Il existe également le filtre bayésien plus complexe mais très puissant qui va utiliser des probabilités statistiques. Ce filtre se base sur le contenu du message et l’on a donc pas d’influence coté serveur. Évitez juste de parler de vente de médicaments en ligne dans vos messages.

Voici deux exemples de scores (spamassassin et un inconnu) plutôt mauvais que j’ai eu sans rien activer :

X-SGSI-SpamCheck: n'est pas un polluriel, SpamAssassin (score=1.982,requis 5,
 BAYES_50 1.00, RDNS_DYNAMIC 0.98, SPF_HELO_PASS -0.00)

Il s’agit du filtre de mon université, il considère que l’on est un spam à partir de 5 et m’a mit 1.982, rien de dramatique.

X-Spam-Flag: YES
X-Spam-Score: 6.782
X-Spam-Level: ******
X-Spam-Status: Yes, score=6.782 tagged_above=4 required=6.3
	tests=[BAYES_50=0.8, EIGHTBIT_ENCODING=1, RCVD_FAKED_HELO=2,
	RCVD_SHORT_HELO=2, RDNS_DYNAMIC=0.982] autolearn=no

Il s’agit ici d’un filtre d’entreprise qui est beaucoup plus sévère. Il m’a mit un score de 6.782 alors que sa tolérance est à 6.3. Cela a eu pour effet de mettre un gros ***SPAM*** dans le sujet du message.

« Capitaine, je crois que notre filtre SpamAssassin est un peu trop permissif… »

Reverse DNS

La résolution DNS classique est de faire correspondre une URL à une IP. Le reverse DNS est le chemin inverse. Par défaut, j’ai, dans le cas de mon serveur OVH, 178-33-111-174.kimsufi.com pour l’IP 178.33.111.174. C’est ça qui explique mon « bonus » RDNS_DYNAMIC 0.98.

En mettant votre nom de domaine à la place, le problème est réglé. Vous pouvez vérifier que vous avez la bonne valeur via la commande $ dig -x 11.22.33.44 dans le champ PTR. J’ai remarqué que certains serveurs prenaient beaucoup de temps à mettre à jour leurs reverse DNS donc prenez votre mal en patience.

Si vous possédez comme moi plusieurs domaines pointant sur la même IP, sachez qu’il est inutile de mettre plusieurs entrées DNS PTR, cela pourrait être même contre productif. [Source]

SPF

Le plus simple. SPF pour Sender Policy Framework est simplement une entrée DNS depuis quel domaine on peut envoyer un email. Dans le cas d’un seul domaine qui n’envoie que depuis ses entrées MX, l’entrée DNS est

mart-e.be    SPF    v=spf1 mx ~all

Vous pouvez ainsi spécifier d’autres serveurs (si par exemple vous utilisez le relais SMTP de votre FAI). Pour les cas plus complexes, je vous renvoie vers le site officiel.

La valeur ajoutée par cette règle est assez faible. J’ai eu droit à SPF_HELO_PASS -0.00 pour le premier serveur mail et SPF_HELO_PASS=-0.001,SPF_PASS=-0.001 pour le deuxième. Vous pouvez vous dire que l’intérêt est donc limité mais voyez plutôt comme ceci : si un jour quelqu’un essaye d’envoyer du spam en se faisant passer pour vous, la vérification SPF échouera et son score de spam augmentera de façon non-négligeable.

Attention: SPF casse la redirection. Si vous envoyez un mail à machin@gmail.com sur laquelle votre correspondant a réglé une redirection automatique vers machin@myopera.com, c’est l’adresse IP de Google qui apparaîtra et pas la votre. La vérification SPF échouera donc. Cependant, ce n’est pas si grave que ça parce que le filtre anti-spam de Opera doit avoir l’habitude de recevoir tous les emails venant de machin@gmail.com qui a des bonnes chances d’être dans une liste blanche. Donc si le serveur du correspondant est bien fait, ça ne devrait pas poser de problèmes.

DKIM

Plus difficile, il s’agit ici de signer les headers du mail envoyé. La clef publique est spécifiée dans les DNS. J’ai choisi d’utiliser OpenDKIM. On génère des clefs asymétriques pour chaque domaine et on publie la clef publique dans les DNS. Celle pour ce domaine est par exemple :

default._domainkey IN TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQChUywElHkvIJwgp9BYce97yhv0ImwK5+2Jm0CHBCfjYpeV7pSaAh/aYmX9+BJcSupVnJYBPf4DT/AFbV7O6snG6rGf3bnJSHdfyGa7Zq8a/7ERdTYo6/W5LJvaenDAxqlWPlgVafQtncRt+4/iF133FXLpC4VL6NmbMirK0yMRKQIDAQAB"

Lorsque que vous signerez un email, on aura par exemple.

DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=mart-e.be; s=default;
t=1353269228; bh=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY=;
h=Date:From:To:Subject:From;
b=VQUjpDqQbD4OH4dRO5Ex3J0Qxoa5aKv6m7EYnxUgXE+SJd45PzJ0ZhIMzlIah3PRJ
6UQX7rZizYU7gp6PioYFovADAW0o31iZYyTpuiUlEXpKAFeoRl0gR7wTNZrPdkSgSr
A8bxxBjJPIUPpYW6ePuRixrumcCiNu2a0o8IC4Bk=

Votre correspondant supportant DKIM vérifiera la signature. Si elle est valide, vous perdez des points à votre score spam, si elle est invalide, vous en gagnez. Ben oui il ne suffit pas dire qu’on fait du DKIM pour ne pas être un spam, il faut encore bien le faire.

Conclusion

En appliquant ces trois méthodes, je suis passé à un score négatif sur le premier serveur et inférieur à 4 sur le deuxième (score à partir duquel il n’indique plus le détail).

Cela ne vous fera pas passer à travers les filtres les plus drastiques mais devrait diminuer le risque. N’hésitez pas à faire des tests sur plusieurs serveurs différents. Les filtres de Google et Yahoo ne donnent pas le détail du score mais les entreprises privées ou gens qui ont installé des filtres maisons sont plus sympa.

Si vous connaissez d’autres moyen de diminuer ce score, n’hésitez pas à partager. Je n’ai pas exemple pas d’explication sur le RCVD_FAKED_HELO et RCVD_SHORT_HELO qui m’ont donné chacun deux points ou le EIGHTBIT_ENCODING que je n’explique pas non plus (tu aurais préféré quoi ?) dans l’exemple ci-dessus.

Nettoyer la base de donnée de Gwibber

Gwibber est un client pour réseaux sociaux (StatusNet, Twitter & co) assez sympa. Seulement, il a un problème : sa base de donnée à tendance à augmenter au fur et à mesure du temps. Aujourd’hui le fichier ~/.config/gwibber/gwibber.sqlite faisait 132MB chez moi, c’est beaucoup pour gérer juste un flux Twitter et un StatusNet.

En fouillant un peu le code, j’ai trouvé la fonction de maintenance qui sert à supprimer les 2000 plus vieux status au démarrage. Seulement, les status ne sont pas la seule chose qui sont stockées dans la base de donnée, il y a aussi les liens, une crasse qui augmente vite. La preuve :

$ cd ~/.config/gwibber/
$ ls -lh gwibber.sqlite
-rw-r--r-- 1 mart users 132M Nov  2 12:34 gwibber.sqlite
$ sqlite3 gwibber.sqlite 
SQLite version 3.7.14.1 2012-10-04 19:37:12
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> select count(*) from messages where stream = 'links';
39622

Ouch 40000 liens stockés, pas étonnant que ça monte. J’avais ouvert un bug report à ce sujet là il y a plus de deux mois mais je n’ai eu aucune réponse. Tant pis, on est jamais mieux servi que par soit même de toute façon. On peut donc supprimer les plus vieux en modifiant un peu la commande (triviale) de la fonction de maintenance pour obtenir:

DELETE FROM messages WHERE operation = 'receive' AND stream = 'links' AND time IN (SELECT CAST (time AS int) FROM (SELECT time FROM messages WHERE operation = 'receive' AND stream = 'links' AND time != 0 ORDER BY time ASC LIMIT (SELECT COUNT(time) FROM messages WHERE operation = 'receive' AND stream = 'links' AND time != 0) - 2000) ORDER BY time ASC);

Après avoir lancé cette commande dans le shell sqlite, on purge le tout et tadaaaaam

sqlite> select count(*) from messages where stream = 'links';
3071
sqlite> VACUUM;

$ ls -lh gwibber.sqlite
-rw-r--r-- 1 mart users 41M Nov  2 12:37 gwibber.sqlite

41MB, pas mal le nettoyage. On peut même faire mieux en supprimant les autres types de status qui prennent un peu trop de place. Testez chez vous pour voir ce qui encombre le plus.

sqlite> select count(*), stream from messages group by stream;
4317|images
3084|links
4372|messages
44|private
6|profile
629|replies
39|send_thread
4703|user
1235|videos

Hmmm un peu trop d’images chez moi, ne gardons que les 1000 dernières.

sqlite> DELETE FROM messages WHERE operation = 'receive' AND stream = 'images' AND time IN (SELECT CAST (time AS int) FROM (SELECT time FROM messages WHERE operation = 'receive' AND stream = 'images' AND time != 0 ORDER BY time ASC LIMIT (SELECT COUNT(time) FROM messages WHERE operation = 'receive' AND stream = 'images' AND time != 0) - 1000) ORDER BY time ASC);
sqlite> VACUUM;

$ ls -lh gwibber.sqlite
-rw-r--r-- 1 mart users 34M Nov  2 13:11 gwibber.sqlite

Vous pouvez facilement jouer un peu avec la base de donnée en changeant les paramètres mais prudence. Je n’ai pas inspecté en long et en large la structure et cette façon de faire n’est peut être pas la plus propre (si vous avez beaucoup de lien par message, vous risquez d’en effacer trop par exemple). Mais bon, je n’avais pas envie de passer 2h là dessus et en sachant que je ne retourne rarement plus loin que les 100 derniers status, je ne prend pas trop de risque en nettoyant dans les 2000 derniers. À vous de voir… Vous pouvez aussi mettre les commandes dans un cron pour automatiser tout ça pour garder la base de donnée à une taille raisonnable.

Le nombre de caractère dans un SMS

Observez les deux messages en cours de rédaction ci dessous. En dehors d’un contenu d’une banalité consternante, on remarque une différence de caractères spéciaux (ç et ô) et surtout le nombre de message envoyé passant de un à deux. Plutôt que les 71 caractères introduits, on m’en comptabiliserait 257 ! Comment expliquer cette différence énorme ? Non pas par un complot des opérateurs mais par une question d’encodage de caractère.

J’avoue j’étais un peu à court d’inspiration pour l’exemple…

Un SMS servant, à la base, à s’assurer du status de certaines lignes inoccupées, on a utilisé un petit paquet de 128 bytes, augmenté à 140 par la suite (d’où l’abréviation de Short Message Service). Ces 140 bytes pouvant stocker du texte, l’on a choisi un encodage sur 7 bits. 140*8/7 = 160 caractères possibles dans un SMS. Seulement 7 bits n’étant pas très grand, on est limité à 2^7, 128 caractères différents. Ce set de caractères est défini par le standard au doux nom de GSM 03.38 et qui englobe, grosso modo, l’alphabet habituel ainsi que quelques caractères accentués et symboles.

Des caractères comme ç (mais pas Ç étrangement), ô, œ ou ñ n’entrant pas dans la table, l’on change l’encodage du simple 7 bits à du beau UCS-2 (ancêtre de UTF-16 à taille fixe). Cet encodage permet de mettre tout ce qu’on veut en utilisant les alphabets les plus exotiques possibles (SMS en Klingon ?) mais consomme désormais 16 bits par caractères. Dans notre paquet de 140 bytes, on est donc réduit à 70 caractères de 16 bits. Ce qui explique que vos messages sont beaucoup plus courts et que la facture peut monter avec le nombre croissant de messages.

Les messages en plusieurs parties ne sont d’ailleurs pas gratuit non plus. Pour pouvoir rassembler les messages par après, on va ajouter un entête (le User Data Header) de 6 bytes (description des champs) qui va empiéter sur la longueur du message. (140 – 6)*8/7 = 153.14… Donc, lorsque vos messages deviennent trop long, on vous facturera un SMS tout les 153 caractères « normaux » ou tout les 67 caractères si un caractère spécial est présent.

Mon message de 71 caractères est donc comptabilisé comme ceci :

 <-6-> <-------------------------134------------------------------------->
| UDP |Salut! Comment ça va ? Quoi de neuf depuis la dernière fois ? A bie|
| UDP |ntôt|///////////////////////////////////////////////////////////////

Pas de complot vous poussant à adopter une mauvaise orthographe, une simple question d’encodage…