Accélérer TLS : réutilisation des sessions

Vincent Bernat

La réutilisation des session est l’un des mécanismes les plus importants pour améliorer les performances de TLS : le client peut effectuer une poignée de mains raccourcie en soumettant dès sa seconde requête une donnée présentée précédemment par le serveur, permettant ainsi de réduire la latence et les temps de calcul. Il existe deux mécanismes distincts à cet effet: les identifiants de session qui sont décrits dans la RFC 5246 et les tickets de session qui sont présentés dans la RFC 5077.

Mise à jour (08.2018)

Bien que le contenu de cet article soit toujours pertinent, il est important de comprendre qu’il a été rédigé en 2011 et qu’il ne prend pas en compte certains aspects contemporains, notamment la progression de la compatibilité avec la RFC 5077 et la chute de RC4 en tant qu’algorithme approprié. De plus, TLS 1.3 remplace à la fois les identifiants de session et les tickets de session par un nouveau mécanisme.

Théorie#

Pour établir une connexion TLS, le client et le serveur doivent échanger quatre messages. Avec une latence de 50 ms, cet échange correspond à une pénalité de 200 ms (sans compter la poignée de mains TCP). De plus, afin de s’échanger un secret commun, les deux parties doivent mettre en oeuvre des primitives cryptographiques à clé publique qui sont coûteuses en temps de calcul.

Poignée de mains TLS complète
Une poignée de main TLS complète

Pour éviter ces quatre échanges lors de chaque requête, le client peut tenter d’initier une poignée de mains raccourcie qui permet d’économiser un aller-retour (100 ms) ainsi que la partie la plus coûteuse en temps de calcul.

Poignée de mains TLS raccourcie
Une poignée de main TLS raccourcie

Deux mécanismes peuvent être utilisés pour mettre en œuvre cette poignée de main raccourcie :

  1. Quand le serveur envoie le message « Server Hello », il y inclut un identifiant de session. Le client doit stocker cet identifiant et le présenter dans le message « Client Hello » lors de la prochaine poignée de mains. Le serveur consulte son cache pour trouver la session correspondant à cet identifiant et embraye sur la poignée de main raccourcie. Dans le cas où la session n’est pas présente dans le cache, il génère un nouvel identifiant de session et effectue la poignée de main complète. Les détails se trouvent dans la RFC 5246. Il s’agit de la façon la plus courante de reprendre une session TLS car ce mécanisme existe depuis longtemps.

  2. Dans le dernier échange d’une poignée de mains complète, le serveur peut inclure un message « New Session Ticket » (non représenté dans la poignée de mains illustrée ci-dessus). Ce message comprend un état complet de la session TLS incluant le secret partagé et les algorithmes cryptographiques à utiliser, le tout chiffré et protégé par une clé connue du serveur. Ce bloc de données opaque est un ticket de session. Les détails se trouvent dans la RFC 5077 qui remplace la RFC 4507.

Le mécanisme de tickets est une extension de TLS. Le client indique s’il supporte celle-ci en envoyant un ticket vide dans le message « Client Hello » à l’établissement de la session. Le serveur indique qu’il support ce mécanisme en incluant également un ticket vide dans le message « Server Hello » en réponse. En cas d’incompatibilité, le mécanisme classique avec identifiant de session est utilisé.

La RFC 5077 identifie plusieurs cas où les tickets sont préférables aux identifiants de session. Le principal avantage à les utiliser réside dans la possibilité d’éviter de maintenir un cache des sessions du côté du serveur : le serveur économise ainsi de la mémoire et n’a plus besoin d’un mécanisme pour partager le cache des sessions avec ses compagnons s’il est derrière un répartiteur de charge.

Support dans les navigateurs#

Les identifiants de session font partie de TLS depuis un petit bout de temps et sont donc bien supportés aussi bien côté client que côté serveur. Par contre, les tickets de session sont une extension optionnelle de TLS et sont beaucoup moins répandus. Le code nécessaire à leur utilisation est présent dans OpenSSL 0.9.8f (octobre 2007), dans GnuTLS 2.9.3 (août 2009) et dans NSS, utilisé par la plupart des navigateurs, 3.12 (juin 2008). Schannel, l’implémentation TLS de Microsoft, sait utiliser les tickets de session depuis Windows 8.1 ou Windows 2012 R2.

