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

randombit / botan / 25139258422

29 Apr 2026 08:02PM UTC coverage: 89.37% (-0.02%) from 89.385%
25139258422

push

github

web-flow
Merge pull request #5550 from randombit/jack/tls-misc

TLS conformance, hardening, and performance fixes

107055 of 119789 relevant lines covered (89.37%)

11415549.66 hits per line

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

98.8
/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/assert.h>
13
   #include <botan/credentials_manager.h>
14
   #include <botan/hex.h>
15
   #include <botan/rng.h>
16
   #include <botan/tls_callbacks.h>
17
   #include <botan/tls_policy.h>
18
   #include <botan/tls_session_manager_hybrid.h>
19
   #include <botan/tls_session_manager_memory.h>
20
   #include <botan/tls_session_manager_stateless.h>
21
   #include <botan/x509cert.h>
22
   #include <botan/internal/fmt.h>
23
   #include <array>
24
   #include <chrono>
25

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

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

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

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

48
namespace Botan_Tests {
49

50
namespace {
51

52
class Test_Credentials_Manager : public Botan::Credentials_Manager {
14✔
53
   public:
54
      Botan::secure_vector<uint8_t> session_ticket_key() override {
55✔
55
         return Botan::hex_decode_locked("DEADFACECAFEBAAD");
55✔
56
      }
57
};
58

59
class Other_Test_Credentials_Manager : public Botan::Credentials_Manager {
2✔
60
   public:
61
      Botan::secure_vector<uint8_t> session_ticket_key() override {
1✔
62
         return Botan::hex_decode_locked("CAFEBAADFACEBABE");
1✔
63
      }
64
};
65

66
class Empty_Credentials_Manager : public Botan::Credentials_Manager {};
8✔
67

68
class Session_Manager_Callbacks : public Botan::TLS::Callbacks {
3✔
69
   public:
70
      void tls_emit_data(std::span<const uint8_t> /*data*/) override { BOTAN_ASSERT_NOMSG(false); }
×
71

72
      void tls_record_received(uint64_t /*record*/, std::span<const uint8_t> /*data*/) override {
×
73
         BOTAN_ASSERT_NOMSG(false);
×
74
      }
75

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

78
      void tls_session_established(const Botan::TLS::Session_Summary& /*summary*/) override {
×
79
         BOTAN_ASSERT_NOMSG(false);
×
80
      }
81

82
      std::chrono::system_clock::time_point tls_current_timestamp() override {
225✔
83
         return std::chrono::system_clock::now() + std::chrono::hours(m_ticks);
225✔
84
      }
85

86
      void tick() { ++m_ticks; }
13✔
87

88
   private:
89
      uint64_t m_ticks = 0;
90
};
91

92
class Session_Manager_Policy : public Botan::TLS::Policy {
6✔
93
   public:
94
      std::chrono::seconds session_ticket_lifetime() const override { return std::chrono::minutes(30); }
198✔
95

96
      bool reuse_session_tickets() const override { return m_allow_session_reuse; }
27✔
97

98
      size_t maximum_session_tickets_per_client_hello() const override { return m_session_limit; }
54✔
99

100
      void set_session_limit(size_t l) { m_session_limit = l; }
6✔
101

102
      void set_allow_session_reuse(bool b) { m_allow_session_reuse = b; }
4✔
103

104
   private:
105
      size_t m_session_limit = 1000;  // basically 'no limit'
106
      bool m_allow_session_reuse = true;
107
};
108

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

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

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

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

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

159
using namespace std::literals;
160

161
std::vector<Test::Result> test_session_manager_in_memory() {
1✔
162
   auto rng = Test::new_shared_rng(__func__);
1✔
163

164
   const Botan::TLS::Session_ID default_id = random_id(*rng);
1✔
165

166
   std::optional<Botan::TLS::Session_Manager_In_Memory> mgr;
1✔
167

168
   Session_Manager_Callbacks cbs;
1✔
169
   Session_Manager_Policy plcy;
1✔
170

171
   return {
1✔
172
      CHECK("creation", [&](auto&) { mgr.emplace(rng, 5); }),
1✔
173

174
      CHECK("empty cache does not obtain anything",
175
            [&](auto& result) {
1✔
176
               result.test_is_true("no session found via server info", mgr->find(server_info(), cbs, plcy).empty());
1✔
177

178
               const Botan::TLS::Session_ID mock_id = random_id(*rng);
1✔
179
               auto mock_ticket = rng->random_vec<Botan::TLS::Session_Ticket>(128);
1✔
180

181
               result.test_is_true("no session found via ID", !mgr->retrieve(mock_id, cbs, plcy));
3✔
182
               result.test_is_true("no session found via ID", !mgr->retrieve(mock_ticket, cbs, plcy));
3✔
183
            }),
2✔
184

185
      CHECK("clearing empty cache",
186
            [&](auto& result) { result.test_sz_eq("does not delete anything", mgr->remove_all(), 0); }),
1✔
187

188
      CHECK("establish new session",
189
            [&](auto& result) {
1✔
190
               auto handle = mgr->establish(default_session(Botan::TLS::Connection_Side::Server, cbs), default_id);
1✔
191
               if(result.test_is_true("establishment was successful", handle.has_value())) {
1✔
192
                  result.require("session id was set", handle->id().has_value());
1✔
193
                  result.test_is_true("session ticket was empty", !handle->ticket().has_value());
1✔
194
                  result.test_bin_eq("session id is correct", handle->id().value(), default_id);
2✔
195
               }
196
            }),
1✔
197

198
      CHECK(
199
         "obtain session from server info",
200
         [&](auto& result) {
1✔
201
            auto sessions = mgr->find(server_info(), cbs, plcy);
1✔
202
            if(result.test_is_true("session was found successfully", sessions.size() == 1)) {
1✔
203
               result.test_u16_eq("protocol version was echoed", sessions[0].session.version().version_code(), 0x0303);
1✔
204
               result.test_u16_eq("ciphersuite was echoed", sessions[0].session.ciphersuite_code(), uint16_t(0x009C));
1✔
205
               result.test_bin_eq("ID was echoed", sessions[0].handle.id().value(), default_id);
2✔
206
               result.test_is_true("not a ticket", !sessions[0].handle.ticket().has_value());
1✔
207
            }
208
         }),
1✔
209

210
      CHECK("obtain session from ID",
211
            [&](auto& result) {
1✔
212
               auto session = mgr->retrieve(default_id, cbs, plcy);
2✔
213
               if(result.test_is_true("session was found successfully", session.has_value())) {
1✔
214
                  result.test_u16_eq("protocol version was echoed", session->version().version_code(), 0x0303);
1✔
215
                  result.test_u16_eq("ciphersuite was echoed", session->ciphersuite_code(), uint16_t(0x009C));
1✔
216
               }
217
            }),
1✔
218

219
      CHECK("obtain session from ID disguised as opaque handle",
220
            [&](auto& result) {
1✔
221
               auto session = mgr->retrieve(Botan::TLS::Opaque_Session_Handle(default_id), cbs, plcy);
2✔
222
               if(result.test_is_true("session was found successfully", session.has_value())) {
1✔
223
                  result.test_u16_eq("protocol version was echoed", session->version().version_code(), 0x0303);
1✔
224
                  result.test_u16_eq("ciphersuite was echoed", session->ciphersuite_code(), uint16_t(0x009C));
1✔
225
               }
226
            }),
1✔
227

228
      CHECK("obtain session from ticket == id does not work",
229
            [&](auto& result) {
1✔
230
               auto session = mgr->retrieve(Botan::TLS::Session_Ticket(default_id), cbs, plcy);
2✔
231
               result.test_is_true("session was not found", !session.has_value());
1✔
232
            }),
1✔
233

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

240
      CHECK("invalid ID causes std::nullopt",
241
            [&](auto& result) {
1✔
242
               auto no_session = mgr->retrieve(random_id(*rng), cbs, plcy);
2✔
243
               result.test_is_true("std::nullopt on bogus ID", !no_session.has_value());
1✔
244
            }),
1✔
245

246
      CHECK("remove_all",
247
            [&](auto& result) {
1✔
248
               result.test_sz_eq("removed one element", mgr->remove_all(), 1);
1✔
249
               result.test_sz_eq("should be empty now", mgr->remove_all(), 0);
1✔
250
            }),
1✔
251

252
      CHECK(
253
         "add session with ID",
254
         [&](auto& result) {
1✔
255
            const Botan::TLS::Session_ID new_id = random_id(*rng);
1✔
256

257
            mgr->store(default_session(Botan::TLS::Connection_Side::Client, cbs), new_id);
3✔
258
            result.require("obtain via ID", mgr->retrieve(new_id, cbs, plcy).has_value());
4✔
259

260
            auto sessions = mgr->find(server_info(), cbs, plcy);
1✔
261
            if(result.test_is_true("found via server info", sessions.size() == 1)) {
1✔
262
               result.test_u16_eq("protocol version was echoed", sessions[0].session.version().version_code(), 0x0303);
1✔
263
               result.test_u16_eq("ciphersuite was echoed", sessions[0].session.ciphersuite_code(), uint16_t(0x009C));
1✔
264
               result.test_bin_eq("ID was echoed", sessions[0].handle.id().value(), new_id);
2✔
265
               result.test_is_true("ticket was not stored", !sessions[0].handle.ticket().has_value());
1✔
266
            }
267

268
            mgr->remove_all();
1✔
269
         }),
2✔
270

271
      CHECK(
272
         "add session with ticket",
273
         [&](auto& result) {
1✔
274
            const Botan::TLS::Session_Ticket new_ticket = random_ticket(*rng);
1✔
275

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

279
            auto sessions = mgr->find(server_info(), cbs, plcy);
1✔
280
            if(result.test_is_true("found via server info", sessions.size() == 1)) {
1✔
281
               result.test_u16_eq("protocol version was echoed", sessions[0].session.version().version_code(), 0x0303);
1✔
282
               result.test_u16_eq("ciphersuite was echoed", sessions[0].session.ciphersuite_code(), uint16_t(0x009C));
1✔
283
               result.test_is_true("ID was not stored", !sessions[0].handle.id().has_value());
1✔
284
               result.test_bin_eq("ticket was echoed", sessions[0].handle.ticket().value(), new_ticket);
2✔
285
            }
286

287
            mgr->remove_all();
1✔
288
         }),
2✔
289

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

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

304
               result.test_sz_eq("can find via server info", local_mgr.find(server_info(), cbs, plcy).size(), 2);
1✔
305

306
               result.test_sz_eq("one was deleted", local_mgr.remove(default_id), 1);
3✔
307
               result.test_is_true("cannot obtain via default ID anymore",
1✔
308
                                   !local_mgr.retrieve(default_id, cbs, plcy).has_value());
4✔
309
               result.test_sz_eq("can find less via server info", local_mgr.find(server_info(), cbs, plcy).size(), 1);
1✔
310

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

403
   auto default_session = [&](const std::string& suite,
9✔
404
                              Botan::TLS::Callbacks& mycbs,
405
                              Botan::TLS::Protocol_Version version = Botan::TLS::Protocol_Version::TLS_V13) {
406
      // The session deserializer enforces 48-byte master_secret on TLS 1.2
407
      // and 32-or-48-byte session_psk on TLS 1.3. 48 bytes covers both.
408
      const Botan::secure_vector<uint8_t> dummy_secret(48, 0x42);
8✔
409
      return (version.is_pre_tls_13())
8✔
410
                ? Botan::TLS::Session(dummy_secret,
8✔
411
                                      version,
412
                                      Botan::TLS::Ciphersuite::from_name(suite)->ciphersuite_code(),
1✔
413
                                      Botan::TLS::Connection_Side::Server,
414
                                      true,
415
                                      true,
416
                                      {},
417
                                      server_info(),
418
                                      0,
419
                                      mycbs.tls_current_timestamp())
1✔
420
                : Botan::TLS::Session(dummy_secret,
421
                                      std::nullopt,
422
                                      0,
423
                                      std::chrono::seconds(1024),
7✔
424
                                      version,
425
                                      Botan::TLS::Ciphersuite::from_name(suite)->ciphersuite_code(),
7✔
426
                                      Botan::TLS::Connection_Side::Server,
427
                                      {},
428
                                      nullptr,
429
                                      server_info(),
430
                                      mycbs.tls_current_timestamp());
15✔
431
   };
8✔
432

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

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

442
               Botan::TLS::Session_Ticket random_session_ticket = random_ticket(*rng);
1✔
443

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

555
   auto rng = Test::new_shared_rng(__func__);
1✔
556

557
   Botan::TLS::Session_Manager_Stateless mgr(creds, rng);
1✔
558

559
   Session_Manager_Callbacks cbs;
1✔
560
   Session_Manager_Policy plcy;
1✔
561

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

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

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

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

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

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

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

604
               result.test_is_true("retrieval by ID does not work",
1✔
605
                                   !mgr.retrieve(random_id(*rng), cbs, plcy).has_value());
3✔
606
            }),
1✔
607

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

613
               result.test_is_true("retrieval by opaque handle",
1✔
614
                                   mgr.retrieve(ticket1->opaque_handle(), cbs, plcy).has_value());
3✔
615
            }),
