Coverage Report

Created: 2026-09-14 07:12

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/curl_fuzzer/proto_fuzzer/tls_mock_server.cc
Line
Count
Source
1
/*
2
 * Copyright (C) Max Dymond, <cmeister2@gmail.com>, et al.
3
 *
4
 * SPDX-License-Identifier: curl
5
 */
6
7
/// @file
8
/// @brief Nonblocking OpenSSL server transport for structured HTTPS inputs.
9
10
#include "proto_fuzzer/tls_mock_server.h"
11
12
#include <openssl/ech.h>
13
#include <openssl/err.h>
14
#include <openssl/pem.h>
15
#include <openssl/ssl.h>
16
#include <sys/socket.h>
17
18
#include <cstddef>
19
#include <string>
20
#include <vector>
21
22
#include "proto_fuzzer/tls_test_credentials.h"
23
24
namespace proto_fuzzer {
25
26
namespace {
27
28
constexpr std::size_t kMaxSessionExportAttempts = 8;
29
constexpr std::size_t kMaxSessionHashSize = 128;
30
constexpr std::size_t kMaxSessionDataSize = 16 * 1024;
31
32
/// Own the only export retained by one probe. curl owns every pointer passed
33
/// to the callback, so both binary fields must be copied before it returns.
34
struct ExportedSession {
35
  std::vector<unsigned char> salted_hash;
36
  std::vector<unsigned char> session_data;
37
};
38
39
/// Retain at most one bounded ticket while allowing curl to finish its cache
40
/// walk. Importing here would recurse into the already-locked session cache.
41
CURLcode CaptureExportedSession(CURL* /*easy*/, void* user_data, const char* /*session_key*/,
42
                                const unsigned char* salted_hash, std::size_t salted_hash_size,
43
                                const unsigned char* session_data, std::size_t session_data_size,
44
                                curl_off_t /*valid_until*/, int /*ietf_tls_id*/, const char* /*alpn*/,
45
115k
                                std::size_t /*early_data_max*/) {
46
115k
  auto* exported = static_cast<ExportedSession*>(user_data);
47
115k
  if (!exported->session_data.empty() || salted_hash == nullptr || salted_hash_size == 0 ||
48
75.2k
      salted_hash_size > kMaxSessionHashSize || session_data == nullptr || session_data_size == 0 ||
49
75.2k
      session_data_size > kMaxSessionDataSize) {
50
40.7k
    return CURLE_OK;
51
40.7k
  }
52
75.2k
  exported->salted_hash.assign(salted_hash, salted_hash + salted_hash_size);
53
75.2k
  exported->session_data.assign(session_data, session_data + session_data_size);
54
75.2k
  return CURLE_OK;
55
115k
}
56
57
/// Isolate the server and curl client even though both OpenSSL instances run
58
/// on one thread. SSL_get_error requires an empty queue before its I/O call;
59
/// clearing at both boundaries also prevents a server failure from changing
60
/// curl's subsequent error classification.
61
class OpenSslErrorQueueGuard {
62
 public:
63
1.45M
  OpenSslErrorQueueGuard() { ERR_clear_error(); }
64
65
1.45M
  ~OpenSslErrorQueueGuard() { ERR_clear_error(); }
66
67
  OpenSslErrorQueueGuard(const OpenSslErrorQueueGuard&) = delete;
68
  OpenSslErrorQueueGuard& operator=(const OpenSslErrorQueueGuard&) = delete;
69
};
70
71
/// Select the one protocol owned by this peer. A fixed server preference keeps
72
/// ordinary HTTPS scripts on HTTP/1.1 while allowing the proxy lane to prove
73
/// curl installed its HTTP/2 connection filter after TLS.
74
int SelectAlpn(SSL* /*ssl*/, const unsigned char** selected, unsigned char* selected_length,
75
99.7k
               const unsigned char* client_protocols, unsigned int client_protocols_length, void* userdata) {
76
99.7k
  const auto protocol = *static_cast<const TlsApplicationProtocol*>(userdata);
77
  // OpenSSL may return a pointer into the server preference list. Function-
78
  // local static storage keeps that result valid for the rest of the
79
  // handshake without reintroducing mutable transport policy as a global.
80
99.7k
  static constexpr unsigned char http11_alpn[] = {8, 'h', 't', 't', 'p', '/', '1', '.', '1'};
81
99.7k
  static constexpr unsigned char http2_alpn[] = {2, 'h', '2'};
82
99.7k
  const unsigned char* server_protocols = protocol == TlsApplicationProtocol::kHttp2 ? http2_alpn : http11_alpn;
83
99.7k
  const unsigned int server_protocols_length =
84
99.7k
      protocol == TlsApplicationProtocol::kHttp2 ? sizeof(http2_alpn) : sizeof(http11_alpn);
85
99.7k
  unsigned char* match = nullptr;
86
99.7k
  unsigned char match_length = 0;
87
99.7k
  const int result = SSL_select_next_proto(&match, &match_length, server_protocols, server_protocols_length,
88
99.7k
                                           client_protocols, client_protocols_length);
89
99.7k
  if (result != OPENSSL_NPN_NEGOTIATED) {
90
10.5k
    return SSL_TLSEXT_ERR_NOACK;
91
10.5k
  }
92
89.1k
  *selected = match;
93
89.1k
  *selected_length = match_length;
94
89.1k
  return SSL_TLSEXT_ERR_OK;
95
99.7k
}
96
97
}  // namespace
98
99
/// Own the certificate, key, session cache, ALPN policy, and observations
100
/// shared by every bounded connection in one Scenario. Keeping both session
101
/// state and its measurements here makes redirects related while preventing
102
/// one fuzz input from affecting the next.
103
class TlsServerContext {
104
 public:
105
  TlsServerContext(TlsApplicationProtocol protocol, curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain)
106
82.0k
      : context_(nullptr),
107
82.0k
        protocol_(protocol),
108
82.0k
        negotiated_tls_version_(0),
109
82.0k
        completed_handshake_count_(0),
110
82.0k
        reused_session_count_(0),
111
82.0k
        write_retry_count_(0),
112
#ifndef OPENSSL_NO_ECH
113
82.0k
        ech_status_(SSL_ECH_STATUS_NOT_TRIED) {
114
#else
115
        ech_status_(-1) {
116
#endif
117
82.0k
    OpenSslErrorQueueGuard error_guard;
118
82.0k
    context_ = SSL_CTX_new(TLS_server_method());
119
82.0k
    if (context_ == nullptr || !LoadCredentials(certificate_chain) || !LoadEchConfig()) {
120
0
      SSL_CTX_free(context_);
121
0
      context_ = nullptr;
122
0
      return;
123
0
    }
124
125
82.0k
    (void)SSL_CTX_set_min_proto_version(context_, TLS1_2_VERSION);
126
82.0k
    (void)SSL_CTX_set_options(context_, SSL_OP_NO_COMPRESSION);
127
82.0k
    (void)SSL_CTX_set_session_cache_mode(context_, SSL_SESS_CACHE_SERVER);
128
82.0k
    constexpr unsigned char session_id_context[] = "curl-fuzzer";
129
82.0k
    (void)SSL_CTX_set_session_id_context(context_, session_id_context, sizeof(session_id_context) - 1);
130
82.0k
    SSL_CTX_set_alpn_select_cb(context_, &SelectAlpn, &protocol_);
131
82.0k
  }
132
133
82.0k
  ~TlsServerContext() {
134
82.0k
    OpenSslErrorQueueGuard error_guard;
135
82.0k
    SSL_CTX_free(context_);
136
82.0k
  }
137
138
  TlsServerContext(const TlsServerContext&) = delete;
139
  TlsServerContext& operator=(const TlsServerContext&) = delete;
140
141
  /// @return the configured context, or nullptr when credential setup failed.
142
106k
  SSL_CTX* get() const { return context_; }
143
144
  /// Retain only scalar handshake results; SSL itself remains connection-owned.
145
  /// @param ssl connection that has just completed SSL_accept.
146
101k
  void RecordHandshake(SSL* ssl) {
147
101k
    if (ssl == nullptr) {
148
0
      return;
149
0
    }
150
101k
    negotiated_tls_version_ = SSL_version(ssl);
151
101k
    const unsigned char* alpn = nullptr;
152
101k
    unsigned int alpn_length = 0;
153
101k
    SSL_get0_alpn_selected(ssl, &alpn, &alpn_length);
154
101k
    if (alpn != nullptr && alpn_length != 0) {
155
87.3k
      negotiated_alpn_.assign(reinterpret_cast<const char*>(alpn), alpn_length);
156
87.3k
    } else {
157
14.3k
      negotiated_alpn_.clear();
158
14.3k
    }
159
101k
    ++completed_handshake_count_;
160
101k
    if (SSL_session_reused(ssl) == 1) {
161
20.8k
      ++reused_session_count_;
162
20.8k
    }
163
101k
#ifndef OPENSSL_NO_ECH
164
101k
    char* inner_name = nullptr;
165
101k
    char* outer_name = nullptr;
166
101k
    ech_status_ = SSL_ech_get1_status(ssl, &inner_name, &outer_name);
167
101k
    ech_inner_name_ = inner_name == nullptr ? std::string() : inner_name;
168
101k
    ech_outer_name_ = outer_name == nullptr ? std::string() : outer_name;
169
101k
    OPENSSL_free(inner_name);
170
101k
    OPENSSL_free(outer_name);
171
101k
#endif
172
101k
  }
173
174
  /// Record that OpenSSL requires an identical application-write retry.
175
0
  void RecordWriteRetry() { ++write_retry_count_; }
176
177
  /// @return protocol version from the most recent completed handshake.
178
0
  int negotiated_tls_version() const { return negotiated_tls_version_; }
179
  /// @return number of connections that completed their handshake.
180
630k
  std::size_t completed_handshake_count() const { return completed_handshake_count_; }
181
  /// @return number of completed handshakes that reused a session.
182
0
  std::size_t reused_session_count() const { return reused_session_count_; }
183
  /// @return number of application writes that OpenSSL asked to retry.
184
0
  std::size_t write_retry_count() const { return write_retry_count_; }
185
  /// @return ALPN protocol from the most recent completed handshake.
186
0
  const std::string& negotiated_alpn() const { return negotiated_alpn_; }
187
  /// @return fixed application protocol this context offers.
188
102k
  TlsApplicationProtocol protocol() const { return protocol_; }
189
  /// @return OpenSSL ECH result from the latest completed handshake.
190
0
  int ech_status() const { return ech_status_; }
191
  /// @return latest decrypted inner SNI, if ECH was attempted.
192
0
  const std::string& ech_inner_name() const { return ech_inner_name_; }
193
  /// @return latest public outer SNI, if ECH was attempted.
194
0
  const std::string& ech_outer_name() const { return ech_outer_name_; }
195
196
 private:
197
  /// Append one profile-selected peer certificate without requiring it to
198
  /// authenticate the TLS handshake. OpenSSL transfers ownership on success;
199
  /// failure leaves cleanup with the caller.
200
17.1k
  bool AddExtraChainCertificate(const char* certificate_pem) {
201
17.1k
    BIO* certificate_bio = BIO_new_mem_buf(certificate_pem, -1);
202
17.1k
    if (certificate_bio == nullptr) {
203
0
      return false;
204
0
    }
205
17.1k
    X509* certificate = PEM_read_bio_X509(certificate_bio, nullptr, nullptr, nullptr);
206
17.1k
    BIO_free(certificate_bio);
207
17.1k
    if (certificate == nullptr) {
208
0
      return false;
209
0
    }
210
17.1k
    if (SSL_CTX_add_extra_chain_cert(context_, certificate) != 1) {
211
0
      X509_free(certificate);
212
0
      return false;
213
0
    }
214
17.1k
    return true;
215
17.1k
  }
216
217
  /// Parse the checked-in test-only PEM values entirely in memory.
218
82.0k
  bool LoadCredentials(curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain) {
219
82.0k
    BIO* certificate_bio = BIO_new_mem_buf(tls_test_credentials::kCertificatePem, -1);
220
82.0k
    BIO* key_bio = BIO_new_mem_buf(tls_test_credentials::kPrivateKeyPem, -1);
221
82.0k
    if (certificate_bio == nullptr || key_bio == nullptr) {
222
0
      BIO_free(certificate_bio);
223
0
      BIO_free(key_bio);
224
0
      return false;
225
0
    }
226
227
82.0k
    X509* certificate = PEM_read_bio_X509(certificate_bio, nullptr, nullptr, nullptr);
228
82.0k
    EVP_PKEY* key = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr, nullptr);
229
82.0k
    BIO_free(certificate_bio);
230
82.0k
    BIO_free(key_bio);
231
82.0k
    if (certificate == nullptr || key == nullptr) {
232
0
      X509_free(certificate);
233
0
      EVP_PKEY_free(key);
234
0
      return false;
235
0
    }
236
237
82.0k
    const bool loaded = SSL_CTX_use_certificate(context_, certificate) == 1 &&
238
82.0k
                        SSL_CTX_use_PrivateKey(context_, key) == 1 && SSL_CTX_check_private_key(context_) == 1;
239
82.0k
    X509_free(certificate);
240
82.0k
    EVP_PKEY_free(key);
241
82.0k
    if (!loaded) {
242
0
      return false;
243
0
    }
244
245
82.0k
    switch (certificate_chain) {
246
5.72k
      case curl::fuzzer::proto::TLS_CERTIFICATE_CHAIN_ALL_KEY_TYPES:
247
5.72k
        return AddExtraChainCertificate(tls_test_credentials::kRsaCertificatePem) &&
248
5.72k
               AddExtraChainCertificate(tls_test_credentials::kDsaCertificatePem) &&
249
5.72k
               AddExtraChainCertificate(tls_test_credentials::kDhCertificatePem);
250
76.3k
      case curl::fuzzer::proto::TLS_CERTIFICATE_CHAIN_DEFAULT_EC:
251
76.3k
      default:
252
76.3k
        return true;
253
82.0k
    }
254
82.0k
  }
255
256
  /// Load a fixed test-only ECH private key and matching public config. This
257
  /// makes a successful encrypted ClientHello deterministic without DNS or
258
  /// filesystem state; builds configured without ECH retain the TLS mock.
259
82.0k
  bool LoadEchConfig() {
260
82.0k
#ifndef OPENSSL_NO_ECH
261
82.0k
    BIO* ech_bio = BIO_new_mem_buf(tls_test_credentials::kEchConfigPem, -1);
262
82.0k
    OSSL_ECHSTORE* store = OSSL_ECHSTORE_new(nullptr, nullptr);
263
82.0k
    if (ech_bio == nullptr || store == nullptr) {
264
0
      BIO_free(ech_bio);
265
0
      OSSL_ECHSTORE_free(store);
266
0
      return false;
267
0
    }
268
82.0k
    const bool loaded =
269
82.0k
        OSSL_ECHSTORE_read_pem(store, ech_bio, OSSL_ECH_NO_RETRY) == 1 && SSL_CTX_set1_echstore(context_, store) == 1;
270
82.0k
    BIO_free(ech_bio);
271
82.0k
    OSSL_ECHSTORE_free(store);
272
82.0k
    return loaded;
273
#else
274
    return true;
275
#endif
276
82.0k
  }
277
278
  SSL_CTX* context_;
279
  TlsApplicationProtocol protocol_;
280
  std::string negotiated_alpn_;
281
  int negotiated_tls_version_;
282
  std::size_t completed_handshake_count_;
283
  std::size_t reused_session_count_;
284
  std::size_t write_retry_count_;
285
  int ech_status_;
286
  std::string ech_inner_name_;
287
  std::string ech_outer_name_;
288
};
289
290
namespace {
291
292
/// TLS-aware MockConnection. Application writes are queued before the client
293
/// starts, then encrypted only after SSL_accept has completed. This preserves
294
/// MockServer's useful initial_response semantics without ever putting those
295
/// plaintext bytes onto the TLS wire.
296
class TlsMockConnection final : public MockConnection {
297
 public:
298
  explicit TlsMockConnection(TlsServerContext* context)
299
106k
      : ssl_(nullptr),
300
106k
        context_(context),
301
106k
        pending_offset_(0),
302
106k
        pending_write_end_(0),
303
106k
        handshake_complete_(false),
304
106k
        shutdown_requested_(false),
305
106k
        write_shutdown_(false),
306
106k
        failed_(false) {
307
106k
    OpenSslErrorQueueGuard error_guard;
308
106k
    SSL_CTX* ssl_context = context_ == nullptr ? nullptr : context_->get();
309
106k
    ssl_ = ssl_context == nullptr ? nullptr : SSL_new(ssl_context);
310
106k
    if (ssl_ != nullptr) {
311
106k
      SSL_set_mode(ssl_, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
312
106k
      if (SSL_set_fd(ssl_, server_fd()) != 1) {
313
0
        SSL_free(ssl_);
314
0
        ssl_ = nullptr;
315
106k
      } else {
316
106k
        SSL_set_accept_state(ssl_);
317
106k
      }
318
106k
    }
319
106k
  }
320
321
106k
  ~TlsMockConnection() override {
322
106k
    OpenSslErrorQueueGuard error_guard;
323
106k
    if (ssl_ != nullptr) {
324
106k
      SSL_free(ssl_);
325
106k
    }
326
106k
  }
327
328
  /// Require both the socketpair and its OpenSSL wrapper to be usable.
329
1.18M
  bool ok() const override { return MockConnection::ok() && ssl_ != nullptr; }
330
331
  /// Queue bounded application bytes until the handshake can encrypt them.
332
231k
  bool WriteAll(const unsigned char* data, std::size_t size) override {
333
231k
    if (failed_ || (data == nullptr && size != 0) || size > pending_plaintext_.max_size() - pending_plaintext_.size()) {
334
416
      return false;
335
416
    }
336
231k
    if (size != 0) {
337
231k
      pending_plaintext_.append(reinterpret_cast<const char*>(data), size);
338
231k
    }
339
231k
    return true;
340
231k
  }
341
342
  /// Advance the handshake, consume encrypted request records, flush queued
343
  /// response records, and finish a requested close without ever waiting.
344
  /// @return decrypted/application bytes plus state transitions made this turn.
345
1.07M
  std::size_t DrainIncoming() override {
346
1.07M
    if (!ok() || failed_) {
347
3.60k
      return 0;
348
3.60k
    }
349
350
1.07M
    OpenSslErrorQueueGuard error_guard;
351
1.07M
    std::size_t progress = 0;
352
1.07M
    if (!handshake_complete_) {
353
311k
      const int state_before = static_cast<int>(SSL_get_state(ssl_));
354
311k
      const int result = SSL_accept(ssl_);
355
      // SSL_get_error must be the next OpenSSL call after failed I/O. Even a
356
      // state query in between would make its use formally unreliable.
357
311k
      const int error = result == 1 ? SSL_ERROR_NONE : SSL_get_error(ssl_, result);
358
311k
      const int state_after = static_cast<int>(SSL_get_state(ssl_));
359
311k
      if (state_after != state_before) {
360
205k
        ++progress;
361
205k
      }
362
311k
      if (result == 1) {
363
101k
        handshake_complete_ = true;
364
101k
        if (context_ != nullptr) {
365
101k
          context_->RecordHandshake(ssl_);
366
101k
        }
367
101k
        ++progress;
368
209k
      } else {
369
209k
        if (error == SSL_ERROR_WANT_READ || error == SSL_ERROR_WANT_WRITE) {
370
209k
          return progress;
371
209k
        }
372
438
        failed_ = true;
373
438
        return progress;
374
209k
      }
375
311k
    }
376
377
    // A WANT result leaves SSL_write_ex in flight. OpenSSL requires the next
378
    // I/O operation to retry that exact write, so do not interpose SSL_read_ex
379
    // merely because the outer driver has yielded back to us.
380
864k
    if (pending_write_end_ != 0) {
381
0
      progress += FlushPendingWrites();
382
0
      if (failed_ || pending_write_end_ != 0) {
383
0
        return progress;
384
0
      }
385
0
    }
386
387
864k
    unsigned char request[4096];
388
1.18M
    while (true) {
389
1.18M
      std::size_t received = 0;
390
1.18M
      const int result = SSL_read_ex(ssl_, request, sizeof(request), &received);
391
1.18M
      if (result == 1 && received != 0) {
392
322k
        progress += received;
393
322k
        continue;
394
322k
      }
395
864k
      if (result != 1) {
396
864k
        const int error = SSL_get_error(ssl_, result);
397
864k
        if (error != SSL_ERROR_WANT_READ && error != SSL_ERROR_WANT_WRITE && error != SSL_ERROR_ZERO_RETURN) {
398
2.49k
          failed_ = true;
399
2.49k
        }
400
864k
      }
401
864k
      break;
402
1.18M
    }
403
404
864k
    progress += FlushPendingWrites();
405
406
864k
    if (!failed_ && shutdown_requested_ && pending_plaintext_.empty() && !write_shutdown_) {
407
77.0k
      const int result = SSL_shutdown(ssl_);
408
77.0k
      if (result >= 0) {
409
        // The first successful call sends close_notify. The fuzzer does not
410
        // need to wait for the client's reciprocal alert before exposing EOF.
411
77.0k
        (void)::shutdown(server_fd(), SHUT_WR);
412
77.0k
        write_shutdown_ = true;
413
77.0k
        ++progress;
414
77.0k
      } else {
415
0
        const int error = SSL_get_error(ssl_, result);
416
0
        if (error != SSL_ERROR_WANT_READ && error != SSL_ERROR_WANT_WRITE) {
417
0
          failed_ = true;
418
0
        }
419
0
      }
420
77.0k
    }
421
864k
    return progress;
422
864k
  }
423
424
  /// Defer close_notify until every queued plaintext byte has become a record.
425
102k
  void ShutdownWrite() override {
426
    // An HTTP/2 proxy connection outlives the scripted origin response inside
427
    // its CONNECT stream. Half-closing TLS when the last script chunk is
428
    // queued would make curl observe a dead proxy rather than exercise its
429
    // own stream/session shutdown and GOAWAY handling.
430
102k
    if (context_ != nullptr && context_->protocol() == TlsApplicationProtocol::kHttp2) {
431
21.1k
      return;
432
21.1k
    }
433
81.3k
    shutdown_requested_ = true;
434
81.3k
    (void)DrainIncoming();
435
81.3k
  }
436
437
 private:
438
  /// Flush queued response bytes while preserving any OpenSSL retry boundary.
439
  /// @return plaintext bytes OpenSSL accepted during this call.
440
864k
  std::size_t FlushPendingWrites() {
441
864k
    std::size_t progress = 0;
442
1.05M
    while (!failed_ && pending_offset_ < pending_plaintext_.size()) {
443
195k
      if (pending_write_end_ == 0) {
444
        // Freeze one write boundary until OpenSSL accepts it. WriteAll may
445
        // append the next scripted chunk after WANT_READ/WANT_WRITE, but the
446
        // retry contract permits only pointer relocation—not changed bytes or
447
        // length—for the outstanding SSL_write_ex call.
448
195k
        pending_write_end_ = pending_plaintext_.size();
449
195k
      }
450
195k
      std::size_t written = 0;
451
195k
      const int result = SSL_write_ex(ssl_, pending_plaintext_.data() + pending_offset_,
452
195k
                                      pending_write_end_ - pending_offset_, &written);
453
195k
      if (result == 1) {
454
195k
        if (written == 0) {
455
0
          failed_ = true;
456
0
          break;
457
0
        }
458
195k
        pending_offset_ += written;
459
195k
        progress += written;
460
195k
        if (pending_offset_ == pending_write_end_) {
461
195k
          pending_write_end_ = 0;
462
195k
        }
463
195k
        continue;
464
195k
      }
465
0
      const int error = SSL_get_error(ssl_, result);
466
0
      if (error == SSL_ERROR_WANT_READ || error == SSL_ERROR_WANT_WRITE) {
467
0
        if (context_ != nullptr) {
468
0
          context_->RecordWriteRetry();
469
0
        }
470
0
      } else {
471
0
        failed_ = true;
472
0
      }
473
0
      break;
474
195k
    }
475
864k
    if (pending_offset_ == pending_plaintext_.size()) {
476
863k
      pending_plaintext_.clear();
477
863k
      pending_offset_ = 0;
478
863k
      pending_write_end_ = 0;
479
863k
    }
480
864k
    return progress;
481
864k
  }
482
483
  SSL* ssl_;
484
  TlsServerContext* context_;
485
  std::string pending_plaintext_;
486
  std::size_t pending_offset_;
487
  /// Exclusive end of a write that OpenSSL may require us to retry exactly.
488
  std::size_t pending_write_end_;
489
  bool handshake_complete_;
490
  bool shutdown_requested_;
491
  bool write_shutdown_;
492
  bool failed_;
493
};
494
495
}  // namespace
496
497
/// Build one reusable in-process server context per fuzz iteration.
498
0
TlsMockServer::TlsMockServer() : TlsMockServer(curl::fuzzer::proto::TLS_CERTIFICATE_CHAIN_DEFAULT_EC) {}
499
500
/// Select one bounded certificate chain while retaining HTTP/1.1 ALPN.
501
TlsMockServer::TlsMockServer(curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain)
502
59.8k
    : TlsMockServer(TlsApplicationProtocol::kHttp11, certificate_chain) {}
503
504
/// Construct the shared TLS transport with one fixed ALPN outcome.
505
TlsMockServer::TlsMockServer(TlsApplicationProtocol protocol,
506
                             curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain)
507
82.0k
    : context_(std::make_unique<TlsServerContext>(protocol, certificate_chain)),
508
82.0k
      saw_live_tls_session_(false),
509
82.0k
      session_export_attempt_count_(0),
510
82.0k
      exported_session_count_(0),
511
82.0k
      imported_session_count_(0) {}
512
513
/// Release SSL objects before their owning server context. OpenSSL reference
514
/// counting makes the reverse order legal, but making the ownership order
515
/// explicit keeps future transport state from acquiring a hidden dependency.
516
82.0k
TlsMockServer::~TlsMockServer() { ResetConnections(); }
517
518
/// Add verification to the common socket callbacks. Curl copies the blob
519
/// descriptor during setopt and borrows the inline certificate bytes, whose
520
/// program lifetime safely exceeds every easy handle.
521
70.7k
void TlsMockServer::Install(CURL* easy) {
522
70.7k
  MockServer::Install(easy);
523
70.7k
  struct curl_blob trust_anchor = {const_cast<char*>(tls_test_credentials::kCertificatePem),
524
70.7k
                                   sizeof(tls_test_credentials::kCertificatePem) - 1, CURL_BLOB_NOCOPY};
525
70.7k
  (void)curl_easy_setopt(easy, CURLOPT_CAINFO_BLOB, &trust_anchor);
526
70.7k
  (void)curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L);
527
70.7k
  (void)curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L);
528
70.7k
}
529
530
/// Report whether the drive reached curl's live TLS backend-query path.
531
0
bool TlsMockServer::saw_live_tls_session() const { return saw_live_tls_session_; }
532
533
/// Report the number of callback-owned session representations copied.
534
0
std::size_t TlsMockServer::exported_session_count() const { return exported_session_count_; }
535
536
/// Report the number of copied representations accepted by curl's import path.
537
0
std::size_t TlsMockServer::imported_session_count() const { return imported_session_count_; }
538
539
/// Report the protocol selected by the latest successful server handshake.
540
0
int TlsMockServer::negotiated_tls_version() const {
541
0
  return context_ == nullptr ? 0 : context_->negotiated_tls_version();
542
0
}
543
544
/// Report how many bounded connections finished their TLS handshake.
545
0
std::size_t TlsMockServer::completed_handshake_count() const {
546
0
  return context_ == nullptr ? 0 : context_->completed_handshake_count();
547
0
}
548
549
/// Report how many later connections resumed state from this scenario.
550
0
std::size_t TlsMockServer::reused_session_count() const {
551
0
  return context_ == nullptr ? 0 : context_->reused_session_count();
552
0
}
553
554
/// Report how often response delivery reached OpenSSL's exact-retry path.
555
0
std::size_t TlsMockServer::write_retry_count() const { return context_ == nullptr ? 0 : context_->write_retry_count(); }
556
557
/// Return the latest negotiated application protocol without exposing SSL.
558
0
std::string TlsMockServer::negotiated_alpn() const {
559
0
  return context_ == nullptr ? std::string() : context_->negotiated_alpn();
560
0
}
561
562
/// Return OpenSSL's latest ECH result without exposing its SSL object.
563
0
int TlsMockServer::ech_status() const { return context_ == nullptr ? -1 : context_->ech_status(); }
564
565
/// Return the SNI that OpenSSL recovered from the encrypted ClientHello.
566
0
std::string TlsMockServer::ech_inner_name() const {
567
0
  return context_ == nullptr ? std::string() : context_->ech_inner_name();
568
0
}
569
570
/// Return the public SNI that remained in the outer ClientHello.
571
0
std::string TlsMockServer::ech_outer_name() const {
572
0
  return context_ == nullptr ? std::string() : context_->ech_outer_name();
573
0
}
574
575
/// Create the polymorphic record-layer connection consumed by MockServer.
576
106k
std::unique_ptr<MockConnection> TlsMockServer::CreateConnection() {
577
106k
  return std::make_unique<TlsMockConnection>(context_.get());
578
106k
}
579
580
/// Query while the connection filters are attached. Result-info probes stop
581
/// after the first live result, and session export has its own fixed attempt
582
/// bound, so neither recurs throughout every HTTP parser iteration.
583
/// @param easy Active easy handle whose connection filters remain attached.
584
630k
void TlsMockServer::ObserveActiveTransfer(CURL* easy) {
585
630k
  if (!saw_live_tls_session_) {
586
259k
    long verify_result = 0;
587
259k
    curl_off_t appconnect_time = 0;
588
259k
    struct curl_certinfo* certificate_info = nullptr;
589
259k
    struct curl_tlssessioninfo* tls_session = nullptr;
590
259k
    (void)curl_easy_getinfo(easy, CURLINFO_SSL_VERIFYRESULT, &verify_result);
591
259k
    (void)curl_easy_getinfo(easy, CURLINFO_APPCONNECT_TIME_T, &appconnect_time);
592
259k
    (void)curl_easy_getinfo(easy, CURLINFO_CERTINFO, &certificate_info);
593
259k
    if (curl_easy_getinfo(easy, CURLINFO_TLS_SSL_PTR, &tls_session) == CURLE_OK && tls_session != nullptr &&
594
259k
        tls_session->internals != nullptr) {
595
64.9k
      saw_live_tls_session_ = true;
596
64.9k
    }
597
259k
  }
598
599
  // A TLS 1.3 ticket may arrive after the peer first completes its handshake.
600
  // Retry only a small fixed number of outer-loop observations, and stop
601
  // permanently once one copied ticket has made one import attempt.
602
630k
  const bool completed_live_handshake = context_ != nullptr && context_->completed_handshake_count() != 0;
603
630k
  if (!completed_live_handshake || exported_session_count_ != 0 ||
604
533k
      session_export_attempt_count_ >= kMaxSessionExportAttempts) {
605
533k
    return;
606
533k
  }
607
97.0k
  ++session_export_attempt_count_;
608
97.0k
  ExportedSession exported;
609
97.0k
  const CURLcode export_result = curl_easy_ssls_export(easy, &CaptureExportedSession, &exported);
610
97.0k
  if (export_result != CURLE_OK || exported.session_data.empty()) {
611
21.8k
    return;
612
21.8k
  }
613
75.2k
  ++exported_session_count_;
614
615
  // curl holds the SSL session-cache lock throughout CaptureExportedSession.
616
  // This deliberately separate call therefore exercises import without
617
  // recursive API use or retaining any callback-owned pointer.
618
75.2k
  if (curl_easy_ssls_import(easy, nullptr, exported.salted_hash.data(), exported.salted_hash.size(),
619
75.2k
                            exported.session_data.data(), exported.session_data.size()) == CURLE_OK) {
620
75.2k
    ++imported_session_count_;
621
75.2k
  }
622
75.2k
}
623
624
}  // namespace proto_fuzzer