/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/err.h> |
13 | | #include <openssl/pem.h> |
14 | | #include <openssl/ssl.h> |
15 | | #include <sys/socket.h> |
16 | | |
17 | | #include <cstddef> |
18 | | #include <string> |
19 | | |
20 | | #include "proto_fuzzer/tls_test_credentials.h" |
21 | | |
22 | | namespace proto_fuzzer { |
23 | | |
24 | | namespace { |
25 | | |
26 | | /// Isolate the server and curl client even though both OpenSSL instances run |
27 | | /// on one thread. SSL_get_error requires an empty queue before its I/O call; |
28 | | /// clearing at both boundaries also prevents a server failure from changing |
29 | | /// curl's subsequent error classification. |
30 | | class OpenSslErrorQueueGuard { |
31 | | public: |
32 | 26.2k | OpenSslErrorQueueGuard() { ERR_clear_error(); } |
33 | | |
34 | 26.2k | ~OpenSslErrorQueueGuard() { ERR_clear_error(); } |
35 | | |
36 | | OpenSslErrorQueueGuard(const OpenSslErrorQueueGuard&) = delete; |
37 | | OpenSslErrorQueueGuard& operator=(const OpenSslErrorQueueGuard&) = delete; |
38 | | }; |
39 | | |
40 | | /// Select HTTP/1.1 when curl offers it. Keeping the server preference fixed |
41 | | /// lets the existing HTTP corpus reach successful application traffic; seeds |
42 | | /// that force HTTP/2 still exercise negotiation and failure handling. |
43 | | int SelectAlpn(SSL* /*ssl*/, const unsigned char** selected, unsigned char* selected_length, |
44 | 3.62k | const unsigned char* client_protocols, unsigned int client_protocols_length, void* /*userdata*/) { |
45 | | // OpenSSL may return a pointer into the server preference list. Function- |
46 | | // local static storage keeps that result valid for the rest of the |
47 | | // handshake without reintroducing transport policy as a namespace global. |
48 | 3.62k | static constexpr unsigned char http11_alpn[] = {8, 'h', 't', 't', 'p', '/', '1', '.', '1'}; |
49 | 3.62k | unsigned char* match = nullptr; |
50 | 3.62k | unsigned char match_length = 0; |
51 | 3.62k | const int result = SSL_select_next_proto(&match, &match_length, http11_alpn, sizeof(http11_alpn), client_protocols, |
52 | 3.62k | client_protocols_length); |
53 | 3.62k | if (result != OPENSSL_NPN_NEGOTIATED) { |
54 | 2 | return SSL_TLSEXT_ERR_NOACK; |
55 | 2 | } |
56 | 3.62k | *selected = match; |
57 | 3.62k | *selected_length = match_length; |
58 | 3.62k | return SSL_TLSEXT_ERR_OK; |
59 | 3.62k | } |
60 | | |
61 | | } // namespace |
62 | | |
63 | | /// Own the certificate, key, session cache, ALPN policy, and observations |
64 | | /// shared by every bounded connection in one Scenario. Keeping both session |
65 | | /// state and its measurements here makes redirects related while preventing |
66 | | /// one fuzz input from affecting the next. |
67 | | class TlsServerContext { |
68 | | public: |
69 | | TlsServerContext() |
70 | 3.73k | : context_(nullptr), |
71 | 3.73k | negotiated_tls_version_(0), |
72 | 3.73k | completed_handshake_count_(0), |
73 | 3.73k | reused_session_count_(0), |
74 | 3.73k | write_retry_count_(0) { |
75 | 3.73k | OpenSslErrorQueueGuard error_guard; |
76 | 3.73k | context_ = SSL_CTX_new(TLS_server_method()); |
77 | 3.73k | if (context_ == nullptr || !LoadCredentials()) { |
78 | 0 | SSL_CTX_free(context_); |
79 | 0 | context_ = nullptr; |
80 | 0 | return; |
81 | 0 | } |
82 | | |
83 | 3.73k | (void)SSL_CTX_set_min_proto_version(context_, TLS1_2_VERSION); |
84 | 3.73k | (void)SSL_CTX_set_options(context_, SSL_OP_NO_COMPRESSION); |
85 | 3.73k | (void)SSL_CTX_set_session_cache_mode(context_, SSL_SESS_CACHE_SERVER); |
86 | 3.73k | constexpr unsigned char session_id_context[] = "curl-fuzzer"; |
87 | 3.73k | (void)SSL_CTX_set_session_id_context(context_, session_id_context, sizeof(session_id_context) - 1); |
88 | 3.73k | SSL_CTX_set_alpn_select_cb(context_, &SelectAlpn, nullptr); |
89 | 3.73k | } |
90 | | |
91 | 3.73k | ~TlsServerContext() { |
92 | 3.73k | OpenSslErrorQueueGuard error_guard; |
93 | 3.73k | SSL_CTX_free(context_); |
94 | 3.73k | } |
95 | | |
96 | | TlsServerContext(const TlsServerContext&) = delete; |
97 | | TlsServerContext& operator=(const TlsServerContext&) = delete; |
98 | | |
99 | | /// @return the configured context, or nullptr when credential setup failed. |
100 | 3.62k | SSL_CTX* get() const { return context_; } |
101 | | |
102 | | /// Retain only scalar handshake results; SSL itself remains connection-owned. |
103 | | /// @param ssl connection that has just completed SSL_accept. |
104 | 3.58k | void RecordHandshake(SSL* ssl) { |
105 | 3.58k | if (ssl == nullptr) { |
106 | 0 | return; |
107 | 0 | } |
108 | 3.58k | negotiated_tls_version_ = SSL_version(ssl); |
109 | 3.58k | ++completed_handshake_count_; |
110 | 3.58k | if (SSL_session_reused(ssl) == 1) { |
111 | 0 | ++reused_session_count_; |
112 | 0 | } |
113 | 3.58k | } |
114 | | |
115 | | /// Record that OpenSSL requires an identical application-write retry. |
116 | 0 | void RecordWriteRetry() { ++write_retry_count_; } |
117 | | |
118 | | /// @return protocol version from the most recent completed handshake. |
119 | 0 | int negotiated_tls_version() const { return negotiated_tls_version_; } |
120 | | /// @return number of connections that completed their handshake. |
121 | 0 | std::size_t completed_handshake_count() const { return completed_handshake_count_; } |
122 | | /// @return number of completed handshakes that reused a session. |
123 | 0 | std::size_t reused_session_count() const { return reused_session_count_; } |
124 | | /// @return number of application writes that OpenSSL asked to retry. |
125 | 0 | std::size_t write_retry_count() const { return write_retry_count_; } |
126 | | |
127 | | private: |
128 | | /// Parse the checked-in test-only PEM values entirely in memory. |
129 | 3.73k | bool LoadCredentials() { |
130 | 3.73k | BIO* certificate_bio = BIO_new_mem_buf(tls_test_credentials::kCertificatePem, -1); |
131 | 3.73k | BIO* key_bio = BIO_new_mem_buf(tls_test_credentials::kPrivateKeyPem, -1); |
132 | 3.73k | if (certificate_bio == nullptr || key_bio == nullptr) { |
133 | 0 | BIO_free(certificate_bio); |
134 | 0 | BIO_free(key_bio); |
135 | 0 | return false; |
136 | 0 | } |
137 | | |
138 | 3.73k | X509* certificate = PEM_read_bio_X509(certificate_bio, nullptr, nullptr, nullptr); |
139 | 3.73k | EVP_PKEY* key = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr, nullptr); |
140 | 3.73k | BIO_free(certificate_bio); |
141 | 3.73k | BIO_free(key_bio); |
142 | 3.73k | if (certificate == nullptr || key == nullptr) { |
143 | 0 | X509_free(certificate); |
144 | 0 | EVP_PKEY_free(key); |
145 | 0 | return false; |
146 | 0 | } |
147 | | |
148 | 3.73k | const bool loaded = SSL_CTX_use_certificate(context_, certificate) == 1 && |
149 | 3.73k | SSL_CTX_use_PrivateKey(context_, key) == 1 && SSL_CTX_check_private_key(context_) == 1; |
150 | 3.73k | X509_free(certificate); |
151 | 3.73k | EVP_PKEY_free(key); |
152 | 3.73k | return loaded; |
153 | 3.73k | } |
154 | | |
155 | | SSL_CTX* context_; |
156 | | int negotiated_tls_version_; |
157 | | std::size_t completed_handshake_count_; |
158 | | std::size_t reused_session_count_; |
159 | | std::size_t write_retry_count_; |
160 | | }; |
161 | | |
162 | | namespace { |
163 | | |
164 | | /// TLS-aware MockConnection. Application writes are queued before the client |
165 | | /// starts, then encrypted only after SSL_accept has completed. This preserves |
166 | | /// MockServer's useful initial_response semantics without ever putting those |
167 | | /// plaintext bytes onto the TLS wire. |
168 | | class TlsMockConnection final : public MockConnection { |
169 | | public: |
170 | | explicit TlsMockConnection(TlsServerContext* context) |
171 | 3.62k | : ssl_(nullptr), |
172 | 3.62k | context_(context), |
173 | 3.62k | pending_offset_(0), |
174 | 3.62k | pending_write_end_(0), |
175 | 3.62k | handshake_complete_(false), |
176 | 3.62k | shutdown_requested_(false), |
177 | 3.62k | write_shutdown_(false), |
178 | 3.62k | failed_(false) { |
179 | 3.62k | OpenSslErrorQueueGuard error_guard; |
180 | 3.62k | SSL_CTX* ssl_context = context_ == nullptr ? nullptr : context_->get(); |
181 | 3.62k | ssl_ = ssl_context == nullptr ? nullptr : SSL_new(ssl_context); |
182 | 3.62k | if (ssl_ != nullptr) { |
183 | 3.62k | SSL_set_mode(ssl_, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); |
184 | 3.62k | if (SSL_set_fd(ssl_, server_fd()) != 1) { |
185 | 0 | SSL_free(ssl_); |
186 | 0 | ssl_ = nullptr; |
187 | 3.62k | } else { |
188 | 3.62k | SSL_set_accept_state(ssl_); |
189 | 3.62k | } |
190 | 3.62k | } |
191 | 3.62k | } |
192 | | |
193 | 3.62k | ~TlsMockConnection() override { |
194 | 3.62k | OpenSslErrorQueueGuard error_guard; |
195 | 3.62k | if (ssl_ != nullptr) { |
196 | 3.62k | SSL_free(ssl_); |
197 | 3.62k | } |
198 | 3.62k | } |
199 | | |
200 | | /// Require both the socketpair and its OpenSSL wrapper to be usable. |
201 | 15.1k | bool ok() const override { return MockConnection::ok() && ssl_ != nullptr; } |
202 | | |
203 | | /// Queue bounded application bytes until the handshake can encrypt them. |
204 | 1.05k | bool WriteAll(const unsigned char* data, std::size_t size) override { |
205 | 1.05k | if (failed_ || (data == nullptr && size != 0) || size > pending_plaintext_.max_size() - pending_plaintext_.size()) { |
206 | 0 | return false; |
207 | 0 | } |
208 | 1.05k | if (size != 0) { |
209 | 1.05k | pending_plaintext_.append(reinterpret_cast<const char*>(data), size); |
210 | 1.05k | } |
211 | 1.05k | return true; |
212 | 1.05k | } |
213 | | |
214 | | /// Advance the handshake, consume encrypted request records, flush queued |
215 | | /// response records, and finish a requested close without ever waiting. |
216 | | /// @return decrypted/application bytes plus state transitions made this turn. |
217 | 11.5k | std::size_t DrainIncoming() override { |
218 | 11.5k | if (!ok() || failed_) { |
219 | 0 | return 0; |
220 | 0 | } |
221 | | |
222 | 11.5k | OpenSslErrorQueueGuard error_guard; |
223 | 11.5k | std::size_t progress = 0; |
224 | 11.5k | if (!handshake_complete_) { |
225 | 11.1k | const int state_before = static_cast<int>(SSL_get_state(ssl_)); |
226 | 11.1k | const int result = SSL_accept(ssl_); |
227 | | // SSL_get_error must be the next OpenSSL call after failed I/O. Even a |
228 | | // state query in between would make its use formally unreliable. |
229 | 11.1k | const int error = result == 1 ? SSL_ERROR_NONE : SSL_get_error(ssl_, result); |
230 | 11.1k | const int state_after = static_cast<int>(SSL_get_state(ssl_)); |
231 | 11.1k | if (state_after != state_before) { |
232 | 7.21k | ++progress; |
233 | 7.21k | } |
234 | 11.1k | if (result == 1) { |
235 | 3.58k | handshake_complete_ = true; |
236 | 3.58k | if (context_ != nullptr) { |
237 | 3.58k | context_->RecordHandshake(ssl_); |
238 | 3.58k | } |
239 | 3.58k | ++progress; |
240 | 7.53k | } else { |
241 | 7.53k | if (error == SSL_ERROR_WANT_READ || error == SSL_ERROR_WANT_WRITE) { |
242 | 7.53k | return progress; |
243 | 7.53k | } |
244 | 0 | failed_ = true; |
245 | 0 | return progress; |
246 | 7.53k | } |
247 | 11.1k | } |
248 | | |
249 | | // A WANT result leaves SSL_write_ex in flight. OpenSSL requires the next |
250 | | // I/O operation to retry that exact write, so do not interpose SSL_read_ex |
251 | | // merely because the outer driver has yielded back to us. |
252 | 4.03k | if (pending_write_end_ != 0) { |
253 | 0 | progress += FlushPendingWrites(); |
254 | 0 | if (failed_ || pending_write_end_ != 0) { |
255 | 0 | return progress; |
256 | 0 | } |
257 | 0 | } |
258 | | |
259 | 4.03k | unsigned char request[4096]; |
260 | 7.75k | while (true) { |
261 | 7.75k | std::size_t received = 0; |
262 | 7.75k | const int result = SSL_read_ex(ssl_, request, sizeof(request), &received); |
263 | 7.75k | if (result == 1 && received != 0) { |
264 | 3.72k | progress += received; |
265 | 3.72k | continue; |
266 | 3.72k | } |
267 | 4.03k | if (result != 1) { |
268 | 4.03k | const int error = SSL_get_error(ssl_, result); |
269 | 4.03k | if (error != SSL_ERROR_WANT_READ && error != SSL_ERROR_WANT_WRITE && error != SSL_ERROR_ZERO_RETURN) { |
270 | 0 | failed_ = true; |
271 | 0 | } |
272 | 4.03k | } |
273 | 4.03k | break; |
274 | 7.75k | } |
275 | | |
276 | 4.03k | progress += FlushPendingWrites(); |
277 | | |
278 | 4.03k | if (!failed_ && shutdown_requested_ && pending_plaintext_.empty() && !write_shutdown_) { |
279 | 3.45k | const int result = SSL_shutdown(ssl_); |
280 | 3.45k | if (result >= 0) { |
281 | | // The first successful call sends close_notify. The fuzzer does not |
282 | | // need to wait for the client's reciprocal alert before exposing EOF. |
283 | 3.45k | (void)::shutdown(server_fd(), SHUT_WR); |
284 | 3.45k | write_shutdown_ = true; |
285 | 3.45k | ++progress; |
286 | 3.45k | } else { |
287 | 0 | const int error = SSL_get_error(ssl_, result); |
288 | 0 | if (error != SSL_ERROR_WANT_READ && error != SSL_ERROR_WANT_WRITE) { |
289 | 0 | failed_ = true; |
290 | 0 | } |
291 | 0 | } |
292 | 3.45k | } |
293 | 4.03k | return progress; |
294 | 4.03k | } |
295 | | |
296 | | /// Defer close_notify until every queued plaintext byte has become a record. |
297 | 3.50k | void ShutdownWrite() override { |
298 | 3.50k | shutdown_requested_ = true; |
299 | 3.50k | (void)DrainIncoming(); |
300 | 3.50k | } |
301 | | |
302 | | private: |
303 | | /// Flush queued response bytes while preserving any OpenSSL retry boundary. |
304 | | /// @return plaintext bytes OpenSSL accepted during this call. |
305 | 4.03k | std::size_t FlushPendingWrites() { |
306 | 4.03k | std::size_t progress = 0; |
307 | 4.75k | while (!failed_ && pending_offset_ < pending_plaintext_.size()) { |
308 | 720 | if (pending_write_end_ == 0) { |
309 | | // Freeze one write boundary until OpenSSL accepts it. WriteAll may |
310 | | // append the next scripted chunk after WANT_READ/WANT_WRITE, but the |
311 | | // retry contract permits only pointer relocation—not changed bytes or |
312 | | // length—for the outstanding SSL_write_ex call. |
313 | 720 | pending_write_end_ = pending_plaintext_.size(); |
314 | 720 | } |
315 | 720 | std::size_t written = 0; |
316 | 720 | const int result = SSL_write_ex(ssl_, pending_plaintext_.data() + pending_offset_, |
317 | 720 | pending_write_end_ - pending_offset_, &written); |
318 | 720 | if (result == 1) { |
319 | 720 | if (written == 0) { |
320 | 0 | failed_ = true; |
321 | 0 | break; |
322 | 0 | } |
323 | 720 | pending_offset_ += written; |
324 | 720 | progress += written; |
325 | 720 | if (pending_offset_ == pending_write_end_) { |
326 | 720 | pending_write_end_ = 0; |
327 | 720 | } |
328 | 720 | continue; |
329 | 720 | } |
330 | 0 | const int error = SSL_get_error(ssl_, result); |
331 | 0 | if (error == SSL_ERROR_WANT_READ || error == SSL_ERROR_WANT_WRITE) { |
332 | 0 | if (context_ != nullptr) { |
333 | 0 | context_->RecordWriteRetry(); |
334 | 0 | } |
335 | 0 | } else { |
336 | 0 | failed_ = true; |
337 | 0 | } |
338 | 0 | break; |
339 | 720 | } |
340 | 4.03k | if (pending_offset_ == pending_plaintext_.size()) { |
341 | 4.03k | pending_plaintext_.clear(); |
342 | 4.03k | pending_offset_ = 0; |
343 | 4.03k | pending_write_end_ = 0; |
344 | 4.03k | } |
345 | 4.03k | return progress; |
346 | 4.03k | } |
347 | | |
348 | | SSL* ssl_; |
349 | | TlsServerContext* context_; |
350 | | std::string pending_plaintext_; |
351 | | std::size_t pending_offset_; |
352 | | /// Exclusive end of a write that OpenSSL may require us to retry exactly. |
353 | | std::size_t pending_write_end_; |
354 | | bool handshake_complete_; |
355 | | bool shutdown_requested_; |
356 | | bool write_shutdown_; |
357 | | bool failed_; |
358 | | }; |
359 | | |
360 | | } // namespace |
361 | | |
362 | | /// Build one reusable in-process server context per fuzz iteration. |
363 | 3.73k | TlsMockServer::TlsMockServer() : context_(std::make_unique<TlsServerContext>()), saw_live_tls_session_(false) {} |
364 | | |
365 | | /// Release SSL objects before their owning server context. OpenSSL reference |
366 | | /// counting makes the reverse order legal, but making the ownership order |
367 | | /// explicit keeps future transport state from acquiring a hidden dependency. |
368 | 3.73k | TlsMockServer::~TlsMockServer() { ResetConnections(); } |
369 | | |
370 | | /// Add verification to the common socket callbacks. Curl copies the blob |
371 | | /// descriptor during setopt and borrows the inline certificate bytes, whose |
372 | | /// program lifetime safely exceeds every easy handle. |
373 | 3.73k | void TlsMockServer::Install(CURL* easy) { |
374 | 3.73k | MockServer::Install(easy); |
375 | 3.73k | struct curl_blob trust_anchor = {const_cast<char*>(tls_test_credentials::kCertificatePem), |
376 | 3.73k | sizeof(tls_test_credentials::kCertificatePem) - 1, CURL_BLOB_NOCOPY}; |
377 | 3.73k | (void)curl_easy_setopt(easy, CURLOPT_CAINFO_BLOB, &trust_anchor); |
378 | 3.73k | (void)curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L); |
379 | 3.73k | (void)curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L); |
380 | 3.73k | } |
381 | | |
382 | | /// Report whether the drive reached curl's live TLS backend-query path. |
383 | 0 | bool TlsMockServer::saw_live_tls_session() const { return saw_live_tls_session_; } |
384 | | |
385 | | /// Report the protocol selected by the latest successful server handshake. |
386 | 0 | int TlsMockServer::negotiated_tls_version() const { |
387 | 0 | return context_ == nullptr ? 0 : context_->negotiated_tls_version(); |
388 | 0 | } |
389 | | |
390 | | /// Report how many bounded connections finished their TLS handshake. |
391 | 0 | std::size_t TlsMockServer::completed_handshake_count() const { |
392 | 0 | return context_ == nullptr ? 0 : context_->completed_handshake_count(); |
393 | 0 | } |
394 | | |
395 | | /// Report how many later connections resumed state from this scenario. |
396 | 0 | std::size_t TlsMockServer::reused_session_count() const { |
397 | 0 | return context_ == nullptr ? 0 : context_->reused_session_count(); |
398 | 0 | } |
399 | | |
400 | | /// Report how often response delivery reached OpenSSL's exact-retry path. |
401 | 0 | std::size_t TlsMockServer::write_retry_count() const { return context_ == nullptr ? 0 : context_->write_retry_count(); } |
402 | | |
403 | | /// Create the polymorphic record-layer connection consumed by MockServer. |
404 | 3.62k | std::unique_ptr<MockConnection> TlsMockServer::CreateConnection() { |
405 | 3.62k | return std::make_unique<TlsMockConnection>(context_.get()); |
406 | 3.62k | } |
407 | | |
408 | | /// Query while the connection filters are attached. Stop after the first live |
409 | | /// result so these coverage probes add a bounded handful of calls during the |
410 | | /// handshake rather than recurring throughout every HTTP parser iteration. |
411 | | /// @param easy Active easy handle whose connection filters remain attached. |
412 | 10.9k | void TlsMockServer::ObserveActiveTransfer(CURL* easy) { |
413 | 10.9k | if (saw_live_tls_session_) { |
414 | 3.63k | return; |
415 | 3.63k | } |
416 | 7.36k | long verify_result = 0; |
417 | 7.36k | curl_off_t appconnect_time = 0; |
418 | 7.36k | struct curl_certinfo* certificate_info = nullptr; |
419 | 7.36k | struct curl_tlssessioninfo* tls_session = nullptr; |
420 | 7.36k | (void)curl_easy_getinfo(easy, CURLINFO_SSL_VERIFYRESULT, &verify_result); |
421 | 7.36k | (void)curl_easy_getinfo(easy, CURLINFO_APPCONNECT_TIME_T, &appconnect_time); |
422 | 7.36k | (void)curl_easy_getinfo(easy, CURLINFO_CERTINFO, &certificate_info); |
423 | 7.36k | if (curl_easy_getinfo(easy, CURLINFO_TLS_SSL_PTR, &tls_session) == CURLE_OK && tls_session != nullptr && |
424 | 7.36k | tls_session->internals != nullptr) { |
425 | 3.58k | saw_live_tls_session_ = true; |
426 | 3.58k | } |
427 | 7.36k | } |
428 | | |
429 | | } // namespace proto_fuzzer |