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

randombit / botan / 5123321399

30 May 2023 04:06PM UTC coverage: 92.213% (+0.004%) from 92.209%
5123321399

Pull #3558

github

web-flow
Merge dd72f7389 into 057bcbc35
Pull Request #3558: Add braces around all if/else statements

75602 of 81986 relevant lines covered (92.21%)

11859779.3 hits per line

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

95.65
/src/lib/tls/tls12/tls_record.cpp
1
/*
2
* TLS Record Handling
3
* (C) 2012,2013,2014,2015,2016,2019 Jack Lloyd
4
*     2016 Juraj Somorovsky
5
*     2016 Matthias Gierlings
6
*
7
* Botan is released under the Simplified BSD License (see license.txt)
8
*/
9

10
#include <botan/internal/tls_record.h>
11

12
#include <botan/rng.h>
13
#include <botan/tls_callbacks.h>
14
#include <botan/tls_ciphersuite.h>
15
#include <botan/tls_exceptn.h>
16
#include <botan/internal/ct_utils.h>
17
#include <botan/internal/loadstor.h>
18
#include <botan/internal/tls_seq_numbers.h>
19
#include <botan/internal/tls_session_key.h>
20
#include <sstream>
21

22
#if defined(BOTAN_HAS_TLS_CBC)
23
   #include <botan/internal/tls_cbc.h>
24
#endif
25