Afin de vérifier si un navigateur supporte la réutilisation des sessions avec et sans ticket, j’ai mis au point un petit serveur web qui écoute sur plusieurs ports avec des configurations différentes (cache de sessions activé ou non, support des tickets activé ou non). Un peu de JavaScript permet de lancer quelques tests sur les capacités du navigateur à reprendre une session.

Capture d'écran de rfc5077-server
Exemple de test avec Chromium 14

Cette approche rend difficile la récolte de résultats exhaustifs puisqu’il faut installer beaucoup de navigateurs pour obtenir un panorama complet. Notons que bizarrement, le navigateur d’Android 2.3.4 n’utilise ni les tickets, ni les identifiants !

Une méthode alternative est d’espionner les messages « Client Hello » qui transitent sur le réseau. Si un client essaie de reprendre une session, il utilisera un identifiant de session non vide ou un ticket. Il est possible de construire un programme analysant des trames PCAP pour automatiser cette tâche et obtenir des statistiques intéressantes. Voici quelques faits extraits d’une capture de 300 000 sessions (pour 38 000 clients) vers un grand site de support pour un opérateur français :

  • 35 % des requêtes indiquent un support des tickets de session ;
  • 53,3 % des requêtes négocient une poignée de mains raccourcie sans utilisation de tickets contre 25,6 % avec les tickets, ce qui laisse une requête sur quatre n’est pas une reprise d’une précédente session ;
  • 67,2 % des requêtes utilisent l’extension TLS SNI ;
  • 86,7 % des requêtes utilisent TLS 1.0 ; le reste utilise SSL 3.0 ; quasiment personne n’utilise TLS 1.1 ou 1.2 ;
  • en moyenne, un client effectue 8 poignées de mains ;
  • les algorithmes supportés par tous les clients sont 3DES-SHA, RC4-MD5 et RC4-SHA.

En corrélant les logs des serveurs web avec les requêtes, il est possible de classer les navigateurs en deux catégories : ceux qui supportent la RFC 5077 (Chrome et Firefox) et ceux qui ne la supportent pas (Internet Explorer, Opera 9.80, Safari pour macOS et iOS et le navigateur d’Android).

Support côté serveur#

Il n’y a pas de recette magique pour configurer un serveur web de manière appropriée pour permettre la réutilisation des sessions TLS. Voici toutefois quelques grandes directions :

  • si le serveur utilise plusieurs processus, un cache de sessions en mémoire partagée (ou sur disque) est nécessaire ; concernant les tickets, cela fonctionne généralement tout seul ;
  • avec un répartiteur de charge local, une façon simple de contourner les problèmes qui peuvent survenir est de s’assurer qu’un client sera toujours affecté à un même frontal (avec une correspondance statique basée sur un condensé de l’IP source ou avec une table de persistance dynamique) ; si le load balancer comprend TLS, il est aussi possible d’utiliser l’identifiant de session (et de désactiver les tickets1) pour construire la table de persistance ;
  • si le conseil précédent ne vous convient pas, le cache des sessions doit être partagé entre plusieurs serveurs en utilisant, par exemple, memcached ; les tickets doivent alors être désactivés ;
  • l’utilisation d’un répartiteur de charge global (DNS) ne nécessite pas, a priori, de partager un cache de sessions entre les pools géographiques : un client reste généralement attaché a un seul d’entre eux.

Il est important de noter que seul un client sur trois supporte les tickets de session. Il n’est donc pas possible de se fier uniquement à ce mécanisme pour permettre de reprendre des sessions : le cache de sessions est indispensable.

Cache de sessions avec Apache & nginx#

Apache dispose de deux moteurs TLS. Le premier est mod_ssl qui utilise OpenSSL. La mise en place d’un cache de sessions partagé en mémoire se fait avec la directive SSLSessionCache. Seule la version de développement dispose d’un cache distribué reposant sur memcached. Le second est mod_gnutls qui utilise GnuTLS. Le cache s’active avec GnuTLSCache. Il est possible de le distribuer avec memcached. Dans ce cas, il faut désactiver les tickets qui ne peuvent pas être partagés.

Avec nginx, le cache de sessions s’active avec la directive ssl_session_cache. Il n’y a pas de support pour un cache distribué mais Matt Palmer a quelques patchs pour ajouter le support de memcached. Toutefois, ces patchs peuvent avoir un impact important sur les performances : récupérer une session depuis memcached se fait de manière synchrone (principalement en raison d’une limitation de l’API d’OpenSSL qui ne permet pas d’enregistrer une fonction de rappel asynchrone avec SSL_CTX_sess_set_get_cb()).

