/
2011-sessions-ssl-rfc5077.html
315 lines (266 loc) · 15.5 KB
/
2011-sessions-ssl-rfc5077.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
---
title: 'Accélérer TLS : réutilisation des sessions'
uuid: 38894698-5e3a-4d47-a4d6-6f5edb836419
attachments:
"https://github.com/vincentbernat/rfc5077": dépôt Git pour les outils associés
tags:
- network-tls
---
La réutilisation des session est l'un des mécanismes les plus
importants pour [améliorer les performances de TLS][1] : 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][rfc5246] et les
**tickets de session** qui sont présentés dans la [RFC 5077][rfc5077].
[1]: [[fr/blog/2011-ssl-benchmark.html]]
!!! "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](rfc://8446#section-2.2).
[TOC]
# 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][s1]
[s1]: [[!!images/benchs-ssl/ssl-handshake.svg]] "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][s2]
[s2]: [[!!images/benchs-ssl/ssl-handshake-with-resume.svg]] "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][rfc5246]. 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][rfc5077] qui remplace la [RFC 4507][rfc4507].
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][rfc5077] 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. <del>Par contre, les tickets de session sont une extension
optionnelle de TLS et sont beaucoup moins répandus.</del> Le code
nécessaire à leur utilisation est présent dans [OpenSSL][openssl]
0.9.8f (octobre 2007), dans [GnuTLS][gnutls] 2.9.3 (août 2009) et
dans [NSS][nss], utilisé par la plupart des navigateurs, 3.12 (juin
2008). [Schannel][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][rfc5077-server] 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][s3]
[s3]: [[!!images/benchs-ssl/rfc5077-screenshot-v2.png]] "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][rfc5077-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][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 tickets[^1]) 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][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.
[^1]: La [RFC 5077][rfc5077] 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.
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][modssl] 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*][modssl-memcached]. Le
second est [mod_gnutls][modgnutls] 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*][nginxcaching]. 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][pr].
[pr]: https://github.com/bumptech/stud/pull/29
## Partager les tickets
La [RFC 5077][rfc5077] 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` :
::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][pr2] :
::c
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));
[pr2]: https://github.com/bumptech/stud/pull/30
!!! "Mise à jour (11.2011)" [Paul Querna](https://twitter.com/pquerna/) a ajouté
dans Apache HTTPD la
[possibilité de spécifier les clefs protégeant les tickets][ticket-httpd]. Deux
nouvelles directives sont fournies à cet effet : `SSLTicketKeyFile` et
`SSLTicketKeyDefault`.
[ticket-httpd]: https://svn.apache.org/viewvc?view=revision&revision=1200040 "Commit 1200040 dans le SVN de Apache HTTPD"
!!! "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][rfc5077-client]. Voici
un exemple de sortie :
![twitter.com RFC session resume][s4]
[s4]: [[!!images/benchs-ssl/rfc5077-twitter.png]] "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é.
*[SSL]: Secure Sockets Layer
*[TLS]: Transport Layer Security
[tls]: https://fr.wikipedia.org/wiki/Transport_Layer_Security
[rfc4507]: rfc://4507
[rfc5077]: rfc://5077
[rfc5246]: rfc://5246
[openssl]: https://www.openssl.org/
[gnutls]: http://www.gnu.org/software/gnutls/
[nss]: https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS
[schannel]: https://docs.microsoft.com/en-us/windows/win32/secauthn/secure-channel
[rfc5077-server]: https://github.com/vincentbernat/rfc5077/blob/master/rfc5077-server.c
[rfc5077-pcap]: https://github.com/vincentbernat/rfc5077/blob/master/rfc5077-pcap.c
[rfc5077-client]: https://github.com/vincentbernat/rfc5077/blob/master/rfc5077-client.c
[sni]: https://en.wikipedia.org/wiki/Server_Name_Indication
[beast]: http://www.schneier.com/blog/archives/2011/09/man-in-the-midd_4.html
[memcached]: http://memcached.org/
[modssl]: http://www.modssl.org/
[modgnutls]: http://www.outoforder.cc/projects/httpd/mod_gnutls/
[nginxcaching]: https://www.hezmatt.org/~mpalmer/blog/2011/06/28/ssl-session-caching-in-nginx.html
[stud]: https://github.com/bumptech/stud
[modssl-memcached]: https://paul.querna.org/articles/2010/07/10/overclocking-mod_ssl/