26
namespace Botan::TLS {
27

28
Connection_Cipher_State::Connection_Cipher_State(Protocol_Version version,
3,507✔
29
                                                 Connection_Side side,
30
                                                 bool our_side,
31
                                                 const Ciphersuite& suite,
32
                                                 const Session_Keys& keys,
33
                                                 bool uses_encrypt_then_mac) {
3,507✔
34
   m_nonce_format = suite.nonce_format();
3,507✔
35
   m_nonce_bytes_from_record = suite.nonce_bytes_from_record(version);
3,507✔
36
   m_nonce_bytes_from_handshake = suite.nonce_bytes_from_handshake();
3,507✔
37

38
   const secure_vector<uint8_t>& aead_key = keys.aead_key(side);
3,507✔
39
   m_nonce = keys.nonce(side);
7,014✔
40

41
   BOTAN_ASSERT_NOMSG(m_nonce.size() == m_nonce_bytes_from_handshake);
3,507✔
42

43
   if(nonce_format() == Nonce_Format::CBC_MODE) {
3,507✔
44
#if defined(BOTAN_HAS_TLS_CBC)
45
      // legacy CBC+HMAC mode
46
      auto mac = MessageAuthenticationCode::create_or_throw("HMAC(" + suite.mac_algo() + ")");
998✔
47
      auto cipher = BlockCipher::create_or_throw(suite.cipher_algo());
499✔
48

49
      if(our_side) {
499✔
50
         m_aead = std::make_unique<TLS_CBC_HMAC_AEAD_Encryption>(std::move(cipher),
245✔
51
                                                                 std::move(mac),
245✔
52
                                                                 suite.cipher_keylen(),
245✔
53
                                                                 suite.mac_keylen(),
490✔
54
                                                                 version,
55
                                                                 uses_encrypt_then_mac);
245✔
56
      } else {
57
         m_aead = std::make_unique<TLS_CBC_HMAC_AEAD_Decryption>(std::move(cipher),
254✔
58
                                                                 std::move(mac),
254✔
59
                                                                 suite.cipher_keylen(),
254✔
60
                                                                 suite.mac_keylen(),
508✔
61
                                                                 version,
62
                                                                 uses_encrypt_then_mac);
254✔
63
      }
64

65
#else
66
      BOTAN_UNUSED(uses_encrypt_then_mac);
67
      throw Internal_Error("Negotiated disabled TLS CBC+HMAC ciphersuite");
68
#endif
69
   } else {
499✔
70
      m_aead =
3,008✔
71
         AEAD_Mode::create_or_throw(suite.cipher_algo(), our_side ? Cipher_Dir::Encryption : Cipher_Dir::Decryption);
8,322✔
72
   }
73

74
   m_aead->set_key(aead_key);
3,507✔
75
}
3,507✔
76

77
std::vector<uint8_t> Connection_Cipher_State::aead_nonce(uint64_t seq, RandomNumberGenerator& rng) {
6,271✔
78
   switch(m_nonce_format) {
6,271✔
79
      case Nonce_Format::CBC_MODE: {
935✔
80
         if(!m_nonce.empty()) {
935✔
81
            std::vector<uint8_t> nonce;
245✔
82
            nonce.swap(m_nonce);
245✔
83
            return nonce;
245✔
84
         }
245✔
85
         std::vector<uint8_t> nonce(nonce_bytes_from_record());
690✔
86
         rng.randomize(nonce.data(), nonce.size());
690✔
87
         return nonce;
690✔
88
      }
935✔
89
      case Nonce_Format::AEAD_XOR_12: {
4,002✔
90
         std::vector<uint8_t> nonce(12);
4,002✔
91
         store_be(seq, nonce.data() + 4);
4,002✔
92
         xor_buf(nonce, m_nonce.data(), m_nonce.size());
4,002✔
93
         return nonce;
4,002✔
94
      }
4,002✔
95
      case Nonce_Format::AEAD_IMPLICIT_4: {
1,334✔
96
         BOTAN_ASSERT_NOMSG(m_nonce.size() == 4);
1,334✔
97
         std::vector<uint8_t> nonce(12);
1,334✔
98
         copy_mem(&nonce[0], m_nonce.data(), 4);
1,334✔
99
         store_be(seq, &nonce[nonce_bytes_from_handshake()]);
1,334✔
100
         return nonce;
1,334✔
101
      }
1,334✔
102
   }
103

104
   throw Invalid_State("Unknown nonce format specified");
×
105
}
106

107
std::vector<uint8_t> Connection_Cipher_State::aead_nonce(const uint8_t record[], size_t record_len, uint64_t seq) {
12,226✔
108
   switch(m_nonce_format) {
12,226✔
109
      case Nonce_Format::CBC_MODE: {
994✔
110
         if(nonce_bytes_from_record() == 0 && !m_nonce.empty()) {
994✔
111
            std::vector<uint8_t> nonce;
×
112
            nonce.swap(m_nonce);
×
113
            return nonce;
×
114
         }
×
115
         if(record_len < nonce_bytes_from_record()) {
994✔
116
            throw Decoding_Error("Invalid CBC packet too short to be valid");
×
117
         }
118
         std::vector<uint8_t> nonce(record, record + nonce_bytes_from_record());
994✔
119
         return nonce;
994✔
120
      }
994✔
121
      case Nonce_Format::AEAD_XOR_12: {
9,751✔
122
         std::vector<uint8_t> nonce(12);
9,751✔
123
         store_be(seq, nonce.data() + 4);
9,751✔
124
         xor_buf(nonce, m_nonce.data(), m_nonce.size());
9,751✔
125
         return nonce;
9,751✔
126
      }
9,751✔
127
      case Nonce_Format::AEAD_IMPLICIT_4: {
1,481✔
128
         BOTAN_ASSERT_NOMSG(m_nonce.size() == 4);
1,481✔
129
         if(record_len < nonce_bytes_from_record()) {
1,481✔
130
            throw Decoding_Error("Invalid AEAD packet too short to be valid");
×
131
         }
132
         std::vector<uint8_t> nonce(12);
1,481✔
133
         copy_mem(&nonce[0], m_nonce.data(), 4);
1,481✔
134
         copy_mem(&nonce[nonce_bytes_from_handshake()], record, nonce_bytes_from_record());
1,481✔
135
         return nonce;
1,481✔
136
      }
1,481✔
137
   }
138

139
   throw Invalid_State("Unknown nonce format specified");
×
140
}
141

142
std::vector<uint8_t> Connection_Cipher_State::format_ad(uint64_t msg_sequence,
18,497✔
143
                                                        Record_Type msg_type,
144
                                                        Protocol_Version version,
145
                                                        uint16_t msg_length) {
146
   std::vector<uint8_t> ad(13);
18,497✔
147

148
   store_be(msg_sequence, &ad[0]);
18,497✔
149
   ad[8] = static_cast<uint8_t>(msg_type);
18,497✔
150
   ad[9] = version.major_version();
18,497✔
151
   ad[10] = version.minor_version();
18,497✔
152
   ad[11] = get_byte<0>(msg_length);
18,497✔
153
   ad[12] = get_byte<1>(msg_length);
18,497✔
154

155
   return ad;
18,497✔
156
}
157

158
namespace {
159

160
inline void append_u16_len(secure_vector<uint8_t>& output, size_t len_field) {
18,462✔
161
   const uint16_t len16 = static_cast<uint16_t>(len_field);
18,462✔
162
   BOTAN_ASSERT_EQUAL(len_field, len16, "No truncation");
18,462✔
163
   output.push_back(get_byte<0>(len16));
18,462✔
164
   output.push_back(get_byte<1>(len16));
18,462✔
165
}
18,462✔
166

167
void write_record_header(secure_vector<uint8_t>& output,
18,462✔
168
                         Record_Type record_type,
169
                         Protocol_Version version,
170
                         uint64_t record_sequence) {
171
   output.clear();
18,462✔
172

173
   output.push_back(static_cast<uint8_t>(record_type));
18,462✔
174
   output.push_back(version.major_version());
18,462✔
175
   output.push_back(version.minor_version());
18,462✔
176

177
   if(version.is_datagram_protocol()) {
18,462✔
178
      for(size_t i = 0; i != 8; ++i) {
64,017✔
179
         output.push_back(get_byte_var(i, record_sequence));
56,904✔
180
      }
181
   }
182
}
18,462✔
183

184
}  // namespace
185

186
void write_unencrypted_record(secure_vector<uint8_t>& output,
12,191✔
187
                              Record_Type record_type,
188
                              Protocol_Version version,
189
                              uint64_t record_sequence,
190
                              const uint8_t* message,
191
                              size_t message_len) {
192
   if(record_type == Record_Type::ApplicationData) {
12,191✔
193
      throw Internal_Error("Writing an unencrypted TLS application data record");
×
194
   }
195
   write_record_header(output, record_type, version, record_sequence);
12,191✔
196
   append_u16_len(output, message_len);
12,191✔
197
   output.insert(output.end(), message, message + message_len);
12,191✔
198
}
12,191✔
199

200
void write_record(secure_vector<uint8_t>& output,
6,271✔
201
                  Record_Type record_type,
202
                  Protocol_Version version,
203
                  uint64_t record_sequence,
204
                  const uint8_t* message,
205
                  size_t message_len,
206
                  Connection_Cipher_State& cs,
207
                  RandomNumberGenerator& rng) {
208
   write_record_header(output, record_type, version, record_sequence);
6,271✔
209

210
   AEAD_Mode& aead = cs.aead();
6,271✔
211
   std::vector<uint8_t> aad = cs.format_ad(record_sequence, record_type, version, static_cast<uint16_t>(message_len));
6,271✔
212

213
   const size_t ctext_size = aead.output_length(message_len);
6,271✔
214

215
   const size_t rec_size = ctext_size + cs.nonce_bytes_from_record();
6,271✔
216

217
   aead.set_associated_data(aad);
6,271✔
218

219
   const std::vector<uint8_t> nonce = cs.aead_nonce(record_sequence, rng);
6,271✔
220

221
   append_u16_len(output, rec_size);
6,271✔
222

223
   if(cs.nonce_bytes_from_record() > 0) {
6,271✔
224
      if(cs.nonce_format() == Nonce_Format::CBC_MODE) {
2,269✔
225
         output += nonce;
935✔
226
      } else {
227
         output += std::make_pair(&nonce[cs.nonce_bytes_from_handshake()], cs.nonce_bytes_from_record());
1,334✔
228
      }
229
   }
230

231
   const size_t header_size = output.size();
6,271✔
232
   output += std::make_pair(message, message_len);
6,271✔
233

234
   aead.start(nonce);
6,271✔
235
   aead.finish(output, header_size);
6,271✔
236

237
   BOTAN_ASSERT(output.size() < MAX_CIPHERTEXT_SIZE, "Produced ciphertext larger than protocol allows");
6,271✔
238
}
12,542✔
239

240
namespace {
241

242
size_t fill_buffer_to(
592,661✔
243
   secure_vector<uint8_t>& readbuf, const uint8_t*& input, size_t& input_size, size_t& input_consumed, size_t desired) {
244
   if(readbuf.size() >= desired) {
592,661✔
245
      return 0;  // already have it
246
   }
247

248
   const size_t taken = std::min(input_size, desired - readbuf.size());
592,660✔
249

250
   readbuf.insert(readbuf.end(), input, input + taken);
592,660✔
251
   input_consumed += taken;
592,660✔
252
   input_size -= taken;
592,660✔
253
   input += taken;
592,660✔
254

255
   return (desired - readbuf.size());  // how many bytes do we still need?
592,660✔
256
}
257

258
void decrypt_record(secure_vector<uint8_t>& output,
12,226✔
259
                    uint8_t record_contents[],
260
                    size_t record_len,
261
                    uint64_t record_sequence,
262
                    Protocol_Version record_version,
263
                    Record_Type record_type,
264
                    Connection_Cipher_State& cs) {
265
   AEAD_Mode& aead = cs.aead();
12,226✔
266

267
   const std::vector<uint8_t> nonce = cs.aead_nonce(record_contents, record_len, record_sequence);
12,226✔
268
   const uint8_t* msg = &record_contents[cs.nonce_bytes_from_record()];
12,226✔
269
   const size_t msg_length = record_len - cs.nonce_bytes_from_record();
12,226✔
270

271
   /*
272
   * This early rejection is based just on public information (length of the
273
   * encrypted packet) and so does not leak any information. We used to use
274
   * decode_error here which really is more appropriate, but that confuses some
275
   * tools which are attempting automated detection of padding oracles,
276
   * including older versions of TLS-Attacker.
277
   */
278
   if(msg_length < aead.minimum_final_size()) {
12,226✔
279
      throw TLS_Exception(Alert::BadRecordMac, "AEAD packet is shorter than the tag");
×
280
   }
281

282
   const size_t ptext_size = aead.output_length(msg_length);
12,226✔
283

284
   aead.set_associated_data(
12,226✔
285
      cs.format_ad(record_sequence, record_type, record_version, static_cast<uint16_t>(ptext_size)));
12,279✔
286

287
   aead.start(nonce);
12,226✔
288

289
   output.assign(msg, msg + msg_length);
12,226✔
290
   aead.finish(output, 0);
12,226✔
291
}
12,173✔
292

293
Record_Header read_tls_record(secure_vector<uint8_t>& readbuf,
115,140✔
294
                              const uint8_t input[],
295
                              size_t input_len,
296
                              size_t& consumed,
297
                              secure_vector<uint8_t>& recbuf,
298
                              Connection_Sequence_Numbers* sequence_numbers,
299
                              const get_cipherstate_fn& get_cipherstate) {
300
   if(readbuf.size() < TLS_HEADER_SIZE)  // header incomplete?
115,140✔
301
   {
302
      if(size_t needed = fill_buffer_to(readbuf, input, input_len, consumed, TLS_HEADER_SIZE)) {
115,140✔
303
         return Record_Header(needed);
4✔
304
      }
305

306
      BOTAN_ASSERT_EQUAL(readbuf.size(), TLS_HEADER_SIZE, "Have an entire header");
115,136✔
307
   }
308

309
   /*
310
   Verify that the record type and record version are within some expected
311
   range, so we can quickly reject totally invalid packets.
312

313
   The version check is a little hacky but given how TLS 1.3 versioning works
314
   this is probably safe
315

316
   - The first byte is the record version which in TLS 1.2 is always in [20..23)
317
   - The second byte is the TLS major version which is effectively fossilized at 3
318
   - The third byte is the TLS minor version which (due to TLS 1.3 versioning changes)
319
     will never be more than 3 (signifying TLS 1.2)
320
   */
321
   const bool bad_record_type = readbuf[0] < 20 || readbuf[0] > 23;
115,136✔
322
   const bool bad_record_version = readbuf[1] != 3 || readbuf[2] >= 4;
115,136✔
323

324
   if(bad_record_type || bad_record_version) {
115,136✔
325
      // We know we read up to at least the 5 byte TLS header
326
      const std::string first5 = std::string(reinterpret_cast<const char*>(readbuf.data()), 5);
1,703✔
327

328
      if(first5 == "GET /" || first5 == "PUT /" || first5 == "POST " || first5 == "HEAD ") {
1,703✔
329
         throw TLS_Exception(Alert::ProtocolVersion, "Client sent plaintext HTTP request instead of TLS handshake");
2✔
330
      }
331

332
      if(first5 == "CONNE") {
1,701✔
333
         throw TLS_Exception(Alert::ProtocolVersion,
1✔
334
                             "Client sent plaintext HTTP proxy CONNECT request instead of TLS handshake");
2✔
335
      }
336

337
      std::ostringstream oss;
1,700✔
338
      oss << "TLS record ";
1,700✔
339
      if(bad_record_type) {
1,700✔
340
         oss << "type";
1,103✔
341
      } else {
342
         oss << "version";
597✔
343
      }
344
      oss << " had unexpected value";
1,700✔
345

346
      throw TLS_Exception(Alert::ProtocolVersion, oss.str());
3,400✔
347
   }
3,403✔
348

349
   const Protocol_Version version(readbuf[1], readbuf[2]);
113,433✔
350

351
   if(version.is_datagram_protocol()) {
113,433✔
352
      throw TLS_Exception(Alert::ProtocolVersion, "Expected TLS but got a record with DTLS version");
×
353
   }
354

355
   const size_t record_size = make_uint16(readbuf[TLS_HEADER_SIZE - 2], readbuf[TLS_HEADER_SIZE - 1]);
113,433✔
356

357
   if(record_size > MAX_CIPHERTEXT_SIZE) {
113,433✔
358
      throw TLS_Exception(Alert::RecordOverflow, "Received a record that exceeds maximum size");
1✔
359
   }
360

361
   if(record_size == 0) {
113,432✔
362
      throw TLS_Exception(Alert::DecodeError, "Received a completely empty record");
9✔
363
   }
364

365
   if(size_t needed = fill_buffer_to(readbuf, input, input_len, consumed, TLS_HEADER_SIZE + record_size)) {
113,423✔
366
      return Record_Header(needed);
2✔
367
   }
368

369
   BOTAN_ASSERT_EQUAL(static_cast<size_t>(TLS_HEADER_SIZE) + record_size, readbuf.size(), "Have the full record");
113,421✔
370

371
   const Record_Type type = static_cast<Record_Type>(readbuf[0]);
113,421✔
372

373
   uint16_t epoch = 0;
113,421✔
374

375
   uint64_t sequence = 0;
113,421✔
376
   if(sequence_numbers) {
113,421✔
377
      sequence = sequence_numbers->next_read_sequence();
112,863✔
378
      epoch = sequence_numbers->current_read_epoch();
112,863✔
379
   } else {
380
      // server initial handshake case
381
      epoch = 0;
382
   }
383

384
   if(epoch == 0)  // Unencrypted initial handshake
112,863✔
385
   {
386
      recbuf.assign(readbuf.begin() + TLS_HEADER_SIZE, readbuf.begin() + TLS_HEADER_SIZE + record_size);
105,588✔
387
      readbuf.clear();
105,588✔
388
      return Record_Header(sequence, version, type);
105,588✔
389
   }
390

391
   // Otherwise, decrypt, check MAC, return plaintext
392
   auto cs = get_cipherstate(epoch);
7,833✔
393

394
   BOTAN_ASSERT(cs, "Have cipherstate for this epoch");
7,833✔
395

396
   decrypt_record(recbuf, &readbuf[TLS_HEADER_SIZE], record_size, sequence, version, type, *cs);
7,833✔
397

398
   if(sequence_numbers) {
7,802✔
399
      sequence_numbers->read_accept(sequence);
7,802✔
400
   }
401

402
   readbuf.clear();
7,802✔
403
   return Record_Header(sequence, version, type);
7,802✔
404
}
113,396✔
405

406
Record_Header read_dtls_record(secure_vector<uint8_t>& readbuf,
182,061✔
407
                               const uint8_t input[],
408
                               size_t input_len,
409
                               size_t& consumed,
410
                               secure_vector<uint8_t>& recbuf,
411
                               Connection_Sequence_Numbers* sequence_numbers,
412
                               const get_cipherstate_fn& get_cipherstate,
413
                               bool allow_epoch0_restart) {
414
   if(readbuf.size() < DTLS_HEADER_SIZE)  // header incomplete?
182,061✔
415
   {
416
      if(fill_buffer_to(readbuf, input, input_len, consumed, DTLS_HEADER_SIZE)) {
182,061✔
417
         readbuf.clear();
10✔
418
         return Record_Header(0);
10✔
419
      }
420

421
      BOTAN_ASSERT_EQUAL(readbuf.size(), DTLS_HEADER_SIZE, "Have an entire header");
182,051✔
422
   }
423

424
   const Protocol_Version version(readbuf[1], readbuf[2]);
182,051✔
425

426
   if(version.is_datagram_protocol() == false) {
182,051✔
427
      readbuf.clear();
13✔
428
      return Record_Header(0);
13✔
429
   }
430

431
   const size_t record_size = make_uint16(readbuf[DTLS_HEADER_SIZE - 2], readbuf[DTLS_HEADER_SIZE - 1]);
182,038✔
432

433
   if(record_size > MAX_CIPHERTEXT_SIZE) {
182,038✔
434
      // Too large to be valid, ignore it
435
      readbuf.clear();
1✔
436
      return Record_Header(0);
1✔
437
   }
438

439
   if(fill_buffer_to(readbuf, input, input_len, consumed, DTLS_HEADER_SIZE + record_size)) {
182,037✔
440
      // Truncated packet?
441
      readbuf.clear();
2✔
442
      return Record_Header(0);
2✔
443
   }
444

445
   BOTAN_ASSERT_EQUAL(static_cast<size_t>(DTLS_HEADER_SIZE) + record_size, readbuf.size(), "Have the full record");
182,035✔
446

447
   const Record_Type type = static_cast<Record_Type>(readbuf[0]);
182,035✔
448

449
   const uint64_t sequence = load_be<uint64_t>(&readbuf[3], 0);
182,035✔
450
   const uint16_t epoch = (sequence >> 48);
182,035✔
451

452
   const bool already_seen = sequence_numbers && sequence_numbers->already_seen(sequence);
182,035✔
453

454
   if(already_seen && !(epoch == 0 && allow_epoch0_restart)) {
625✔
455
      readbuf.clear();
624✔
456
      return Record_Header(0);
182,061✔
457
   }
458

459
   if(epoch == 0)  // Unencrypted initial handshake
181,411✔
460
   {
461
      recbuf.assign(readbuf.begin() + DTLS_HEADER_SIZE, readbuf.begin() + DTLS_HEADER_SIZE + record_size);
177,002✔
462
      readbuf.clear();
177,002✔
463
      if(sequence_numbers) {
177,002✔
464
         sequence_numbers->read_accept(sequence);
176,629✔
465
      }
466
      return Record_Header(sequence, version, type);
177,002✔
467
   }
468

469
   try {
4,409✔
470
      // Otherwise, decrypt, check MAC, return plaintext
471
      auto cs = get_cipherstate(epoch);
4,409✔
472

473
      BOTAN_ASSERT(cs, "Have cipherstate for this epoch");
4,393✔
474

475
      decrypt_record(recbuf, &readbuf[DTLS_HEADER_SIZE], record_size, sequence, version, type, *cs);
4,393✔
476
   } catch(std::exception&) {
38✔
477
      readbuf.clear();
38✔
478
      return Record_Header(0);
38✔
479
   }
38✔
480

481
   if(sequence_numbers) {
4,371✔
482
      sequence_numbers->read_accept(sequence);
4,371✔
483
   }
484

485
   readbuf.clear();
4,371✔
486
   return Record_Header(sequence, version, type);
4,371✔
487
}
488

489
}  // namespace
490

491
Record_Header read_record(bool is_datagram,
297,201✔
492
                          secure_vector<uint8_t>& readbuf,
493
                          const uint8_t input[],
494
                          size_t input_len,
495
                          size_t& consumed,
496
                          secure_vector<uint8_t>& recbuf,
497
                          Connection_Sequence_Numbers* sequence_numbers,
498
                          const get_cipherstate_fn& get_cipherstate,
499
                          bool allow_epoch0_restart) {
500
   if(is_datagram) {
297,201✔
501
      return read_dtls_record(
182,061✔
502
         readbuf, input, input_len, consumed, recbuf, sequence_numbers, get_cipherstate, allow_epoch0_restart);
182,061✔
503
   } else {
504
      return read_tls_record(readbuf, input, input_len, consumed, recbuf, sequence_numbers, get_cipherstate);
115,140✔
505
   }
506
}
507

508
}  // namespace Botan::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

© 2025 Coveralls, Inc