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