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