/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 | | |
21 | | #include "proto_fuzzer/tls_test_credentials.h" |
22 | | |
23 | | namespace proto_fuzzer { |
24 | | |
25 | | namespace { |
26 | | |
27 | | /// Isolate the server and curl client even though both OpenSSL instances run |
28 | | /// on one thread. SSL_get_error requires an empty queue before its I/O call; |
29 | | /// clearing at both boundaries also prevents a server failure from changing |
30 | | /// curl's subsequent error classification. |
31 | | class OpenSslErrorQueueGuard { |
32 | | public: |
33 | 328k | OpenSslErrorQueueGuard() { ERR_clear_error(); } |
34 | | |
35 | 328k | ~OpenSslErrorQueueGuard() { ERR_clear_error(); } |
36 | | |
37 | | OpenSslErrorQueueGuard(const OpenSslErrorQueueGuard&) = delete; |
38 | | OpenSslErrorQueueGuard& operator=(const OpenSslErrorQueueGuard&) = delete; |
39 | | }; |
40 | | |
41 | | /// Select the one protocol owned by this peer. A fixed server preference keeps |
42 | | /// ordinary HTTPS scripts on HTTP/1.1 while allowing the proxy lane to prove |
43 | | /// curl installed its HTTP/2 connection filter after TLS. |
44 | | int SelectAlpn(SSL* /*ssl*/, const unsigned char** selected, unsigned char* selected_length, |
45 | 28.0k | const unsigned char* client_protocols, unsigned int client_protocols_length, void* userdata) { |
46 | 28.0k | const auto protocol = *static_cast<const TlsApplicationProtocol*>(userdata); |
47 | | // OpenSSL may return a pointer into the server preference list. Function- |
48 | | // local static storage keeps that result valid for the rest of the |
49 | | // handshake without reintroducing mutable transport policy as a global. |
50 | 28.0k | static constexpr unsigned char http11_alpn[] = {8, 'h', 't', 't', 'p', '/', '1', '.', '1'}; |
51 | 28.0k | static constexpr unsigned char http2_alpn[] = {2, 'h', '2'}; |
52 | 28.0k | const unsigned char* server_protocols = protocol == TlsApplicationProtocol::kHttp2 ? http2_alpn : http11_alpn; |
53 | 28.0k | const unsigned int server_protocols_length = |
54 | 28.0k | protocol == TlsApplicationProtocol::kHttp2 ? sizeof(http2_alpn) : sizeof(http11_alpn); |
55 | 28.0k | unsigned char* match = nullptr; |
56 | 28.0k | unsigned char match_length = 0; |
57 | 28.0k | const int result = SSL_select_next_proto(&match, &match_length, server_protocols, server_protocols_length, |
58 | 28.0k | client_protocols, client_protocols_length); |
59 | 28.0k | if (result != OPENSSL_NPN_NEGOTIATED) { |
60 | 2.19k | return SSL_TLSEXT_ERR_NOACK; |
61 | 2.19k | } |
62 | 25.8k | *selected = match; |
63 | 25.8k | *selected_length = match_length; |
64 | 25.8k | return SSL_TLSEXT_ERR_OK; |
65 | 28.0k | } |
66 | | |
67 | | } // namespace |
68 | | |
69 | | /// Own the certificate, key, session cache, ALPN policy, and observations |
70 | | /// shared by every bounded connection in one Scenario. Keeping both session |
71 | | /// state and its measurements here makes redirects related while preventing |
72 | | /// one fuzz input from affecting the next. |
73 | | class TlsServerContext { |
74 | | public: |
75 | | TlsServerContext(TlsApplicationProtocol protocol, curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain) |
76 | 23.3k | : context_(nullptr), |
77 | 23.3k | protocol_(protocol), |
78 | 23.3k | negotiated_tls_version_(0), |
79 | 23.3k | completed_handshake_count_(0), |
80 | 23.3k | reused_session_count_(0), |
81 | 23.3k | write_retry_count_(0), |
82 | | #ifndef OPENSSL_NO_ECH |
83 | 23.3k | ech_status_(SSL_ECH_STATUS_NOT_TRIED) { |
84 | | #else |
85 | | ech_status_(-1) { |
86 | | #endif |
87 | 23.3k | OpenSslErrorQueueGuard error_guard; |
88 | 23.3k | context_ = SSL_CTX_new(TLS_server_method()); |
89 | 23.3k | if (context_ == nullptr || !LoadCredentials(certificate_chain) || !LoadEchConfig()) { |
90 | 0 | SSL_CTX_free(context_); |
91 | 0 | context_ = nullptr; |
92 | 0 | return; |
93 | 0 | } |
94 | | |
95 | 23.3k | (void)SSL_CTX_set_min_proto_version(context_, TLS1_2_VERSION); |
96 | 23.3k | (void)SSL_CTX_set_options(context_, SSL_OP_NO_COMPRESSION); |
97 | 23.3k | (void)SSL_CTX_set_session_cache_mode(context_, SSL_SESS_CACHE_SERVER); |
98 | 23.3k | constexpr unsigned char session_id_context[] = "curl-fuzzer"; |
99 | 23.3k | (void)SSL_CTX_set_session_id_context(context_, session_id_context, sizeof(session_id_context) - 1); |
100 | 23.3k | SSL_CTX_set_alpn_select_cb(context_, &SelectAlpn, &protocol_); |
101 | 23.3k | } |
102 | | |
103 | 23.3k | ~TlsServerContext() { |
104 | 23.3k | OpenSslErrorQueueGuard error_guard; |
105 | 23.3k | SSL_CTX_free(context_); |
106 | 23.3k | } |
107 | | |
108 | | TlsServerContext(const TlsServerContext&) = delete; |
109 | | TlsServerContext& operator=(const TlsServerContext&) = delete; |
110 | | |
111 | | /// @return the configured context, or nullptr when credential setup failed. |
112 | 28.8k | SSL_CTX* get() const { return context_; } |
113 | | |
114 | | /// Retain only scalar handshake results; SSL itself remains connection-owned. |
115 | | /// @param ssl connection that has just completed SSL_accept. |
116 | 27.6k | void RecordHandshake(SSL* ssl) { |
117 | 27.6k | if (ssl == nullptr) { |
118 | 0 | return; |
119 | 0 | } |
120 | 27.6k | negotiated_tls_version_ = SSL_version(ssl); |
121 | 27.6k | const unsigned char* alpn = nullptr; |
122 | 27.6k | unsigned int alpn_length = 0; |
123 | 27.6k | SSL_get0_alpn_selected(ssl, &alpn, &alpn_length); |
124 | 27.6k | if (alpn != nullptr && alpn_length != 0) { |
125 | 25.3k | negotiated_alpn_.assign(reinterpret_cast<const char*>(alpn), alpn_length); |
126 | 25.3k | } else { |
127 | 2.37k | negotiated_alpn_.clear(); |
128 | 2.37k | } |
129 | 27.6k | ++completed_handshake_count_; |
130 | 27.6k | if (SSL_session_reused(ssl) == 1) { |
131 | 4.72k | ++reused_session_count_; |
132 | 4.72k | } |
133 | 27.6k | #ifndef OPENSSL_NO_ECH |
134 | 27.6k | char* inner_name = nullptr; |
135 | 27.6k | char* outer_name = nullptr; |
136 | 27.6k | ech_status_ = SSL_ech_get1_status(ssl, &inner_name, &outer_name); |
137 | 27.6k | ech_inner_name_ = inner_name == nullptr ? std::string() : inner_name; |
138 | 27.6k | ech_outer_name_ = outer_name == nullptr ? std::string() : outer_name; |
139 | 27.6k | OPENSSL_free(inner_name); |
140 | 27.6k | OPENSSL_free(outer_name); |
141 | 27.6k | #endif |
142 | 27.6k | } |
143 | | |
144 | | /// Record that OpenSSL requires an identical application-write retry. |
145 | 0 | void RecordWriteRetry() { ++write_retry_count_; } |
146 | | |
147 | | /// @return protocol version from the most recent completed handshake. |
148 | 0 | int negotiated_tls_version() const { return negotiated_tls_version_; } |
149 | | /// @return number of connections that completed their handshake. |
150 | 0 | std::size_t completed_handshake_count() const { return completed_handshake_count_; } |
151 | | /// @return number of completed handshakes that reused a session. |
152 | 0 | std::size_t reused_session_count() const { return reused_session_count_; } |
153 | | /// @return number of application writes that OpenSSL asked to retry. |
154 | 0 | std::size_t write_retry_count() const { return write_retry_count_; } |
155 | | /// @return ALPN protocol from the most recent completed handshake. |
156 | 0 | const std::string& negotiated_alpn() const { return negotiated_alpn_; } |
157 | | /// @return fixed application protocol this context offers. |
158 | 27.5k | TlsApplicationProtocol protocol() const { return protocol_; } |
159 | | /// @return OpenSSL ECH result from the latest completed handshake. |
160 | 0 | int ech_status() const { return ech_status_; } |
161 | | /// @return latest decrypted inner SNI, if ECH was attempted. |
162 | 0 | const std::string& ech_inner_name() const { return ech_inner_name_; } |
163 | | /// @return latest public outer SNI, if ECH was attempted. |
164 | 0 | const std::string& ech_outer_name() const { return ech_outer_name_; } |
165 | | |
166 | | private: |
167 | | /// Append one profile-selected peer certificate without requiring it to |
168 | | /// authenticate the TLS handshake. OpenSSL transfers ownership on success; |
169 | | /// failure leaves cleanup with the caller. |
170 | 90 | bool AddExtraChainCertificate(const char* certificate_pem) { |
171 | 90 | BIO* certificate_bio = BIO_new_mem_buf(certificate_pem, -1); |
172 | 90 | if (certificate_bio == nullptr) { |
173 | 0 | return false; |
174 | 0 | } |
175 | 90 | X509* certificate = PEM_read_bio_X509(certificate_bio, nullptr, nullptr, nullptr); |
176 | 90 | BIO_free(certificate_bio); |
177 | 90 | if (certificate == nullptr) { |
178 | 0 | return false; |
179 | 0 | } |
180 | 90 | if (SSL_CTX_add_extra_chain_cert(context_, certificate) != 1) { |
181 | 0 | X509_free(certificate); |
182 | 0 | return false; |
183 | 0 | } |
184 | 90 | return true; |
185 | 90 | } |
186 | | |
187 | | /// Parse the checked-in test-only PEM values entirely in memory. |
188 | 23.3k | bool LoadCredentials(curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain) { |
189 | 23.3k | BIO* certificate_bio = BIO_new_mem_buf(tls_test_credentials::kCertificatePem, -1); |
190 | 23.3k | BIO* key_bio = BIO_new_mem_buf(tls_test_credentials::kPrivateKeyPem, -1); |
191 | 23.3k | if (certificate_bio == nullptr || key_bio == nullptr) { |
192 | 0 | BIO_free(certificate_bio); |
193 | 0 | BIO_free(key_bio); |
194 | 0 | return false; |
195 | 0 | } |
196 | | |
197 | 23.3k | X509* certificate = PEM_read_bio_X509(certificate_bio, nullptr, nullptr, nullptr); |
198 | 23.3k | EVP_PKEY* key = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr, nullptr); |
199 | 23.3k | BIO_free(certificate_bio); |
200 | 23.3k | BIO_free(key_bio); |
201 | 23.3k | if (certificate == nullptr || key == nullptr) { |
202 | 0 | X509_free(certificate); |
203 | 0 | EVP_PKEY_free(key); |
204 | 0 | return false; |
205 | 0 | } |
206 | | |
207 | 23.3k | const bool loaded = SSL_CTX_use_certificate(context_, certificate) == 1 && |
208 | 23.3k | SSL_CTX_use_PrivateKey(context_, key) == 1 && SSL_CTX_check_private_key(context_) == 1; |
209 | 23.3k | X509_free(certificate); |
210 | 23.3k | EVP_PKEY_free(key); |
211 | 23.3k | if (!loaded) { |
212 | 0 | return false; |
213 | 0 | } |
214 | | |
215 | 23.3k | switch (certificate_chain) { |
216 | 30 | case curl::fuzzer::proto::TLS_CERTIFICATE_CHAIN_ALL_KEY_TYPES: |
217 | 30 | return AddExtraChainCertificate(tls_test_credentials::kRsaCertificatePem) && |
218 | 30 | AddExtraChainCertificate(tls_test_credentials::kDsaCertificatePem) && |
219 | 30 | AddExtraChainCertificate(tls_test_credentials::kDhCertificatePem); |
220 | 23.3k | case curl::fuzzer::proto::TLS_CERTIFICATE_CHAIN_DEFAULT_EC: |
221 | 23.3k | default: |
222 | 23.3k | return true; |
223 | 23.3k | } |
224 | 23.3k | } |
225 | | |
226 | | /// Load a fixed test-only ECH private key and matching public config. This |
227 | | /// makes a successful encrypted ClientHello deterministic without DNS or |
228 | | /// filesystem state; builds configured without ECH retain the TLS mock. |
229 | 23.3k | bool LoadEchConfig() { |
230 | 23.3k | #ifndef OPENSSL_NO_ECH |
231 | 23.3k | BIO* ech_bio = BIO_new_mem_buf(tls_test_credentials::kEchConfigPem, -1); |
232 | 23.3k | OSSL_ECHSTORE* store = OSSL_ECHSTORE_new(nullptr, nullptr); |
233 | 23.3k | if (ech_bio == nullptr || store == nullptr) { |
234 | 0 | BIO_free(ech_bio); |
235 | 0 | OSSL_ECHSTORE_free(store); |
236 | 0 | return false; |
237 | 0 | } |
238 | 23.3k | const bool loaded = |
239 | 23.3k | OSSL_ECHSTORE_read_pem(store, ech_bio, OSSL_ECH_NO_RETRY) == 1 && SSL_CTX_set1_echstore(context_, store) == 1; |
240 | 23.3k | BIO_free(ech_bio); |
241 | 23.3k | OSSL_ECHSTORE_free(store); |
242 | 23.3k | return loaded; |
243 | | #else |
244 | | return true; |
245 | | #endif |
246 | 23.3k | } |
247 | | |
248 | | SSL_CTX* context_; |
249 | | TlsApplicationProtocol protocol_; |
250 | | std::string negotiated_alpn_; |
251 | | int negotiated_tls_version_; |
252 | | std::size_t completed_handshake_count_; |
253 | | std::size_t reused_session_count_; |
254 | | std::size_t write_retry_count_; |
255 | | int ech_status_; |
256 | | std::string ech_inner_name_; |
257 | | std::string ech_outer_name_; |
258 | | }; |
259 | | |
260 | | namespace { |
261 | | |
262 | | /// TLS-aware MockConnection. Application writes are queued before the client |
263 | | /// starts, then encrypted only after SSL_accept has completed. This preserves |
264 | | /// MockServer's useful initial_response semantics without ever putting those |
265 | | /// plaintext bytes onto the TLS wire. |
266 | | class TlsMockConnection final : public MockConnection { |
267 | | public: |
268 | | explicit TlsMockConnection(TlsServerContext* context) |
269 | 28.8k | : ssl_(nullptr), |
270 | 28.8k | context_(context), |
271 | 28.8k | pending_offset_(0), |
272 | 28.8k | pending_write_end_(0), |
273 | 28.8k | handshake_complete_(false), |
274 | 28.8k | shutdown_requested_(false), |
275 | 28.8k | write_shutdown_(false), |
276 | 28.8k | failed_(false) { |
277 | 28.8k | OpenSslErrorQueueGuard error_guard; |
278 | 28.8k | SSL_CTX* ssl_context = context_ == nullptr ? nullptr : context_->get(); |
279 | 28.8k | ssl_ = ssl_context == nullptr ? nullptr : SSL_new(ssl_context); |
280 | 28.8k | if (ssl_ != nullptr) { |
281 | 28.8k | SSL_set_mode(ssl_, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); |
282 | 28.8k | if (SSL_set_fd(ssl_, server_fd()) != 1) { |
283 | 0 | SSL_free(ssl_); |
284 | 0 | ssl_ = nullptr; |
285 | 28.8k | } else { |
286 | 28.8k | SSL_set_accept_state(ssl_); |
287 | 28.8k | } |
288 | 28.8k | } |
289 | 28.8k | } |
290 | | |
291 | 28.8k | ~TlsMockConnection() override { |
292 | 28.8k | OpenSslErrorQueueGuard error_guard; |
293 | 28.8k | if (ssl_ != nullptr) { |
294 | 28.8k | SSL_free(ssl_); |
295 | 28.8k | } |
296 | 28.8k | } |
297 | | |
298 | | /// Require both the socketpair and its OpenSSL wrapper to be usable. |
299 | 254k | bool ok() const override { return MockConnection::ok() && ssl_ != nullptr; } |
300 | | |
301 | | /// Queue bounded application bytes until the handshake can encrypt them. |
302 | 59.1k | bool WriteAll(const unsigned char* data, std::size_t size) override { |
303 | 59.1k | if (failed_ || (data == nullptr && size != 0) || size > pending_plaintext_.max_size() - pending_plaintext_.size()) { |
304 | 213 | return false; |
305 | 213 | } |
306 | 58.9k | if (size != 0) { |
307 | 58.9k | pending_plaintext_.append(reinterpret_cast<const char*>(data), size); |
308 | 58.9k | } |
309 | 58.9k | return true; |
310 | 59.1k | } |
311 | | |
312 | | /// Advance the handshake, consume encrypted request records, flush queued |
313 | | /// response records, and finish a requested close without ever waiting. |
314 | | /// @return decrypted/application bytes plus state transitions made this turn. |
315 | 225k | std::size_t DrainIncoming() override { |
316 | 225k | if (!ok() || failed_) { |
317 | 1.75k | return 0; |
318 | 1.75k | } |
319 | | |
320 | 223k | OpenSslErrorQueueGuard error_guard; |
321 | 223k | std::size_t progress = 0; |
322 | 223k | if (!handshake_complete_) { |
323 | 85.7k | const int state_before = static_cast<int>(SSL_get_state(ssl_)); |
324 | 85.7k | const int result = SSL_accept(ssl_); |
325 | | // SSL_get_error must be the next OpenSSL call after failed I/O. Even a |
326 | | // state query in between would make its use formally unreliable. |
327 | 85.7k | const int error = result == 1 ? SSL_ERROR_NONE : SSL_get_error(ssl_, result); |
328 | 85.7k | const int state_after = static_cast<int>(SSL_get_state(ssl_)); |
329 | 85.7k | if (state_after != state_before) { |
330 | 56.0k | ++progress; |
331 | 56.0k | } |
332 | 85.7k | if (result == 1) { |
333 | 27.6k | handshake_complete_ = true; |
334 | 27.6k | if (context_ != nullptr) { |
335 | 27.6k | context_->RecordHandshake(ssl_); |
336 | 27.6k | } |
337 | 27.6k | ++progress; |
338 | 58.0k | } else { |
339 | 58.0k | if (error == SSL_ERROR_WANT_READ || error == SSL_ERROR_WANT_WRITE) { |
340 | 57.8k | return progress; |
341 | 57.8k | } |
342 | 161 | failed_ = true; |
343 | 161 | return progress; |
344 | 58.0k | } |
345 | 85.7k | } |
346 | | |
347 | | // A WANT result leaves SSL_write_ex in flight. OpenSSL requires the next |
348 | | // I/O operation to retry that exact write, so do not interpose SSL_read_ex |
349 | | // merely because the outer driver has yielded back to us. |
350 | 165k | if (pending_write_end_ != 0) { |
351 | 0 | progress += FlushPendingWrites(); |
352 | 0 | if (failed_ || pending_write_end_ != 0) { |
353 | 0 | return progress; |
354 | 0 | } |
355 | 0 | } |
356 | | |
357 | 165k | unsigned char request[4096]; |
358 | 232k | while (true) { |
359 | 232k | std::size_t received = 0; |
360 | 232k | const int result = SSL_read_ex(ssl_, request, sizeof(request), &received); |
361 | 232k | if (result == 1 && received != 0) { |
362 | 67.2k | progress += received; |
363 | 67.2k | continue; |
364 | 67.2k | } |
365 | 165k | if (result != 1) { |
366 | 165k | const int error = SSL_get_error(ssl_, result); |
367 | 165k | if (error != SSL_ERROR_WANT_READ && error != SSL_ERROR_WANT_WRITE && error != SSL_ERROR_ZERO_RETURN) { |
368 | 91 | failed_ = true; |
369 | 91 | } |
370 | 165k | } |
371 | 165k | break; |
372 | 232k | } |
373 | | |
374 | 165k | progress += FlushPendingWrites(); |
375 | | |
376 | 165k | if (!failed_ && shutdown_requested_ && pending_plaintext_.empty() && !write_shutdown_) { |
377 | 21.2k | const int result = SSL_shutdown(ssl_); |
378 | 21.2k | if (result >= 0) { |
379 | | // The first successful call sends close_notify. The fuzzer does not |
380 | | // need to wait for the client's reciprocal alert before exposing EOF. |
381 | 21.2k | (void)::shutdown(server_fd(), SHUT_WR); |
382 | 21.2k | write_shutdown_ = true; |
383 | 21.2k | ++progress; |
384 | 21.2k | } else { |
385 | 0 | const int error = SSL_get_error(ssl_, result); |
386 | 0 | if (error != SSL_ERROR_WANT_READ && error != SSL_ERROR_WANT_WRITE) { |
387 | 0 | failed_ = true; |
388 | 0 | } |
389 | 0 | } |
390 | 21.2k | } |
391 | 165k | return progress; |
392 | 165k | } |
393 | | |
394 | | /// Defer close_notify until every queued plaintext byte has become a record. |
395 | 27.5k | void ShutdownWrite() override { |
396 | | // An HTTP/2 proxy connection outlives the scripted origin response inside |
397 | | // its CONNECT stream. Half-closing TLS when the last script chunk is |
398 | | // queued would make curl observe a dead proxy rather than exercise its |
399 | | // own stream/session shutdown and GOAWAY handling. |
400 | 27.5k | if (context_ != nullptr && context_->protocol() == TlsApplicationProtocol::kHttp2) { |
401 | 5.18k | return; |
402 | 5.18k | } |
403 | 22.3k | shutdown_requested_ = true; |
404 | 22.3k | (void)DrainIncoming(); |
405 | 22.3k | } |
406 | | |
407 | | private: |
408 | | /// Flush queued response bytes while preserving any OpenSSL retry boundary. |
409 | | /// @return plaintext bytes OpenSSL accepted during this call. |
410 | 165k | std::size_t FlushPendingWrites() { |
411 | 165k | std::size_t progress = 0; |
412 | 213k | while (!failed_ && pending_offset_ < pending_plaintext_.size()) { |
413 | 47.6k | if (pending_write_end_ == 0) { |
414 | | // Freeze one write boundary until OpenSSL accepts it. WriteAll may |
415 | | // append the next scripted chunk after WANT_READ/WANT_WRITE, but the |
416 | | // retry contract permits only pointer relocation—not changed bytes or |
417 | | // length—for the outstanding SSL_write_ex call. |
418 | 47.6k | pending_write_end_ = pending_plaintext_.size(); |
419 | 47.6k | } |
420 | 47.6k | std::size_t written = 0; |
421 | 47.6k | const int result = SSL_write_ex(ssl_, pending_plaintext_.data() + pending_offset_, |
422 | 47.6k | pending_write_end_ - pending_offset_, &written); |
423 | 47.6k | if (result == 1) { |
424 | 47.6k | if (written == 0) { |
425 | 0 | failed_ = true; |
426 | 0 | break; |
427 | 0 | } |
428 | 47.6k | pending_offset_ += written; |
429 | 47.6k | progress += written; |
430 | 47.6k | if (pending_offset_ == pending_write_end_) { |
431 | 47.6k | pending_write_end_ = 0; |
432 | 47.6k | } |
433 | 47.6k | continue; |
434 | 47.6k | } |
435 | 0 | const int error = SSL_get_error(ssl_, result); |
436 | 0 | if (error == SSL_ERROR_WANT_READ || error == SSL_ERROR_WANT_WRITE) { |
437 | 0 | if (context_ != nullptr) { |
438 | 0 | context_->RecordWriteRetry(); |
439 | 0 | } |
440 | 0 | } else { |
441 | 0 | failed_ = true; |
442 | 0 | } |
443 | 0 | break; |
444 | 47.6k | } |
445 | 165k | if (pending_offset_ == pending_plaintext_.size()) { |
446 | 165k | pending_plaintext_.clear(); |
447 | 165k | pending_offset_ = 0; |
448 | 165k | pending_write_end_ = 0; |
449 | 165k | } |
450 | 165k | return progress; |
451 | 165k | } |
452 | | |
453 | | SSL* ssl_; |
454 | | TlsServerContext* context_; |
455 | | std::string pending_plaintext_; |
456 | | std::size_t pending_offset_; |
457 | | /// Exclusive end of a write that OpenSSL may require us to retry exactly. |
458 | | std::size_t pending_write_end_; |
459 | | bool handshake_complete_; |
460 | | bool shutdown_requested_; |
461 | | bool write_shutdown_; |
462 | | bool failed_; |
463 | | }; |
464 | | |
465 | | } // namespace |
466 | | |
467 | | /// Build one reusable in-process server context per fuzz iteration. |
468 | 0 | TlsMockServer::TlsMockServer() : TlsMockServer(curl::fuzzer::proto::TLS_CERTIFICATE_CHAIN_DEFAULT_EC) {} |
469 | | |
470 | | /// Select one bounded certificate chain while retaining HTTP/1.1 ALPN. |
471 | | TlsMockServer::TlsMockServer(curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain) |
472 | 17.8k | : TlsMockServer(TlsApplicationProtocol::kHttp11, certificate_chain) {} |
473 | | |
474 | | /// Construct the shared TLS transport with one fixed ALPN outcome. |
475 | | TlsMockServer::TlsMockServer(TlsApplicationProtocol protocol, |
476 | | curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain) |
477 | 23.3k | : context_(std::make_unique<TlsServerContext>(protocol, certificate_chain)), saw_live_tls_session_(false) {} |
478 | | |
479 | | /// Release SSL objects before their owning server context. OpenSSL reference |
480 | | /// counting makes the reverse order legal, but making the ownership order |
481 | | /// explicit keeps future transport state from acquiring a hidden dependency. |
482 | 23.3k | TlsMockServer::~TlsMockServer() { ResetConnections(); } |
483 | | |
484 | | /// Add verification to the common socket callbacks. Curl copies the blob |
485 | | /// descriptor during setopt and borrows the inline certificate bytes, whose |
486 | | /// program lifetime safely exceeds every easy handle. |
487 | 17.8k | void TlsMockServer::Install(CURL* easy) { |
488 | 17.8k | MockServer::Install(easy); |
489 | 17.8k | struct curl_blob trust_anchor = {const_cast<char*>(tls_test_credentials::kCertificatePem), |
490 | 17.8k | sizeof(tls_test_credentials::kCertificatePem) - 1, CURL_BLOB_NOCOPY}; |
491 | 17.8k | (void)curl_easy_setopt(easy, CURLOPT_CAINFO_BLOB, &trust_anchor); |
492 | 17.8k | (void)curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L); |
493 | 17.8k | (void)curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L); |
494 | 17.8k | } |
495 | | |
496 | | /// Report whether the drive reached curl's live TLS backend-query path. |
497 | 0 | bool TlsMockServer::saw_live_tls_session() const { return saw_live_tls_session_; } |
498 | | |
499 | | /// Report the protocol selected by the latest successful server handshake. |
500 | 0 | int TlsMockServer::negotiated_tls_version() const { |
501 | 0 | return context_ == nullptr ? 0 : context_->negotiated_tls_version(); |
502 | 0 | } |
503 | | |
504 | | /// Report how many bounded connections finished their TLS handshake. |
505 | 0 | std::size_t TlsMockServer::completed_handshake_count() const { |
506 | 0 | return context_ == nullptr ? 0 : context_->completed_handshake_count(); |
507 | 0 | } |
508 | | |
509 | | /// Report how many later connections resumed state from this scenario. |
510 | 0 | std::size_t TlsMockServer::reused_session_count() const { |
511 | 0 | return context_ == nullptr ? 0 : context_->reused_session_count(); |
512 | 0 | } |
513 | | |
514 | | /// Report how often response delivery reached OpenSSL's exact-retry path. |
515 | 0 | std::size_t TlsMockServer::write_retry_count() const { return context_ == nullptr ? 0 : context_->write_retry_count(); } |
516 | | |
517 | | /// Return the latest negotiated application protocol without exposing SSL. |
518 | 0 | std::string TlsMockServer::negotiated_alpn() const { |
519 | 0 | return context_ == nullptr ? std::string() : context_->negotiated_alpn(); |
520 | 0 | } |
521 | | |
522 | | /// Return OpenSSL's latest ECH result without exposing its SSL object. |
523 | 0 | int TlsMockServer::ech_status() const { return context_ == nullptr ? -1 : context_->ech_status(); } |
524 | | |
525 | | /// Return the SNI that OpenSSL recovered from the encrypted ClientHello. |
526 | 0 | std::string TlsMockServer::ech_inner_name() const { |
527 | 0 | return context_ == nullptr ? std::string() : context_->ech_inner_name(); |
528 | 0 | } |
529 | | |
530 | | /// Return the public SNI that remained in the outer ClientHello. |
531 | 0 | std::string TlsMockServer::ech_outer_name() const { |
532 | 0 | return context_ == nullptr ? std::string() : context_->ech_outer_name(); |
533 | 0 | } |
534 | | |
535 | | /// Create the polymorphic record-layer connection consumed by MockServer. |
536 | 28.8k | std::unique_ptr<MockConnection> TlsMockServer::CreateConnection() { |
537 | 28.8k | return std::make_unique<TlsMockConnection>(context_.get()); |
538 | 28.8k | } |
539 | | |
540 | | /// Query while the connection filters are attached. Stop after the first live |
541 | | /// result so these coverage probes add a bounded handful of calls during the |
542 | | /// handshake rather than recurring throughout every HTTP parser iteration. |
543 | | /// @param easy Active easy handle whose connection filters remain attached. |
544 | 140k | void TlsMockServer::ObserveActiveTransfer(CURL* easy) { |
545 | 140k | if (saw_live_tls_session_) { |
546 | 47.6k | return; |
547 | 47.6k | } |
548 | 92.6k | long verify_result = 0; |
549 | 92.6k | curl_off_t appconnect_time = 0; |
550 | 92.6k | struct curl_certinfo* certificate_info = nullptr; |
551 | 92.6k | struct curl_tlssessioninfo* tls_session = nullptr; |
552 | 92.6k | (void)curl_easy_getinfo(easy, CURLINFO_SSL_VERIFYRESULT, &verify_result); |
553 | 92.6k | (void)curl_easy_getinfo(easy, CURLINFO_APPCONNECT_TIME_T, &appconnect_time); |
554 | 92.6k | (void)curl_easy_getinfo(easy, CURLINFO_CERTINFO, &certificate_info); |
555 | 92.6k | if (curl_easy_getinfo(easy, CURLINFO_TLS_SSL_PTR, &tls_session) == CURLE_OK && tls_session != nullptr && |
556 | 92.6k | tls_session->internals != nullptr) { |
557 | 16.6k | saw_live_tls_session_ = true; |
558 | 16.6k | } |
559 | 92.6k | } |
560 | | |
561 | | } // namespace proto_fuzzer |