1✔
616

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

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

624
               Botan::TLS::Session_Manager_Stateless local_mgr2(std::make_shared<Other_Test_Credentials_Manager>(),
2✔
625
                                                                rng);
626

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

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

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

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

656
               result.test_sz_eq("remove the ticket", mgr.remove(ticket1.value()), 0);
1✔
657
               result.test_is_true("successful retrieval 1", mgr.retrieve(ticket1.value(), cbs, plcy).has_value());
1✔
658

659
               result.test_sz_eq("remove the ticket", mgr.remove_all(), 0);
1✔
660
               result.test_is_true("successful retrieval 1", mgr.retrieve(ticket1.value(), cbs, plcy).has_value());
2✔
661
            }),
1✔
662

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

672
            result.test_u64_eq(
×
673
               "timestamps match",
674
               std::chrono::duration_cast<std::chrono::seconds>(session_before.start_time().time_since_epoch()).count(),
1✔
675
               std::chrono::duration_cast<std::chrono::seconds>(session_after->start_time().time_since_epoch())
1✔
676
                  .count());
1✔
677
         }),
2✔
678
   };
11✔
679
}
4✔
680

681
std::vector<Test::Result> test_session_manager_hybrid() {
1✔
682
   auto rng = Test::new_shared_rng(__func__);
1✔
683

684
   auto creds = std::make_shared<Test_Credentials_Manager>();
1✔
685
   Session_Manager_Callbacks cbs;
1✔
686
   Session_Manager_Policy plcy;
1✔
687

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

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

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

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

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

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

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

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

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

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

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

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

778
                   result.test_is_true("does not emit tickets", !empty_mgr.emits_session_tickets());
2✔
779
                   result.test_is_true("does emit tickets 1", mgr_prefers_tickets.emits_session_tickets());
2✔
780
                   result.test_is_true("does emit tickets 2", mgr_prefers_ids.emits_session_tickets());
2✔
781
                }),
