curl/docs/internals/TLS-SESSIONS.md
Stefan Eissing fa0ccd9f1f
lib: TLS session ticket caching reworked
Described in detail in internal doc TLS-SESSIONS.md

Main points:
- use a new `ssl_peer_key` for cache lookups by connection filters
- recognize differences between TLSv1.3 and other tickets
  * TLSv1.3 tickets are single-use, cache can hold several of them for a peer
  * TLSv1.2 are reused, keep only a single one per peer
- differentiate between ticket BLOB to store (that could be persisted) and object instances
- use put/take/return pattern for cache access
- remember TLS version, ALPN protocol, time received and lifetime of ticket
- auto-expire tickets after their lifetime

Closes #15774
2024-12-20 14:59:23 +01:00

169 lines
6.8 KiB
Markdown

<!--
Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
SPDX-License-Identifier: curl
-->
# TLS Sessions and Tickets
The TLS protocol offers methods of "resuming" a previous "session". A
TLS "session" is a negotiated security context across a connection
(which may be via TCP or UDP or other transports.)
By "resuming", the TLS protocol means that the security context from
before can be fully or partially resurrected when the TLS client presents
the proper crypto stuff to the server. This saves on the amount of
TLS packets that need to be sent back and forth, reducing amount
of data and even latency. In the case of QUIC, resumption may send
application data without having seen any reply from the server, hence
this is named 0-RTT data.
The exact mechanism of session tickets in TLSv1.2 (and earlier) and
TLSv1.3 differs. TLSv1.2 tickets have several weaknesses (that can
be exploited by attackers) which TLSv1.3 then fixed. See
[Session Tickets in the real world](https://words.filippo.io/we-need-to-talk-about-session-tickets/)
for an insight into this topic.
These difference between TLS protocol versions are reflected in curl's
handling of session tickets. More below.
## Curl's `ssl_peer_key`
In order to find a ticket from a previous TLS session, curl
needs a name for TLS sessions that uniquely identifies the peer
it talks to.
This name has to reflect also the various TLS parameters that can
be configured in curl for a connection. We do not want to use
a ticket from an different configuration. Example: when setting
the maximum TLS version to 1.2, we do not want to reuse a ticket
we got from a TLSv1.3 session, although we are talking to the
same host.
Internally, we call this name a `ssl_peer_key`. It is a printable
string that carries hostname and port and any non-default TLS
parameters involved in the connection.
Examples:
- `curl.se:443:CA-/etc/ssl/cert.pem:IMPL-GnuTLS/3.8.7` is a peer key for
a connection to `curl.se:443` using `/etc/ssl/cert.pem` as CA
trust anchors and GnuTLS/3.8.7 as TLS backend.
- `curl.se:443:TLSVER-6-6:CA-/etc/ssl/cert.pem:IMPL-GnuTLS/3.8.7` is the
same as the previous, except it is configured to use TLSv1.2 as
min and max versions.
Different configurations produce different keys which is just what
curl needs when handling SSL session tickets.
One important thing: peer keys do not contain confidential
information. If you configure a client certificate or SRP authentication
with username/password, these will not be part of the peer key.
However, peer keys carry the hostnames you use curl for. The *do*
leak the privacy of your communication. We recommend to *not* persist
peer keys for this reason.
**Caveat**: The key may contain file names or paths. It does not
reflect the *contents* in the filesystem. If you change `/etc/ssl/cert.pem`
and reuse a previous ticket, curl might trust a server which no
longer has a root certificate in the file.
## Session Cache Access
#### Lookups
When a new connection is being established, each SSL connection filter creates
its own peer_key and calls into the cache. The cache then looks for a ticket
with exactly this peer_key. Peer keys between proxy SSL filters and SSL
filters talking through a tunnel will differ, as they talk to different
peers.
If the connection filter wants to use a client certificate or SRP
authentication, the cache will check those as well. If the cache peer
carries client cert or SRP auth, the connection filter must have
those with the same values (and vice versa).
On a match, the connection filter gets the session ticket and feeds that
to the TLS implementation which, on accepting it, will try to resume it
for a shorter handshake. In addition, the filter gets the ALPN used
before and the amount of 0-RTT data that the server announced to be
willing to accept. The filter can then decide if it wants to attempt
0-RTT or not. (The ALPN is needed to know if the server speaks the
protocol you want to send in 0-RTT. It makes no sense to send HTTP/2
requests to a server that only knows HTTP/1.1.)
#### Updates
When a new TLS session ticket is received by a filter, it adds it to the
cache using its peer_key and SSL configuration. The cache looks for
a matching entry and, should it find one, adds the ticket for this
peer.
### Put, Take and Return
when a filter accesses the session cache, it *takes*
a ticket from the cache, meaning a returned ticket is removed. The filter
then configures its TLS backend and *returns* the ticket to the cache.
The cache needs to treat tickets from TLSv1.2 and 1.3 differently.
1.2 tickets should be reused, but 1.3 tickets SHOULD NOT (RFC 8446).
The session cache will simply drop 1.3 tickets when they are returned
after use, but keep a 1.2 ticket.
When a ticket is *put* into the cache, there is also a difference. There
can be several 1.3 tickets at the same time, but only a single 1.2 ticket.
TLSv1.2 tickets replace any other. 1.3 tickets accumulate up to a max
amount.
By having a "put/take/return" we reflect the 1.3 use case nicely. Two
concurrent connections will not reuse the same ticket.
## Session Ticket Persistence
#### Privacy and Security
As mentioned above, ssl peer keys are not intended for storage in a
file system. They'll clearly show which hosts the user talked to. This
maybe "just" privacy relevant, but has security implications as an
attacker might find worthy targets among your peer keys.
Also, we do not recommend to persist TLSv1.2 tickets.
### Salted Hashes
The TLS session cache offers an alternative to storing peer keys:
it provides a salted SHA256 hash of the peer key for import and export.
#### Export
The salt is generated randomly for each peer key on export. The
SHA256 makes sure that the peer key cannot be reversed and that
a slightly different key still produces a very different result.
This means an attacker cannot just "grep" a session file for a
particular entry, e.g. if they want to know if you accessed a
specific host. They *can* however compute the SHA256 hashes for
all salts in the file and find a specific entry. But they *cannot*
find a hostname they do not know. They'd have to brute force by
guessing.
#### Import
When session tickets are imported from a file, curl only gets the
salted hashes. The tickets imported will belong to an *unknown*
peer key.
When a connection filter tries to *take* a session ticket, it will
pass its peer key. This peer key will initially not match any
tickets in the cache. The cache then checks all entries with
unknown peer keys if the passed key matches their salted hash. If
it does, the peer key is recovered and remembered at the cache
entry.
This is a performance penalty in the order of "unknown" peer keys
which will diminish over time when keys are rediscovered. Note that
this also works for putting a new ticket into the cache: when no
present entry matches, a new one with peer key is created. This
peer key will then no longer bear the cost of hash computes.