Suite au dernier article sur Let’s Encrypt, je voulais aborder la question d’un aspect de TLS qui pourrait devenir intéressant dans les années à venir, à savoir la publication d’enregistrements liés dans le DNS. Cela se fait sous deux formes actuellement, la première est CAA, la deuxième DANE.
CAA
Commençons par le plus simple. CAA, pour Certification Authority Authorization, est un enregistrement qui se place à la racine de votre domaine afin de déclarer l’autorité utilisée pour les certificats. Il s’agit d’un système déclaratif, qui n’a pas vocation a être vérifié par les clients, mais plutôt par les CA qui seraient démarchées pour sécuriser votre domaine. Son but principal est donc de limiter une faiblesse propre à l’écosystème X.509, la capacité d’un tiers malicieux à obtenir un certificat alternatif pour le même domaine en allant voir une CA différente.
Cet enregistrement est héréditaire, il est inutile d’en placer un par site, sauf s’ils utilisent une CA différente. L’enregistrement de la racine sera reprit pour tous les sous-domaines.
En pratique, à quoi ça ressemble ? Pour rester dans la thématique Let’s Encrypt, à ça.
@ IN CAA 1 issue "letsencrypt.org"
@ IN CAA 1 iodef "mailto:notarealemail@lordran.net"
Oublions le début qui est coutumier pour du DNS et passons directement à la partie spécifique. Le premier champ est un drapeau qui permet d’influencer l’interprétation. Sa seule valeur possible pour l’instant est 1 pour “Issuer Critical”, qui demande un refus par défaut si le champ n’est pas compris pour une raison ou pour une autre. Le champ suivant indique le type de valeur qui suivra et le dernier la valeur à proprement parler.
Il existe trois types possibles pour l’instant, issue qui désigne votre CA (via son domaine court), iodef qui permet de donner une adresse de feedback en cas de problème ou irrégularité constatée, et issuewild qui reprend issue mais pour les certificats wildcard.
Si vous utilisez bien Let’s Encrypt, vous pouvez reprendre les enregistrements tel quel, sous réserve d’avoir un serveur DNS un tant soit peu récent (BIND 9.10.1B, NSD 4.0.1, Knot 2.2, PowerDNS 4, etc). Sinon, il faudra utiliser la notation pour créer des enregistrements custom via l’ID du type CAA (257).
DANE
Préambule
Un peu plus compliqué mais aussi un peu plus ambitieux, DANE est un protocole permettant de poser des signatures de certificats TLS dans le DNS afin de théoriquement s’affranchir complètement des CA, permettant même d’utiliser des certificats auto-signé en déléguant toute la partie “authentification” de TLS au DNS.
Ces grandes ambitions de mettre toutes les CA au chômage se heurtent toutefois pour l’instant à deux problèmes. Le premier est le faible déploiement de DNSSEC. Le second est l’absence de support dans les clients les plus utilisés, en particulier les navigateurs Web. À titre d’exemple, le bug associé chez Mozilla date de 2011 et tourne plus ou moins dans le vide depuis, ce qui ne laisse que des extensions pas toujours très fiables pour un rapport purement informatif, pas du tout appliqué. En pratique, le seul client/serveur répandu capable d’appliquer DANE nativement, à ma connaissance, est Postfix (2.11 ou plus), ce qui n’est pas entièrement inattendu au vu de l’état de TLS dans le domaine des MTA.
DANE s’appuie sur la présence de DNSSEC pour garantir son bon fonctionnement, plus spécifiquement pour assurer l’authenticité du message. Il est possible que votre registrar propose ce support directement sur leur infrastructure, c’est le cas d’OVH, Gandi et sûrement bien d’autres. Il est également possible que seul un support limité soit proposé, tel qu’une publication des enregistrements DS sur le tld de votre domaine, auquel cas vous serez obligé de trouver un hébergeur capable de publier des zones signées. Dans le pire des cas, aucun support n’existe et je serais tenté de dire qu’il va falloir songer à changer de registrar, cette absence indiquant une certaine absence de veille technologique et d’écoute du marché.
La question de l’auto-hébergement d’une zone DNSSEC sera traité dans un futur article quand je remettrais mon infrastructure à jour après la prochaine sortie de Debian Stretch, mais si vous êtes pressés, je recommande un système avec un autoritaire principal basé sur nsd+opendnssec et un secondaire basé sur knot.
Structure de l’enregistrement
La mise en œuvre de DANE passe par la publication d’un enregistrement de type TLSA (ID 52). Un enregistrement complet ressemble à ça.
_443._tcp.alpha IN TLSA 3 1 1 bccb988128f8e715ad92fffb5c649cd3ba92c520f34033faf1d29cc3ad48c830
Si vous êtes familier avec les enregistrements un peu avancés de DNS, vous reconnaîtrez à gauche une structure similaire aux SRV sous la forme _{port}._{transport}.sousdomaine. Elle vous permet d’être très spécifique et de pouvoir donner un certificat différent pour à peu près tous les services possibles de votre domaine. Contrairement à CAA, TLSA n’est pas héréditaire. Il faudra donc en spécifier un pour tous les sous-domaines couverts, comme par exemple www. L’exemple donné doit être lu comme s’appliquant au port TCP 443 (donc HTTPS) du sous-domaine alpha.
La partie de droite est spécifique à TLSA, comprend trois drapeaux et un champ textuel de données. Le premier est le plus important, indique le mode de fonctionnement de DANE.
- 0 - PKIX-TA : le condensat sera celui de la CA et une validation X.509 traditionnelle sera également appliquée
- 1 - PKIX-EE : le condensat sera celui du serveur et une validation X.509 traditionnelle sera également appliquée
- 2 - DANE-TA : le condensat sera celui de la CA, DANE sera autoritaire, pas de validation X.509
- 3 - DANE-EE : le condensat sera celui de la CA, DANE sera autoritaire, pas de validation X.509, un certificat auto-signé peut être utilisé
Les deux autres drapeaux sont un peu moins importants. Le second détermine le type de données, 0 pour le certificat entier, 1 pour spécifiquement le champ subjectPublicKeyInfo du certificat. Le dernier indique le format du champ de données, 0 pour les données complètes, 1 pour un condensat SHA-256, 2 pour un condensat SHA-512. Pour reprendre l’exemple donné plus haut, cela veut dire que notre enregistrement TLSA demande l’application du mode DANE-EE, publie uniquement sa clé publique, sous la forme d’un condensat SHA-256.
Rendu à ce point, il sera probablement de mise de donner quelques conseils. Choisir entre PKIX et DANE est assez politique, PKIX permet une sorte de double sécurité alors que DANE vous libère du monde des CA. Le mode TA ou EE a un impact un peu plus concret, le TA étant réutilisable, vous pouvez vous permettre de faire un CNAME pour simplifier votre zone sur un seul TLSA. EE vous imposera d’être un peu plus verbeux dans votre zone, mais a par contre l’avantage de bloquer les problèmes de piratage si jamais quelqu’un prend le contrôle de votre compte chez la CA, étant donné que le certificat ou la clé publique actuelle sont des données infalsifiables et inutilisables seuls, sans la clé privée associée qui devrait être bien planquée au chaud sur votre serveur.
Si vous faites en plus de la génération automatique de certificat comme avec Let’s Encrypt, notez aussi que votre CA est susceptible de changer ses certificats à tout moment, en particulier si vous utilisez de l’ECDSA, étant donné que les certificats intermédiaires sont encore en RSA et doivent proposé une alternative ECDSA d’ici quelques mois, qui sera probablement utilisée par défaut dans votre cas.
Pour les autres drapeaux, le mode certificat complet est plutôt déconseillé et ce d’autant plus si vous utilisez Let’s Encrypt, vu leur faible durée de vie. La clé publique est une donnée beaucoup plus stable sous réserve de ne pas en changer à chaque renouvellement. Quand au dernier, un condensat SHA-256 devrait être le meilleur compromis pratique.
En conséquence, je considère comme préférable avec Let’s Encrypt l’utilisation des politiques 3 1 1 ou 1 1 1 à l’heure actuelle.
Calcul du condensat
Trêve d’explication et passons à la pratique. Une fois que vous aurez fait vos choix, l’enregistrement peut tout à fait se faire à la main, mais demandera à minima d’extraire des données du certificat actuel. Si vous partez pour un mode 1 1 comme conseillé plus haut, voici la commande utilisée pour obtenir le condensat d’une clé publique ECDSA.
openssl x509 -in /etc/letsencrypt/live/alpha.lordran.net/latest_cert.crt -pubkey -noout | openssl ec -pubin -outform DER | sha256sum
La commande sera légèrement différente dans le cas d’une clé RSA.
openssl x509 -in /etc/ssl/joker.lordran.net.crt.old -pubkey -noout | openssl rsa -pubin -outform DER | sha256sum
Enfin, si vous avez préféré utiliser comme base le certificat dans son entier, openssl x509 suffit pour avoir ce qu’on veut.
openssl x509 -in /etc/letsencrypt/live/alpha.lordran.net/latest_cert.crt -noout -sha256 -fingerprint
openssl x509 -in /etc/letsencrypt/live/alpha.lordran.net/latest_cert.crt -outform DER | sha256sum
Vous pouvez même utiliser la commande sha256sum directement sur votre fichier de certificat dans ce dernier cas, mais gardez en tête que le condensat doit être calculé sur la version DER (binaire) des données, pas PEM (armure ASCII) couramment utilisé dans l’écosystème libre.
Construction automatisée
Comme rien ne serait complet sans un petit script, voici un bout de code Python capable de générer des enregistrements TLSA automatiquement.
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import os, sys, hashlib, argparse
from pyasn1_modules import pem, rfc2459
from pyasn1.codec.der import decoder, encoder
from pyasn1.type.univ import OctetString
def parse_certificate(certificate_path):
fqdns = set()
substrate = pem.readPemFromFile(open(certificate_path))
cert = decoder.decode(substrate, asn1Spec=rfc2459.Certificate())[0]
core = cert['tbsCertificate']
# Hash public key
der = encoder.encode(core.getComponentByName('subjectPublicKeyInfo'))
hash_der = hashlib.sha256()
hash_der.update(der)
pkhash = hash_der.hexdigest()
# Extract CommonName
for rdnss in core['subject']:
for rdns in rdnss:
for name in rdns:
if name.getComponentByName('type') == rfc2459.id_at_commonName:
value = decoder.decode(name.getComponentByName('value'), asn1Spec=rfc2459.DirectoryString())[0]
fqdns.add(str(value.getComponent()))
# Extract SubjectAltName
for extension in core['extensions']:
if extension['extnID'] == rfc2459.id_ce_subjectAltName:
octet_string = decoder.decode(extension.getComponentByName('extnValue'), asn1Spec=OctetString())[0]
(san_list, r) = decoder.decode(octet_string, rfc2459.SubjectAltName())
for san_struct in san_list:
if san_struct.getName() == 'dNSName':
fqdns.add(str(san_struct.getComponent()))
return (pkhash, fqdns)
def create_tlsa(certificate_path, stream, port):
(pkhash, fqdns) = parse_certificate(certificate_path)
for fqdn in fqdns:
print('_{}._{}.{} IN TLSA 3 1 1 {}'.format(port, stream, fqdn, pkhash))
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('certificate', help='path to certificate')
parser.add_argument('stream', default='tcp', help='stream type (eg: tcp, udp), default to tcp')
parser.add_argument('port', default='443', help='network port, default to 443')
args = parser.parse_args()
create_tlsa(args.certificate, args.stream, args.port)