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

randombit / botan / 30496461195

29 Jul 2026 08:43PM UTC coverage: 89.519% (+0.09%) from 89.425%
30496461195

push

github

web-flow
Merge pull request #5776 from Rohde-Schwarz/fix/tls_anvil

116233 of 129841 relevant lines covered (89.52%)

10620449.77 hits per line

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

72.81
/src/cli/tls_client.cpp
1
/*
2
* (C) 2014,2015 Jack Lloyd
3
*     2016 Matthias Gierlings
4
*     2017 René Korthaus, Rohde & Schwarz Cybersecurity
5
*     2022 René Meusel, Hannes Rantzsch - neXenio GmbH
6
*     2023 René Meusel, Rohde & Schwarz Cybersecurity
7
*     2026 René Meusel, Rohde & Schwarz Networks and Cybersecurity
8
*
9
* Botan is released under the Simplified BSD License (see license.txt)
10
*/
11

12
#include "cli.h"
13

14
#include <botan/internal/stl_util.h>
15
#include <botan/internal/target_info.h>
16

17
#if defined(BOTAN_HAS_TLS) && defined(BOTAN_TARGET_OS_HAS_FILESYSTEM) && defined(BOTAN_TARGET_OS_HAS_SOCKETS)
18

19
   #include <botan/hex.h>
20
   #include <botan/ocsp.h>
21
   #include <botan/tls_callbacks.h>
22
   #include <botan/tls_client.h>
23
   #include <botan/tls_exceptn.h>
24
   #include <botan/tls_policy.h>
25
   #include <botan/tls_session_manager_memory.h>
26
   #include <botan/x509path.h>
27

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

32
   #include <memory>
33
   #include <string>
34

35
   #include "socket_utils.h"
36
   #include "tls_helpers.h"
37

