Botan 3.12.0
Crypto and TLS for C&
tls_session_manager.cpp
Go to the documentation of this file.
1/**
2 * TLS Session Manger base class implementations
3 * (C) 2011-2023 Jack Lloyd
4 * 2022-2023 René Meusel - Rohde & Schwarz Cybersecurity
5 *
6 * Botan is released under the Simplified BSD License (see license.txt)
7 */
8
9#include <botan/tls_session_manager.h>
10
11#include <botan/assert.h>
12#include <botan/rng.h>
13#include <botan/tls_callbacks.h>
14#include <botan/tls_policy.h>
15#include <botan/tls_session.h>
16#include <algorithm>
17
18#if defined(BOTAN_HAS_TLS_13)
19 #include <botan/tls_psk_identity_13.h>
20#endif
21
22namespace Botan::TLS {
23
24Session_Manager::Session_Manager(const std::shared_ptr<RandomNumberGenerator>& rng) : m_rng(rng) {
26}
27
28std::optional<Session_Handle> Session_Manager::establish(const Session& session,
29 const std::optional<Session_ID>& id,
30 bool tls12_no_ticket) {
31 // Establishing a session does not require locking at this level as
32 // concurrent TLS instances on a server will create unique sessions.
33
34 // By default, the session manager does not emit session tickets anyway
35 BOTAN_UNUSED(tls12_no_ticket);
36 BOTAN_ASSERT(session.side() == Connection_Side::Server, "Client tried to establish a session");
37
38 Session_Handle handle(id.value_or(m_rng->random_vec<Session_ID>(32)));
39 store(session, handle);
40 return handle;
41}
42
43std::optional<Session> Session_Manager::retrieve(const Session_Handle& handle,
44 Callbacks& callbacks,
45 const Policy& policy) {
46 // Retrieving a session for a given handle does not require locking on this
47 // level. Concurrent threads might handle the removal of an expired ticket
48 // more than once, but removing an already removed ticket is a harmless NOOP.
49
50 auto session = retrieve_one(handle);
51 if(!session.has_value()) {
52 return std::nullopt;
53 }
54
55 // A value of '0' means: No policy restrictions.
56 const std::chrono::seconds policy_lifetime =
57 (policy.session_ticket_lifetime().count() > 0) ? policy.session_ticket_lifetime() : std::chrono::seconds::max();
58
59 // RFC 5077 3.3 -- "Old Session Tickets"
60 // A server MAY treat a ticket as valid for a shorter or longer period of
61 // time than what is stated in the ticket_lifetime_hint.
62 //
63 // RFC 5246 F.1.4 -- TLS 1.2
64 // If either party suspects that the session may have been compromised, or
65 // that certificates may have expired or been revoked, it should force a
66 // full handshake. An upper limit of 24 hours is suggested for session ID
67 // lifetimes.
68 //
69 // RFC 8446 4.6.1 -- TLS 1.3
70 // A server MAY treat a ticket as valid for a shorter period of time than
71 // what is stated in the ticket_lifetime.
72 //
73 // Note: This disregards what is stored in the session (e.g. "lifetime_hint")
74 // and only takes the local policy into account. The lifetime stored in
75 // the sessions was taken from the same policy anyways and changes by
76 // the application should have an immediate effect.
77 const auto ticket_age =
78 std::chrono::duration_cast<std::chrono::seconds>(callbacks.tls_current_timestamp() - session->start_time());
79 const bool expired = ticket_age > policy_lifetime;
80
81 if(expired) {
82 remove(handle);
83 return std::nullopt;
84 } else {
85 return session;
86 }
87}
88
89std::vector<Session_with_Handle> Session_Manager::find_and_filter(const Server_Information& info,
90 Callbacks& callbacks,
91 const Policy& policy) {
92 // A value of '0' means: No policy restrictions. Session ticket lifetimes as
93 // communicated by the server apply regardless.
94 const std::chrono::seconds policy_lifetime =
95 (policy.session_ticket_lifetime().count() > 0) ? policy.session_ticket_lifetime() : std::chrono::seconds::max();
96
97 const size_t max_sessions_hint = std::max(policy.maximum_session_tickets_per_client_hello(), size_t(1));
98 const auto now = callbacks.tls_current_timestamp();
99
100 // An arbitrary number of loop iterations to perform before giving up
101 // to avoid a potential endless loop with a misbehaving session manager.
102 constexpr unsigned int max_attempts = 10;
103 std::vector<Session_with_Handle> sessions_and_handles;
104
105 // Query the session manager implementation for new sessions until at least
106 // one session passes the filter or no more sessions are found.
107 for(unsigned int attempt = 0; attempt < max_attempts && sessions_and_handles.empty(); ++attempt) {
108 sessions_and_handles = find_some(info, max_sessions_hint);
109
110 // ... underlying implementation didn't find anything. Early exit.
111 if(sessions_and_handles.empty()) {
112 break;
113 }
114
115 std::erase_if(sessions_and_handles, [&](const auto& session) {
116 const auto age = std::chrono::duration_cast<std::chrono::seconds>(now - session.session.start_time());
117
118 // RFC 5077 3.3 -- "Old Session Tickets"
119 // The ticket_lifetime_hint field contains a hint from the
120 // server about how long the ticket should be stored. [...]
121 // A client SHOULD delete the ticket and associated state when
122 // the time expires. It MAY delete the ticket earlier based on
123 // local policy.
124 //
125 // A value [in ticket_lifetime_hint] of zero is reserved to indicate
126 // that the lifetime of the ticket is unspecified.
127 //
128 // RFC 5246 F.1.4 -- TLS 1.2
129 // If either party suspects that the session may have been
130 // compromised, or that certificates may have expired or been
131 // revoked, it should force a full handshake. An upper limit of
132 // 24 hours is suggested for session ID lifetimes.
133 //
134 // RFC 8446 4.2.11.1 -- TLS 1.3
135 // The client's view of the age of a ticket is the time since the
136 // receipt of the NewSessionTicket message. Clients MUST NOT
137 // attempt to use tickets which have ages greater than the
138 // "ticket_lifetime" value which was provided with the ticket.
139 //
140 // RFC 8446 4.6.1 -- TLS 1.3
141 // Clients MUST NOT cache tickets for longer than 7 days,
142 // regardless of the ticket_lifetime, and MAY delete tickets
143 // earlier based on local policy.
144 //
145 // Note: TLS 1.3 tickets with a lifetime longer than 7 days are
146 // rejected during parsing with an "Illegal Parameter" alert.
147 // Other suggestions are left to the application via
148 // Policy::session_ticket_lifetime(). Session lifetimes as
149 // communicated by the server via the "lifetime_hint" are
150 // obeyed regardless of the policy setting.
151 const auto session_lifetime_hint = session.session.lifetime_hint();
152
153 const bool is_rfc5077_unspecified =
154 (session_lifetime_hint.count() == 0 && session.session.version().is_pre_tls_13());
155 const auto effective_hint = is_rfc5077_unspecified ? std::chrono::seconds::max() : session_lifetime_hint;
156 const bool expired = age > std::min(policy_lifetime, effective_hint);
157
158 if(expired) {
159 remove(session.handle);
160 }
161
162 return expired;
163 });
164 }
165
166 return sessions_and_handles;
167}
168
169std::vector<Session_with_Handle> Session_Manager::find(const Server_Information& info,
170 Callbacks& callbacks,
171 const Policy& policy) {
172 auto allow_reusing_tickets = policy.reuse_session_tickets();
173
174 // Session_Manager::find() must be an atomic getter if ticket reuse is not
175 // allowed. I.e. each ticket handed to concurrently requesting threads must
176 // be unique. In that case we must hold a lock while retrieving a ticket.
177 // Otherwise, no locking is required on this level.
178 std::optional<lock_guard_type<recursive_mutex_type>> lk;
179 if(!allow_reusing_tickets) {
180 lk.emplace(mutex());
181 }
182
183 auto sessions_and_handles = find_and_filter(info, callbacks, policy);
184
185 // std::vector::resize() cannot be used as the vector's members aren't
186 // default constructible.
187 const auto session_limit = policy.maximum_session_tickets_per_client_hello();
188 while(session_limit > 0 && sessions_and_handles.size() > session_limit) {
189 sessions_and_handles.pop_back();
190 }
191
192 // RFC 8446 Appendix C.4
193 // Clients SHOULD NOT reuse a ticket for multiple connections. Reuse of
194 // a ticket allows passive observers to correlate different connections.
195 //
196 // When reuse of session tickets is not allowed, remove all tickets to be
197 // returned from the implementation's internal storage.
198 if(!allow_reusing_tickets) {
199 // The lock must be held here, otherwise we cannot guarantee the
200 // transactional retrieval of tickets to concurrently requesting clients.
201 BOTAN_ASSERT_NOMSG(lk.has_value());
202 for(const auto& [session, handle] : sessions_and_handles) {
203 if(!session.version().is_pre_tls_13() || !handle.is_id()) {
204 remove(handle);
205 }
206 }
207 }
208
209 return sessions_and_handles;
210}
211
212#if defined(BOTAN_HAS_TLS_13)
213
214std::optional<std::pair<Session, uint16_t>> Session_Manager::choose_from_offered_tickets(
215 const std::vector<PskIdentity>& tickets,
216 std::string_view hash_function,
217 Callbacks& callbacks,
218 const Policy& policy) {
219 // Note that the TLS server currently does not ensure that tickets aren't
220 // reused. As a result, no locking is required on this level.
221
222 // Limit how many identities we attempt to look up to prevent a client
223 // from forcing excessive session store queries.
224 const size_t max_attempts = std::min<size_t>(tickets.size(), 5);
225
226 for(size_t i = 0; i < max_attempts; ++i) {
227 const auto& ticket = tickets[i];
228 auto session = retrieve(Session_Handle(Opaque_Session_Handle(ticket.identity())), callbacks, policy);
229 if(session.has_value() && session->ciphersuite().prf_algo() == hash_function &&
230 session->version().is_tls_13_or_later()) {
231 return std::pair{std::move(session.value()), static_cast<uint16_t>(i)};
232 }
233
234 // RFC 8446 4.2.10
235 // For PSKs provisioned via NewSessionTicket, a server MUST validate
236 // that the ticket age for the selected PSK identity [...] is within a
237 // small tolerance of the time since the ticket was issued. If it is
238 // not, the server SHOULD proceed with the handshake but reject 0-RTT,
239 // and SHOULD NOT take any other action that assumes that this
240 // ClientHello is fresh.
241 //
242 // TODO: The ticket-age is currently not checked (as 0-RTT is not
243 // implemented) and we simply take the SHOULD at face value.
244 // Instead we could add a policy check letting the user decide.
245 }
246
247 return std::nullopt;
248}
249
250#endif
251
252} // namespace Botan::TLS
#define BOTAN_UNUSED
Definition assert.h:144
#define BOTAN_ASSERT_NOMSG(expr)
Definition assert.h:75
#define BOTAN_ASSERT_NONNULL(ptr)
Definition assert.h:114
#define BOTAN_ASSERT(expr, assertion_made)
Definition assert.h:62
virtual std::chrono::system_clock::time_point tls_current_timestamp()
virtual bool reuse_session_tickets() const
virtual size_t maximum_session_tickets_per_client_hello() const
virtual std::chrono::seconds session_ticket_lifetime() const
Connection_Side side() const
Definition tls_session.h:89
Helper class to embody a session handle in all protocol versions.
virtual std::optional< std::pair< Session, uint16_t > > choose_from_offered_tickets(const std::vector< PskIdentity > &tickets, std::string_view hash_function, Callbacks &callbacks, const Policy &policy)
virtual size_t remove(const Session_Handle &handle)=0
virtual std::optional< Session > retrieve_one(const Session_Handle &handle)=0
Internal retrieval function for a single session.
virtual void store(const Session &session, const Session_Handle &handle)=0
Save a Session under a Session_Handle (TLS Client).
recursive_mutex_type & mutex()
BOTAN_FUTURE_EXPLICIT Session_Manager(const std::shared_ptr< RandomNumberGenerator > &rng)
virtual std::vector< Session_with_Handle > find_some(const Server_Information &info, size_t max_sessions_hint)=0
Internal retrieval function to find sessions to resume.
virtual std::optional< Session_Handle > establish(const Session &session, const std::optional< Session_ID > &id=std::nullopt, bool tls12_no_ticket=false)
Save a new Session and assign a Session_Handle (TLS Server).
virtual std::vector< Session_with_Handle > find(const Server_Information &info, Callbacks &callbacks, const Policy &policy)
Find all sessions that match a given server info.
std::shared_ptr< RandomNumberGenerator > m_rng
virtual std::optional< Session > retrieve(const Session_Handle &handle, Callbacks &callbacks, const Policy &policy)
Retrieves a specific session given a handle.
Strong< std::vector< uint8_t >, struct Session_ID_ > Session_ID
holds a TLS 1.2 session ID for stateful resumption
Strong< std::vector< uint8_t >, struct Opaque_Session_Handle_ > Opaque_Session_Handle
holds an opaque session handle as used in TLS 1.3 that could be either a ticket for stateless resumpt...