À titre d’illustration, j’ai implémenté de manière similaire un support pour memcached dans stud, un proxy réseau destiné à terminer les connexions TLS de manière efficace. Les limitations sont similaires à celles indiquées pour nginx : les performances se dégradent fortement. Pour plus de détails, jetez un œil sur la demande d’inclusion correspondante.

Partager les tickets#

La RFC 5077 indique que les tickets permettent de répartir les requêtes sur plusieurs frontaux (sans utiliser un cache distribué). Cependant, à ma connaissance, il n’existe actuellement aucun serveur web implémentant cette fonctionnalité. Lors de l’initialisation de la pile TLS, le serveur génère aléatoirement des clefs qui permettront de protéger et chiffrer les tickets. Par exemple, pour OpenSSL, dans ssl/ssl_lib.c :

/* Setup RFC4507 ticket keys */
if ((RAND_pseudo_bytes(ret->tlsext_tick_key_name, 16) <= 0)
        || (RAND_bytes(ret->tlsext_tick_hmac_key, 16) <= 0)
        || (RAND_bytes(ret->tlsext_tick_aes_key, 16) <= 0))
        ret->options |= SSL_OP_NO_TICKET;

Ainsi, avec une répartition de charge classique, les tickets doivent être désactivés car les serveurs disposent chacun de leurs propres clefs et ne peuvent donc pas accepter les tickets générés par le voisin. Il est possible de contourner ce problème en générant les clefs à partir d’une base commune, par exemple un secret signé avec la clé privée. J’ai tenté une telle approche avec stud. Voici la version simplifiée (pas de gestion des erreurs, pas d’allocation) de l’implémentation proposée :

unsigned char keys[48];
EVP_PKEY *pkey = grab_private_key();

/* To get our key, we sign the seed with the private key */
unsigned int siglen;
unsigned char sign[LARGE_ENOUGH];
EVP_MD_CTX mdctx;
EVP_MD_CTX_init(&mdctx);
EVP_SignInit(&mdctx, EVP_sha256());
EVP_SignUpdate(&mdctx, some_secret, strlen(some_secret));
EVP_SignFinal(&mdctx, sign, &siglen, pkey);

/* And we keep only the first bytes. */
memcpy(keys, sign, sizeof(keys));

/* Tell OpenSSL to use these keys */
SSL_CTX_set_tlsext_ticket_keys(ctx, keys, sizeof(keys));

Mise à jour (11.2011)

Paul Querna a ajouté dans Apache HTTPD la possibilité de spécifier les clefs protégeant les tickets. Deux nouvelles directives sont fournies à cet effet : SSLTicketKeyFile et SSLTicketKeyDefault.

Mise à jour (11.2011)

Si vous attachez de l’importance à la propriété de Perfect Forward Secrecy fournie par des suites telles que DHE-RSA-AES128-SHA, sachez que l’utilisation des tickets telle que décrite ci-dessus l’affaiblit grandement. Les clefs protégeant les tickets doivent être changées très souvent et ce changement doit s’effectuer de manière synchronisée.

Tests#

Il est possible de mener les tests avec uniquement openssl s_client et openssl sess_id. Cependant, tester de manière exhaustive un ensemble de serveurs de cette façon peut être assez long. J’ai donc écrit un outil automatisant les tests avec et sans tickets. Voici un exemple de sortie :

twitter.com RFC session resume
Test de twitter.com sur la réutilisation des sessions TLS

L’outil va effectuer cinq connexions successives pour chaque IP, avec et sans tickets. Dans l’idéal, pour chaque IP, les quatre dernières connexions doivent réutiliser la session TLS de la première connexion. Ici, les serveurs Twitter semblent configurer correctement. Il n’y a pas de support des tickets, soit parce que les serveurs ne supportent pas cette fonctionnalité soit parce qu’elle a été désactivée pour permettre la mise en œuvre d’un cache distribué.


  1. La RFC 5077 décrit les interactions entre les identifiants et les tickets. Lorsque le serveur prévoit de répondre avec un ticket, il devrait envoyer un identifiant vide. Le client peut également utiliser un identifiant vide ou alors en générer un de son cru. Cependant, en pratique, il semble que le serveur renvoie un identifiant de session valide et que le client l’utilise également. Cependant, c’est un aspect à explorer si l’on souhaite utiliser les identifiants de session comme méthode de répartition de charge tout en gardant les tickets activés. ↩︎