2✔
782
   });
4✔
783
}
4✔
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
std::vector<Test::Result> test_session_manager_sqlite() {
1✔
814
   #if defined(BOTAN_HAS_TLS_SQLITE3_SESSION_MANAGER)
815
   auto rng = Test::new_shared_rng(__func__);
1✔
816
   Session_Manager_Callbacks cbs;
1✔
817
   Session_Manager_Policy plcy;
1✔
818

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

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

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

852
            result.test_sz_eq("empty database won't get more empty", legacy_db.remove_all(), 0);
1✔
853
         }),
1✔
854

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

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

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

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

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

889
               auto session1 = mgr.retrieve(some_random_handle.value(), cbs, plcy);
1✔
890
               if(result.test_is_true("found session by user-provided ID", session1.has_value())) {
1✔
891
                  result.test_u16_eq("protocol version was echoed", session1->version().version_code(), 0x0303);
1✔
892
                  result.test_u16_eq("ciphersuite was echoed", session1->ciphersuite_code(), uint16_t(0x009C));
1✔
893
               }
894

895
               auto session2 = mgr.retrieve(some_virtual_handle.value(), cbs, plcy);
1✔
896
               if(result.test_is_true("found session by manager-generated ID", session2.has_value())) {
1✔
897
                  result.test_u16_eq("protocol version was echoed", session2->version().version_code(), 0x0303);
1✔
898
                  result.test_u16_eq("ciphersuite was echoed", session2->ciphersuite_code(), uint16_t(0x009C));
1✔
899
               }
