• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

randombit / botan / 21768358452

06 Feb 2026 10:35PM UTC coverage: 90.064% (-0.003%) from 90.067%
21768358452

Pull #5289

github

web-flow
Merge f589db195 into 8ea0ca252
Pull Request #5289: Further misc header reductions, forward declarations, etc

102238 of 113517 relevant lines covered (90.06%)

11357432.36 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.98
/src/tests/test_tls_session_manager.cpp
1
/*
2
* (C) 2022 Jack Lloyd
3
* (C) 2022 René Meusel - Rohde & Schwarz Cybersecurity
4
*
5
* Botan is released under the Simplified BSD License (see license.txt)
6
*/
7

8
#include "tests.h"
9

10
#if defined(BOTAN_HAS_TLS)
11

12
   #include <botan/credentials_manager.h>
13
   #include <botan/hex.h>
14
   #include <botan/rng.h>
15
   #include <botan/tls_callbacks.h>
16
   #include <botan/tls_policy.h>
17
   #include <botan/tls_session_manager_hybrid.h>
18
   #include <botan/tls_session_manager_memory.h>
19
   #include <botan/tls_session_manager_stateless.h>
20
   #include <botan/internal/fmt.h>
21
   #include <array>
22
   #include <chrono>
23

24
   #if defined(BOTAN_HAS_TLS_SQLITE3_SESSION_MANAGER)
25
      #include <botan/tls_session_manager_sqlite.h>
26
   #endif
27

28
   #if defined(BOTAN_HAS_TLS_13)
29
      #include <botan/tls_psk_identity_13.h>
30
   #endif
31

32
   #if defined(BOTAN_TARGET_OS_HAS_FILESYSTEM)
33
      #include <version>
34
      #if defined(__cpp_lib_filesystem)
35
         #include <filesystem>
36
      #endif
37
   #endif
38

39
// This file contains a number of `Botan::TLS::Version_Code::TLS_V**` protocol
40
// version specifications. This is to work around a compiler bug in GCC 11.3
41
// where `Botan::TLS::Protocol_Version::TLS_V12` would lead to an "Internal
42
// Compiler Error" when used in the affected context.
43
//
44
// TODO: remove the workaround once GCC 11 is not supported anymore.
45

