/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 |