38
namespace Botan_CLI {
39

40
class TLS_Client;
41

42
namespace {
43

44
class Callbacks : public Botan::TLS::Callbacks {
10✔
45
   public:
46
      explicit Callbacks(TLS_Client& client_command) : m_client_command(client_command), m_peer_closed(false) {}
10✔
47

48
      std::ostream& output();
49
      bool flag_set(const std::string& flag_name) const;
50
      std::string get_arg(const std::string& arg_name) const;
51
      void send(std::span<const uint8_t> buffer);
52

53
      bool peer_closed() const { return m_peer_closed; }
1✔
54

55
      void tls_verify_cert_chain(const std::vector<Botan::X509_Certificate>& cert_chain,
7✔
56
                                 const std::vector<std::optional<Botan::OCSP::Response>>& ocsp,
57
                                 const std::vector<Botan::Certificate_Store*>& trusted_roots,
58
                                 Botan::Usage_Type usage,
59
                                 std::string_view hostname,
60
                                 const Botan::TLS::Policy& policy) override {
61
         if(cert_chain.empty()) {
7✔
62
            throw Botan::Invalid_Argument("Certificate chain was empty");
×
63
         }
64

65
         const Botan::Path_Validation_Restrictions restrictions(policy.require_cert_revocation_info(),
7✔
66
                                                                policy.minimum_signature_strength());
14✔
67

68
         auto ocsp_timeout = std::chrono::milliseconds(1000);
7✔
69

70
         const std::string checked_name = flag_set("skip-hostname-check") ? "" : std::string(hostname);
21✔
71

72
         const Botan::Path_Validation_Result result = Botan::x509_path_validate(
7✔
73
            cert_chain, restrictions, trusted_roots, checked_name, usage, tls_current_timestamp(), ocsp_timeout, ocsp);
7✔
74

75
         if(result.successful_validation()) {
7✔
76
            output() << "Certificate validation status: " << result.result_string() << "\n";
14✔
77
            const auto& status = result.all_statuses();
7✔
78

79
            if(!status.empty() && status[0].contains(Botan::Certificate_Status_Code::OCSP_RESPONSE_GOOD)) {
14✔
80
               output() << "Valid OCSP response for this server\n";
×
81
            }
82
         } else {
83
            if(flag_set("ignore-cert-error")) {
×
84
               output() << "Certificate validation status: " << result.result_string() << "\n";
×
85
            } else {
86
               throw Botan::TLS::TLS_Exception(Botan::TLS::Alert::BadCertificate,
×
87
                                               "Certificate validation failure: " + result.result_string());
×
88
            }
89
         }
90
      }
7✔
91

92
      void tls_verify_raw_public_key(const Botan::Public_Key& raw_public_key,
×
93
                                     Botan::Usage_Type /* usage */,
94
                                     std::string_view /* hostname */,
95
                                     const Botan::TLS::Policy& /* policy */) override {
96
         const auto fingerprint = raw_public_key.fingerprint_public("SHA-256");
×
97
         const auto trusted = (fingerprint == get_arg("trusted-pubkey-sha256"));
×
98
         output() << "Raw Public Key (" << fingerprint
×
99
                  << ") validation status: " << (trusted ? "trusted" : "NOT trusted") << "\n";
×
100
      }
×
101

102
      void tls_session_activated() override { output() << "Handshake complete\n"; }
9✔
103

104
      void tls_session_established(const Botan::TLS::Session_Summary& session) override {
9✔
105
         output() << "Handshake complete, " << session.version().to_string() << "\n";
18✔
106

107
         if(const auto& psk = session.external_psk_identity()) {
9✔
108
            output() << "Utilized PSK identity: " << maybe_hex_encode(psk.value()) << "\n";
6✔
109
         }
110

111
         output() << "Negotiated ciphersuite " << session.ciphersuite().to_string() << "\n";
18✔
112

113
         if(auto kex_params = session.kex_parameters()) {
9✔
114
            output() << "Key exchange using " << *kex_params << "\n";
6✔
115
         }
×
116

117
         if(const auto& session_id = session.session_id(); !session_id.empty()) {
9✔
118
            output() << "Session ID " << Botan::hex_encode(session_id.get()) << "\n";
18✔
119
         }
120

121
         if(const auto& session_ticket = session.session_ticket()) {
9✔
122
            output() << "Session ticket " << Botan::hex_encode(session_ticket->get()) << "\n";
×
123
         }
124

125
         if(flag_set("print-certs")) {
18✔
126
            const std::vector<Botan::X509_Certificate>& certs = session.peer_certs();
127

128
            for(size_t i = 0; i != certs.size(); ++i) {
×
129
               output() << "Certificate " << i + 1 << "/" << certs.size() << "\n";
×
130
               output() << certs[i].to_string();
×
131
               output() << certs[i].PEM_encode();
×
132
            }
133
         }
134
         output() << std::flush;
9✔
135
      }
9✔
136

137
      void tls_emit_data(std::span<const uint8_t> buf) override {
54✔
138
         if(flag_set("debug")) {
108✔
139
            output() << "<< " << Botan::hex_encode(buf) << "\n";
×
140
         }
141

142
         send(buf);
108✔
143
      }
54✔
144

145
      void tls_alert(Botan::TLS::Alert alert) override { output() << "Alert: " << alert.type_string() << "\n"; }
3✔
146

147
      void tls_record_received(uint64_t /*seq_no*/, std::span<const uint8_t> buf) override {
12✔
148
         for(const auto c : buf) {
2,064✔
149
            output() << c;
2,052✔
150
         }
151
         output() << std::flush;
12✔
152
      }
12✔
153

154
      std::vector<uint8_t> tls_sign_message(const Botan::Private_Key& key,
×
155
                                            Botan::RandomNumberGenerator& rng,
156
                                            const std::string_view padding,
157
                                            Botan::Signature_Format format,
158
                                            const std::vector<uint8_t>& msg) override {
159
         output() << "Performing client authentication\n";
×
160
         return Botan::TLS::Callbacks::tls_sign_message(key, rng, padding, format, msg);
×
161
      }
162

163
      bool tls_peer_closed_connection() override {
×
164
         m_peer_closed = true;
×
165
         return Botan::TLS::Callbacks::tls_peer_closed_connection();
×
166
      }
167

168
   private:
169
      TLS_Client& m_client_command;
170
      bool m_peer_closed;
171
};
172

173
}  // namespace
174

175
class TLS_Client final : public Command {
176
   public:
177
      TLS_Client() :
11✔
178
            Command(
179
               "tls_client host --port=443 --print-certs --policy=default "
180
               "--skip-system-cert-store --trusted-cas= --trusted-pubkey-sha256= "
181
               "--skip-hostname-check --ignore-cert-error "
182
               "--tls-version=default --session-db= --session-db-pass= "
183
               "--next-protocols= --type=tcp --client-cert= --client-cert-key= "
184
               "--psk= --psk-identity= --psk-prf=SHA-256 --debug") {
11✔
185
         init_sockets();
11✔
186
      }
11✔
187

188
      ~TLS_Client() override {
11✔
189
         shutdown_socket();
11✔
190
         stop_sockets();
11✔
191
      }
11✔
192

193
      TLS_Client(const TLS_Client& other) = delete;
194
      TLS_Client(TLS_Client&& other) = delete;
195
      TLS_Client& operator=(const TLS_Client& other) = delete;
196
      TLS_Client& operator=(TLS_Client&& other) = delete;
197

198
      std::string group() const override { return "tls"; }
1✔
199

200
      std::string description() const override { return "Connect to a host using TLS/DTLS"; }
1✔
201

202
      void go() override {
10✔
203
         std::shared_ptr<Botan::TLS::Session_Manager> session_mgr;
10✔
204

205
         auto callbacks = std::make_shared<Callbacks>(*this);
10✔
206

207
         const std::string sessions_db = get_arg("session-db");
10✔
208
         const std::string host = get_arg("host");
10✔
209
         const uint16_t port = get_arg_u16("port");
10✔
210
         const std::string transport = get_arg("type");
10✔
211
         const std::string next_protos = get_arg("next-protocols");
10✔
212
         const bool use_system_cert_store = !flag_set("skip-system-cert-store");
10✔
213
         const std::string trusted_CAs = get_arg("trusted-cas");
10✔
214
         const auto tls_version = get_arg("tls-version");
10✔
215

216
         if(!sessions_db.empty()) {
10✔
217
   #if defined(BOTAN_HAS_TLS_SQLITE3_SESSION_MANAGER)
218
            const std::string sessions_passphrase = get_passphrase_arg("Session DB passphrase", "session-db-pass");
×
219
            session_mgr =
×
220
               std::make_shared<Botan::TLS::Session_Manager_SQLite>(sessions_passphrase, rng_as_shared(), sessions_db);
×
221
   #else
222
            error_output() << "Ignoring session DB file, sqlite not enabled\n";
223
   #endif
224
         }
×
225

226
         if(!session_mgr) {
10✔
227
            session_mgr = std::make_shared<Botan::TLS::Session_Manager_In_Memory>(rng_as_shared());
30✔
228
         }
229

230
         auto policy = load_tls_policy(get_arg("policy"));
20✔
231

232
         if(transport != "tcp" && transport != "udp") {
10✔
233
            throw CLI_Usage_Error("Invalid transport type '" + transport + "' for TLS");
×
234
         }
235

236
         const std::vector<std::string> protocols_to_offer = Command::split_on(next_protos, ',');
10✔
237

238
         if(!policy) {
10✔
239
            policy = std::make_shared<Botan::TLS::Policy>();
×
240
         }
241

242
         const bool use_tcp = (transport == "tcp");
10✔
243
         Botan::TLS::Protocol_Version version = policy->latest_supported_version(!use_tcp);
10✔
244

245
         if(tls_version != "default") {
10✔
246
            if(tls_version == "1.2") {
10✔
247
               version = use_tcp ? Botan::TLS::Protocol_Version::TLS_V12 : Botan::TLS::Protocol_Version::DTLS_V12;
3✔
248
            } else if(tls_version == "1.3") {
7✔
249
               version = use_tcp ? Botan::TLS::Protocol_Version::TLS_V13 : Botan::TLS::Protocol_Version::DTLS_V13;
7✔
250
            } else {
251
               error_output() << "Unknown TLS protocol version " << tls_version << '\n';
×
252
            }
253
         }
254

255
         m_sockfd = connect_to_host(host, port, use_tcp);
10✔
256

257
         const auto client_crt_path = get_arg_maybe("client-cert");
10✔
258
         const auto client_key_path = get_arg_maybe("client-cert-key");
10✔
259

260
         auto psk = [this]() -> std::optional<Botan::secure_vector<uint8_t>> {
×
261
            auto psk_hex = get_arg_maybe("psk");
10✔
262
            if(psk_hex) {
10✔
263
               return Botan::hex_decode_locked(psk_hex.value());
2✔
264
            } else {
265
               return {};
8✔
266
            }
267
         }();
20✔
268
         const std::optional<std::string> psk_identity = get_arg_maybe("psk-identity");
10✔
269
         const std::optional<std::string> psk_prf = get_arg_maybe("psk-prf");
10✔
270

271
         auto creds = std::make_shared<Basic_Credentials_Manager>(use_system_cert_store,
10✔
272
                                                                  trusted_CAs,
273
                                                                  client_crt_path,
274
                                                                  client_key_path,
275
                                                                  std::move(psk),
276
                                                                  psk_identity,
277
                                                                  psk_prf);
10✔
278

279
         Botan::TLS::Client client(callbacks,
10✔
280
                                   session_mgr,
281
                                   creds,
282
                                   policy,
283
                                   rng_as_shared(),
10✔
284
                                   Botan::TLS::Server_Information(host, port),
10✔
285
                                   version,
286
                                   protocols_to_offer);
40✔
287

288
         bool first_active = true;
10✔
289
         bool we_closed = false;
10✔
290

291
         while(!client.is_closed()) {
59✔
292
            fd_set readfds;
293
            FD_ZERO(&readfds);
986✔
294
            FD_SET(m_sockfd, &readfds);
58✔
295

296
            if(client.is_active()) {
58✔
297
               FD_SET(STDIN_FILENO, &readfds);
33✔
298
               if(first_active && !protocols_to_offer.empty()) {
33✔
299
                  const std::string app = client.application_protocol();
×
300
                  if(!app.empty()) {
×
301
                     output() << "Server choose protocol: " << client.application_protocol() << "\n";
×
302
                  }
303
                  first_active = false;
×
304
               }
×
305
            }
306

307
            struct timeval timeout = {1, 0};
58✔
308

309
            ::select(static_cast<int>(m_sockfd + 1), &readfds, nullptr, nullptr, &timeout);
58✔
310

311
            if(FD_ISSET(m_sockfd, &readfds)) {
58✔
312
               uint8_t buf[4 * 1024] = {0};
40✔
313

314
               const ssize_t got = ::read(m_sockfd, buf, sizeof(buf));
40✔
315

316
               if(got == 0) {
40✔
317
                  output() << "EOF on socket\n";
×
318
                  break;
×
319
               } else if(got == -1) {
40✔
320
                  output() << "Socket error: " << errno << " " << err_to_string(errno) << "\n";
×
321
                  continue;
×
322
               }
323

324
               if(flag_set("debug")) {
40✔
325
                  output() << ">> " << Botan::hex_encode(buf, got) << "\n";
×
326
               }
327

328
               client.received_data(buf, got);
40✔
329
            }
330

331
            if(FD_ISSET(STDIN_FILENO, &readfds)) {
58✔
332
               uint8_t buf[1024] = {0};
18✔
333
               const ssize_t got = read(STDIN_FILENO, buf, sizeof(buf));
18✔
334

335
               if(got == 0) {
18✔
336
                  output() << "EOF on stdin\n";
9✔
337
                  client.close();
9✔
338
                  we_closed = true;
9✔
339
                  break;
9✔
340
               } else if(got == -1) {
9✔
341
                  output() << "Stdin error: " << errno << " " << err_to_string(errno) << "\n";
×
342
                  continue;
×
343
               }
344

345
               if(got == 2 && buf[1] == '\n') {
9✔
346
                  const char cmd = buf[0];
×
347

348
                  if(cmd == 'R' || cmd == 'r') {
×
349
                     output() << "Client initiated renegotiation\n";
×
350
                     client.renegotiate(cmd == 'R');
×
351
                  } else if(cmd == 'Q') {
×
352
                     output() << "Client initiated close\n";
×
353
                     client.close();
×
354
                     we_closed = true;
355
                  }
356
               } else {
357
                  client.send(buf, got);
9✔
358
               }
359
            }
360

361
            if(client.timeout_check()) {
49✔
362
               output() << "Timeout detected\n";
×
363
            }
364
         }
365

366
         set_return_code((we_closed || callbacks->peer_closed()) ? 0 : 1);
19✔
367

368
         shutdown_socket();
10✔
369
      }
62✔
370

371
   public:
372
      using Command::flag_set;
373
      using Command::get_arg;
374
      using Command::output;
375

376
      void send(std::span<const uint8_t> buf) const {
54✔
377
         while(!buf.empty()) {
108✔
378
            ssize_t sent = ::send(m_sockfd, buf.data(), buf.size(), MSG_NOSIGNAL);
54✔
379

380
            if(sent == -1) {
54✔
381
               if(errno == EINTR) {
×
382
                  sent = 0;
383
               } else {
384
                  throw CLI_Error("Socket write failed errno=" + std::to_string(errno));
×
385
               }
386
            }
387

388
            buf = buf.subspan(sent);
54✔
389
         }
390
      }
54✔
391

392
   private:
393
      static socket_type connect_to_host(const std::string& host, uint16_t port, bool tcp) {
10✔
394
         addrinfo hints{};
10✔
395
         hints.ai_family = AF_UNSPEC;
10✔
396
         hints.ai_socktype = tcp ? SOCK_STREAM : SOCK_DGRAM;
10✔
397

398
         unique_addr_info_ptr res = nullptr;
10✔
399

400
         if(::getaddrinfo(host.c_str(), std::to_string(port).c_str(), &hints, Botan::out_ptr(res)) != 0) {
10✔
401
            throw CLI_Error("getaddrinfo failed for " + host);
×
402
         }
403

404
         socket_type fd = 0;
10✔
405
         bool success = false;
10✔
406

407
         for(const addrinfo* rp = res.get(); rp != nullptr; rp = rp->ai_next) {
20✔
408
            fd = ::socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
20✔
409

410
            if(fd == invalid_socket()) {
20✔
411
               continue;
×
412
            }
413

414
            if(fd >= FD_SETSIZE) {
20✔
415
               ::close(fd);
×
416
               throw CLI_Error("Socket descriptor exceeds FD_SETSIZE; select() would be unsafe");
×
417
            }
418

419
            if(::connect(fd, rp->ai_addr, rp->ai_addrlen) != 0) {
20✔
420
               ::close(fd);
10✔
421
               continue;
10✔
422
            }
423

424
            success = true;
425
            break;
426
         }
427

428
         if(!success) {
10✔
429
            // no address succeeded
430
            throw CLI_Error("Connecting to host failed");
×
431
         }
432

433
         return fd;
10✔
434
      }
10✔
435

436
      static void dgram_socket_write(int sockfd, const uint8_t buf[], size_t length) {
437
         auto r = ::send(sockfd, buf, length, MSG_NOSIGNAL);
438

439
         if(r == -1) {
440
            throw CLI_Error("Socket write failed errno=" + std::to_string(errno));
441
         }
442
      }
443

444
      void shutdown_socket() {
21✔
445
         if(m_sockfd == invalid_socket()) {
21✔
446
            return;
11✔
447
         }
448

449
         // Signal that we are done writing so pending alert records are
450
         // delivered with a FIN rather than lost to a RST.
451
         ::shutdown(m_sockfd, SHUT_WR);
10✔
452

453
         // Drain unread incoming data; if the receive buffer is non-empty when
454
         // we close(), the kernel sends RST which discards our outgoing data
455
         // (including any alert we sent).
456
         char buf[256];
10✔
457
         while(::read(m_sockfd, buf, sizeof(buf)) > 0) {}
29✔
458
         ::close(m_sockfd);
10✔
459

460
         m_sockfd = invalid_socket();
10✔
461
      }
462

463
      socket_type m_sockfd = invalid_socket();
464

465
      using unique_addr_info_ptr = std::unique_ptr<addrinfo, decltype([](addrinfo* p) {
10✔
466
                                                      if(p != nullptr) {
10✔
467
                                                         ::freeaddrinfo(p);
10✔
468
                                                      }
469
                                                   })>;
10✔
470
};
471

472
namespace {
473

474
std::ostream& Callbacks::output() {
2,125✔
475
   return m_client_command.output();
2,125✔
476
}
477

478
bool Callbacks::flag_set(const std::string& flag_name) const {
70✔
479
   return m_client_command.flag_set(flag_name);
70✔
480
}
481

482
std::string Callbacks::get_arg(const std::string& arg_name) const {
×
483
   return m_client_command.get_arg(arg_name);
×
484
}
485

486
void Callbacks::send(std::span<const uint8_t> buffer) {
54✔
487
   m_client_command.send(buffer);
54✔
488
}
489

490
}  // namespace
491

492
BOTAN_REGISTER_COMMAND("tls_client", TLS_Client);
11✔
493

494
}  // namespace Botan_CLI
495

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

© 2026 Coveralls, Inc