Opened 3 years ago

Closed 5 months ago

#1892 closed defect (fixed)

TLSv1.3 session resumption - session tickets renewing

Reported by: bartebor@… Owned by:
Priority: minor Milestone:
Component: nginx-core Version: 1.16.x
Keywords: TLS Cc:
uname -a: Linux a974b548d355 5.3.0-1-amd64 #1 SMP Debian 5.3.7-1 (2019-10-19) x86_64 x86_64 x86_64 GNU/Linux
nginx -V: nginx version: nginx/1.16.1 (nginx)
built by gcc 7.3.1 20180303 (Red Hat 7.3.1-5) (GCC)
built with OpenSSL 1.1.1c 28 May 2019
TLS SNI support enabled
configure arguments: --with-ld-opt=-Wl,-rpath,/usr/lib64 --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-http_ssl_module --with-http_realip_module --with-http_addition_module --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_stub_status_module --with-http_auth_request_module --with-http_geoip_module=dynamic --with-threads --with-stream --with-stream_ssl_module --with-http_slice_module --with-mail --with-mail_ssl_module --with-file-aio --with-ipv6 --with-libatomic --with-openssl=../openssl --with-pcre=../pcre --with-pcre-jit --with-http_v2_hpack_enc --with-compat --add-module=../ngx_devel_kit --add-module=../headersmore --add-dynamic-module=../pushstream --add-module=../lua --add-module=../stream-lua --add-module=../nginx-module-vts --add-module=../nginx-auth-ldap --add-module=../nginx-module-sts --add-module=../nginx-module-stream-sts --add-module=../ngx_brotli --add-module=../nginx_upstream_check_module --add-dynamic-module=../ngx_aws_auth --add-dynamic-module=../nginx-module-opentracing/opentracing --add-dynamic-module=../nginx-rtmp-module --with-http_v2_module --with-cc-opt='-O3 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mmmx -msse -msse2 -DTCP_FASTOPEN=23 -Wno-error=strict-aliasing' --with-openssl-opt='no-ssl3 no-dtls no-rc5 enable-weak-ssl-ciphers' --build=nginx --with-debug

Description

When using TLSv1.3, sessions tickets are not renewed while decrypting tickets and session resumption works only for two requests, because this is the default number of issued tickets.

On the contrary, default session ticket handler in openssl (used when no callbacks were registered via SSL_CTX_set_tlsext_ticket_key_cb) renews session ticket whenever ticket is decrypted when TLSv1.3 is in use.

Nginx provides its own ticket callback and renews session ticket only when client came with a ticket encrypted with an expired key (ssl_session_ticket_key rotated).

I believe ngx_ssl_session_ticket_key_callback should return 2 not only when expired key was used, but also when TLSv1.3 is in use.

Simple working solution (not taking into account future TLS versions):

--- nginx-1.16.1-orig/src/event/ngx_event_openssl.c     2019-11-08 12:59:34.026387380 +0100
+++ nginx-1.16.1/src/event/ngx_event_openssl.c  2019-11-18 11:48:24.872995156 +0100
@@ -4138,7 +4222,8 @@
             return -1;
         }
 
-        return (i == 0) ? 1 : 2 /* renew */;
+        // renew ticket when using TLSv1.3 or ticket was encrypted with expired key
+        return (i > 0 || ngx_strcmp(SSL_get_version(ssl_conn), "TLSv1.3") == 0) ? 2 /* renew */ : 1;
     }
 }
 

Change History (4)

comment:1 by Seirdy, 8 months ago

In the meantime, you can rotate tickets with a script like https://github.com/GrapheneOS/nginx-rotate-session-ticket-keys. You can use the provided systemd units/timers or add it to your crontab.

Relevant nginx config: https://github.com/GrapheneOS/grapheneos.org/blob/c78cf741be1148eb2378bac071954c813750b3e0/nginx/nginx.conf#L50

comment:2 by Maxim Dounin, 6 months ago

Thanks for reporting this. I did some testing with various browsers, notably:

  • Safari on Mac, version 15.2 (17612.3.6.1.6)

With TLS 1.2, only reuses sessions with ssl_session_cache, but not with session tickets. Further, a session is reused only once.
With TLS 1.3, does not reuse sessions at all regardless of nginx settings. Likely because all session reuse in TLS 1.3 uses session tickets.

  • Firefox on Mac, version 96.0.1 (64-bit)

With TLS 1.2, only reuses session with session tickets, but not with ssl_session_cache (with ssl_session_tickets off;).
With TLS 1.3, reuses sessions with any settings. That's expected, as using session cache with TLS 1.3 effectively means sending session id as a ticket.

  • Chrome on Mac, version 96.0.4664.110 (Official Build) (x86_64)

