Ce billet explique comment on peut monter une autorité de certification à 2 sous et surtout l’utiliser au quotidien sans pour autant sacrifier les exigences techniques en terme de sécurité.
Avant la technique, remettons en contexte, une autorité de certification doit répondre à au moins ces deux conditions :
- les clés privées des autorités racines et intermédiaires doivent être tenues secrètes
- les clés privées des autorités racines et intermédiaires ne doivent pas être perdues, autrement dit, il faut en garder au moins une copie, également secrète ailleurs.
Le mot « secret » est bien entendu à comprendre au sens « chiffré » et non pas caché en loose dans un dossier invisible ou similaire.
Et la contrainte :
- la solution doit coûter un minimum
D’où la solution suivante, qui rempli toutes les exigences :
- le stockage se fait sur une Dropbox (ou hébergement cloud similaire)
- toutes les clés sont chiffrées
- l’outil utilisé pour la manipulation et génération des certificats sera OpenSSL, qui est open-source et multi-plateforme (vraiment multi-plateforme, c’est à dire Linux, Cygwin et Windows)
Et maintenant la technique !
L’autorité de certification
OpenSSL est assez mal fichu sur la manière de configurer car il va nous falloir un fichier de configuration pour les certificats d’autorité, et un séparé pour les autres type de certificats. Voyons le contenu pour les certificats d’autorité, puisque c’est ce qui nous intéresse maintenant :
oid_section = new_oids [ new_oids ] STREET = 2.5.4.9 S = 2.5.4.8 PostalCode = 2.5.4.17 [ distinguished_name ] C = FR O = Kveer OU = Root CA CN = Kveer Root CA [ req ] default_bits = 4096 default_md = sha1 distinguished_name = distinguished_name reqs_extensions = x509_ca prompt = no utf8 = yes [ x509_ca ] basicConstraints = critical,CA:true subjectKeyIdentifier = hash authorityKeyIdentifier = keyid, issuer keyUsage = critical, keyCertSign, cRLSign crlDistributionPoints = URI:http://pki.kveer.com/my.crl
Rien d’extraordinaire ici. Le certificat à générer est marqué comme une autorité de certification, on précise son usage : signer des certificats et des listes de révocation de certification (CRL) et on précise le point de distribution des CRLs. On aurait pu également limiter la longueur de la chaîne de certification, je ne l’ai pas fait.
Et le script bash qui va bien :
#!/bin/bash #cannot use ecdsa as windows nt 5.x does not support it yet. # prime192v1 seems to be the best one otherwise. CA_KEY=ca.key CA_CERT=ca.crt CONF=kveer-ca.openssl.cnf if [[ -f ca.key && -f ca.crt ]]; then echo 'There is already an existing CA root' exit 1 fi # password stty -echo read -p 'Please enter a password that will be used to encrypt all private keys: ' PASSWD;echo stty echo # key generation openssl genrsa -aes192 -out ca.key -passout pass:"$PASSWD" 4096 # root ca cert generation openssl req -new -x509 -extensions x509_ca -key ca.key -passin pass:"$PASSWD" -days 7300 -config $CONF -utf8 -out tmp.crt mkdir certs reqs keys touch index.txt echo 01 > serial.txt echo 00 > crlnumber
J’aurais souhaité utiliser l’algorithme ECDSA qui est pour le moment considéré plus sur, à taille de clé équivalente, mais pour conserver une compatibilité avec Windows NT5.1 (XP et 2003 Server), je suis resté à l’algorithme RSA.
- Le script commence par une petite vérification : ça serait dommage d’écraser son certificat racine, n’est-ce pas ?
- Il va demander un mot de passe (ce qui évitera de le taper 36 fois), lequel va servir à chiffrer la clé privée. Attention : c’est à ce moment qu’est définit LE mot de passe pour chiffrer toutes vos clés privées, il a donc intérêt à être costaud
- Il génère une clé privée, qu’il encode avant d’écrire sur le disque
- Il crée et signe un certificat dont la clé privée vient d’être généré
- Il crée quelques fichiers nécessaire à l’utilisation du module « ca » de openssl
Une fois le certificat créée, il ne reste plus qu’à déployer la partie publique (sous la forme du fichier ca.crt) vers tous vos postes pour que les futurs certificats émis puisse être validées par les postes clients.
Les certificats d’autorités intermédiaires
Avant de créer un certificat « standard », c’est à dire ceux qui servent à faire du SSL ou du S/MIME pour l’utilisateur final, je recommande vivement de créer encore un certificat d’autorité, signé par le précédent certificat, qu’on appelle alors certificat d’autorité intermédiaire.
La raison est que si par malheur le certificat racine vient à être compromis, vous êtes mort, tout le château de cartes tombe, vous devez repartir de zéro.
En revanche, si c’est un certificat d’autorité intermédiaire qui est compromis, vous êtes dans la merde également, mais il est possible de récupérer le coup en révoquant ce certificat au niveau du CRL de l’autorité racine, puis en recréant ce certificat d’autorité intermédiaire ainsi que de re-signer tous les certificats qu’il aurait pu émettre. Il n’est pas nécessaire de redéployer le certificat racine sur tous les postes, puisqu’il est safe.
Dans le schéma que je propose, la seule faille est la robustesse du mot de passe généré précédemment et d’éviter de le taper sur un poste dpoé au keylogger.
Si on stocke les fichiers sur une Dropbox, on peut considérer que les fichiers sont publics, et bien que cela puisse ne pas être agréable (on aurait la liste des certificats émis, leur type, leur usage…), l’autorité reste sûre tant que les clés privées ne sont pas déchiffrées, donc si le mot de passe utilisé ainsi que l’algorithme de chiffrage (ici : AES 192 bits) sont solides. Etant donné que AES 192bits est plus que solide, que la connaissance des certificats émis n’a que peu d’importance (avec toutes les conséquences que cela entraîne, puisque indirectement j’accepte de dévoiler une partie du réseau), je réduis la faiblesse de mon système au seul mot de passe.
Par ailleurs l’utilisation de certificats d’autorité intermédiaire permet de déléguer la gestion à un tiers ou un collègue à cet seule autorité intermédiaire sans risquer de tout compromettre. Pour rappel, il est possible de créer une autorité intermédiaire qui ne peut pas émettre de certificat d’autorité, en fixant la chaîne de certification à 0 sur cette autorité.
Les scripts maintenant. Tout d’abord le fichier de configuration :
oid_section = new_oids [ new_oids ] STREET = 2.5.4.9 S = 2.5.4.8 PostalCode = 2.5.4.17 [ req ] default_bits = 4096 default_md = sha1 distinguished_name = distinguished_name [ distinguished_name ] countryName = Pays countryName_default = FR countryName_min = 2 countryName_max = 2 O = Sujet O_default = Kveer OU = Unité organisationnelle CN = Common Name [ x509_ca ] basicConstraints = critical,CA:true subjectKeyIdentifier = hash authorityKeyIdentifier = keyid, issuer keyUsage = critical, keyCertSign, cRLSign crlDistributionPoints = URI:http://pki.kveer.com/my.crl
Puis le script :
#!/bin/bash #cannot user ecdsa because windows nt 5.x does not support it yet. # prime192v1 seems to be the best one otherwise. CA_KEY=ca.key CA_CERT=ca.crt CONF=kveer-subca.openssl.cnf CONF_CA=kveer.openssl.cnf if [[ ! (-f ca.key && -f ca.crt) ]]; then echo 'There is no CA root' exit 1 fi if [ $# < 1 ]; then echo "$0 certificate_name" exit 2 fi # password stty -echo read -p 'Please enter a password that will be used to encrypt all private keys: ' PASSWD;echo stty echo # key generation openssl genrsa -aes192 -out tmp.key -passout pass:"$PASSWD" 4096 # root ca cert generation openssl req -new -extensions x509_ca -key tmp.key -passin pass:"$PASSWD" -config $CONF -utf8 -out tmp.csr openssl ca -config $CONF_CA -extensions x509_ca -days 3650 -keyfile $CA_KEY -key $PASSWD -cert $CA_CERT -in tmp.csr -out "$1.crt" if [[ ! -f "$1.crt" || ! -f tmp.key || ! -f tmp.csr ]]; then echo 'Missing files, stopping here' exit 3 fi mv "$1.crt" "certs/$1.crt" mv tmp.key "keys/$1.key" mv tmp.csr "reqs/$1.csr" mkdir -p "$1/certs" "$1/keys" "$1/reqs" cp "certs/$1.crt" "$1/ca.crt" cp "keys/$1.key" "$1/ca.key" cp *.sh "$1/" cp *.cnf "$1/" touch "$1/index.txt" echo '00' > "$1/serial.txt"
En essence, c’est similaire au script précédent, mais ici le fichier n’est pas auto-signé, et un dossier est créer pour préparer la gestion de l’autorité intermédiaire. On notera les quelques vérifications pour éviter tout accident. Ce n’est pas exhaustif, mais ça me convient pour le moment.
Arrivé à cet étape nous avons une autorité de certification racine, ainsi qu’au moins une autorité de certification intermédiaire. On peut s’attaquer au plus intéressant.
Les certificats « serveur » et « client »
Par certificat « serveur » j’entends ceux utilisé pour authentifier le serveur, donc ceux servant pour initier une connexion SSL sur un serveur web, un protocol de messagerie comme SMTP, POP, IMAP en SSL ou TLS, ou encore un serveur FTP.
Et par certificat « client » j’entends ceux utilisés par Outlook pour signer ses messages.
On va avoir un « gros » fichier de configuration pour ces deux types de certificats :
oid_section = new_oids [ new_oids ] STREET = 2.5.4.9 S = 2.5.4.8 PostalCode = 2.5.4.17 [ ca ] default_ca = default_ca [ default_ca ] dir = . new_certs_dir = $dir/certs certificate = $dir/ca.pem private_key = $dir/private/ca.key RANDFILE = $dir/private/.rand default_days = 365 default_crl_days = 30 default_md = sha1 database = $dir/index.txt # list of all signed certificates unique_subject = no # allows to generate multiple certs with same subject name serial = $dir/serial.txt # next cetificate serial crlnumber = $dir/crlnumber # next crl number email_in_dn = no # no email in distinguished name name_opt = ca_default cert_opt = ca_default copy_extensions = copy # copy all new extensions from request to certificate policy = policy [ policy ] C = match O = match OU = match CN = supplied [ x509_server ] basicConstraints = CA:false subjectKeyIdentifier = hash authorityKeyIdentifier = keyid, issuer keyUsage = digitalSignature, keyEncipherment extendedKeyUsage = serverAuth crlDistributionPoints = URI:http://pki.kveer.com/my_kveer_vpn_ca.crl [ x509_client ] basicConstraints = CA:false subjectKeyIdentifier = hash authorityKeyIdentifier = keyid, issuer keyUsage = digitalSignature, keyAgreement extendedKeyUsage = clientAuth crlDistributionPoints = URI:http://pki.kveer.com/my_kveer_vpn_ca.crl [ req ] default_bits = 4096 default_md = sha1 distinguished_name = distinguished_name [ distinguished_name ] countryName = Pays countryName_default = FR countryName_min = 2 countryName_max = 2 O = Sujet O_default = Kveer OU = Unité organisationnelle OU_default = VPN Services CN = Common Name
Là j’ai pris l’exemple de mon autorité intermédiaire gérant les certificats pour un VPN. On prendra soin de changer le point de distribution des CRL : chaque autorité de certification doit avoir un point de distribution distinct, sinon il ne sert à rien et risque d’invalider les certificats d’une autre autorité.
Le script bash :
#!/bin/bash #cannot user ecdsa because windows nt 5.x does not support it yet. # prime192v1 seems to be the best one otherwise. CA_KEY=ca.key CA_CERT=ca.crt CONF=kveer.openssl.cnf CONF_TMP=_openssl.conf if [[ ! (-f ca.key && -f ca.crt) ]]; then echo 'There is no CA root' exit 1 fi if [ $# -lt 1 ]; then echo "$0 -n [certificate_name] -p [pathlen] -c [crlurl] [-e] [-s altsubject, -s...]" echo '-e for using an ecdsa key, else an rsa key' exit 2 fi ECDSA=0 REUSE_PKEY=0 REUSE_REQ=0 REGEN=0 ONLY_REQUEST=0 SHA1=0 TEMPLATE= FILENAME= declare -a ALTSUBJECT while getopts 'cen:rs:t:f:1' OPT do case $OPT in c) ONLY_REQUEST=1 ;; e) ECDSA=1 ;; n) NAME=$OPTARG ;; r) REGEN=1 REUSE_PKEY=1 ;; s) ALTSUBJECT+=($OPTARG) ;; 1) SHA1=1 ;; t) TEMPLATE=$OPTARG ;; f) FILENAME=$OPTARG ;; esac done if [ "$FILENAME" = "" ]; then FILENAME=$NAME fi if [ -z "$NAME" ]; then echo 'Use -n to specify the certificate name.' exit 6 fi if [ "$REGEN" -eq 0 -a -f "certs/$FILENAME.crt" ]; then echo 'A certificate with the same name exists already.' exit 5 fi if [ -f "keys/$FILENAME.key" ]; then read -p 'An existing private has been found. Use it ? [yes|no] ' TEST;echo [[ "$TEST" = 'yes' || "$TEST" = 'y' ]] && REUSE_PKEY=1 fi if [ -f "reqs/$FILENAME.csr" ]; then read -p 'An existing certificat request has been found. Use it ? [yes|no] ' TEST;echo [[ "$TEST" = 'yes' || "$TEST" = 'y' ]] && REUSE_REQ=1 fi if [ "$TEMPLATE" = "" ]; then TEMPLATE=x509_server fi # password stty -echo read -p 'Please enter the master password: ' PASSWD;echo stty echo # key generation if [ $REUSE_PKEY -eq 0 ]; then if [ $ECDSA -eq 1 ]; then openssl ecparam -conv_form compressed -name prime256v1 -genkey | openssl ec -aes128 -out tmp.key -passout pass:$PASSWD else openssl genrsa -aes192 -out tmp.key -passout pass:"$PASSWD" 2048 fi else ln -f "keys/$FILENAME.key" tmp.key fi if [ $REUSE_REQ -eq 0 ]; then # generate conf sed -e "s/^CN_default.*/CN_default = $NAME/g" \ $CONF > $CONF_TMP if [ $SHA1 = 1 ]; then sed -i -e 's/sha256/sha1/g' $CONF_TMP fi if [ ${#ALTSUBJECT[@]} -gt 0 ]; then dns=0 ip=0 sed -i -e "/\\[[ $TEMPLATE \\]]/a subjectAltName = @subjectAltName" -e '$a [ subjectAltName ]' $CONF_TMP #sed -i "/\[ $TEMPLATE \]/a subjectAltName = @subjectAltName" -i '$a [ subjectAltName ]' $CONF_TMP for s in "${ALTSUBJECT[@]}" do if [[ $s =~ ^([0-9]+\.){3}[0-9]+$ || $s =~ ^[0-9\:]+$ ]]; then let ip++ sed -i "/\[ subjectAltName \]/a IP.$ip=$s" $CONF_TMP else let dns++ sed -i "/\[ subjectAltName \]/a DNS.$dns=$s" $CONF_TMP fi done fi # cert generation openssl req -new -extensions $TEMPLATE -key tmp.key -passin pass:$PASSWD -config $CONF_TMP -utf8 -out tmp.csr else cp "reqs/$FILENAME.csr" tmp.csr fi [[ $ONLY_REQUEST == 0 ]] && openssl ca -config $CONF_TMP -days 3650 -extensions $TEMPLATE -keyfile $CA_KEY -key $PASSWD -cert $CA_CERT -in tmp.csr -out "$FILENAME.crt" if [[ $ONLY_REQUEST == 0 && ! -f "$FILENAME.crt" || ! -f tmp.key || ! -f tmp.csr ]]; then echo 'Something wrong, stopping here' exit 3 fi [[ $ONLY_REQUEST == 0 ]] && mv "$FILENAME.crt" "certs/$FILENAME.crt" mv tmp.key "keys/$FILENAME.key" mv tmp.csr "reqs/$FILENAME.csr"
Une seule ligne change en fonction de si l’on souhaite un certificat serveur ou client, c’est celle où la signature du certificat intervient avec le paramètre -extensions, qui indique ou bien la section x509_server pour un certificat serveur, ou bien la section x509_client pour un certificat client.
Les certificats EV
Ces certificats EV, ou extended validation, sont un peu spéciaux, précieux même en ce sens que du point de vue de l’utilisateur, cela rend la barre du navigateur vert, alors qu’avec les autres certificats, le navigateur se content de rendre la barre bleue ou bien d’afficher uniquement un cadenas.
ll s’agit de certificats ayant des informations supplémentaires, tout comme le certificat d’autorité qui l’a émis, dont certains non-référencé par OpenSSL d’ailleurs, donc à rajouter manuellement dans la section new_oids . Mais la liste des certificats d’autorité pouvant émettre des certificats EV est codé en dur dans chaque navigateur, donc même s’il est possible de forger un certificat en tout point identique au niveau de la liste des attributs contenus à un vrai certificat EV, y compris pour un usage interne, le navigateur ne reconnaîtra jamais un certificat que vous avez créée comme EV, il sera considéré comme un certificat standard.
Les autres script
Afin de parfaire l’autorité, d’autres script sont nécessaires.
Script de génération de la liste de révocation de certificats
Cette liste, aussi appelée plus simplement CRL, publiquement accessible et signée, permet d’informer sur les certificats qui ont été prématurément révoqués, pour une raison ou une autre (certificat compromis, perdu, clé privée faible, révocation d’accès…)
#!/bin/bash # password stty -echo read -p 'Please enter the master password: ' PASSWD;echo stty echo openssl ca -gencrl -crldays 365 -keyfile ca.key -key $PASSWD -cert ca.crt -config kveer.openssl.cnf | openssl crl -outform DER -out crl.crl
Une fois la liste générée, il faudra la rendre accessible à l’adresse qui a été spécifiée par le paramètre crlDistributionPoints . Ce paramètre est présent dans chaque certificat généré, c’est de cette manière que le point de communication est connu. A noté que la CRL ne contenant que les numéros de séries des certificats émis par une autorité de certification, une CRL est propre à chaque autorité de certification.
Tout comme un certificat, la CRL a une durée de vie, ici fixé à 1 an, dont le but est de forcer régulièrement le programme à récupérer la version à jour de la liste des certificats périmés. Surtout qu’un certificat révoqué ne se fait que lorsqu’un problème de sécurité est rencontré, il est impératif que l’information se propage au plus vite afin de limiter l’utilisation abusive d’un certificat périmé.
Remarque : tous les certificats émis par une même autorité de certification doivent avoir la même adresse dans le paramètre crlDistributionPoints .
Corollaire 1 : Pour une autorité de certification intermédiaire B, son point de distribution est donc celle décidé par l’autorité A ayant signée le certificat intermédiaire, dans mon cas, c’est donc l’autorité racine.
Corollaire 2 : Une autorité intermédiaire B ne peut pas se révoquer elle-même. Son autorité parente A peut révoquer B et publier un nouveau CRL signé avec la clé de l’autorité parente A.
Script de révocation
Ce script permet de révoquer un certificat émis, c’est-à-dire le rendre invalide avant sa date d’expiration.
#!/bin/bash CA_KEY=ca.key CA_CERT=ca.crt CONF_CA=kveer.openssl.cnf if [[ ! (-f ca.key && -f ca.crt) ]]; then echo 'There is no CA root' exit 1 fi if [[ $# < 1 ]]; then echo "$0 certificate_name" exit 2 fi # password stty -echo read -p 'Please enter the master password: ' PASSWD;echo stty echo openssl ca -revoke certs/$1.crt -keyfile ca.key -key $PASSWD -cert ca.crt -config $CONF_CA
Ce script est inutile tout seul. Une fois un certificat révoqué, la CRL doit être mise à jour afin de publier l’information.
Script d’export au format PFX
Le seul moyen d’importer un certificat avec sa clé privée dans un magasin de certificats géré par Windows est de passer par un package PFX ou PKCS12. Il s’agit d’un standard permettant d’échanger du contenu sensible à l’aide d’un mot de passe.
#!/bin/bash #cannot user ecdsa because windows nt 5.x does not support it yet. # prime192v1 seems to be the best one otherwise. if [[ $# < 1 ]]; then echo "$0 certificate_name" exit 2 fi # Generate a random password # $1 = number of characters; defaults to 32 # $2 = include special characters; 1 = yes, 0 = no; defaults to 1 function randpass { [ "$2" == "0" ] && CHAR="[:alnum:]" || CHAR="[:graph:]" cat /dev/urandom | tr -cd "$CHAR" | head -c ${1:-32} } EXPORT=$(randpass 20 0) echo "Note this import password somewhere:" $EXPORT # password stty -echo read -p "Please enter the password: " PASSWD;echo stty echo cat "keys/$1.key" "certs/$1.crt" ca.crt ../ca.crt | openssl pkcs12 -export -name "$1" -passout pass:$EXPORT -passin pass:$PASSWD -aes192 -out "certs/$1.pfx" -CAfile ca.crt
Il suffit de spécifier le nom d’un certificat précédemment généré et le script génère le PFX ainsi que son mot de passe de manière aléatoire.