Coverage Report

Created: 2026-10-15 06:25

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/bio.h>
13
#include <openssl/ech.h>
14
#include <openssl/err.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_credential_cache.h"
23
#include "proto_fuzzer/tls_test_credentials.h"
24
25
namespace proto_fuzzer {
26
27
namespace {
28
29
constexpr std::size_t kMaxSessionExportAttempts = 8;
30
constexpr std::size_t kMaxSessionHashSize = 128;
31
constexpr std::size_t kMaxSessionDataSize = 16 * 1024;
32
33
/// Own the only export retained by one probe. curl owns every pointer passed
34
/// to the callback, so both binary fields must be copied before it returns.
35
struct ExportedSession {
36
  std::vector<unsigned char> salted_hash;
37
  std::vector<unsigned char> session_data;
38
};
39
40
/// Retain at most one bounded ticket while allowing curl to finish its cache
41
/// walk. Importing here would recurse into the already-locked session cache.
42
CURLcode CaptureExportedSession(CURL* /*easy*/, void* user_data, const char* /*session_key*/,
43
                                const unsigned char* salted_hash, std::size_t salted_hash_size,
44
                                const unsigned char* session_data, std::size_t session_data_size,
45
                                curl_off_t /*valid_until*/, int /*ietf_tls_id*/, const char* /*alpn*/,
46
137k
                                std::size_t /*early_data_max*/) {
47
137k
  auto* exported = static_cast<ExportedSession*>(user_data);
48
137k
  if (!exported->session_data.empty() || salted_hash == nullptr || salted_hash_size == 0 ||
49
88.1k
      salted_hash_size > kMaxSessionHashSize || session_data == nullptr || session_data_size == 0 ||
50
88.1k
      session_data_size > kMaxSessionDataSize) {
51
49.3k
    return CURLE_OK;
52
49.3k
  }
53
88.1k
  exported->salted_hash.assign(salted_hash, salted_hash + salted_hash_size);
54
88.1k
  exported->session_data.assign(session_data, session_data + session_data_size);
55
88.1k
  return CURLE_OK;
56
137k
}
57
58
/// Isolate the server and curl client even though both OpenSSL instances run
59
/// on one thread. SSL_get_error requires an empty queue before its I/O call;
60
/// clearing at both boundaries also prevents a server failure from changing
61
/// curl's subsequent error classification.
62
class OpenSslErrorQueueGuard {
63
 public:
64
2.06M
  OpenSslErrorQueueGuard() { ERR_clear_error(); }
65
66
2.06M
  ~OpenSslErrorQueueGuard() { ERR_clear_error(); }
67
68
  OpenSslErrorQueueGuard(const OpenSslErrorQueueGuard&) = delete;
69
  OpenSslErrorQueueGuard& operator=(const OpenSslErrorQueueGuard&) = delete;
70
};
71
72
/// Select the one protocol owned by this peer. A fixed server preference keeps
73
/// ordinary HTTPS scripts on HTTP/1.1 while allowing the proxy lane to prove
74
/// curl installed its HTTP/2 connection filter after TLS.
75
int SelectAlpn(SSL* /*ssl*/, const unsigned char** selected, unsigned char* selected_length,
76
116k
               const unsigned char* client_protocols, unsigned int client_protocols_length, void* userdata) {
77
116k
  const auto protocol = *static_cast<const TlsApplicationProtocol*>(userdata);
78
  // OpenSSL may return a pointer into the server preference list. Function-
79
  // local static storage keeps that result valid for the rest of the
80
  // handshake without reintroducing mutable transport policy as a global.
81
116k
  static constexpr unsigned char http11_alpn[] = {8, 'h', 't', 't', 'p', '/', '1', '.', '1'};
82
116k
  static constexpr unsigned char http2_alpn[] = {2, 'h', '2'};
83
116k
  const unsigned char* server_protocols = protocol == TlsApplicationProtocol::kHttp2 ? http2_alpn : http11_alpn;
84
116k
  const unsigned int server_protocols_length =
85
116k
      protocol == TlsApplicationProtocol::kHttp2 ? sizeof(http2_alpn) : sizeof(http11_alpn);
86
116k
  unsigned char* match = nullptr;
87
116k
  unsigned char match_length = 0;
88
116k
  const int result = SSL_select_next_proto(&match, &match_length, server_protocols, server_protocols_length,
89
116k
                                           client_protocols, client_protocols_length);
90
116k
  if (result != OPENSSL_NPN_NEGOTIATED) {
91
12.9k
    return SSL_TLSEXT_ERR_NOACK;
92
12.9k
  }
93
103k
  *selected = match;
94
103k
  *selected_length = match_length;
95
103k
  return SSL_TLSEXT_ERR_OK;
96
116k
}
97
98
}  // namespace
99
100
/// Own the certificate, key, session cache, ALPN policy, and observations
101
/// shared by every bounded connection in one Scenario. Keeping both session
102
/// state and its measurements here makes redirects related while preventing
103
/// one fuzz input from affecting the next.
104
class TlsServerContext {
105
 public:
106
  // Doxygen's no-preprocessing pass would see both conditional body openings.
107
  // Skip this implementation-only constructor to preserve the enclosing scope.
108
  /// @cond
109
  TlsServerContext(TlsApplicationProtocol protocol, curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain,
110
                   TlsGroupPolicy group_policy)
111
109k
      : context_(nullptr),
112
109k
        protocol_(protocol),
113
109k
        group_policy_(group_policy),
114
109k
        negotiated_tls_version_(0),
115
109k
        completed_handshake_count_(0),
116
109k
        reused_session_count_(0),
117
109k
        write_retry_count_(0),
118
#ifndef OPENSSL_NO_ECH
119
109k
        ech_status_(SSL_ECH_STATUS_NOT_TRIED) {
120
#else
121
        ech_status_(-1) {
122
#endif
123
109k
    OpenSslErrorQueueGuard error_guard;
124
109k
    context_ = SSL_CTX_new(TLS_server_method());
125
109k
    if (context_ == nullptr || !ApplyGroupPolicy() || !InstallCachedTlsTestCredentials(context_, certificate_chain) ||
126
109k
        !LoadEchConfig()) {
127
0
      SSL_CTX_free(context_);
128
0
      context_ = nullptr;
129
0
      return;
130
0
    }
131
132
109k
    (void)SSL_CTX_set_min_proto_version(context_, TLS1_2_VERSION);
133
109k
    (void)SSL_CTX_set_options(context_, SSL_OP_NO_COMPRESSION);
134
109k
    (void)SSL_CTX_set_session_cache_mode(context_, SSL_SESS_CACHE_SERVER);
135
109k
    constexpr unsigned char session_id_context[] = "curl-fuzzer";
136
109k
    (void)SSL_CTX_set_session_id_context(context_, session_id_context, sizeof(session_id_context) - 1);
137
109k
    SSL_CTX_set_alpn_select_cb(context_, &SelectAlpn, &protocol_);
138
109k
  }
139
  /// @endcond
140
141
109k
  ~TlsServerContext() {
142
109k
    OpenSslErrorQueueGuard error_guard;
143
109k
    SSL_CTX_free(context_);
144
109k
  }
145
146
  TlsServerContext(const TlsServerContext&) = delete;
147
  TlsServerContext& operator=(const TlsServerContext&) = delete;
148
149
  /// @return the configured context, or nullptr when credential setup failed.
150
134k
  SSL_CTX* get() const { return context_; }
151
152
  /// Retain only scalar handshake results; SSL itself remains connection-owned.
153
  /// @param ssl connection that has just completed SSL_accept.
154
127k
  void RecordHandshake(SSL* ssl) {
155
127k
    if (ssl == nullptr) {
156
0
      return;
157
0
    }
158
127k
    negotiated_tls_version_ = SSL_version(ssl);
159
127k
    const unsigned char* alpn = nullptr;
160
127k
    unsigned int alpn_length = 0;
161
127k
    SSL_get0_alpn_selected(ssl, &alpn, &alpn_length);
162
127k
    if (alpn != nullptr && alpn_length != 0) {
163
101k
      negotiated_alpn_.assign(reinterpret_cast<const char*>(alpn), alpn_length);
164
101k
    } else {
165
26.0k
      negotiated_alpn_.clear();
166
26.0k
    }
167
127k
    ++completed_handshake_count_;
168
127k
    if (SSL_session_reused(ssl) == 1) {
169
23.0k
      ++reused_session_count_;
170
23.0k
    }
171
127k
    const int negotiated_group = SSL_get_negotiated_group(ssl);
172
127k
    const char* group_name = negotiated_group == 0 ? nullptr : SSL_group_to_name(ssl, negotiated_group);
173
127k
    negotiated_group_ = group_name == nullptr ? std::string() : group_name;
174
127k
#ifndef OPENSSL_NO_ECH
175
127k
    char* inner_name = nullptr;
176
127k
    char* outer_name = nullptr;
177
127k
    ech_status_ = SSL_ech_get1_status(ssl, &inner_name, &outer_name);
178
127k
    ech_inner_name_ = inner_name == nullptr ? std::string() : inner_name;
179
127k
    ech_outer_name_ = outer_name == nullptr ? std::string() : outer_name;
180
127k
    OPENSSL_free(inner_name);
181
127k
    OPENSSL_free(outer_name);
182
127k
#endif
183
127k
  }
184
185
  /// Record that OpenSSL requires an identical application-write retry.
186
0
  void RecordWriteRetry() { ++write_retry_count_; }
187
188
  /// @return protocol version from the most recent completed handshake.
189
6
  int negotiated_tls_version() const { return negotiated_tls_version_; }
190
  /// @return number of connections that completed their handshake.
191
864k
  std::size_t completed_handshake_count() const { return completed_handshake_count_; }
192
  /// @return number of completed handshakes that reused a session.
193
6
  std::size_t reused_session_count() const { return reused_session_count_; }
194
  /// @return number of application writes that OpenSSL asked to retry.
195
6
  std::size_t write_retry_count() const { return write_retry_count_; }
196
  /// @return ALPN protocol from the most recent completed handshake.
197
6
  const std::string& negotiated_alpn() const { return negotiated_alpn_; }
198
  /// @return key-exchange group from the most recent completed handshake.
199
6
  const std::string& negotiated_group() const { return negotiated_group_; }
200
  /// @return fixed application protocol this context offers.
201
121k
  TlsApplicationProtocol protocol() const { return protocol_; }
202
  /// @return OpenSSL ECH result from the latest completed handshake.
203
6
  int ech_status() const { return ech_status_; }
204
  /// @return latest decrypted inner SNI, if ECH was attempted.
205
6
  const std::string& ech_inner_name() const { return ech_inner_name_; }
206
  /// @return latest public outer SNI, if ECH was attempted.
207
6
  const std::string& ech_outer_name() const { return ech_outer_name_; }
208
209
 private:
210
  /// Apply the constructor-selected group policy before any SSL is created.
211
  /// @return true when the provider accepted the requested policy.
212
109k
  bool ApplyGroupPolicy() {
213
109k
    switch (group_policy_) {
214
92.8k
      case TlsGroupPolicy::kProviderDefault:
215
92.8k
        return true;
216
16.1k
      case TlsGroupPolicy::kX25519:
217
16.1k
        return SSL_CTX_set1_groups_list(context_, "X25519") == 1;
218
109k
    }
219
0
    return false;
220
109k
  }
221
222
  /// Load a fixed test-only ECH private key and matching public config. This
223
  /// makes a successful encrypted ClientHello deterministic without DNS or
224
  /// filesystem state; builds configured without ECH retain the TLS mock.
225
109k
  bool LoadEchConfig() {
226
109k
#ifndef OPENSSL_NO_ECH
227
109k
    BIO* ech_bio = BIO_new_mem_buf(tls_test_credentials::kEchConfigPem, -1);
228
109k
    OSSL_ECHSTORE* store = OSSL_ECHSTORE_new(nullptr, nullptr);
229
109k
    if (ech_bio == nullptr || store == nullptr) {
230
0
      BIO_free(ech_bio);
231
0
      OSSL_ECHSTORE_free(store);
232
0
      return false;
233
0
    }
234
109k
    const bool loaded =
235
109k
        OSSL_ECHSTORE_read_pem(store, ech_bio, OSSL_ECH_NO_RETRY) == 1 && SSL_CTX_set1_echstore(context_, store) == 1;
236
109k
    BIO_free(ech_bio);
237
109k
    OSSL_ECHSTORE_free(store);
238
109k
    return loaded;
239
#else
240
    return true;
241
#endif
242
109k
  }
243
244
  SSL_CTX* context_;
245
  TlsApplicationProtocol protocol_;
246
  const TlsGroupPolicy group_policy_;
247
  std::string negotiated_alpn_;
248
  std::string negotiated_group_;
249
  int negotiated_tls_version_;
250
  std::size_t completed_handshake_count_;
251
  std::size_t reused_session_count_;
252
  std::size_t write_retry_count_;
253
  int ech_status_;
254
  std::string ech_inner_name_;
255
  std::string ech_outer_name_;
256
};
257
258
namespace {
259
260
/// TLS-aware MockConnection. Application writes are queued before the client
261
/// starts, then encrypted only after SSL_accept has completed. This preserves
262
/// MockServer's useful initial_response semantics without ever putting those
263
/// plaintext bytes onto the TLS wire.
264
class TlsMockConnection final : public MockConnection {
265
 public:
266
  explicit TlsMockConnection(TlsServerContext* context)
267
134k
      : ssl_(nullptr),
268
134k
        context_(context),
269
134k
        pending_offset_(0),
270
134k
        pending_write_end_(0),
271
134k
        handshake_complete_(false),
272
134k
        shutdown_requested_(false),
273
134k
        write_shutdown_(false),
274
134k
        failed_(false) {
275
134k
    OpenSslErrorQueueGuard error_guard;
276
134k
    SSL_CTX* ssl_context = context_ == nullptr ? nullptr : context_->get();
277
134k
    ssl_ = ssl_context == nullptr ? nullptr : SSL_new(ssl_context);
278
134k
    if (ssl_ != nullptr) {
279
134k
      SSL_set_mode(ssl_, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
280
134k
      if (SSL_set_fd(ssl_, server_fd()) != 1) {
281
0
        SSL_free(ssl_);
282
0
        ssl_ = nullptr;
283
134k
      } else {
284
134k
        SSL_set_accept_state(ssl_);
285
134k
      }
286
134k
    }
287
134k
  }
288
289
134k
  ~TlsMockConnection() override {
290
134k
    OpenSslErrorQueueGuard error_guard;
291
134k
    if (ssl_ != nullptr) {
292
134k
      SSL_free(ssl_);
293
134k
    }
294
134k
  }
295
296
  /// Require both the socketpair and its OpenSSL wrapper to be usable.
297
1.71M
  bool ok() const override { return MockConnection::ok() && ssl_ != nullptr; }
298
299
  /// Queue bounded application bytes until the handshake can encrypt them.
300
312k
  bool WriteAll(const unsigned char* data, std::size_t size) override {
301
312k
    if (failed_ || (data == nullptr && size != 0) || size > pending_plaintext_.max_size() - pending_plaintext_.size()) {
302
590
      return false;
303
590
    }
304
312k
    if (size != 0) {
305
312k
      pending_plaintext_.append(reinterpret_cast<const char*>(data), size);
306
312k
    }
307
312k
    return true;
308
312k
  }
309
310
  /// Advance the handshake, consume encrypted request records, flush queued
311
  /// response records, and finish a requested close without ever waiting.
312
  /// @return decrypted/application bytes plus state transitions made this turn.
313
1.58M
  std::size_t DrainIncoming() override {
314
1.58M
    if (!ok() || failed_) {
315
4.02k
      return 0;
316
4.02k
    }
317
318
1.58M
    OpenSslErrorQueueGuard error_guard;
319
1.58M
    std::size_t progress = 0;
320
1.58M
    if (!handshake_complete_) {
321
379k
      const int state_before = static_cast<int>(SSL_get_state(ssl_));
322
379k
      const int result = SSL_accept(ssl_);
323
      // SSL_get_error must be the next OpenSSL call after failed I/O. Even a
324
      // state query in between would make its use formally unreliable.
325
379k
      const int error = result == 1 ? SSL_ERROR_NONE : SSL_get_error(ssl_, result);
326
379k
      const int state_after = static_cast<int>(SSL_get_state(ssl_));
327
379k
      if (state_after != state_before) {
328
257k
        ++progress;
329
257k
      }
330
379k
      if (result == 1) {
331
127k
        handshake_complete_ = true;
332
127k
        if (context_ != nullptr) {
333
127k
          context_->RecordHandshake(ssl_);
334
127k
        }
335
127k
        ++progress;
336
252k
      } else {
337
252k
        if (error == SSL_ERROR_WANT_READ || error == SSL_ERROR_WANT_WRITE) {
338
250k
          return progress;
339
250k
        }
340
1.64k
        failed_ = true;
341
1.64k
        return progress;
342
252k
      }
343
379k
    }
344
345
    // A WANT result leaves SSL_write_ex in flight. OpenSSL requires the next
346
    // I/O operation to retry that exact write, so do not interpose SSL_read_ex
347
    // merely because the outer driver has yielded back to us.
348
1.32M
    if (pending_write_end_ != 0) {
349
0
      progress += FlushPendingWrites();
350
0
      if (failed_ || pending_write_end_ != 0) {
351
0
        return progress;
352
0
      }
353
0
    }
354
355
1.32M
    unsigned char request[4096];
356
1.90M
    while (true) {
357
1.90M
      std::size_t received = 0;
358
1.90M
      const int result = SSL_read_ex(ssl_, request, sizeof(request), &received);
359
1.90M
      if (result == 1 && received != 0) {
360
576k
        NotifyIncomingData(request, received);
361
576k
        progress += received;
362
576k
        continue;
363
576k
      }
364
1.32M
      if (result != 1) {
365
1.32M
        const int error = SSL_get_error(ssl_, result);
366
1.32M
        if (error != SSL_ERROR_WANT_READ && error != SSL_ERROR_WANT_WRITE && error != SSL_ERROR_ZERO_RETURN) {
367
2.52k
          failed_ = true;
368
2.52k
        }
369
1.32M
      }
370
1.32M
      break;
371
1.90M
    }
372
373
1.32M
    progress += FlushPendingWrites();
374
375
1.32M
    if (!failed_ && shutdown_requested_ && pending_plaintext_.empty() && !write_shutdown_) {
376
87.2k
      const int result = SSL_shutdown(ssl_);
377
87.2k
      if (result >= 0) {
378
        // The first successful call sends close_notify. The fuzzer does not
379
        // need to wait for the client's reciprocal alert before exposing EOF.
380
87.2k
        (void)::shutdown(server_fd(), SHUT_WR);
381
87.2k
        write_shutdown_ = true;
382
87.2k
        ++progress;
383
87.2k
      } else {
384
0
        const int error = SSL_get_error(ssl_, result);
385
0
        if (error != SSL_ERROR_WANT_READ && error != SSL_ERROR_WANT_WRITE) {
386
0
          failed_ = true;
387
0
        }
388
0
      }
389
87.2k
    }
390
1.32M
    return progress;
391
1.32M
  }
392
393
  /// Defer close_notify until every queued plaintext byte has become a record.
394
121k
  void ShutdownWrite() override {
395
    // An HTTP/2 proxy connection outlives the scripted origin response inside
396
    // its CONNECT stream. Half-closing TLS when the last script chunk is
397
    // queued would make curl observe a dead proxy rather than exercise its
398
    // own stream/session shutdown and GOAWAY handling.
399
121k
    if (context_ != nullptr && context_->protocol() == TlsApplicationProtocol::kHttp2) {
400
28.7k
      return;
401
28.7k
    }
402
92.3k
    shutdown_requested_ = true;
403
92.3k
    (void)DrainIncoming();
404
92.3k
  }
405
406
 private:
407
  /// Flush queued response bytes while preserving any OpenSSL retry boundary.
408
  /// @return plaintext bytes OpenSSL accepted during this call.
409
1.32M
  std::size_t FlushPendingWrites() {
410
1.32M
    std::size_t progress = 0;
411
1.60M
    while (!failed_ && pending_offset_ < pending_plaintext_.size()) {
412
278k
      if (pending_write_end_ == 0) {
413
        // Freeze one write boundary until OpenSSL accepts it. WriteAll may
414
        // append the next scripted chunk after WANT_READ/WANT_WRITE, but the
415
        // retry contract permits only pointer relocation—not changed bytes or
416
        // length—for the outstanding SSL_write_ex call.
417
278k
        pending_write_end_ = pending_plaintext_.size();
418
278k
      }
419
278k
      std::size_t written = 0;
420
278k
      const int result = SSL_write_ex(ssl_, pending_plaintext_.data() + pending_offset_,
421
278k
                                      pending_write_end_ - pending_offset_, &written);
422
278k
      if (result == 1) {
423
278k
        if (written == 0) {
424
0
          failed_ = true;
425
0
          break;
426
0
        }
427
278k
        pending_offset_ += written;
428
278k
        progress += written;
429
278k
        if (pending_offset_ == pending_write_end_) {
430
278k
          pending_write_end_ = 0;
431
278k
        }
432
278k
        continue;
433
278k
      }
434
5
      const int error = SSL_get_error(ssl_, result);
435
5
      if (error == SSL_ERROR_WANT_READ || error == SSL_ERROR_WANT_WRITE) {
436
0
        if (context_ != nullptr) {
437
0
          context_->RecordWriteRetry();
438
0
        }
439
5
      } else {
440
5
        failed_ = true;
441
5
      }
442
5
      break;
443
278k
    }
444
1.32M
    if (pending_offset_ == pending_plaintext_.size()) {
445
1.32M
      pending_plaintext_.clear();
446
1.32M
      pending_offset_ = 0;
447
1.32M
      pending_write_end_ = 0;
448
1.32M
    }
449
1.32M
    return progress;
450
1.32M
  }
451
452
  SSL* ssl_;
453
  TlsServerContext* context_;
454
  std::string pending_plaintext_;
455
  std::size_t pending_offset_;
456
  /// Exclusive end of a write that OpenSSL may require us to retry exactly.
457
  std::size_t pending_write_end_;
458
  bool handshake_complete_;
459
  bool shutdown_requested_;
460
  bool write_shutdown_;
461
  bool failed_;
462
};
463
464
}  // namespace
465
466
/// Construct one reusable TLS context for a protocol-specific peer.
467
/// @param protocol Fixed ALPN application protocol offered by the server.
468
/// @param certificate_chain Certificate-chain profile presented to curl.
469
/// @param group_policy Immutable server key-exchange group policy.
470
TlsMockTransport::TlsMockTransport(TlsApplicationProtocol protocol,
471
                                   curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain,
472
                                   TlsGroupPolicy group_policy)
473
109k
    : group_policy_(group_policy),
474
109k
      context_(std::make_unique<TlsServerContext>(protocol, certificate_chain, group_policy)) {}
475
476
109k
TlsMockTransport::~TlsMockTransport() = default;
477
478
/// Install the stable local trust anchor and strict peer verification. Socket
479
/// callbacks remain owned by the protocol-specific server using this carrier.
480
/// @param easy Easy handle that will connect to this TLS transport.
481
95.5k
void TlsMockTransport::Install(CURL* easy) const {
482
95.5k
  struct curl_blob trust_anchor = {const_cast<char*>(tls_test_credentials::kCertificatePem),
483
95.5k
                                   sizeof(tls_test_credentials::kCertificatePem) - 1, CURL_BLOB_NOCOPY};
484
95.5k
  (void)curl_easy_setopt(easy, CURLOPT_CAINFO_BLOB, &trust_anchor);
485
95.5k
  (void)curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L);
486
95.5k
  (void)curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L);
487
95.5k
}
488
489
/// Wrap one socketpair server endpoint in an OpenSSL acceptor.
490
/// @return A failed connection when context setup was unavailable.
491
134k
std::unique_ptr<MockConnection> TlsMockTransport::CreateConnection() {
492
134k
  return std::make_unique<TlsMockConnection>(context_.get());
493
134k
}
494
495
/// @return Protocol version selected by the most recent handshake, or zero.
496
6
int TlsMockTransport::negotiated_tls_version() const {
497
6
  return context_ == nullptr ? 0 : context_->negotiated_tls_version();
498
6
}
499
500
/// @return Number of connections that completed TLS negotiation.
501
864k
std::size_t TlsMockTransport::completed_handshake_count() const {
502
864k
  return context_ == nullptr ? 0 : context_->completed_handshake_count();
503
864k
}
504
505
/// @return Number of completed connections that resumed a cached session.
506
6
std::size_t TlsMockTransport::reused_session_count() const {
507
6
  return context_ == nullptr ? 0 : context_->reused_session_count();
508
6
}
509
510
/// @return Number of application writes OpenSSL required an exact retry for.
511
6
std::size_t TlsMockTransport::write_retry_count() const {
512
6
  return context_ == nullptr ? 0 : context_->write_retry_count();
513
6
}
514
515
/// @return ALPN protocol selected by the most recent handshake.
516
6
std::string TlsMockTransport::negotiated_alpn() const {
517
6
  return context_ == nullptr ? std::string() : context_->negotiated_alpn();
518
6
}
519
520
/// @return Immutable key-exchange group policy selected at construction.
521
16.5k
TlsGroupPolicy TlsMockTransport::group_policy() const { return group_policy_; }
522
523
/// @return Key-exchange group from the most recent completed handshake.
524
6
std::string TlsMockTransport::negotiated_group() const {
525
6
  return context_ == nullptr ? std::string() : context_->negotiated_group();
526
6
}
527
528
/// @return OpenSSL's ECH result, or a negative sentinel when unavailable.
529
6
int TlsMockTransport::ech_status() const { return context_ == nullptr ? -1 : context_->ech_status(); }
530
531
/// @return Decrypted inner SNI recovered by the ECH-capable server.
532
6
std::string TlsMockTransport::ech_inner_name() const {
533
6
  return context_ == nullptr ? std::string() : context_->ech_inner_name();
534
6
}
535
536
/// @return Public outer SNI visible before ECH decryption.
537
6
std::string TlsMockTransport::ech_outer_name() const {
538
6
  return context_ == nullptr ? std::string() : context_->ech_outer_name();
539
6
}
540
541
/// Build one reusable in-process server context per fuzz iteration.
542
0
TlsMockServer::TlsMockServer() : TlsMockServer(curl::fuzzer::proto::TLS_CERTIFICATE_CHAIN_DEFAULT_EC) {}
543
544
/// Select one bounded certificate-chain profile for an HTTP/1.1 TLS peer.
545
/// @param certificate_chain Peer certificate chain presented to curl.
546
TlsMockServer::TlsMockServer(curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain)
547
67.7k
    : TlsMockServer(TlsApplicationProtocol::kHttp11, certificate_chain) {}
548
549
/// Construct the shared TLS transport for a protocol-specific derived peer
550
/// with one fixed ALPN outcome.
551
/// @param protocol Fixed application protocol the server must negotiate.
552
/// @param certificate_chain Peer certificate chain presented to curl.
553
/// @param group_policy Immutable server key-exchange group policy.
554
TlsMockServer::TlsMockServer(TlsApplicationProtocol protocol,
555
                             curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain,
556
                             TlsGroupPolicy group_policy)
557
97.3k
    : transport_(protocol, certificate_chain, group_policy),
558
97.3k
      saw_live_tls_session_(false),
559
97.3k
      session_export_attempt_count_(0),
560
97.3k
      exported_session_count_(0),
561
97.3k
      imported_session_count_(0) {}
562
563
/// Release SSL objects before their owning server context. OpenSSL reference
564
/// counting makes the reverse order legal, but making the ownership order
565
/// explicit keeps future transport state from acquiring a hidden dependency.
566
97.3k
TlsMockServer::~TlsMockServer() { ResetConnections(); }
567
568
/// Install common socket callbacks and a stable local trust anchor. Curl copies
569
/// the blob descriptor during setopt and borrows the inline certificate bytes,
570
/// whose program lifetime safely exceeds every easy handle. Scenario options run
571
/// afterwards and can still disable verification or select failing TLS settings.
572
/// @param easy Easy handle that will connect to this TLS peer.
573
83.9k
void TlsMockServer::Install(CURL* easy) {
574
83.9k
  MockServer::Install(easy);
575
83.9k
  transport_.Install(easy);
576
83.9k
}
577
578
/// @return true once curl exposed a live TLS backend session during drive.
579
6
bool TlsMockServer::saw_live_tls_session() const { return saw_live_tls_session_; }
580
581
/// @return number of session representations copied by the export callback
582
/// out of curl's cache.
583
6
std::size_t TlsMockServer::exported_session_count() const { return exported_session_count_; }
584
585
/// @return number of copied session representations successfully imported
586
/// into curl's cache again.
587
6
std::size_t TlsMockServer::imported_session_count() const { return imported_session_count_; }
588
589
/// @return protocol version selected by the most recent completed handshake,
590
/// or zero when no connection completed TLS negotiation.
591
6
int TlsMockServer::negotiated_tls_version() const { return transport_.negotiated_tls_version(); }
592
593
/// @return number of connections that completed TLS negotiation.
594
6
std::size_t TlsMockServer::completed_handshake_count() const { return transport_.completed_handshake_count(); }
595
596
/// @return number of completed connections that resumed an earlier session.
597
6
std::size_t TlsMockServer::reused_session_count() const { return transport_.reused_session_count(); }
598
599
/// @return number of application writes OpenSSL required an exact retry for.
600
6
std::size_t TlsMockServer::write_retry_count() const { return transport_.write_retry_count(); }
601
602
/// @return ALPN protocol selected by the most recent completed handshake.
603
6
std::string TlsMockServer::negotiated_alpn() const { return transport_.negotiated_alpn(); }
604
605
/// @return Immutable key-exchange group policy selected at construction.
606
16.5k
TlsGroupPolicy TlsMockServer::group_policy() const { return transport_.group_policy(); }
607
608
/// @return Key-exchange group from the most recent completed handshake.
609
6
std::string TlsMockServer::negotiated_group() const { return transport_.negotiated_group(); }
610
611
/// @return OpenSSL's ECH status for the most recent completed handshake.
612
/// Builds without ECH return a negative sentinel.
613
6
int TlsMockServer::ech_status() const { return transport_.ech_status(); }
614
615
/// @return encrypted inner SNI recovered by the ECH-capable server.
616
6
std::string TlsMockServer::ech_inner_name() const { return transport_.ech_inner_name(); }
617
618
/// @return public outer SNI visible before ECH decryption.
619
6
std::string TlsMockServer::ech_outer_name() const { return transport_.ech_outer_name(); }
620
621
/// Wrap one socketpair server endpoint in an OpenSSL acceptor consumed by
622
/// MockServer.
623
/// @return a failed connection when context setup was unavailable.
624
122k
std::unique_ptr<MockConnection> TlsMockServer::CreateConnection() { return transport_.CreateConnection(); }
625
626
/// Query while the connection filters are attached. Result-info probes stop
627
/// after the first live result, and session export has its own fixed attempt
628
/// bound, so neither recurs throughout every HTTP parser iteration.
629
/// @param easy Active easy handle whose connection filters remain attached.
630
852k
void TlsMockServer::ObserveActiveTransfer(CURL* easy) {
631
852k
  if (!saw_live_tls_session_) {
632
297k
    long verify_result = 0;
633
297k
    curl_off_t appconnect_time = 0;
634
297k
    struct curl_certinfo* certificate_info = nullptr;
635
297k
    struct curl_tlssessioninfo* tls_session = nullptr;
636
297k
    (void)curl_easy_getinfo(easy, CURLINFO_SSL_VERIFYRESULT, &verify_result);
637
297k
    (void)curl_easy_getinfo(easy, CURLINFO_APPCONNECT_TIME_T, &appconnect_time);
638
297k
    (void)curl_easy_getinfo(easy, CURLINFO_CERTINFO, &certificate_info);
639
297k
    if (curl_easy_getinfo(easy, CURLINFO_TLS_SSL_PTR, &tls_session) == CURLE_OK && tls_session != nullptr &&
640
297k
        tls_session->internals != nullptr) {
641
76.7k
      saw_live_tls_session_ = true;
642
76.7k
    }
643
297k
  }
644
645
  // A TLS 1.3 ticket may arrive after the peer first completes its handshake.
646
  // Retry only a small fixed number of outer-loop observations, and stop
647
  // permanently once one copied ticket has made one import attempt.
648
852k
  const bool completed_live_handshake = transport_.completed_handshake_count() != 0;
649
852k
#if defined(PROTO_FUZZER_OBSERVE_STATE)
650
  // Read the owned peer observations once after a real handshake. Signed
651
  // status values retain their unavailable sentinels; string results are
652
  // consumed immediately while their temporaries and context are still live.
653
852k
  static bool observed = false;
654
852k
  if (completed_live_handshake && !observed) {
655
6
    observed = true;
656
6
    const volatile bool live = saw_live_tls_session();
657
6
    const volatile int status[] = {negotiated_tls_version(), ech_status()};
658
6
    const volatile std::size_t counts[] = {
659
6
        exported_session_count(),  imported_session_count(), completed_handshake_count(),
660
6
        reused_session_count(),    write_retry_count(),      negotiated_alpn().size(),
661
6
        negotiated_group().size(), ech_inner_name().size(),  ech_outer_name().size()};
662
6
    (void)live;
663
6
    (void)status;
664
6
    (void)counts;
665
6
  }
666
852k
#endif
667
852k
  if (!completed_live_handshake || exported_session_count_ != 0 ||
668
739k
      session_export_attempt_count_ >= kMaxSessionExportAttempts) {
669
739k
    return;
670
739k
  }
671
112k
  ++session_export_attempt_count_;
672
112k
  ExportedSession exported;
673
112k
  const CURLcode export_result = curl_easy_ssls_export(easy, &CaptureExportedSession, &exported);
674
112k
  if (export_result != CURLE_OK || exported.session_data.empty()) {
675
24.4k
    return;
676
24.4k
  }
677
88.1k
  ++exported_session_count_;
678
679
  // curl holds the SSL session-cache lock throughout CaptureExportedSession.
680
  // This deliberately separate call therefore exercises import without
681
  // recursive API use or retaining any callback-owned pointer.
682
88.1k
  if (curl_easy_ssls_import(easy, nullptr, exported.salted_hash.data(), exported.salted_hash.size(),
683
88.1k
                            exported.session_data.data(), exported.session_data.size()) == CURLE_OK) {
684
88.1k
    ++imported_session_count_;
685
88.1k
  }
686
88.1k
}
687
688
}  // namespace proto_fuzzer