900

901
               auto session3 = mgr.retrieve(random_id(*rng), cbs, plcy);
2✔
902
               result.test_is_true("random ID creates empty result", !session3.has_value());
1✔
903
            }),
6✔
904

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

913
               result.test_is_true("std::nullopt on random ticket",
1✔
914
                                   !mgr.retrieve(random_ticket(*rng), cbs, plcy).has_value());
3✔
915
            }),
2✔
916

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

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

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

945
               result.test_sz_eq("deletes one session by ID", mgr.remove(id), 1);
2✔
946
               result.test_sz_eq("deletes one session by ticket", mgr.remove(ticket), 1);
2✔
947

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

957
               result.test_sz_eq("removing the rest of the sessions", mgr.remove_all(), 2);
1✔
958
            }),
3✔
959

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

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

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

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

983
               result.test_sz_eq("only one entry exists", mgr.remove_all(), 1);
1✔
984
            }),
2✔
985

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

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

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

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

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

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

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

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

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

1059
                      auto mgr = factory();
2✔
1060

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

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

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

1076
                      result.test_sz_eq("old session was deleted when it expired", mgr->remove_all(), 1);
2✔
1077
                   }),
8✔
1078

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

1086
               auto mgr = factory();
2✔
1087

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

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

1104
               plcy.set_allow_session_reuse(false);