46
namespace Botan_Tests {
47

48
class Test_Credentials_Manager : public Botan::Credentials_Manager {
7✔
49
   public:
50
      Botan::secure_vector<uint8_t> session_ticket_key() override {
55✔
51
         return Botan::hex_decode_locked("DEADFACECAFEBAAD");
55✔
52
      }
53
};
54

55
class Other_Test_Credentials_Manager : public Botan::Credentials_Manager {
1✔
56
   public:
57
      Botan::secure_vector<uint8_t> session_ticket_key() override {
1✔
58
         return Botan::hex_decode_locked("CAFEBAADFACEBABE");
1✔
59
      }
60
};
61

62
class Empty_Credentials_Manager : public Botan::Credentials_Manager {};
4✔
63

64
class Session_Manager_Callbacks : public Botan::TLS::Callbacks {
3✔
65
   public:
66
      void tls_emit_data(std::span<const uint8_t> /*data*/) override { BOTAN_ASSERT_NOMSG(false); }
×
67

68
      void tls_record_received(uint64_t /*record*/, std::span<const uint8_t> /*data*/) override {
×
69
         BOTAN_ASSERT_NOMSG(false);
×
70
      }
71

72
      void tls_alert(Botan::TLS::Alert /*alert*/) override { BOTAN_ASSERT_NOMSG(false); }
×
73

74
      void tls_session_established(const Botan::TLS::Session_Summary& /*summary*/) override {
×
75
         BOTAN_ASSERT_NOMSG(false);
×
76
      }
77

78
      std::chrono::system_clock::time_point tls_current_timestamp() override {
225✔
79
         return std::chrono::system_clock::now() + std::chrono::hours(m_ticks);
225✔
80
      }
81

82
      void tick() { ++m_ticks; }
13✔
83

84
   private:
85
      uint64_t m_ticks = 0;
86
};
87

88
class Session_Manager_Policy : public Botan::TLS::Policy {
6✔
89
   public:
90
      std::chrono::seconds session_ticket_lifetime() const override { return std::chrono::minutes(30); }
198✔
91

92
      bool reuse_session_tickets() const override { return m_allow_session_reuse; }
27✔
93

94
      size_t maximum_session_tickets_per_client_hello() const override { return m_session_limit; }
58✔
95

96
      void set_session_limit(size_t l) { m_session_limit = l; }
6✔
97

98
      void set_allow_session_reuse(bool b) { m_allow_session_reuse = b; }
4✔
99

100
   private:
101
      size_t m_session_limit = 1000;  // basically 'no limit'
102
      bool m_allow_session_reuse = true;
103
};
104

105
namespace {
106

107
decltype(auto) random_id(Botan::RandomNumberGenerator& rng) {
60✔
108
   return rng.random_vec<Botan::TLS::Session_ID>(32);
58✔
109
}
110

111
decltype(auto) random_ticket(Botan::RandomNumberGenerator& rng) {
29✔
112
   return rng.random_vec<Botan::TLS::Session_Ticket>(32);
27✔
113
}
114

115
decltype(auto) random_opaque_handle(Botan::RandomNumberGenerator& rng) {
1✔
116
   return rng.random_vec<Botan::TLS::Opaque_Session_Handle>(32);
1✔
117
}
118

119
const Botan::TLS::Server_Information& server_info() {
151✔
120
   static const Botan::TLS::Server_Information si("botan.randombit.net");
151✔
121
   return si;
151✔
122
}
123

124
decltype(auto) default_session(Botan::TLS::Connection_Side side,
118✔
125
                               Botan::TLS::Callbacks& cbs,
126
                               Botan::TLS::Protocol_Version version = Botan::TLS::Protocol_Version::TLS_V12) {
127
   if(version.is_pre_tls_13()) {
118✔
128
      return Botan::TLS::Session(
108✔
129
         {}, version, 0x009C, side, true, true, {}, server_info(), 0, cbs.tls_current_timestamp());
216✔
130
   } else {
131
   #if defined(BOTAN_HAS_TLS_13)
132
      return Botan::TLS::Session({},
10✔
133
                                 std::nullopt,
134
                                 0,
135
                                 std::chrono::seconds(1024),
10✔
136
                                 Botan::TLS::Protocol_Version::TLS_V13,
137
                                 Botan::TLS::Ciphersuite::from_name("AES_128_GCM_SHA256")->ciphersuite_code(),
10✔
138
                                 side,
139
                                 {},
140
                                 nullptr,
141
                                 server_info(),
142
                                 cbs.tls_current_timestamp());
20✔
143
   #else
144
      throw Test_Error("TLS 1.3 is not available in this build");
145
   #endif
146
   }
147
}
148

149
using namespace std::literals;
150

151
std::vector<Test::Result> test_session_manager_in_memory() {
1✔
152
   auto rng = Test::new_shared_rng(__func__);
1✔
153

154
   const Botan::TLS::Session_ID default_id = random_id(*rng);
1✔
155

156
   std::optional<Botan::TLS::Session_Manager_In_Memory> mgr;
1✔
157

158
   Session_Manager_Callbacks cbs;
1✔
159
   Session_Manager_Policy plcy;
1✔
160

161
   return {
1✔
162
      CHECK("creation", [&](auto&) { mgr.emplace(rng, 5); }),
1✔
163

164
      CHECK("empty cache does not obtain anything",
165
            [&](auto& result) {
1✔
166
               result.confirm("no session found via server info", mgr->find(server_info(), cbs, plcy).empty());
2✔
167

168
               Botan::TLS::Session_ID const mock_id = random_id(*rng);
1✔
169
               auto mock_ticket = rng->random_vec<Botan::TLS::Session_Ticket>(128);
1✔
170

171
               result.confirm("no session found via ID", !mgr->retrieve(mock_id, cbs, plcy));
4✔
172
               result.confirm("no session found via ID", !mgr->retrieve(mock_ticket, cbs, plcy));
4✔
173
            }),
2✔
174

175
      CHECK("clearing empty cache",
176
            [&](auto& result) { result.test_eq("does not delete anything", mgr->remove_all(), 0); }),
2✔
177

178
      CHECK("establish new session",
179
            [&](auto& result) {
1✔
180
               auto handle = mgr->establish(default_session(Botan::TLS::Connection_Side::Server, cbs), default_id);
1✔
181
               if(result.confirm("establishment was successful", handle.has_value())) {
2✔
182
                  result.require("session id was set", handle->id().has_value());
2✔
183
                  result.confirm("session ticket was empty", !handle->ticket().has_value());
2✔
184
                  result.test_is_eq("session id is correct", handle->id().value(), default_id);
3✔
185
               }
186
            }),
1✔
187

188
      CHECK("obtain session from server info",
189
            [&](auto& result) {
1✔
190
               auto sessions = mgr->find(server_info(), cbs, plcy);
1✔
191
               if(result.confirm("session was found successfully", sessions.size() == 1)) {
2✔
192
                  result.test_is_eq("protocol version was echoed",
1✔
193
                                    sessions[0].session.version(),
1✔
194
                                    Botan::TLS::Protocol_Version(Botan::TLS::Version_Code::TLS_V12));
1✔
195
                  result.test_is_eq("ciphersuite was echoed", sessions[0].session.ciphersuite_code(), uint16_t(0x009C));
1✔
196
                  result.test_is_eq("ID was echoed", sessions[0].handle.id().value(), default_id);
3✔
197
                  result.confirm("not a ticket", !sessions[0].handle.ticket().has_value());
2✔
198
               }
199
            }),
1✔
200

201
      CHECK("obtain session from ID",
202
            [&](auto& result) {
1✔
203
               auto session = mgr->retrieve(default_id, cbs, plcy);
2✔
204
               if(result.confirm("session was found successfully", session.has_value())) {
2✔
205
                  result.test_is_eq("protocol version was echoed",
1✔
206
                                    session->version(),
1✔
207
                                    Botan::TLS::Protocol_Version(Botan::TLS::Version_Code::TLS_V12));
1✔
208
                  result.test_is_eq("ciphersuite was echoed", session->ciphersuite_code(), uint16_t(0x009C));
2✔
209
               }
210
            }),
1✔
211

212
      CHECK("obtain session from ID disguised as opaque handle",
213
            [&](auto& result) {
1✔
214
               auto session = mgr->retrieve(Botan::TLS::Opaque_Session_Handle(default_id), cbs, plcy);
2✔
215
               if(result.confirm("session was found successfully", session.has_value())) {
2✔
216
                  result.test_is_eq("protocol version was echoed",
1✔
217
                                    session->version(),
1✔
218
                                    Botan::TLS::Protocol_Version(Botan::TLS::Version_Code::TLS_V12));
1✔
219
                  result.test_is_eq("ciphersuite was echoed", session->ciphersuite_code(), uint16_t(0x009C));
2✔
220
               }
221
            }),
1✔
222

223
      CHECK("obtain session from ticket == id does not work",
224
            [&](auto& result) {
1✔
225
               auto session = mgr->retrieve(Botan::TLS::Session_Ticket(default_id), cbs, plcy);
2✔
226
               result.confirm("session was not found", !session.has_value());
2✔
227
            }),
1✔
228

229
      CHECK("invalid ticket causes std::nullopt",
230
            [&](auto& result) {
1✔
231
               auto no_session = mgr->retrieve(random_ticket(*rng), cbs, plcy);
2✔
232
               result.confirm("std::nullopt on bogus ticket", !no_session.has_value());
2✔
233
            }),
1✔
234

235
      CHECK("invalid ID causes std::nullopt",
236
            [&](auto& result) {
1✔
237
               auto no_session = mgr->retrieve(random_id(*rng), cbs, plcy);
2✔
238
               result.confirm("std::nullopt on bogus ID", !no_session.has_value());
2✔
239
            }),
1✔
240

241
      CHECK("remove_all",
242
            [&](auto& result) {
1✔
243
               result.test_eq("removed one element", mgr->remove_all(), 1);
1✔
244
               result.test_eq("should be empty now", mgr->remove_all(), 0);
1✔
245
            }),
1✔
246

247
      CHECK("add session with ID",
248
            [&](auto& result) {
1✔
249
               Botan::TLS::Session_ID const new_id = random_id(*rng);
1✔
250

251
               mgr->store(default_session(Botan::TLS::Connection_Side::Client, cbs), new_id);
3✔
252
               result.require("obtain via ID", mgr->retrieve(new_id, cbs, plcy).has_value());
5✔
253

254
               auto sessions = mgr->find(server_info(), cbs, plcy);
1✔
255
               if(result.confirm("found via server info", sessions.size() == 1)) {
2✔
256
                  result.test_is_eq("protocol version was echoed",
1✔
257
                                    sessions[0].session.version(),
1✔
258
                                    Botan::TLS::Protocol_Version(Botan::TLS::Version_Code::TLS_V12));
1✔
259
                  result.test_is_eq("ciphersuite was echoed", sessions[0].session.ciphersuite_code(), uint16_t(0x009C));
2✔
260
                  result.test_is_eq("ID was echoed", sessions[0].handle.id().value(), new_id);
3✔
261
                  result.confirm("ticket was not stored", !sessions[0].handle.ticket().has_value());
2✔
262
               }
263

264
               mgr->remove_all();
1✔
265
            }),
2✔
266

267
      CHECK("add session with ticket",
268
            [&](auto& result) {
1✔
269
               Botan::TLS::Session_Ticket const new_ticket = random_ticket(*rng);
1✔
270

271
               mgr->store(default_session(Botan::TLS::Connection_Side::Client, cbs), new_ticket);
3✔
272
               // cannot be obtained by (non-existent) ID or randomly generated ticket
273

274
               auto sessions = mgr->find(server_info(), cbs, plcy);
1✔
275
               if(result.confirm("found via server info", sessions.size() == 1)) {
2✔
276
                  result.test_is_eq("protocol version was echoed",
1✔
277
                                    sessions[0].session.version(),
1✔
278
                                    Botan::TLS::Protocol_Version(Botan::TLS::Version_Code::TLS_V12));
1✔
279
                  result.test_is_eq("ciphersuite was echoed", sessions[0].session.ciphersuite_code(), uint16_t(0x009C));
2✔
280
                  result.confirm("ID was not stored", !sessions[0].handle.id().has_value());
2✔
281
                  result.test_is_eq("ticket was echoed", sessions[0].handle.ticket().value(), new_ticket);
3✔
282
               }
283

284
               mgr->remove_all();
1✔
285
            }),
2✔
286

287
      CHECK("removing by ID or opaque handle",
288
            [&](auto& result) {
1✔
289
               Botan::TLS::Session_Manager_In_Memory local_mgr(rng);
1✔
290

291
               const auto new_session1 =
1✔
292
                  local_mgr.establish(default_session(Botan::TLS::Connection_Side::Server, cbs), default_id);
293
               const auto new_session2 = local_mgr.establish(default_session(Botan::TLS::Connection_Side::Server, cbs));
1✔
294
               result.require(
1✔
295
                  "saving worked",
296
                  new_session1.has_value() && new_session1->id().has_value() && !new_session1->ticket().has_value());
4✔
297
               result.require(
1✔
298
                  "saving worked",
299
                  new_session2.has_value() && new_session2->id().has_value() && !new_session2->ticket().has_value());
4✔
300

301
               result.test_is_eq(
1✔
302
                  "can find via server info", local_mgr.find(server_info(), cbs, plcy).size(), size_t(2));
2✔
303

304
               result.test_is_eq("one was deleted", local_mgr.remove(default_id), size_t(1));
4✔
305
               result.confirm("cannot obtain via default ID anymore",
1✔
306
                              !local_mgr.retrieve(default_id, cbs, plcy).has_value());
4✔
307
               result.test_is_eq(
1✔
308
                  "can find less via server info", local_mgr.find(server_info(), cbs, plcy).size(), size_t(1));
2✔
309

310
               result.test_is_eq("last one was deleted",
2✔
311
                                 local_mgr.remove(Botan::TLS::Opaque_Session_Handle(new_session2->id().value())),
3✔
312
                                 size_t(1));
1✔
313
               result.confirm("cannot obtain via ID anymore",
1✔
314
                              !local_mgr.retrieve(new_session2->id().value(), cbs, plcy).has_value());
4✔
315
               result.confirm("cannot find via server info", local_mgr.find(server_info(), cbs, plcy).empty());
2✔
316
            }),
2✔
317

318
      CHECK("removing by ticket or opaque handle",
319
            [&](auto& result) {
1✔
320
               Botan::TLS::Session_Manager_In_Memory local_mgr(rng);
1✔
321

322
               Botan::TLS::Session_Ticket const ticket1 = random_ticket(*rng);
1✔
323
               Botan::TLS::Session_Ticket const ticket2 = random_ticket(*rng);
1✔
324
               Botan::TLS::Session_Ticket ticket3 = random_ticket(*rng);
1✔
325

326
               local_mgr.store(default_session(Botan::TLS::Connection_Side::Client, cbs), ticket1);
2✔
327
               local_mgr.store(default_session(Botan::TLS::Connection_Side::Client, cbs), ticket2);
2✔
328
               local_mgr.store(default_session(Botan::TLS::Connection_Side::Client, cbs), ticket3);
2✔
329
               result.test_is_eq(
1✔
330
                  "can find them via server info ", local_mgr.find(server_info(), cbs, plcy).size(), size_t(3));
2✔
331

332
               result.test_is_eq("remove one session by ticket", local_mgr.remove(ticket2), size_t(1));
4✔
333
               result.test_is_eq(
1✔
334
                  "can find two via server info", local_mgr.find(server_info(), cbs, plcy).size(), size_t(2));
2✔
335

336
               result.test_is_eq("remove one session by opaque handle",
2✔
337
                                 local_mgr.remove(Botan::TLS::Opaque_Session_Handle(ticket3.get())),
2✔
338
                                 size_t(1));
1✔
339
               result.test_is_eq(
1✔
340
                  "can find only one via server info", local_mgr.find(server_info(), cbs, plcy).size(), size_t(1));
2✔
341
            }),
3✔
342

343
      CHECK(
344
         "session purging",
345
         [&](auto& result) {
1✔
346
            result.require("max sessions is 5", mgr->capacity() == 5);
1✔
347

348
            // fill the Session_Manager up fully
349
            std::vector<Botan::TLS::Session_Handle> handles;
1✔
350
            handles.reserve(mgr->capacity());
1✔
351
            for(size_t i = 0; i < mgr->capacity(); ++i) {
6✔
352
               handles.push_back(
5✔
353
                  mgr->establish(default_session(Botan::TLS::Connection_Side::Server, cbs), random_id(*rng)).value());
15✔
354
            }
355

356
            for(const auto& handle : handles) {
6✔
357
               result.confirm("session still there", mgr->retrieve(handle, cbs, plcy).has_value());
15✔
358
            }
359

360
            // add one more session (causing a first purge to happen)
361
            handles.push_back(
1✔
362
               mgr->establish(default_session(Botan::TLS::Connection_Side::Server, cbs), random_id(*rng)).value());
3✔
363

364
            result.confirm("oldest session gone", !mgr->retrieve(handles[0], cbs, plcy).has_value());
2✔
365
            for(size_t i = 1; i < handles.size(); ++i) {
6✔
366
               result.confirm("session still there", mgr->retrieve(handles[i], cbs, plcy).has_value());
15✔
367
            }
368

369
            // remove a session to cause a 'gap' in the FIFO
370
            mgr->remove(handles[4]);
1✔
371
            result.confirm("oldest session gone", !mgr->retrieve(handles[0], cbs, plcy).has_value());
2✔
372
            result.confirm("deleted session gone", !mgr->retrieve(handles[4], cbs, plcy).has_value());
2✔
373
            for(size_t i = 1; i < handles.size(); ++i) {
6✔
374
               result.confirm("session still there", i == 4 || mgr->retrieve(handles[i], cbs, plcy).has_value());
14✔
375
            }
376

377
            // insert enough new sessions to fully purge the ones currently held
378
            for(size_t i = 0; i < mgr->capacity(); ++i) {
6✔
379
               handles.push_back(
5✔
380
                  mgr->establish(default_session(Botan::TLS::Connection_Side::Server, cbs), random_id(*rng)).value());
15✔
381
            }
382

383
            for(size_t i = 0; i < handles.size() - mgr->capacity(); ++i) {
7✔
384
               result.confirm("session gone", !mgr->retrieve(handles[i], cbs, plcy).has_value());
12✔
385
            }
386

387
            for(size_t i = handles.size() - mgr->capacity(); i < handles.size(); ++i) {
6✔
388
               result.confirm("session still there", mgr->retrieve(handles[i], cbs, plcy).has_value());
15✔
389
            }
390

391
            // clear it all out
392
            result.test_eq("rest of the sessions removed", mgr->remove_all(), size_t(5));
1✔
393
         }),
1✔
394
   };
17✔
395
}
5✔
396

397
std::vector<Test::Result> test_session_manager_choose_ticket() {
1✔
398
   #if defined(BOTAN_HAS_TLS_13)
399
   Session_Manager_Callbacks cbs;
1✔
400
   Session_Manager_Policy plcy;
1✔
401

402
   auto rng = Test::new_shared_rng(__func__);
1✔
403

404
   auto default_session = [&](const std::string& suite,
9✔
405
                              Botan::TLS::Callbacks& mycbs,
406
                              Botan::TLS::Protocol_Version version = Botan::TLS::Protocol_Version::TLS_V13) {
407
      return (version.is_pre_tls_13())
8✔
408
                ? Botan::TLS::Session({},
8✔
409
                                      version,
410
                                      Botan::TLS::Ciphersuite::from_name(suite)->ciphersuite_code(),
1✔
411
                                      Botan::TLS::Connection_Side::Server,
412
                                      true,
413
                                      true,
414
                                      {},
415
                                      server_info(),
416
                                      0,
417
                                      mycbs.tls_current_timestamp())
1✔
418
                : Botan::TLS::Session({},
419
                                      std::nullopt,
420
                                      0,
421
                                      std::chrono::seconds(1024),
7✔
422
                                      version,
423
                                      Botan::TLS::Ciphersuite::from_name(suite)->ciphersuite_code(),
7✔
424
                                      Botan::TLS::Connection_Side::Server,
425
                                      {},
426
                                      nullptr,
427
                                      server_info(),
428
                                      mycbs.tls_current_timestamp());
23✔
429
   };
430

431
   auto ticket = [&](std::span<const uint8_t> identity) {
16✔
432
      return Botan::TLS::PskIdentity(std::vector(identity.begin(), identity.end()), 0);
15✔
433
   };
434

435
   return {
1✔
436
      CHECK("empty manager has nothing to choose from",
437
            [&](auto& result) {
1✔
438
               Botan::TLS::Session_Manager_In_Memory mgr(rng);
1✔
439

440
               Botan::TLS::Session_Ticket random_session_ticket = random_ticket(*rng);
1✔
441

442
               result.confirm("empty ticket list, no session",
1✔
443
                              !mgr.choose_from_offered_tickets({}, "SHA-256", cbs, plcy).has_value());
2✔
444
               result.confirm(
1✔
445
                  "empty session manager, no session",
446
                  !mgr.choose_from_offered_tickets(std::vector{ticket(random_session_ticket)}, "SHA-256", cbs, plcy)
3✔
447
                      .has_value());
3✔
448
            }),
3✔
449

450
      CHECK("choose ticket by ID",
451
            [&](auto& result) {
1✔
452
               Botan::TLS::Session_Manager_In_Memory mgr(rng);
1✔
453
               std::vector<Botan::TLS::Session_Handle> handles;
1✔
454

455
               handles.push_back(mgr.establish(default_session("AES_128_GCM_SHA256", cbs)).value());
3✔
456
               handles.push_back(mgr.establish(default_session("AES_128_GCM_SHA256", cbs)).value());
3✔
457

458
               // choose from a list of tickets that contains only handles[0]
459
               auto session1 =
2✔
460
                  mgr.choose_from_offered_tickets(std::vector{ticket(handles[0].id().value())}, "SHA-256", cbs, plcy);
3✔
461
               result.require("ticket was chosen and produced a session (1)", session1.has_value());
1✔
462
               result.test_is_eq("chosen offset", session1->second, uint16_t(0));
1✔
463

464
               // choose from a list of tickets that contains only handles[1]
465
               auto session2 =
2✔
466
                  mgr.choose_from_offered_tickets(std::vector{ticket(handles[1].id().value())}, "SHA-256", cbs, plcy);
3✔
467
               result.require("ticket was chosen and produced a session (2)", session2.has_value());
1✔
468
               result.test_is_eq("chosen offset", session2->second, uint16_t(0));
1✔
469

470
               // choose from a list of tickets that contains a random ticket and handles[1]
471
               auto session3 = mgr.choose_from_offered_tickets(
2✔
472
                  std::vector{ticket(random_ticket(*rng)), ticket(handles[1].id().value())}, "SHA-256", cbs, plcy);
5✔
473
               result.require("ticket was chosen and produced a session (3)", session3.has_value());
1✔
474
               result.test_is_eq("chosen second offset", session3->second, uint16_t(1));
2✔
475
            }),
13✔
476

477
      CHECK("choose ticket by ticket",
478
            [&](auto& result) {
1✔
479
               auto creds = std::make_shared<Test_Credentials_Manager>();
480
               Botan::TLS::Session_Manager_Stateless mgr(creds, rng);
1✔
481
               std::vector<Botan::TLS::Session_Handle> handles;
1✔
482

483
               handles.push_back(mgr.establish(default_session("AES_128_GCM_SHA256", cbs)).value());
3✔
484
               handles.push_back(mgr.establish(default_session("AES_128_GCM_SHA256", cbs)).value());
3✔
485

486
               // choose from a list of tickets that contains only handles[0]
487
               auto session1 = mgr.choose_from_offered_tickets(
2✔
488
                  std::vector{ticket(handles[0].ticket().value())}, "SHA-256", cbs, plcy);
4✔
489
               result.require("ticket was chosen and produced a session (1)", session1.has_value());
1✔
490
               result.test_is_eq("chosen offset", session1->second, uint16_t(0));
1✔
491

492
               // choose from a list of tickets that contains only handles[1]
493
               auto session2 = mgr.choose_from_offered_tickets(
2✔
494
                  std::vector{ticket(handles[1].ticket().value())}, "SHA-256", cbs, plcy);
4✔
495
               result.require("ticket was chosen and produced a session (2)", session2.has_value());
1✔
496
               result.test_is_eq("chosen offset", session2->second, uint16_t(0));
1✔
497

498
               // choose from a list of tickets that contains a random ticket and handles[1]
499
               auto session3 = mgr.choose_from_offered_tickets(
2✔
500
                  std::vector{ticket(random_ticket(*rng)), ticket(handles[1].ticket().value())}, "SHA-256", cbs, plcy);
5✔
501
               result.require("ticket was chosen and produced a session (3)", session3.has_value());
1✔
502
               result.test_is_eq("chosen second offset", session3->second, uint16_t(1));
2✔
503
            }),
14✔
504

505
      CHECK("choose ticket based on requested hash function",
506
            [&](auto& result) {
1✔
507
               auto creds = std::make_shared<Test_Credentials_Manager>();
508
               Botan::TLS::Session_Manager_Stateless mgr(creds, rng);
1✔
509
               std::vector<Botan::TLS::Session_Handle> handles;
1✔
510

511
               handles.push_back(mgr.establish(default_session("AES_128_GCM_SHA256", cbs)).value());
3✔
512
               handles.push_back(mgr.establish(default_session("AES_256_GCM_SHA384", cbs)).value());
3✔
513

514
               auto session = mgr.choose_from_offered_tickets(std::vector{ticket(random_ticket(*rng)),
5✔
515
                                                                          ticket(handles[0].ticket().value()),
1✔
516
                                                                          ticket(handles[1].ticket().value())},
1✔
517
                                                              "SHA-384",
518
                                                              cbs,
519
                                                              plcy);
520
               result.require("ticket was chosen and produced a session", session.has_value());
1✔
521
               result.test_is_eq("chosen second offset", session->second, uint16_t(2));
2✔
522
            }),
7✔
523

524
      CHECK("choose ticket based on protocol version",
525
            [&](auto& result) {
1✔
526
               auto creds = std::make_shared<Test_Credentials_Manager>();
527
               Botan::TLS::Session_Manager_Stateless mgr(creds, rng);
1✔
528
               std::vector<Botan::TLS::Session_Handle> handles;
1✔
529

530
               handles.push_back(
1✔
531
                  mgr.establish(default_session("AES_128_GCM_SHA256", cbs, Botan::TLS::Version_Code::TLS_V12)).value());
3✔
532
               handles.push_back(
1✔
533
                  mgr.establish(default_session("AES_128_GCM_SHA256", cbs, Botan::TLS::Version_Code::TLS_V13)).value());
3✔
534

535
               auto session = mgr.choose_from_offered_tickets(std::vector{ticket(random_ticket(*rng)),
5✔
536
                                                                          ticket(handles[0].ticket().value()),
1✔
537
                                                                          ticket(handles[1].ticket().value())},
1✔
538
                                                              "SHA-256",
539
                                                              cbs,
540
                                                              plcy);
541
               result.require("ticket was chosen and produced a session", session.has_value());
1✔
542
               result.test_is_eq("chosen second offset (TLS 1.3 ticket)", session->second, uint16_t(2));
2✔
543
            }),
7✔
544
   };
7✔
545
   #else
546
   return {};
547
   #endif
548
}
2✔
549

550
std::vector<Test::Result> test_session_manager_stateless() {
1✔
551
   auto creds = std::make_shared<Test_Credentials_Manager>();
1✔
552

553
   auto rng = Test::new_shared_rng(__func__);
1✔
554

555
   Botan::TLS::Session_Manager_Stateless mgr(creds, rng);
1✔
556

557
   Session_Manager_Callbacks cbs;
1✔
558
   Session_Manager_Policy plcy;
1✔
559

560
   return {
1✔
561
      CHECK("establish with default parameters",
562
            [&](auto& result) {
1✔
563
               result.confirm("will emit tickets", mgr.emits_session_tickets());
2✔
564
               auto ticket = mgr.establish(default_session(Botan::TLS::Connection_Side::Server, cbs));
1✔
565
               result.confirm("returned ticket", ticket.has_value() && ticket->is_ticket());
2✔
566
            }),
1✔
567

568
      CHECK("establish with disabled tickets",
569
            [&](auto& result) {
1✔
570
               result.confirm("will emit tickets", mgr.emits_session_tickets());
2✔
571
               auto ticket =
1✔
572
                  mgr.establish(default_session(Botan::TLS::Connection_Side::Server, cbs), std::nullopt, true);
1✔
573
               result.confirm("returned std::nullopt", !ticket.has_value());
2✔
574
            }),
1✔
575

576
      CHECK("establish without ticket key in credentials manager",
577
            [&](auto& result) {
1✔
578
               Botan::TLS::Session_Manager_Stateless local_mgr(std::make_shared<Empty_Credentials_Manager>(), rng);
2✔
579

580
               result.confirm("won't emit tickets", !local_mgr.emits_session_tickets());
2✔
581
               auto ticket = local_mgr.establish(default_session(Botan::TLS::Connection_Side::Server, cbs));
1✔
582
               result.confirm("returned std::nullopt", !ticket.has_value());
2✔
583
            }),
1✔
584

585
      CHECK("retrieve via ticket",
586
            [&](auto& result) {
1✔
587
               auto ticket1 = mgr.establish(default_session(Botan::TLS::Connection_Side::Server, cbs));
1✔
588
               auto ticket2 = mgr.establish(default_session(Botan::TLS::Connection_Side::Server, cbs));
1✔
589
               result.require("tickets created successfully", ticket1.has_value() && ticket2.has_value());
1✔
590

591
               Botan::TLS::Session_Manager_Stateless local_mgr(creds, rng);
1✔
592
               result.confirm("can retrieve ticket 1", mgr.retrieve(ticket1.value(), cbs, plcy).has_value());
2✔
593
               result.confirm("can retrieve ticket 2 from different manager but sam credentials",
1✔
594
                              local_mgr.retrieve(ticket2.value(), cbs, plcy).has_value());
1✔
595
            }),
3✔
596

597
      CHECK("retrieve via ID does not work",
598
            [&](auto& result) {
1✔
599
               auto ticket1 = mgr.establish(default_session(Botan::TLS::Connection_Side::Server, cbs));
1✔
600
               result.require("tickets created successfully", ticket1.has_value() && ticket1.has_value());
1✔
601

602
               result.confirm("retrieval by ID does not work", !mgr.retrieve(random_id(*rng), cbs, plcy).has_value());
3✔
603
            }),
1✔
604

605
      CHECK("retrieve via opaque handle does work",
606
            [&](auto& result) {
1✔
607
               auto ticket1 = mgr.establish(default_session(Botan::TLS::Connection_Side::Server, cbs));
1✔
608
               result.require("tickets created successfully", ticket1.has_value() && ticket1.has_value());
1✔
609

610
               result.confirm("retrieval by opaque handle",
1✔
611
                              mgr.retrieve(ticket1->opaque_handle(), cbs, plcy).has_value());
3✔
612
            }),
1✔
613

614
      CHECK("no retrieve without or with wrong ticket key",
615
            [&](auto& result) {
1✔
616
               auto ticket1 = mgr.establish(default_session(Botan::TLS::Connection_Side::Server, cbs));
1✔
617
               result.require("tickets created successfully", ticket1.has_value() && ticket1.has_value());
1✔
618

619
               Botan::TLS::Session_Manager_Stateless local_mgr1(std::make_shared<Empty_Credentials_Manager>(), rng);
2✔
620

621
               Botan::TLS::Session_Manager_Stateless local_mgr2(std::make_shared<Other_Test_Credentials_Manager>(),
2✔
622
                                                                rng);
623

624
               result.confirm("no successful retrieval (without key)",
1✔
625
                              !local_mgr1.retrieve(ticket1.value(), cbs, plcy).has_value());
1✔
626
               result.confirm("no successful retrieval (with wrong key)",
1✔
627
                              !local_mgr2.retrieve(ticket1.value(), cbs, plcy).has_value());
1✔
628
               result.confirm("successful retrieval", mgr.retrieve(ticket1.value(), cbs, plcy).has_value());
2✔
629
            }),
2✔
630

631
      CHECK("Clients cannot be stateless",
632
            [&](auto& result) {
1✔
633
               result.test_throws("::store() does not work with ID", [&] {
2✔
634
                  mgr.store(default_session(Botan::TLS::Connection_Side::Client, cbs), random_id(*rng));
3✔
635
               });
636
               result.test_throws("::store() does not work with ticket", [&] {
2✔
637
                  mgr.store(default_session(Botan::TLS::Connection_Side::Client, cbs), random_ticket(*rng));
3✔
638
               });
639
               result.test_throws("::store() does not work with opaque handle", [&] {
2✔
640
                  mgr.store(default_session(Botan::TLS::Connection_Side::Client, cbs), random_opaque_handle(*rng));
3✔
641
               });
642

643
               auto ticket1 = mgr.establish(default_session(Botan::TLS::Connection_Side::Server, cbs));
1✔
644
               result.require("tickets created successfully", ticket1.has_value() && ticket1.has_value());
1✔
645
               result.confirm("finding tickets does not work", mgr.find(server_info(), cbs, plcy).empty());
2✔
646
            }),
1✔
647

648
      CHECK("remove is a NOOP",
649
            [&](auto& result) {
1✔
650
               auto ticket1 = mgr.establish(default_session(Botan::TLS::Connection_Side::Server, cbs));
1✔
651
               result.require("tickets created successfully", ticket1.has_value() && ticket1.has_value());
1✔
652

653
               result.test_is_eq("remove the ticket", mgr.remove(ticket1.value()), size_t(0));
1✔
654
               result.confirm("successful retrieval 1", mgr.retrieve(ticket1.value(), cbs, plcy).has_value());
2✔
655

656
               result.test_is_eq("remove the ticket", mgr.remove_all(), size_t(0));
1✔
657
               result.confirm("successful retrieval 1", mgr.retrieve(ticket1.value(), cbs, plcy).has_value());
3✔
658
            }),
1✔
659

660
      CHECK(
661
         "retrieval via ticket reconstructs the start_time stamp",
662
         [&](auto& result) {
1✔
663
            auto session_before = default_session(Botan::TLS::Connection_Side::Server, cbs);
1✔
664
            auto ticket = mgr.establish(session_before);
2✔
665
            result.require("got a ticket", ticket.has_value() && ticket->is_ticket());
1✔
666
            auto session_after = mgr.retrieve(ticket.value(), cbs, plcy);
1✔
667
            result.require("got the session back", session_after.has_value());
2✔
668

669
            result.test_is_eq(
1✔
670
               "timestamps match",
671
               std::chrono::duration_cast<std::chrono::seconds>(session_before.start_time().time_since_epoch()).count(),
2✔
672
               std::chrono::duration_cast<std::chrono::seconds>(session_after->start_time().time_since_epoch())
1✔
673
                  .count());
2✔
674
         }),
2✔
675
   };
11✔
676
}
4✔
677

678
std::vector<Test::Result> test_session_manager_hybrid() {
1✔
679
   auto rng = Test::new_shared_rng(__func__);
1✔
680

681
   auto creds = std::make_shared<Test_Credentials_Manager>();
1✔
682
   Session_Manager_Callbacks cbs;
1✔
683
   Session_Manager_Policy plcy;
1✔
684

685
   // Runs the passed-in hybrid manager test lambdas for all available stateful
686
   // managers. The `make_manager()` helper is passed into the test code and
687
   // transparently constructs a hybrid manager with the respective internal
688
   // stateful manager.
689
   auto CHECK_all = [&](const std::string& name, auto lambda) -> std::vector<Test::Result> {
4✔
690
      std::vector<std::pair<std::string, std::function<std::unique_ptr<Botan::TLS::Session_Manager>()>>> const
691
         stateful_manager_factories = {
9✔
692
            {"In Memory",
693
             [&rng]() -> std::unique_ptr<Botan::TLS::Session_Manager> {
9✔
694
                return std::make_unique<Botan::TLS::Session_Manager_In_Memory>(rng, 10);
6✔
695
             }},
696
   #if defined(BOTAN_HAS_TLS_SQLITE3_SESSION_MANAGER)
697
            {"SQLite",
698
             [&rng]() -> std::
9✔
699
                         unique_ptr<Botan::TLS::Session_Manager> {
700
                            return std::make_unique<Botan::TLS::Session_Manager_SQLite>(
701
                               "secure_pw", rng, Test::temp_file_name("tls_session_manager_sqlite"), 10);
6✔
702
                         }},
703
   #endif
704
         };
705

706
      std::vector<Test::Result> results;
3✔
707
      using namespace std::placeholders;
708
      for(const auto& factory_and_name : stateful_manager_factories) {
15✔
709
         const auto& stateful_manager_name = factory_and_name.first;
6✔
710
         const auto& stateful_manager_factory = factory_and_name.second;
6✔
711
         auto make_manager = [stateful_manager_factory, &creds, &rng](bool prefer_tickets) {
42✔
712
            return Botan::TLS::Session_Manager_Hybrid(stateful_manager_factory(), creds, rng, prefer_tickets);
48✔
713
         };
714

715
         auto nm = Botan::fmt("{} ({})", name, stateful_manager_name);
6✔
716
         auto fn = std::bind(lambda, make_manager, _1);  // NOLINT(*-avoid-bind)
6✔
717
         results.push_back(CHECK(nm.c_str(), fn));
18✔
718
      }
719
      return results;
3✔
720
   };
7✔
721

722
   return Test::flatten_result_lists({
3✔
723
      CHECK_all("ticket vs ID preference in establishment",
1✔
724
                [&](const auto& make_manager, auto& result) {
2✔
725
                   auto mgr_prefers_tickets = make_manager(true);
2✔
726
                   auto ticket1 =
2✔
727
                      mgr_prefers_tickets.establish(default_session(Botan::TLS::Connection_Side::Server, cbs));
2✔
728
                   result.confirm("emits a ticket", ticket1.has_value() && ticket1->is_ticket());
4✔
729

730
                   auto mgr_prefers_ids = make_manager(false);
2✔
731
                   auto ticket2 = mgr_prefers_ids.establish(default_session(Botan::TLS::Connection_Side::Server, cbs));
2✔
732
                   result.confirm("emits an ID", ticket2.has_value() && ticket2->is_id());
4✔
733

734
                   auto ticket3 = mgr_prefers_ids.establish(default_session(Botan::TLS::Connection_Side::Server, cbs),
2✔
735
                                                            std::nullopt,
736
                                                            true /* TLS 1.2 no ticket support */);
737
                   result.confirm("emits an ID", ticket3.has_value() && ticket3->is_id());
4✔
738

739
                   auto ticket4 = mgr_prefers_ids.establish(default_session(Botan::TLS::Connection_Side::Server, cbs),
2✔
740
                                                            std::nullopt,
741
                                                            true /* TLS 1.2 no ticket support */);
742
                   result.confirm("emits an ID", ticket4.has_value() && ticket4->is_id());
4✔
743
                }),
8✔
744

745
      CHECK_all("ticket vs ID preference in retrieval",
1✔
746
                [&](const auto& make_manager, auto& result) {
2✔
747
                   auto mgr_prefers_tickets = make_manager(true);
2✔
748
                   auto mgr_prefers_ids = make_manager(false);
2✔
749

750
                   auto id1 = mgr_prefers_tickets.underlying_stateful_manager()->establish(
2✔
751
                      default_session(Botan::TLS::Connection_Side::Server, cbs));
752
                   auto id2 = mgr_prefers_ids.underlying_stateful_manager()->establish(
2✔
753
                      default_session(Botan::TLS::Connection_Side::Server, cbs));
754
                   auto ticket =
2✔
755
                      mgr_prefers_tickets.establish(default_session(Botan::TLS::Connection_Side::Server, cbs));
2✔
756

757
                   result.require("establishments worked", id1.has_value() && id2.has_value() && ticket.has_value());
2✔
758

759
                   result.confirm("mgr1 + ID1", mgr_prefers_tickets.retrieve(id1.value(), cbs, plcy).has_value());
4✔
760
                   result.confirm("mgr1 + ID2", !mgr_prefers_tickets.retrieve(id2.value(), cbs, plcy).has_value());
4✔
761
                   result.confirm("mgr2 + ID1", !mgr_prefers_ids.retrieve(id1.value(), cbs, plcy).has_value());
4✔
762
                   result.confirm("mgr2 + ID2", mgr_prefers_ids.retrieve(id2.value(), cbs, plcy).has_value());
4✔
763
                   result.confirm("mgr1 + ticket", mgr_prefers_tickets.retrieve(ticket.value(), cbs, plcy).has_value());
4✔
764
                   result.confirm("mgr2 + ticket", mgr_prefers_ids.retrieve(ticket.value(), cbs, plcy).has_value());
6✔
765
                }),
6✔
766

767
      CHECK_all("no session tickets if hybrid manager cannot create them",
1✔
768
                [&](const auto& make_manager, auto& result) {
2✔
769
                   Botan::TLS::Session_Manager_Hybrid empty_mgr(
8✔
770
                      std::make_unique<Botan::TLS::Session_Manager_In_Memory>(rng, 10),
4✔
771
                      std::make_shared<Empty_Credentials_Manager>(),
772
                      rng);
773
                   auto mgr_prefers_tickets = make_manager(true);
2✔
774
                   auto mgr_prefers_ids = make_manager(false);
2✔
775

776
                   result.confirm("does not emit tickets", !empty_mgr.emits_session_tickets());
4✔
777
                   result.confirm("does emit tickets 1", mgr_prefers_tickets.emits_session_tickets());
4✔
778
                   result.confirm("does emit tickets 2", mgr_prefers_ids.emits_session_tickets());
4✔
779
                }),
2✔
780
   });
4✔
781
}
4✔
782

783
namespace {
784

785
class Temporary_Database_File {
786
   private:
787
      std::string m_temp_file;
788

789
   public:
790
      explicit Temporary_Database_File(const std::string& db_file) :
1✔
791
            m_temp_file(Test::data_file_as_temporary_copy(db_file)) {
1✔
792
         if(m_temp_file.empty()) {
1✔
793
            throw Test_Error("Failed to create temporary database file");
×
794
         }
795
      }
1✔
796

797
      ~Temporary_Database_File() {
1✔
798
   #if defined(BOTAN_TARGET_OS_HAS_FILESYSTEM) && defined(__cpp_lib_filesystem)
799
         if(!m_temp_file.empty()) {
1✔
800
            std::filesystem::remove(m_temp_file);
1✔
801
         }
802
   #endif
803
      }
1✔
804

805
      const std::string& get() const { return m_temp_file; }
806

807
      Temporary_Database_File(const Temporary_Database_File&) = delete;
808
      Temporary_Database_File& operator=(const Temporary_Database_File&) = delete;
809
      Temporary_Database_File(Temporary_Database_File&&) = delete;
810
      Temporary_Database_File& operator=(Temporary_Database_File&&) = delete;
811
};
812

813
}  // namespace
814

815
std::vector<Test::Result> test_session_manager_sqlite() {
1✔
816
   #if defined(BOTAN_HAS_TLS_SQLITE3_SESSION_MANAGER)
817
   auto rng = Test::new_shared_rng(__func__);
1✔
818
   Session_Manager_Callbacks cbs;
1✔
819
   Session_Manager_Policy plcy;
1✔
820

821
   return {
1✔
822
      CHECK("migrate session database scheme (purges database)",
823
            [&](auto& result) {
1✔
824
               Temporary_Database_File const dbfile("tls-sessions/botan-2.19.3.sqlite");
1✔
825

826
               // legacy database (encrypted with 'thetruthisoutthere') containing:
827
               //    $ sqlite3 src/tests/data/tls-sessions/botan-2.19.3.sqlite  'SELECT * FROM tls_sessions;'
828
               //    63C136FAD49F05A184F910FD6568A3884164216C11E41CEBFDCD149AF66C1714|1673606906|cloudflare.com|443|...
829
               //    63C137030387E4A6CDAD303CCB1F53884944FDE5B4EDD91E6FCF74DCB033DCEB|1673606915|randombit.net|443|...
830
               Botan::TLS::Session_Manager_SQLite legacy_db("thetruthisoutthere", rng, dbfile.get());
1✔
831

832
               result.confirm("Session_ID for randombit.net is gone",
1✔
833
                              !legacy_db
834
                                  .retrieve(Botan::TLS::Session_ID(Botan::hex_decode(
2✔
835
                                               "63C137030387E4A6CDAD303CCB1F53884944FDE5B4EDD91E6FCF74DCB033DCEB")),
836
                                            cbs,
837
                                            plcy)
838
                                  .has_value());
3✔
839
               result.confirm("Session_ID for cloudflare.com is gone",
1✔
840
                              !legacy_db
841
                                  .retrieve(Botan::TLS::Session_ID(Botan::hex_decode(
2✔
842
                                               "63C136FAD49F05A184F910FD6568A3884164216C11E41CEBFDCD149AF66C1714")),
843
                                            cbs,
844
                                            plcy)
845
                                  .has_value());
3✔
846
               result.confirm("no more session for randombit.net",
1✔
847
                              legacy_db.find(Botan::TLS::Server_Information("randombit.net", 443), cbs, plcy).empty());
2✔
848
               result.confirm("no more session for cloudflare.com",
1✔
849
                              legacy_db.find(Botan::TLS::Server_Information("cloudflare.com", 443), cbs, plcy).empty());
2✔
850

851
               result.test_is_eq("empty database won't get more empty", legacy_db.remove_all(), size_t(0));
2✔
852
            }),
1✔
853

854
      CHECK("clearing empty database",
855
            [&](auto& result) {
1✔
856
               Botan::TLS::Session_Manager_SQLite mgr("thetruthisoutthere", rng, Test::temp_file_name("empty.sqlite"));
1✔
857
               result.test_eq("does not delete anything", mgr.remove_all(), 0);
2✔
858
            }),
1✔
859

860
      CHECK("establish new session",
861
            [&](auto& result) {
1✔
862
               Botan::TLS::Session_Manager_SQLite mgr(
1✔
863
                  "thetruthisoutthere", rng, Test::temp_file_name("new_session.sqlite"));
1✔
864
               auto some_random_id = random_id(*rng);
1✔
865
               auto some_random_handle =
1✔
866
                  mgr.establish(default_session(Botan::TLS::Connection_Side::Server, cbs), some_random_id);
867
               result.require("establishment was successful", some_random_handle.has_value());
2✔
868
               result.require("session id was set", some_random_handle->id().has_value());
2✔
869
               result.test_is_eq("session id is correct", some_random_handle->id().value(), some_random_id);
3✔
870

871
               auto some_virtual_handle = mgr.establish(default_session(Botan::TLS::Connection_Side::Server, cbs));
1✔
872
               result.require("establishment was successful", some_virtual_handle.has_value());
2✔
873
               result.require("session id was set", some_virtual_handle->id().has_value());
2✔
874
            }),
3✔
875

876
      CHECK("retrieve session by ID",
877
            [&](auto& result) {
1✔
878
               Botan::TLS::Session_Manager_SQLite mgr(
1✔
879
                  "thetruthisoutthere", rng, Test::temp_file_name("retrieve_by_id.sqlite"));
1✔
880
               auto some_random_id = random_id(*rng);
1✔
881
               auto some_random_handle =
1✔
882
                  mgr.establish(default_session(Botan::TLS::Connection_Side::Server, cbs), some_random_id);
883
               auto some_virtual_handle = mgr.establish(default_session(Botan::TLS::Connection_Side::Server, cbs));
1✔
884

885
               result.require("establishment was successful",
1✔
886
                              some_random_handle->is_id() && some_virtual_handle->is_id());
1✔
887

888
               auto session1 = mgr.retrieve(some_random_handle.value(), cbs, plcy);
1✔
889
               if(result.confirm("found session by user-provided ID", session1.has_value())) {
2✔
890
                  result.test_is_eq("protocol version was echoed",
1✔
891
                                    session1->version(),
1✔
892
                                    Botan::TLS::Protocol_Version(Botan::TLS::Version_Code::TLS_V12));
1✔
893
                  result.test_is_eq("ciphersuite was echoed", session1->ciphersuite_code(), uint16_t(0x009C));
2✔
894
               }
895

896
               auto session2 = mgr.retrieve(some_virtual_handle.value(), cbs, plcy);
1✔
897
               if(result.confirm("found session by manager-generated ID", session2.has_value())) {
2✔
898
                  result.test_is_eq("protocol version was echoed",
1✔
899
                                    session2->version(),
1✔
900
                                    Botan::TLS::Protocol_Version(Botan::TLS::Version_Code::TLS_V12));
1✔
901
                  result.test_is_eq("ciphersuite was echoed", session2->ciphersuite_code(), uint16_t(0x009C));
2✔
902
               }
903

904
               auto session3 = mgr.retrieve(random_id(*rng), cbs, plcy);
2✔
905
               result.confirm("random ID creates empty result", !session3.has_value());
2✔
906
            }),
6✔
907

908
      CHECK("retrieval via ticket creates empty result",
909
            [&](auto& result) {
1✔
910
               Botan::TLS::Session_Manager_SQLite mgr(
1✔
911
                  "thetruthisoutthere", rng, Test::temp_file_name("retrieve_by_ticket.sqlite"));
1✔
912
               auto some_random_handle =
1✔
913
                  mgr.establish(default_session(Botan::TLS::Connection_Side::Server, cbs), random_id(*rng));
1✔
914
               auto some_virtual_handle = mgr.establish(default_session(Botan::TLS::Connection_Side::Server, cbs));
1✔
915

916
               result.confirm("std::nullopt on random ticket",
1✔
917
                              !mgr.retrieve(random_ticket(*rng), cbs, plcy).has_value());
3✔
918
            }),
2✔
919

920
      CHECK("storing sessions and finding them by server info",
921
            [&](auto& result) {
1✔
922
               Botan::TLS::Session_Manager_SQLite mgr(
1✔
923
                  "thetruthisoutthere", rng, Test::temp_file_name("store_and_find.sqlite"));
1✔
924
               auto id = random_id(*rng);
1✔
925
               auto ticket = random_ticket(*rng);
1✔
926
               mgr.store(default_session(Botan::TLS::Connection_Side::Client, cbs), id);
2✔
927
               mgr.store(default_session(Botan::TLS::Connection_Side::Client, cbs), ticket);
2✔
928

929
               auto found_sessions = mgr.find(server_info(), cbs, plcy);
1✔
930
               if(result.test_is_eq("found both sessions", found_sessions.size(), size_t(2))) {
1✔
931
                  for(const auto& [session, handle] : found_sessions) {
3✔
932
                     result.confirm("ID matches", !handle.is_id() || handle.id().value() == id);
5✔
933
                     result.confirm("ticket matches", !handle.is_ticket() || handle.ticket().value() == ticket);
5✔
934
                  }
935
               }
936
            }),
3✔
937

938
      CHECK("removing sessions",
939
            [&](auto& result) {
1✔
940
               Botan::TLS::Session_Manager_SQLite mgr("thetruthisoutthere", rng, Test::temp_file_name("remove.sqlite"));
1✔
941
               auto id = random_id(*rng);
1✔
942
               auto ticket = random_ticket(*rng);
1✔
943
               mgr.store(default_session(Botan::TLS::Connection_Side::Client, cbs), id);
2✔
944
               mgr.store(default_session(Botan::TLS::Connection_Side::Client, cbs), ticket);
2✔
945
               mgr.store(default_session(Botan::TLS::Connection_Side::Client, cbs), random_id(*rng));
2✔
946
               mgr.store(default_session(Botan::TLS::Connection_Side::Client, cbs), random_ticket(*rng));
2✔
947

948
               result.test_is_eq("deletes one session by ID", mgr.remove(id), size_t(1));
4✔
949
               result.test_is_eq("deletes one session by ticket", mgr.remove(ticket), size_t(1));
4✔
950

951
               auto found_sessions = mgr.find(server_info(), cbs, plcy);
1✔
952
               if(result.test_is_eq("found some other sessions", found_sessions.size(), size_t(2))) {
1✔
953
                  for(const auto& [session, handle] : found_sessions) {
3✔
954
                     result.confirm("ID does not match", !handle.is_id() || handle.id().value() != id);
6✔
955
                     result.confirm("ticket does not match", !handle.is_ticket() || handle.ticket().value() != ticket);
6✔
956
                  }
957
               }
958

959
               result.test_is_eq("removing the rest of the sessions", mgr.remove_all(), size_t(2));
1✔
960
            }),
3✔
961

962
      CHECK("old sessions are purged when needed",
963
            [&](auto& result) {
1✔
964
               Botan::TLS::Session_Manager_SQLite mgr(
1✔
965
                  "thetruthisoutthere", rng, Test::temp_file_name("purging.sqlite"), 1);
1✔
966

967
               std::vector<Botan::TLS::Session_ID> ids = {random_id(*rng), random_id(*rng), random_id(*rng)};
5✔
968
               mgr.establish(default_session(Botan::TLS::Connection_Side::Server, cbs), ids[0]);
2✔
969
               result.require("new ID exists", mgr.retrieve(ids[0], cbs, plcy).has_value());
5✔
970

971
               // Session timestamps are saved with second-resolution. If more than
972
               // one session has the same (coarse) timestamp it is undefined which
973
               // will be purged first. The clock tick ensures that session's
974
               // timestamps are unique.
975
               cbs.tick();
1✔
976
               mgr.establish(default_session(Botan::TLS::Connection_Side::Server, cbs), ids[1]);
2✔
977
               result.require("first ID is gone", !mgr.retrieve(ids[0], cbs, plcy).has_value());
4✔
978
               result.require("new ID exists", mgr.retrieve(ids[1], cbs, plcy).has_value());
5✔
979

980
               cbs.tick();
1✔
981
               mgr.establish(default_session(Botan::TLS::Connection_Side::Server, cbs), ids[2]);
2✔
982
               result.require("second ID is gone", !mgr.retrieve(ids[1], cbs, plcy).has_value());
4✔
983
               result.require("new ID exists", mgr.retrieve(ids[2], cbs, plcy).has_value());
5✔
984

985
               result.test_is_eq("only one entry exists", mgr.remove_all(), size_t(1));
1✔
986
            }),
2✔
987

988
      CHECK("session purging can be disabled",
989
            [&](auto& result) {
1✔
990
               Botan::TLS::Session_Manager_SQLite mgr(
1✔
991
                  "thetruthisoutthere", rng, Test::temp_file_name("purging.sqlite"), 0 /* no pruning! */);
1✔
992

993
               for(size_t i = 0; i < 25; ++i) {
26✔
994
                  mgr.establish(default_session(Botan::TLS::Connection_Side::Server, cbs), random_id(*rng));
50✔
995
               }
996

997
               result.test_is_eq("no entries were purged along the way", mgr.remove_all(), size_t(25));
2✔
998
            }),
1✔
999
   };
10✔
1000
   #else
1001
   return {};
1002
   #endif
1003
}
3✔
1004

1005
std::vector<Test::Result> tls_session_manager_expiry() {
1✔
1006
   auto rng = Test::new_shared_rng(__func__);
1✔
1007
   Session_Manager_Callbacks cbs;
1✔
1008
   Session_Manager_Policy plcy;
1✔
1009

1010
   auto CHECK_all = [&](const std::string& name, auto lambda) -> std::vector<Test::Result> {
6✔
1011
      const std::vector<std::pair<std::string, std::function<std::unique_ptr<Botan::TLS::Session_Manager>()>>>
1012
         stateful_manager_factories = {
20✔
1013
            {"In Memory",
1014
             [&rng]() -> std::unique_ptr<Botan::TLS::Session_Manager> {
10✔
1015
                return std::make_unique<Botan::TLS::Session_Manager_In_Memory>(rng, 10);
5✔
1016
             }},
1017
            {"Stateless",
1018
             [&]() -> std::unique_ptr<Botan::TLS::Session_Manager> {
7✔
1019
                return std::make_unique<Botan::TLS::Session_Manager_Stateless>(
1020
                   std::make_shared<Test_Credentials_Manager>(), rng);
2✔
1021
             }},
1022
   #if defined(BOTAN_HAS_TLS_SQLITE3_SESSION_MANAGER)
1023
            {"SQLite",
1024
             [&rng]() -> std::unique_ptr<Botan::TLS::Session_Manager> {
10✔
1025
                return std::make_unique<Botan::TLS::Session_Manager_SQLite>(
1026
                   "secure_pw", rng, Test::temp_file_name("tls_session_manager_sqlite"), 10);
5✔
1027
             }},
1028
   #endif
1029
         };
1030

1031
      std::vector<Test::Result> results;
5✔
1032
      results.reserve(stateful_manager_factories.size());
5✔
1033
      using namespace std::placeholders;
1034
      for(const auto& [sub_name, factory] : stateful_manager_factories) {
20✔
1035
         auto nm = Botan::fmt("{} ({})", name, sub_name);
15✔
1036
         auto fn = std::bind(lambda, sub_name, factory, _1);  // NOLINT(*-avoid-bind)
15✔
1037
         results.push_back(CHECK(nm.c_str(), fn));
30✔
1038
      }
1039
      return results;
5✔
1040
   };
11✔
1041

1042
   return Test::flatten_result_lists({
5✔
1043
      CHECK_all("sessions expire",
1✔
1044
                [&](const auto&, const auto& factory, auto& result) {
3✔
1045
                   auto mgr = factory();
3✔
1046

1047
                   auto handle = mgr->establish(default_session(Botan::TLS::Connection_Side::Server, cbs));
3✔
1048
                   result.require("saved successfully", handle.has_value());
6✔
1049
                   result.require("session was found", mgr->retrieve(handle.value(), cbs, plcy).has_value());
6✔
1050
                   cbs.tick();
3✔
1051
                   result.confirm("session has expired", !mgr->retrieve(handle.value(), cbs, plcy).has_value());
6✔
1052
                   result.test_is_eq("session was deleted when it expired", mgr->remove_all(), size_t(0));
6✔
1053
                }),
6✔
1054

1055
         CHECK_all("expired sessions are not found",
1✔
1056
                   [&](const std::string& type, const auto& factory, auto& result) {
3✔
1057
                      if(type == "Stateless") {
3✔
1058
                         return;  // this manager can neither store nor find anything
1✔
1059
                      }
1060

1061
                      auto mgr = factory();
2✔
1062

1063
                      auto handle_old = random_id(*rng);
2✔
1064
                      mgr->store(default_session(Botan::TLS::Connection_Side::Client, cbs), handle_old);
6✔
1065
                      result.require("session was found", mgr->retrieve(handle_old, cbs, plcy).has_value());
10✔
1066

1067
                      cbs.tick();
2✔
1068
                      auto handle_new = random_id(*rng);
2✔
1069
                      mgr->store(default_session(Botan::TLS::Connection_Side::Client, cbs), handle_new);
6✔
1070
                      result.require("session was found", mgr->retrieve(handle_new, cbs, plcy).has_value());
10✔
1071

1072
                      auto sessions_and_handles = mgr->find(server_info(), cbs, plcy);
2✔
1073
                      result.require("sessions are found", !sessions_and_handles.empty());
2✔
1074
                      result.test_is_eq("exactly one session is found", sessions_and_handles.size(), size_t(1));
4✔
1075
                      result.test_is_eq(
4✔
1076
                         "the new session is found", sessions_and_handles.front().handle.id().value(), handle_new);
4✔
1077

1078
                      result.test_is_eq("old session was deleted when it expired", mgr->remove_all(), size_t(1));
2✔
1079
                   }),
8✔
1080

1081
         CHECK_all(
1✔
1082
            "session tickets are not reused",
1083
            [&](const std::string& type, const auto& factory, auto& result) {
3✔
1084
               if(type == "Stateless") {
3✔
1085
                  return;  // this manager can neither store nor find anything
1✔
1086
               }
1087

1088
               auto mgr = factory();
2✔
1089

1090
               auto handle_1 = random_id(*rng);
2✔
1091
               mgr->store(default_session(Botan::TLS::Connection_Side::Client, cbs, Botan::TLS::Version_Code::TLS_V12),
6✔
1092
                          handle_1);
1093
               auto handle_2 = random_ticket(*rng);
2✔
1094
               mgr->store(default_session(Botan::TLS::Connection_Side::Client, cbs, Botan::TLS::Version_Code::TLS_V12),
6✔
1095
                          handle_2);
1096

1097
   #if defined(BOTAN_HAS_TLS_13)
1098
               auto handle_3 = random_id(*rng);
2✔
1099
               mgr->store(default_session(Botan::TLS::Connection_Side::Client, cbs, Botan::TLS::Version_Code::TLS_V13),
6✔
1100
                          handle_3);
1101
               auto handle_4 = random_ticket(*rng);
2✔
1102
               mgr->store(default_session(Botan::TLS::Connection_Side::Client, cbs, Botan::TLS::Version_Code::TLS_V13),
6✔
1103
                          handle_4);
1104
   #endif
1105

1106
               plcy.set_allow_session_reuse(false);
2✔
1107

1108
               auto sessions_and_handles1 = mgr->find(server_info(), cbs, plcy);
2✔
1109
               result.require("all sessions are found", sessions_and_handles1.size() > 1);
4✔
1110

1111
               auto sessions_and_handles2 = mgr->find(server_info(), cbs, plcy);
2✔
1112
               result.test_is_eq("only one session is found", sessions_and_handles2.size(), size_t(1));
4✔
1113
               result.confirm("found session is the Session_ID", sessions_and_handles2.front().handle.is_id());
4✔
1114
               result.test_is_eq(
4✔
1115
                  "found session is the Session_ID", sessions_and_handles2.front().handle.id().value(), handle_1);
6✔
1116
               result.confirm("found session is TLS 1.2",
4✔
1117
                              sessions_and_handles2.front().session.version().is_pre_tls_13());
2✔
1118
            }),
12✔
1119

1120
         CHECK_all("number of found tickets is capped",
1✔
1121
                   [&](const std::string& type, const auto& factory, auto& result) {
3✔
1122
                      if(type == "Stateless") {
3✔
1123
                         return;  // this manager can neither store nor find anything
1✔
1124
                      }
1125

1126
                      auto mgr = factory();
2✔
1127

1128
                      std::array<Botan::TLS::Session_Ticket, 5> tickets;
2✔
1129
                      for(auto& ticket : tickets) {
12✔
1130
                         ticket = random_ticket(*rng);
10✔
1131
                         mgr->store(default_session(Botan::TLS::Connection_Side::Client, cbs), ticket);
30✔
1132
                      }
1133

1134
                      plcy.set_allow_session_reuse(true);
2✔
1135

1136
                      plcy.set_session_limit(1);
2✔
1137
                      result.test_is_eq("find one",
2✔
1138
                                        mgr->find(server_info(), cbs, plcy).size(),
4✔
1139
                                        plcy.maximum_session_tickets_per_client_hello());
2✔
1140

1141
                      plcy.set_session_limit(3);
2✔
1142
                      result.test_is_eq("find three",
2✔
1143
                                        mgr->find(server_info(), cbs, plcy).size(),
4✔
1144
                                        plcy.maximum_session_tickets_per_client_hello());
2✔
1145

1146
                      plcy.set_session_limit(10);
2✔
1147
                      result.test_is_eq("find all five", mgr->find(server_info(), cbs, plcy).size(), size_t(5));
4✔
1148
                   }),
4✔
1149

1150
   #if defined(BOTAN_HAS_TLS_13)
1151
         CHECK_all("expired tickets are not selected for PSK resumption",
1✔
1152
                   [&](const auto&, const auto& factory, auto& result) {
3✔
1153
                      auto ticket = [&](const Botan::TLS::Session_Handle& handle) {
12✔
1154
                         return Botan::TLS::PskIdentity(handle.opaque_handle().get(), 0);
12✔
1155
                      };
1156

1157
                      auto mgr = factory();
3✔
1158

1159
                      auto old_handle = mgr->establish(
3✔
1160
                         default_session(Botan::TLS::Connection_Side::Server, cbs, Botan::TLS::Version_Code::TLS_V13));
1161
                      cbs.tick();
3✔
1162
                      auto new_handle = mgr->establish(
3✔
1163
                         default_session(Botan::TLS::Connection_Side::Server, cbs, Botan::TLS::Version_Code::TLS_V13));
1164
                      result.require("both sessions are stored", old_handle.has_value() && new_handle.has_value());
6✔
1165

1166
                      auto session_and_index = mgr->choose_from_offered_tickets(
12✔
1167
                         std::vector{ticket(old_handle.value()), ticket(new_handle.value())}, "SHA-256", cbs, plcy);
6✔
1168
                      result.require("a ticket was chosen", session_and_index.has_value());
3✔
1169
                      result.test_is_eq("the new ticket was chosen", session_and_index->second, uint16_t(1));
3✔
1170

1171
                      cbs.tick();
3✔
1172

1173
                      auto nothing = mgr->choose_from_offered_tickets(
12✔
1174
                         std::vector{ticket(new_handle.value()), ticket(old_handle.value())}, "SHA-256", cbs, plcy);
6✔
1175
                      result.require("all tickets are expired", !nothing.has_value());
6✔
1176
                   }),
33✔
1177
   #endif
1178
   });
6✔
1179
}
3✔
1180

1181
}  // namespace
1182

1183
BOTAN_REGISTER_TEST_FN("tls",
1184
                       "tls_session_manager",
1185
                       test_session_manager_in_memory,
1186
                       test_session_manager_choose_ticket,
1187
                       test_session_manager_stateless,
1188
                       test_session_manager_hybrid,
1189
                       test_session_manager_sqlite,
1190
                       tls_session_manager_expiry);
1191

1192
}  // namespace Botan_Tests
1193

1194
#endif  // BOTAN_HAS_TLS
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc