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