2✔
1105

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

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

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

1124
                      auto mgr = factory();
2✔
1125

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

1132
                      plcy.set_allow_session_reuse(true);
2✔
1133

1134
                      plcy.set_session_limit(1);
2✔
1135
                      result.test_sz_eq("find one",
2✔
1136
                                        mgr->find(server_info(), cbs, plcy).size(),
2✔
1137
                                        plcy.maximum_session_tickets_per_client_hello());
1138

1139
                      plcy.set_session_limit(3);
2✔
1140
                      result.test_sz_eq("find three",
2✔
1141
                                        mgr->find(server_info(), cbs, plcy).size(),
2✔
1142
                                        plcy.maximum_session_tickets_per_client_hello());
1143

1144
                      plcy.set_session_limit(10);
2✔
1145
                      result.test_sz_eq("find all five", mgr->find(server_info(), cbs, plcy).size(), 5);
2✔
1146
                   }),
4✔
1147

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

1155
                      auto mgr = factory();
3✔
1156

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

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

1169
                      cbs.tick();
3✔
1170

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

1179
BOTAN_REGISTER_TEST_FN("tls",
1180
                       "tls_session_manager",
1181
                       test_session_manager_in_memory,
1182
                       test_session_manager_choose_ticket,
1183
                       test_session_manager_stateless,
1184
                       test_session_manager_hybrid,
1185
                       test_session_manager_sqlite,
1186
                       tls_session_manager_expiry);
1187

1188
}  // namespace
1189

1190
}  // namespace Botan_Tests
1191

1192
#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