Coverage Report

Created: 2026-10-28 07:17

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