With TLS 1.2, reuses sessions with both session cache and session tickets.
With TLS 1.3, reuses sessions with both session cache and session tickets. With ticket keys explicitly set via ssl_session_ticket_key, only reuses session twice after the initial handshake, using different session tickets obtained during the initial handshake.

  • Opera on Mac, version 83.0.4254.19 (x86_64)

With TLS 1.2, reuses sessions with both session cache and session tickets.
With TLS 1.3, reuses sessions with both session cache and session tickets. With ticket keys explicitly set via ssl_session_ticket_key, only reuses session twice after the initial handshake, using different session tickets obtained during the initial handshake.

Summing the above, it looks like the Chrome and Chrome-based browsers only use each session ticket once with TLS 1.3, and need tickets to be re-generated on each connection.

Most likely it is done as per this recommendation in RFC 8446 / TLS 1.3:

C.4.  Client Tracking Prevention

   Clients SHOULD NOT reuse a ticket for multiple connections.  Reuse of
   a ticket allows passive observers to correlate different connections.
   Servers that issue tickets SHOULD offer at least as many tickets as
   the number of connections that a client might use; for example, a web
   browser using HTTP/1.1 [RFC7230] might open six connections to a
   server.  Servers SHOULD issue new tickets with every connection.
   This ensures that clients are always able to use a new ticket when
   creating a new connection.

I personally don't see how passive observers correlating different connections might be a threat here, since TCP already makes it possible to correlate different connections by using client addresses. Anyway, this probably needs addressing to ensure proper session reuse in Chrome and Chrome-based browsers when using ssl_session_ticket_key.

OpenSSL's default behaviour for built-in tickets handling is to always renew tickets in case of TLS 1.3. We probably should do the same.

Just for the record, with BoringSSL tickets are always renewed with TLS 1.3, so sessions are properly reused regardless of ssl_session_ticket_key being used. With LibreSSL, session reuse with TLS 1.3 does not seem to be implemented yet (as of LibreSSL 3.4.2).

Slightly cleaned up patch:

# HG changeset patch
# User Maxim Dounin <mdounin@mdounin.ru>
# Date 1642737110 -10800
#      Fri Jan 21 06:51:50 2022 +0300
# Node ID cff51689a4a182cb11cba2eb9303e2bc21815432
# Parent  96ae8e57b3dd1b10f29d3060bbad93b7f9357b92
SSL: always renewing tickets with TLSv1.3 (ticket #1892).

Chrome only use TLS session tickets once with TLS 1.3, likely following
RFC 8446 Appendix C.4 recommendation.  With OpenSSL, this works fine with
built-in session tickets, since these are explicitly renewed in case of
TLS 1.3 on each session reuse, but results in only two connections being
reused after an initial handshake when using ssl_session_ticket_key.

Fix is to always renew TLS session tickets in case of TLS 1.3 when using
ssl_session_ticket_key, similarly to how it is done by OpenSSL internally.

diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c
--- a/src/event/ngx_event_openssl.c
+++ b/src/event/ngx_event_openssl.c
@@ -4448,7 +4448,21 @@ ngx_ssl_session_ticket_key_callback(ngx_
             return -1;
         }
 
-        return (i == 0) ? 1 : 2 /* renew */;
+        /* renew if TLSv1.3 */
+
+#ifdef TLS1_3_VERSION
+        if (SSL_version(ssl_conn) == TLS1_3_VERSION) {
+            return 2;
+        }
+#endif
+
+        /* renew if non-default key */
+
+        if (i != 0) {
+            return 2;
+        }
+
+        return 1;
     }
 }
 

comment:3 by Maxim Dounin <mdounin@…>, 5 months ago

In 7997:e30f7dc7f143/nginx:

SSL: always renewing tickets with TLSv1.3 (ticket #1892).

Chrome only uses TLS session tickets once with TLS 1.3, likely following
RFC 8446 Appendix C.4 recommendation. With OpenSSL, this works fine with
built-in session tickets, since these are explicitly renewed in case of
TLS 1.3 on each session reuse, but results in only two connections being
reused after an initial handshake when using ssl_session_ticket_key.

Fix is to always renew TLS session tickets in case of TLS 1.3 when using
ssl_session_ticket_key, similarly to how it is done by OpenSSL internally.

comment:4 by Maxim Dounin, 5 months ago

Resolution: fixed
Status: newclosed

Fix committed, thanks for reporting this.

Note: See TracTickets for help on using tickets.