Session reuse is one of the most important mechanism to improve TLS performance: by submitting an appropriate blob to the server, a client is able to trigger an abbreviated handshake, improving latency and computation time. There exists two distinct ways to achieve session reuse: session identifiers as described in RFC 5246 and session tickets as depicted in RFC 5077.
While the content of this article is still technically sound, ensure you understand it was written by the end of 2011 and therefore doesn’t take into account many important aspects, like the compatibility progression of implementations with RFC 5077 and the fall of RC4 as an appropriate cipher.
Theory of operation⚓︎
To establish a TLS connection, four messages need to be exchanged between client and server. With a latency of 50 ms, we have a 200 ms overhead to establish the connection (plus TCP handshake). Moreover, to share a common secret, both the client and the server needs to achieve some public-key cryptographic operations which are costly, computation-wise.
To avoid a full TLS handshake each time a client request a resource, it is possible for the client to request an abbreviated handshake, saving a complete round-trip (100 ms) and avoiding the costliest part of the full TLS handshake.
Two mechanisms can be used to accomplish an abbreviated handshake:
When the server sends the “Server Hello” message, it can include a session identifier. The client should store it and present it in the “Client Hello” message of the next session. If the server finds the corresponding session in its cache and accepts to resume the session, it will send back the same session identifier and will continue with the abbreviated TLS handshake. Otherwise, it will issue a new session identifier and switch to a full handshake. This mechanism is detailed in RFC 5246. It is the most common mechanism because it exists since earlier versions of TLS.
In the last exchange of a full TLS handshake, the server can include a “New Session Ticket” message (not represented in the handshake described in the picture) which will contain the complete session state (including the master secret negotiated between the client and the server and the cipher suite used). Therefore, this state is encrypted and integrity-protected by a key known only by the server. This opaque datum is known as a session ticket. The details lie in RFC 5077 which supersedes RFC 4507.
The ticket mechanism is a TLS extension. The client can advertise its support by sending an empty “Session Ticket” extension in the “Client Hello” message. The server will answer with an empty “Session Ticket” extension in its “Server Hello” message if it supports it. If one of them does not support this extension, they can fallback to the session identifier mechanism built into TLS.
RFC 5077 identifies situations where tickets are desirable over session identifiers. The main improvement is to avoid to maintain a server-side session cache since the whole session state is remembered by the client, not the server. A session cache can be costly in term of memory and difficult to share between multiple hosts when requests are load-balanced across servers.
Session identifiers are part of TLS and are therefore supported since
a long time by clients and servers.
However, session tickets are
an optional TLS extension and therefore, the support is not as
widespread as for session identifiers. Support for tickets was
added in OpenSSL 0.9.8f (October 2007). In GnuTLS, this is version 2.9.3 (August 2009). For NSS, the set of libraries behind most browsers,
support of RFC 5077 is in version 3.12 (June 2008). For Schannel, Microsoft implementation of TLS,
support was added in Windows 8.1 and Windows 2012 R2.
It is difficult to get extensive results this way since you need to install a lot of browsers to get something valuable. Surprisingly, Android 2.3.4 browser (as shipped by Cyanogen Mod) does not support any session resume.
Another way to check if a browser supports session resume is to use a sniffer and to spot “Client Hello” messages. If a client tries to resume a session, it will use an appropriate session identifier or send a ticket. A simple program parsing PCAP files allows one to automate this task and get some interesting statistics. Here are random facts from more than 300 000 requests (38 000 clients) to a customer care service of an important French telco:
- 35% of requests exhibit support of tickets;
- 53.3% of requests ask to resume without tickets; 25.6% with tickets;
- 67.2% of requests exhibit support of SNI;
- 86.7% of requests use TLS 1.0; the remaining uses SSL 3.0; almost no TLS 1.1/1.2;
- in average, a client execute 8 TLS handshakes;
- ciphers supported by all clients are 3DES-SHA, RC4-MD5 and RC4-SHA.
With some log analysis to match each requests with the appropriate user agent, we can split the world in two parts: browsers supporting RFC 5077 (Chrome and Firefox) and browsers not supporting RFC 5077 (Internet Explorer, Opera 9.80, Safari for MacOS and iOS and Android browser).
Web server support⚓︎
There is no unique recipe to configure a web server to handle appropriately and efficiently session resume. Here are some generic directions:
- if the web server uses multiple processes, you need to configure a shared memory session cache; usually, tickets will work just fine in this setup;
- if you use local load-balancing, one easy way to avoid any problem is to use ensure that one IP is mapped to one server (either statically with hashing or dynamically with a stick table); if your load-balancer understands TLS, you can also use session identifier (and disable tickets1) to build a stick table;
- if you don’t want to apply the previous advice, you need to share the session cache between the servers with something like memcached and disable tickets;
- if you use global load-balancing (DNS based), there is no need to ensure that session resumption works across geographic pools since a user will usually stick to one pool.
Remember that only one client out of three supports TLS tickets. Therefore, you cannot rely on them to ensure proper session resume. You need a session cache.
Setting a session cache with Apache & nginx⚓︎
Apache features two different TLS engines. The first one is
mod_ssl and uses OpenSSL as backend. It is possible to setup
a shared session cache with
SSLSessionCache but the
memcached-enabled session cache is
available only in the development branch. You can
also get one by switching to mod_gnutls backend. The
cache is enabled with
GnuTLSCache. In this case, do not enable
tickets because they cannot be shared.
With nginx, you need to enable the shared session cache with
ssl_session_cache. Currently, it lacks a memcached-enabled session
cache but Matt Palmer has some patches to add it to
the 0.8 branch. However, these patches can have a serious impact on
nginx performance since retrieving a session from memcached is done
synchronously (mostly because a limitation in OpenSSL design who does
not allow us to register asynchronous callbacks with
As a proof of concept, I have also added a similar feature to stud, the scalable TLS unwrapping daemon, a very efficient network proxy terminating TLS connections. The same limitation as for nginx applies: performances will suffer. Look at the pull request for more details.
RFC 5077 tells tickets allow us to load-balance requests
across servers. However, to the best of my knowledge, there is
currently no web server able to share tickets across a pool. Actually,
when a server initializes its TLS stack, it will randomly generate
some keys that will be used to protect and encrypt tickets. In
/* 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;
Therefore, with a round-robin load-balanced pool of servers, tickets have to be disabled because servers would not accept tickets from neighbors. One way to solve this is to generate keys deterministically by hashing some common secret with the private key. I have implemented this approach for stud. Here is a simplified version (no error handling, no allocation) of the pull request for this feature:
unsigned char keys; 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));
Paul Querna has
added support for configuring keys protecting session tickets in
Apache HTTPD. This adds two new directives:
If you care about perfect forward secrecy like
the one provided by cipher suites like
tickets this way will weaken it. Keys should be rotated in some
While tests can be done with just
openssl s_client and
sess_id, testing all aspects can take a lot of time. For this
purpose, I have written a
client testing session resume with and without tickets. Here
is a typical result:
The program will try to establish five consecutive TLS connections for each IP without and with tickets. For each IP, the last four TLS connections should always reuse the first TLS session. Here, Twitter web servers seem properly configured: for each IP, session resume is successful. Their web servers do not support tickets or they may have disabled it because they share the session cache between servers inside the same pool.
RFC 5077 describes interactions between session identifiers and session tickets. When using tickets, the server should send back an empty session identifier. The client may present an empty session identifier or generate one. However, for all practical purposes, it seems that servers send back a non-empty session identifier and clients just stick to this identifier. Please, investigate if you want to use TLS session identifiers for load-balancing while keeping tickets enabled. ↩︎