src/openssl/src/openssl_stream.cpp

89.3% Lines (317/355) 100.0% List of functions (40/40)
openssl_stream.cpp
f(x) Functions (40)
Function Calls Lines Blocks
boost::corosio::(anonymous namespace)::tls_method_compat() :68 2040x 100.0% 100.0% boost::corosio::(anonymous namespace)::apply_hostname_verification(ssl_st*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) :78 2066x 100.0% 100.0% boost::corosio::(anonymous namespace)::openssl_proto_version(boost::corosio::tls_version) :95 4080x 100.0% 100.0% boost::corosio::(anonymous namespace)::build_alpn_wire(std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > const&) :104 4x 77.8% 83.0% boost::corosio::(anonymous namespace)::normalize_openssl_shutdown_read_error(std::error_code) :118 11x 85.7% 95.0% boost::corosio::(anonymous namespace)::openssl_category_impl::name() const :136 1x 100.0% 100.0% boost::corosio::(anonymous namespace)::openssl_category_impl::message(int) const :142 2x 100.0% 73.0% boost::corosio::(anonymous namespace)::make_openssl_error(unsigned long) :155 18x 60.0% 70.0% boost::corosio::openssl_category() :166 21x 100.0% 100.0% boost::corosio::detail::password_callback(char*, int, int, void*) :181 1x 84.6% 81.0% boost::corosio::detail::verify_callback_trampoline(int, x509_store_ctx_st*) :208 10x 82.6% 82.0% boost::corosio::detail::alpn_select_cb(ssl_st*, unsigned char const**, unsigned char*, unsigned char const*, unsigned int, void*) :267 2x 87.5% 89.0% boost::corosio::detail::sni_callback(ssl_st*, int*, void*) :301 3x 100.0% 94.0% boost::corosio::detail::openssl_native_context::openssl_native_context(boost::corosio::detail::tls_context_data const&) :342 2040x 91.1% 90.0% boost::corosio::detail::openssl_native_context::~openssl_native_context() :628 4080x 100.0% 100.0% boost::corosio::detail::get_openssl_native_context(boost::corosio::detail::tls_context_data const&) :636 3478x 100.0% 100.0% boost::corosio::detail::get_openssl_native_context(boost::corosio::detail::tls_context_data const&)::{lambda()#1}::operator()() const :639 2040x 100.0% 71.0% boost::corosio::detail::get_openssl_context(boost::corosio::detail::tls_context_data const&) :644 2046x 100.0% 100.0% boost::corosio::openssl_stream::impl::impl(boost::capy::any_stream&, boost::corosio::tls_context) :667 2046x 100.0% 59.0% boost::corosio::openssl_stream::impl::~impl() :673 2046x 100.0% 100.0% boost::corosio::openssl_stream::impl::reset() :681 20x 80.0% 79.0% boost::corosio::openssl_stream::impl::capture_alpn() :703 758x 100.0% 100.0% boost::corosio::openssl_stream::impl::flush_output() :712 78906x 100.0% 44.0% boost::corosio::openssl_stream::impl::read_input() :743 4201x 100.0% 44.0% boost::corosio::openssl_stream::impl::do_read_some(boost::capy::detail::buffer_array<16ul, false>) :764 73841x 100.0% 42.0% boost::corosio::openssl_stream::impl::do_write_some(boost::capy::detail::buffer_array<16ul, true>) :847 73919x 100.0% 42.0% boost::corosio::openssl_stream::impl::do_handshake(int) :907 1432x 100.0% 44.0% boost::corosio::openssl_stream::impl::do_shutdown() :985 39x 100.0% 44.0% boost::corosio::openssl_stream::impl::init_ssl() :1052 2046x 57.9% 68.0% boost::corosio::openssl_stream::make_impl(boost::capy::any_stream&, boost::corosio::tls_context const&) :1087 2046x 71.4% 52.0% boost::corosio::openssl_stream::~openssl_stream() :1101 2048x 100.0% 100.0% boost::corosio::openssl_stream::openssl_stream(boost::corosio::openssl_stream&&) :1106 2x 100.0% 100.0% boost::corosio::openssl_stream::operator=(boost::corosio::openssl_stream&&) :1116 1x 100.0% 100.0% boost::corosio::openssl_stream::do_read_some(boost::capy::detail::buffer_array<16ul, false>) :1131 73841x 100.0% 42.0% boost::corosio::openssl_stream::do_write_some(boost::capy::detail::buffer_array<16ul, true>) :1138 73919x 100.0% 42.0% boost::corosio::openssl_stream::handshake(boost::corosio::tls_stream::handshake_type) :1145 1432x 100.0% 44.0% boost::corosio::openssl_stream::shutdown() :1151 39x 100.0% 44.0% boost::corosio::openssl_stream::reset() :1157 16x 100.0% 100.0% boost::corosio::openssl_stream::name() const :1163 1x 100.0% 100.0% boost::corosio::openssl_stream::alpn_protocol() const :1169 3x 100.0% 100.0%
Line TLA Hits Source Code
1 //
2 // Copyright (c) 2025 Vinnie Falco ([email protected])
3 // Copyright (c) 2026 Michael Vandeberg
4 //
5 // Distributed under the Boost Software License, Version 1.0. (See accompanying
6 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7 //
8 // Official repository: https://github.com/cppalliance/corosio
9 //
10
11 #include <boost/corosio/openssl_stream.hpp>
12 #include <boost/corosio/detail/config.hpp>
13 #include <boost/capy/detail/buffer_array.hpp>
14 #include <boost/capy/ex/async_mutex.hpp>
15 #include <boost/capy/cond.hpp>
16 #include <boost/capy/error.hpp>
17 #include <boost/capy/write.hpp>
18
19 // Internal context implementation
20 #include "src/tls/detail/context_impl.hpp"
21
22 #include <openssl/ssl.h>
23 #include <openssl/err.h>
24 #include <openssl/bio.h>
25 #include <openssl/x509.h>
26 #include <openssl/pkcs12.h>
27
28 #include <algorithm>
29 #include <array>
30 #include <cstring>
31 #include <vector>
32
33 /*
34 openssl_stream Architecture
35 ===========================
36
37 TLS layer wrapping an underlying stream (via any_stream). Supports one
38 concurrent read_some and one concurrent write_some (like Asio's ssl::stream).
39
40 Data Flow (using BIO pairs)
41 ---------------------------
42 App -> SSL_write -> int_bio_ -> BIO_read(ext_bio_) -> out_buf_ -> s_->write_some -> Network
43 App <- SSL_read <- int_bio_ <- BIO_write(ext_bio_) <- in_buf_ <- s_->read_some <- Network
44
45 WANT_READ / WANT_WRITE Pattern
46 ------------------------------
47 OpenSSL's SSL_read/SSL_write return SSL_ERROR_WANT_READ or SSL_ERROR_WANT_WRITE
48 when they need I/O. Our coroutine handles this by:
49
50 1. Call SSL_read or SSL_write
51 2. Check for pending output in ext_bio_ via BIO_ctrl_pending
52 3. If output pending: write to network via s_->write_some
53 4. If SSL_ERROR_WANT_READ: read from network into ext_bio_ via s_->read_some + BIO_write
54 5. Loop back to step 1
55
56 Renegotiation causes cross-direction I/O: SSL_read may need to write
57 handshake data, SSL_write may need to read. Each operation handles
58 whatever I/O direction OpenSSL requests.
59 */
60
61 namespace boost::corosio {
62
63 namespace {
64
65 constexpr std::size_t default_buffer_size = 16384;
66
67 inline SSL_METHOD const*
68 2040x tls_method_compat() noexcept
69 {
70 #if OPENSSL_VERSION_NUMBER >= 0x10100000L
71 2040x return TLS_method();
72 #else
73 return SSLv23_method();
74 #endif
75 }
76
77 inline void
78 2066x apply_hostname_verification(SSL* ssl, std::string const& hostname)
79 {
80 2066x if (hostname.empty())
81 2062x return;
82
83 4x SSL_set_tlsext_host_name(ssl, hostname.c_str());
84
85 #if OPENSSL_VERSION_NUMBER >= 0x10100000L
86 4x SSL_set1_host(ssl, hostname.c_str());
87 #else
88 if (auto* param = SSL_get0_param(ssl))
89 X509_VERIFY_PARAM_set1_host(param, hostname.c_str(), 0);
90 #endif
91 }
92
93 // Map a portable protocol version to the OpenSSL version constant.
94 inline int
95 4080x openssl_proto_version(tls_version v) noexcept
96 {
97 4080x return v == tls_version::tls_1_3 ? TLS1_3_VERSION : TLS1_2_VERSION;
98 }
99
100 // Encode a protocol list into ALPN wire format: each entry is a
101 // one-byte length followed by that many bytes. Entries longer than 255
102 // bytes are skipped (invalid per RFC 7301).
103 inline std::string
104 4x build_alpn_wire(std::vector<std::string> const& protocols)
105 {
106 4x std::string wire;
107 10x for (auto const& p : protocols)
108 {
109 6x if (p.empty() || p.size() > 255)
110 continue;
111 6x wire.push_back(static_cast<char>(p.size()));
112 6x wire.append(p);
113 }
114 4x return wire;
115 }
116
117 inline std::error_code
118 11x normalize_openssl_shutdown_read_error(std::error_code ec) noexcept
119 {
120 11x if (!ec)
121 return ec;
122
123 // A peer that closed without a proper close_notify is a truncated stream.
124 // Cancellation is deliberately excluded: a stopped shutdown must surface as
125 // canceled, not stream_truncated.
126 17x if (ec == capy::cond::eof || ec == std::errc::connection_reset ||
127 17x ec == std::errc::connection_aborted || ec == std::errc::broken_pipe)
128 9x return make_error_code(capy::error::stream_truncated);
129
130 2x return ec;
131 }
132
133 class openssl_category_impl final : public std::error_category
134 {
135 char const*
136 1x name() const noexcept override
137 {
138 1x return "corosio.openssl";
139 }
140
141 std::string
142 2x message(int value) const override
143 {
144 char buf[256];
145 2x ::ERR_error_string_n(
146 static_cast<unsigned long>(value), buf, sizeof(buf));
147 4x return buf;
148 }
149 };
150
151 // Convert a packed OpenSSL error (from ERR_get_error) into an error_code.
152 // Codes from the ERR_LIB_SYS library carry a genuine errno reason and are
153 // reported with the system category; everything else uses openssl_category.
154 inline std::error_code
155 18x make_openssl_error(unsigned long err) noexcept
156 {
157 18x if (ERR_GET_LIB(err) == ERR_LIB_SYS)
158 return std::error_code(
159 static_cast<int>(ERR_GET_REASON(err)), std::system_category());
160 18x return std::error_code(static_cast<int>(err), openssl_category());
161 }
162
163 } // namespace
164
165 std::error_category const&
166 21x openssl_category() noexcept
167 {
168 21x static openssl_category_impl instance;
169 21x return instance;
170 }
171
172 //
173 // Native context caching
174 //
175
176 namespace detail {
177
178 static int sni_ctx_data_index = -1;
179
180 static int
181 1x password_callback(char* buf, int size, int rwflag, void* userdata)
182 {
183 1x auto* cd = static_cast<tls_context_data const*>(userdata);
184 1x if (!cd || !cd->password_callback)
185 return 0;
186
187 1x tls_password_purpose purpose = (rwflag == 0)
188 1x ? tls_password_purpose::for_reading
189 : tls_password_purpose::for_writing;
190
191 std::string password =
192 1x cd->password_callback(static_cast<std::size_t>(size), purpose);
193
194 1x int len = static_cast<int>(password.size());
195 1x if (len > size)
196 len = size;
197
198 1x std::memcpy(buf, password.data(), static_cast<std::size_t>(len));
199 1x return len;
200 1x }
201
202 // Trampoline installed via SSL_CTX_set_verify. Recovers the portable
203 // context data from the SSL_CTX ex_data (populated for every context)
204 // and applies, in order: the revocation policy's soft-fail downgrade,
205 // then the user's verify callback. Installed whenever a verify callback
206 // or a non-disabled revocation policy is configured.
207 static int
208 10x verify_callback_trampoline(int preverified, X509_STORE_CTX* store_ctx)
209 {
210 10x SSL* ssl = static_cast<SSL*>(X509_STORE_CTX_get_ex_data(
211 store_ctx, SSL_get_ex_data_X509_STORE_CTX_idx()));
212 10x if (!ssl)
213 return preverified;
214
215 auto* cd = static_cast<tls_context_data const*>(
216 10x SSL_CTX_get_ex_data(SSL_get_SSL_CTX(ssl), sni_ctx_data_index));
217 10x if (!cd)
218 return preverified;
219
220 10x bool ok = preverified != 0;
221
222 // Soft-fail revocation: accept certificates whose revocation status
223 // could not be determined (missing/expired CRL), but never downgrade
224 // an actual revocation. hard_fail leaves every CRL error fatal.
225 10x if (!ok && cd->revocation == tls_revocation_policy::soft_fail)
226 {
227 1x int const err = X509_STORE_CTX_get_error(store_ctx);
228 1x if (err == X509_V_ERR_UNABLE_TO_GET_CRL ||
229 err == X509_V_ERR_UNABLE_TO_GET_CRL_ISSUER ||
230 err == X509_V_ERR_CRL_HAS_EXPIRED ||
231 err == X509_V_ERR_CRL_NOT_YET_VALID)
232 1x ok = true;
233 }
234
235 10x if (cd->verify_callback)
236 {
237 // Expose the current certificate's DER so the callback can inspect
238 // it portably. i2d_X509 allocates; free it after the callback.
239 6x X509* cert = X509_STORE_CTX_get_current_cert(store_ctx);
240 6x unsigned char* der = nullptr;
241 6x int der_len = cert ? i2d_X509(cert, &der) : 0;
242
243 verify_context vc(
244 store_ctx, der,
245 6x der_len > 0 ? static_cast<std::size_t>(der_len) : 0);
246 6x ok = cd->verify_callback(ok, vc);
247
248 6x if (der)
249 6x OPENSSL_free(der);
250 }
251
252 10x return ok ? 1 : 0;
253 }
254
255 // Server-side ALPN selection. Chooses the server's most-preferred
256 // protocol that the client also offered. On no overlap it sends a fatal
257 // no_application_protocol alert (RFC 7301 §3.2).
258 //
259 // `arg` points at the native context's build-time snapshot of the server
260 // preference list (a std::vector<std::string>), so client offer and server
261 // selection are both taken from the same immutable snapshot.
262 //
263 // The selected protocol pointer must stay valid until the callback runs
264 // again, so we point *out into the client list `in` (OpenSSL keeps it
265 // valid for the connection) rather than into a local buffer.
266 static int
267 2x alpn_select_cb(
268 SSL* /* ssl */, unsigned char const** out, unsigned char* outlen,
269 unsigned char const* in, unsigned int inlen, void* arg)
270 {
271 2x auto const* prefs = static_cast<std::vector<std::string> const*>(arg);
272 2x if (!prefs || prefs->empty())
273 return SSL_TLSEXT_ERR_NOACK; // nothing configured (defensive)
274
275 // Server preference order wins: for each server protocol, look for a
276 // matching entry in the client's offered list.
277 3x for (auto const& pref : *prefs)
278 {
279 3x for (unsigned int i = 0; i + 1 <= inlen;)
280 {
281 2x unsigned int len = in[i];
282 2x if (i + 1 + len > inlen)
283 break; // malformed
284 3x if (len == pref.size() &&
285 1x std::memcmp(in + i + 1, pref.data(), len) == 0)
286 {
287 1x *out = in + i + 1;
288 1x *outlen = static_cast<unsigned char>(len);
289 1x return SSL_TLSEXT_ERR_OK;
290 }
291 1x i += 1 + len;
292 }
293 }
294
295 // The server supports ALPN but shares no protocol with the client.
296 // RFC 7301 §3.2: fail the handshake with a fatal alert.
297 1x return SSL_TLSEXT_ERR_ALERT_FATAL;
298 }
299
300 static int
301 3x sni_callback(SSL* ssl, int* /* alert */, void* /* arg */)
302 {
303 3x char const* servername = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
304 3x if (!servername)
305 1x return SSL_TLSEXT_ERR_NOACK;
306
307 2x SSL_CTX* ctx = SSL_get_SSL_CTX(ssl);
308 auto* cd = static_cast<tls_context_data const*>(
309 2x SSL_CTX_get_ex_data(ctx, sni_ctx_data_index));
310
311 2x if (cd && cd->servername_callback)
312 {
313 2x if (!cd->servername_callback(servername))
314 1x return SSL_TLSEXT_ERR_ALERT_FATAL;
315 }
316
317 1x return SSL_TLSEXT_ERR_OK;
318 }
319
320 class openssl_native_context : public native_context_base
321 {
322 public:
323 SSL_CTX* ctx_;
324 tls_context_data const* cd_;
325 // Set when a requested configuration could not be applied: an inverted
326 // protocol window (min > max), a cipher list / suite the library
327 // rejected, a protocol-version bound that would not set, or a CRL that
328 // parsed as neither PEM nor DER. Silently proceeding would negotiate an
329 // unexpected version, ignore the requested ciphers, or weaken revocation
330 // (fail-open under soft_fail), so do_handshake refuses the handshake.
331 bool setup_failed_ = false;
332 // ALPN offer in wire format (length-prefixed), encoded once from the
333 // immutable protocol list. The client sets it per-SSL each handshake;
334 // caching it here avoids re-encoding and re-allocating per connection.
335 std::string alpn_wire_;
336 // Server preference snapshot, captured at build time so the select
337 // callback matches against the same immutable list the client offers
338 // from (see alpn_select_cb). Its address is handed to OpenSSL as the
339 // callback arg, so it must outlive the SSL_CTX (it does — same object).
340 std::vector<std::string> alpn_snapshot_;
341
342 2040x explicit openssl_native_context(tls_context_data const& cd)
343 4080x : ctx_(nullptr)
344 2040x , cd_(&cd)
345 {
346 2040x ctx_ = SSL_CTX_new(tls_method_compat());
347 2040x if (!ctx_)
348 return;
349
350 2040x if (sni_ctx_data_index < 0)
351 2x sni_ctx_data_index =
352 2x SSL_CTX_get_ex_new_index(0, nullptr, nullptr, nullptr, nullptr);
353
354 2040x SSL_CTX_set_ex_data(
355 ctx_, sni_ctx_data_index, const_cast<tls_context_data*>(&cd));
356
357 2040x if (cd.servername_callback)
358 3x SSL_CTX_set_tlsext_servername_callback(ctx_, sni_callback);
359
360 // ALPN server-side selection. The callback only fires when this
361 // context is used as a server; the client offer (encoded once here)
362 // is set per-SSL from alpn_wire_. Snapshot the preference list so the
363 // callback and the client offer share one immutable source.
364 2040x if (!cd.alpn_protocols.empty())
365 {
366 4x alpn_snapshot_ = cd.alpn_protocols;
367 4x SSL_CTX_set_alpn_select_cb(ctx_, alpn_select_cb, &alpn_snapshot_);
368 4x alpn_wire_ = build_alpn_wire(cd.alpn_protocols);
369 }
370
371 2040x SSL_CTX_set_mode(ctx_, SSL_MODE_ENABLE_PARTIAL_WRITE);
372 2040x SSL_CTX_set_mode(ctx_, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
373 #if defined(SSL_MODE_RELEASE_BUFFERS)
374 2040x SSL_CTX_set_mode(ctx_, SSL_MODE_RELEASE_BUFFERS);
375 #endif
376
377 // Enforce the configured protocol version window (role-agnostic).
378 // An inverted window (min > max) admits no protocol; fail closed
379 // rather than silently negotiate an unexpected version.
380 2040x if (cd.min_version > cd.max_version)
381 1x setup_failed_ = true;
382 2040x if (!SSL_CTX_set_min_proto_version(
383 ctx_, openssl_proto_version(cd.min_version)))
384 setup_failed_ = true;
385 2040x if (!SSL_CTX_set_max_proto_version(
386 ctx_, openssl_proto_version(cd.max_version)))
387 setup_failed_ = true;
388
389 2040x int verify_mode_flag = SSL_VERIFY_NONE;
390 2040x if (cd.verification_mode == tls_verify_mode::peer)
391 1017x verify_mode_flag = SSL_VERIFY_PEER;
392 1023x else if (cd.verification_mode == tls_verify_mode::require_peer)
393 3x verify_mode_flag =
394 SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
395 // The trampoline runs the revocation soft-fail downgrade and the
396 // user callback, so install it if either is configured.
397 bool const need_trampoline =
398 4075x cd.verify_callback ||
399 4075x cd.revocation != tls_revocation_policy::disabled;
400 2040x SSL_CTX_set_verify(
401 ctx_, verify_mode_flag,
402 need_trampoline ? &verify_callback_trampoline : nullptr);
403
404 // PKCS#12 bundle: decode cert + key + chain directly into the
405 // context. This is an alternative credential source; the PEM/DER
406 // fields below are only consulted when no bundle is supplied.
407 2040x if (!cd.pkcs12_data.empty())
408 {
409 // A bundle that fails to decode or parse (wrong passphrase,
410 // malformed) must not leave the context silently credential-less:
411 // a client using PKCS#12 for mTLS would then fail open against a
412 // verify_mode::peer server. Fail closed like every other setup
413 // error.
414 5x BIO* bio = BIO_new_mem_buf(
415 5x cd.pkcs12_data.data(),
416 5x static_cast<int>(cd.pkcs12_data.size()));
417 5x if (!bio)
418 setup_failed_ = true;
419 else
420 {
421 5x PKCS12* p12 = d2i_PKCS12_bio(bio, nullptr);
422 5x if (!p12)
423 setup_failed_ = true;
424 else
425 {
426 5x EVP_PKEY* pkey = nullptr;
427 5x X509* cert = nullptr;
428 5x STACK_OF(X509)* chain = nullptr;
429 5x if (PKCS12_parse(
430 p12, cd.pkcs12_password.c_str(), &pkey, &cert,
431 &chain))
432 {
433 3x if (cert)
434 3x SSL_CTX_use_certificate(ctx_, cert);
435 3x if (pkey)
436 3x SSL_CTX_use_PrivateKey(ctx_, pkey);
437 3x if (chain)
438 2x for (int i = 0; i < sk_X509_num(chain); ++i)
439 {
440 // add_extra_chain_cert takes ownership of the
441 // dup only on success; free it (and fail
442 // closed) otherwise so a partial chain isn't
443 // sent silently.
444 1x X509* dup = X509_dup(sk_X509_value(chain, i));
445 2x if (!dup ||
446 1x !SSL_CTX_add_extra_chain_cert(ctx_, dup))
447 {
448 X509_free(dup);
449 setup_failed_ = true;
450 }
451 }
452 }
453 else
454 2x setup_failed_ = true;
455 5x EVP_PKEY_free(pkey);
456 5x X509_free(cert);
457 5x if (chain)
458 1x sk_X509_pop_free(chain, X509_free);
459 5x PKCS12_free(p12);
460 }
461 5x ERR_clear_error();
462 5x BIO_free(bio);
463 }
464 }
465
466 2040x if (cd.pkcs12_data.empty() && !cd.entity_certificate.empty())
467 {
468 1016x BIO* bio = BIO_new_mem_buf(
469 1016x cd.entity_certificate.data(),
470 1016x static_cast<int>(cd.entity_certificate.size()));
471 1016x if (bio)
472 {
473 1016x X509* cert = nullptr;
474 1016x if (cd.entity_cert_format == tls_file_format::pem)
475 1016x cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
476 else
477 cert = d2i_X509_bio(bio, nullptr);
478 1016x if (cert)
479 {
480 1015x SSL_CTX_use_certificate(ctx_, cert);
481 1015x X509_free(cert);
482 }
483 1016x BIO_free(bio);
484 }
485 }
486
487 2040x if (cd.pkcs12_data.empty() && !cd.certificate_chain.empty())
488 {
489 1x BIO* bio = BIO_new_mem_buf(
490 1x cd.certificate_chain.data(),
491 1x static_cast<int>(cd.certificate_chain.size()));
492 1x if (bio)
493 {
494 X509* entity =
495 1x PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
496 1x if (entity)
497 {
498 1x SSL_CTX_use_certificate(ctx_, entity);
499 1x X509_free(entity);
500 }
501
502 X509* cert;
503 2x while ((cert = PEM_read_bio_X509(
504 2x bio, nullptr, nullptr, nullptr)) != nullptr)
505 {
506 1x SSL_CTX_add_extra_chain_cert(ctx_, cert);
507 }
508 1x ERR_clear_error();
509 1x BIO_free(bio);
510 }
511 }
512
513 2040x if (cd.pkcs12_data.empty() && !cd.private_key.empty())
514 {
515 1017x BIO* bio = BIO_new_mem_buf(
516 1017x cd.private_key.data(), static_cast<int>(cd.private_key.size()));
517 1017x if (bio)
518 {
519 1017x EVP_PKEY* pkey = nullptr;
520 1017x if (cd.private_key_format == tls_file_format::pem)
521 {
522 1017x if (cd.password_callback)
523 1x pkey = PEM_read_bio_PrivateKey(
524 bio, nullptr, password_callback,
525 const_cast<tls_context_data*>(&cd));
526 else
527 1016x pkey = PEM_read_bio_PrivateKey(
528 bio, nullptr, nullptr, nullptr);
529 }
530 else
531 pkey = d2i_PrivateKey_bio(bio, nullptr);
532 1017x if (pkey)
533 {
534 1016x SSL_CTX_use_PrivateKey(ctx_, pkey);
535 1016x EVP_PKEY_free(pkey);
536 }
537 1017x BIO_free(bio);
538 }
539 }
540
541 2040x X509_STORE* store = SSL_CTX_get_cert_store(ctx_);
542 3069x for (auto const& ca : cd.ca_certificates)
543 {
544 1029x BIO* bio = BIO_new_mem_buf(ca.data(), static_cast<int>(ca.size()));
545 1029x if (bio)
546 {
547 1029x X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
548 1029x if (cert)
549 {
550 1026x X509_STORE_add_cert(store, cert);
551 1026x X509_free(cert);
552 }
553 1029x BIO_free(bio);
554 }
555 }
556
557 // Trust anchors from the system store and explicit directories.
558 // Failures leave the affected source unloaded rather than aborting
559 // context creation; the error queue is cleared so it does not leak
560 // into a later handshake.
561 2040x if (cd.use_default_verify_paths)
562 1x SSL_CTX_set_default_verify_paths(ctx_);
563 2041x for (auto const& path : cd.verify_paths)
564 1x SSL_CTX_load_verify_locations(ctx_, nullptr, path.c_str());
565 2040x ERR_clear_error();
566
567 // Certificate revocation via CRLs. Load any supplied CRLs and, when
568 // a revocation policy is active, enable leaf CRL checking. soft_fail
569 // vs hard_fail is applied in the verify trampoline. CRL_CHECK (leaf
570 // only) is used so a missing CRL for a trusted root is not itself an
571 // error.
572 2040x if (cd.revocation != tls_revocation_policy::disabled)
573 {
574 5x for (auto const& crl_data : cd.crls)
575 {
576 2x BIO* bio = BIO_new_mem_buf(
577 2x crl_data.data(), static_cast<int>(crl_data.size()));
578 2x if (!bio)
579 {
580 setup_failed_ = true;
581 continue;
582 }
583 // Accept PEM or DER (the documented contract). Try PEM first,
584 // then rewind and try DER.
585 X509_CRL* crl =
586 2x PEM_read_bio_X509_CRL(bio, nullptr, nullptr, nullptr);
587 2x if (!crl)
588 {
589 1x BIO_reset(bio);
590 1x crl = d2i_X509_CRL_bio(bio, nullptr);
591 }
592 2x if (crl)
593 {
594 1x X509_STORE_add_crl(store, crl);
595 1x X509_CRL_free(crl);
596 }
597 else
598 {
599 // A supplied CRL that parses as neither PEM nor DER must
600 // not be silently dropped; record it so the handshake
601 // fails closed rather than weakening revocation.
602 1x setup_failed_ = true;
603 }
604 2x BIO_free(bio);
605 }
606 3x X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK);
607 3x ERR_clear_error();
608 }
609
610 2040x SSL_CTX_set_verify_depth(ctx_, cd.verify_depth);
611
612 // Cipher configuration. TLS 1.2-and-below use the cipher list;
613 // TLS 1.3 uses the separate ciphersuites API. The security level
614 // is deliberately left at the library default: a weak cipher
615 // string should fail loudly rather than be silently permitted via
616 // a forced @SECLEVEL=0. Callers that genuinely need a lower level
617 // can express it in the cipher string (e.g. "...:@SECLEVEL=0").
618 // A cipher string the library rejects must not silently fall back to
619 // the default suites; fail closed instead.
620 2044x if (!cd.ciphersuites.empty() &&
621 4x !SSL_CTX_set_cipher_list(ctx_, cd.ciphersuites.c_str()))
622 setup_failed_ = true;
623 2045x if (!cd.ciphersuites_tls13.empty() &&
624 5x !SSL_CTX_set_ciphersuites(ctx_, cd.ciphersuites_tls13.c_str()))
625 1x setup_failed_ = true;
626 }
627
628 4080x ~openssl_native_context() override
629 2040x {
630 2040x if (ctx_)
631 2040x SSL_CTX_free(ctx_);
632 4080x }
633 };
634
635 inline openssl_native_context*
636 3478x get_openssl_native_context(tls_context_data const& cd)
637 {
638 static char key;
639 5518x auto* p = cd.find(&key, [&] { return new openssl_native_context(cd); });
640 3478x return static_cast<openssl_native_context*>(p);
641 }
642
643 SSL_CTX*
644 2046x get_openssl_context(tls_context_data const& cd)
645 {
646 2046x return get_openssl_native_context(cd)->ctx_;
647 }
648
649 } // namespace detail
650
651 struct openssl_stream::impl
652 {
653 capy::any_stream* s_;
654 tls_context ctx_;
655 SSL* ssl_ = nullptr;
656 BIO* ext_bio_ = nullptr;
657 bool used_ = false;
658
659 // ALPN protocol negotiated during the handshake (empty if none).
660 std::string alpn_selected_;
661
662 std::vector<char> in_buf_;
663 std::vector<char> out_buf_;
664
665 capy::async_mutex io_cm_;
666
667 2046x impl(capy::any_stream& s, tls_context ctx) : s_(&s), ctx_(std::move(ctx))
668 {
669 2046x in_buf_.resize(default_buffer_size);
670 2046x out_buf_.resize(default_buffer_size);
671 2046x }
672
673 2046x ~impl()
674 {
675 2046x if (ext_bio_)
676 2046x BIO_free(ext_bio_);
677 2046x if (ssl_)
678 2046x SSL_free(ssl_);
679 2046x }
680
681 20x void reset()
682 {
683 20x if (!ssl_)
684 return;
685
686 // Preserves SSL* and BIO pair, releases session state
687 20x SSL_clear(ssl_);
688
689 // Drain stale data from the external BIO
690 char drain[1024];
691 20x while (BIO_ctrl_pending(ext_bio_) > 0)
692 BIO_read(ext_bio_, drain, sizeof(drain));
693
694 // SSL_clear clears per-session settings; reapply hostname
695 20x auto& cd = detail::get_tls_context_data(ctx_);
696 20x apply_hostname_verification(ssl_, cd.hostname);
697
698 20x alpn_selected_.clear();
699 20x used_ = false;
700 }
701
702 // Record the ALPN protocol selected during the handshake, if any.
703 758x void capture_alpn()
704 {
705 758x unsigned char const* data = nullptr;
706 758x unsigned int len = 0;
707 758x SSL_get0_alpn_selected(ssl_, &data, &len);
708 758x if (data && len)
709 2x alpn_selected_.assign(reinterpret_cast<char const*>(data), len);
710 758x }
711
712 78906x capy::task<std::error_code> flush_output()
713 {
714 while (BIO_ctrl_pending(ext_bio_) > 0)
715 {
716 std::size_t got = 0;
717 while (BIO_ctrl_pending(ext_bio_) > 0 && got < out_buf_.size())
718 {
719 int put = static_cast<int>(BIO_ctrl_pending(ext_bio_));
720 put = (std::min)(put, static_cast<int>(out_buf_.size() - got));
721 int r = BIO_read(ext_bio_, out_buf_.data() + got, put);
722 if (r <= 0)
723 break;
724 got += static_cast<std::size_t>(r);
725 }
726 if (got == 0)
727 break;
728
729 {
730 auto [lec] = co_await io_cm_.lock();
731 if (lec)
732 co_return lec;
733 capy::async_mutex::lock_guard io_guard(&io_cm_);
734 auto [ec, n] = co_await capy::write(
735 *s_, capy::const_buffer(out_buf_.data(), got));
736 if (ec)
737 co_return ec;
738 }
739 }
740 co_return std::error_code{};
741 157812x }
742
743 4201x capy::task<std::error_code> read_input()
744 {
745 auto [lec] = co_await io_cm_.lock();
746 if (lec)
747 co_return lec;
748 capy::async_mutex::lock_guard io_guard(&io_cm_);
749 auto [ec, n] = co_await s_->read_some(
750 capy::mutable_buffer(in_buf_.data(), in_buf_.size()));
751 if (ec)
752 co_return ec;
753
754 int got = BIO_write(ext_bio_, in_buf_.data(), static_cast<int>(n));
755 if (got < static_cast<int>(n))
756 {
757 co_return make_error_code(std::errc::no_buffer_space);
758 }
759
760 co_return std::error_code{};
761 8402x }
762
763 capy::io_task<std::size_t>
764 73841x do_read_some(capy::detail::mutable_buffer_array<capy::detail::max_iovec_> buffers)
765 {
766 std::error_code ec;
767 std::size_t total_read = 0;
768
769 for (auto& buf : buffers)
770 {
771 char* dest = static_cast<char*>(buf.data());
772 int remaining = static_cast<int>(buf.size());
773
774 while (remaining > 0)
775 {
776 ERR_clear_error();
777 int ret = SSL_read(ssl_, dest, remaining);
778
779 if (ret > 0)
780 {
781 dest += ret;
782 remaining -= ret;
783 total_read += static_cast<std::size_t>(ret);
784
785 if (total_read > 0)
786 co_return {std::error_code{}, total_read};
787 }
788 else
789 {
790 int err = SSL_get_error(ssl_, ret);
791
792 if (err == SSL_ERROR_WANT_WRITE)
793 {
794 ec = co_await flush_output();
795 if (ec)
796 co_return {ec, total_read};
797 }
798 else if (err == SSL_ERROR_WANT_READ)
799 {
800 ec = co_await flush_output();
801 if (ec)
802 co_return {ec, total_read};
803
804 ec = co_await read_input();
805 if (ec)
806 {
807 if (ec == capy::cond::eof)
808 {
809 if (SSL_get_shutdown(ssl_) &
810 SSL_RECEIVED_SHUTDOWN)
811 ec = make_error_code(capy::error::eof);
812 else
813 ec = make_error_code(
814 capy::error::stream_truncated);
815 }
816 co_return {ec, total_read};
817 }
818 }
819 else if (err == SSL_ERROR_ZERO_RETURN)
820 {
821 co_return {
822 make_error_code(capy::error::eof), total_read};
823 }
824 else if (err == SSL_ERROR_SYSCALL)
825 {
826 unsigned long ssl_err = ERR_get_error();
827 if (ssl_err == 0)
828 ec = make_error_code(capy::error::stream_truncated);
829 else
830 ec = make_openssl_error(ssl_err);
831 co_return {ec, total_read};
832 }
833 else
834 {
835 unsigned long ssl_err = ERR_get_error();
836 ec = make_openssl_error(ssl_err);
837 co_return {ec, total_read};
838 }
839 }
840 }
841 }
842
843 co_return {std::error_code{}, total_read};
844 147682x }
845
846 capy::io_task<std::size_t>
847 73919x do_write_some(capy::detail::const_buffer_array<capy::detail::max_iovec_> buffers)
848 {
849 std::error_code ec;
850 std::size_t total_written = 0;
851
852 for (auto const& buf : buffers)
853 {
854 char const* src = static_cast<char const*>(buf.data());
855 int remaining = static_cast<int>(buf.size());
856
857 while (remaining > 0)
858 {
859 ERR_clear_error();
860 int ret = SSL_write(ssl_, src, remaining);
861
862 if (ret > 0)
863 {
864 src += ret;
865 remaining -= ret;
866 total_written += static_cast<std::size_t>(ret);
867
868 if (total_written > 0)
869 {
870 ec = co_await flush_output();
871 co_return {ec, total_written};
872 }
873 }
874 else
875 {
876 int err = SSL_get_error(ssl_, ret);
877
878 if (err == SSL_ERROR_WANT_WRITE)
879 {
880 ec = co_await flush_output();
881 if (ec)
882 co_return {ec, total_written};
883 }
884 else if (err == SSL_ERROR_WANT_READ)
885 {
886 ec = co_await flush_output();
887 if (ec)
888 co_return {ec, total_written};
889
890 ec = co_await read_input();
891 if (ec)
892 co_return {ec, total_written};
893 }
894 else
895 {
896 unsigned long ssl_err = ERR_get_error();
897 ec = make_openssl_error(ssl_err);
898 co_return {ec, total_written};
899 }
900 }
901 }
902 }
903
904 co_return {std::error_code{}, total_written};
905 147838x }
906
907 1432x capy::io_task<> do_handshake(int type)
908 {
909 // A requested configuration could not be applied when the native
910 // context was built (inverted protocol window, rejected cipher/
911 // version, or an unparseable CRL). Refuse the handshake rather than
912 // proceed with weakened or unexpected settings.
913 auto* nc = detail::get_openssl_native_context(
914 detail::get_tls_context_data(ctx_));
915 if (nc->setup_failed_)
916 co_return std::make_error_code(std::errc::invalid_argument);
917
918 if (used_)
919 reset();
920
921 std::error_code ec;
922
923 // Client offers its ALPN protocol list; the server selects via the
924 // context callback. Role is only known here, so set the pre-encoded
925 // wire offer per-SSL.
926 if (type == openssl_stream::client && !nc->alpn_wire_.empty())
927 {
928 // SSL_set_alpn_protos uses the inverted convention: 0 = success.
929 // A non-zero return (allocation failure) means the offer was not
930 // installed; fail closed rather than negotiate nothing silently.
931 if (SSL_set_alpn_protos(
932 ssl_,
933 reinterpret_cast<unsigned char const*>(
934 nc->alpn_wire_.data()),
935 static_cast<unsigned int>(nc->alpn_wire_.size())) != 0)
936 co_return std::make_error_code(std::errc::invalid_argument);
937 }
938
939 while (true)
940 {
941 ERR_clear_error();
942 int ret;
943 if (type == openssl_stream::client)
944 ret = SSL_connect(ssl_);
945 else
946 ret = SSL_accept(ssl_);
947
948 if (ret == 1)
949 {
950 used_ = true;
951 capture_alpn();
952 ec = co_await flush_output();
953 co_return {ec};
954 }
955 else
956 {
957 int err = SSL_get_error(ssl_, ret);
958
959 if (err == SSL_ERROR_WANT_WRITE)
960 {
961 ec = co_await flush_output();
962 if (ec)
963 co_return {ec};
964 }
965 else if (err == SSL_ERROR_WANT_READ)
966 {
967 ec = co_await flush_output();
968 if (ec)
969 co_return {ec};
970
971 ec = co_await read_input();
972 if (ec)
973 co_return {ec};
974 }
975 else
976 {
977 unsigned long ssl_err = ERR_get_error();
978 ec = make_openssl_error(ssl_err);
979 co_return {ec};
980 }
981 }
982 }
983 2864x }
984
985 39x capy::io_task<> do_shutdown()
986 {
987 std::error_code ec;
988
989 while (true)
990 {
991 ERR_clear_error();
992 int ret = SSL_shutdown(ssl_);
993
994 if (ret == 1)
995 {
996 ec = co_await flush_output();
997 co_return {ec};
998 }
999 else if (ret == 0)
1000 {
1001 ec = co_await flush_output();
1002 if (ec)
1003 co_return {ec};
1004
1005 ec = co_await read_input();
1006 if (ec)
1007 {
1008 ec = normalize_openssl_shutdown_read_error(ec);
1009 co_return {ec};
1010 }
1011 }
1012 else
1013 {
1014 int err = SSL_get_error(ssl_, ret);
1015
1016 if (err == SSL_ERROR_WANT_WRITE)
1017 {
1018 ec = co_await flush_output();
1019 if (ec)
1020 co_return {ec};
1021 }
1022 else if (err == SSL_ERROR_WANT_READ)
1023 {
1024 ec = co_await flush_output();
1025 if (ec)
1026 co_return {ec};
1027
1028 ec = co_await read_input();
1029 if (ec)
1030 {
1031 ec = normalize_openssl_shutdown_read_error(ec);
1032 co_return {ec};
1033 }
1034 }
1035 else
1036 {
1037 unsigned long ssl_err = ERR_get_error();
1038 if (ssl_err == 0 && err == SSL_ERROR_SYSCALL)
1039 {
1040 ec = {};
1041 }
1042 else
1043 {
1044 ec = make_openssl_error(ssl_err);
1045 }
1046 co_return {ec};
1047 }
1048 }
1049 }
1050 78x }
1051
1052 2046x std::error_code init_ssl()
1053 {
1054 2046x auto& cd = detail::get_tls_context_data(ctx_);
1055 2046x SSL_CTX* native_ctx = detail::get_openssl_context(cd);
1056 2046x if (!native_ctx)
1057 {
1058 unsigned long err = ERR_get_error();
1059 return make_openssl_error(err);
1060 }
1061
1062 2046x ssl_ = SSL_new(native_ctx);
1063 2046x if (!ssl_)
1064 {
1065 unsigned long err = ERR_get_error();
1066 return make_openssl_error(err);
1067 }
1068
1069 2046x BIO* int_bio = nullptr;
1070 2046x if (!BIO_new_bio_pair(&int_bio, 0, &ext_bio_, 0))
1071 {
1072 unsigned long err = ERR_get_error();
1073 SSL_free(ssl_);
1074 ssl_ = nullptr;
1075 return make_openssl_error(err);
1076 }
1077
1078 2046x SSL_set_bio(ssl_, int_bio, int_bio);
1079
1080 2046x apply_hostname_verification(ssl_, cd.hostname);
1081
1082 2046x return {};
1083 }
1084 };
1085
1086 openssl_stream::impl*
1087 2046x openssl_stream::make_impl(capy::any_stream& stream, tls_context const& ctx)
1088 {
1089 2046x auto* p = new impl(stream, ctx);
1090
1091 2046x auto ec = p->init_ssl();
1092 2046x if (ec)
1093 {
1094 delete p;
1095 return nullptr;
1096 }
1097
1098 2046x return p;
1099 }
1100
1101 2048x openssl_stream::~openssl_stream()
1102 {
1103 2048x delete impl_;
1104 2048x }
1105
1106 2x openssl_stream::openssl_stream(openssl_stream&& other) noexcept
1107 2x : stream_(std::move(other.stream_))
1108 2x , impl_(other.impl_)
1109 {
1110 2x other.impl_ = nullptr;
1111 2x if (impl_)
1112 2x impl_->s_ = &stream_;
1113 2x }
1114
1115 openssl_stream&
1116 1x openssl_stream::operator=(openssl_stream&& other) noexcept
1117 {
1118 1x if (this != &other)
1119 {
1120 1x delete impl_;
1121 1x stream_ = std::move(other.stream_);
1122 1x impl_ = other.impl_;
1123 1x other.impl_ = nullptr;
1124 1x if (impl_)
1125 1x impl_->s_ = &stream_;
1126 }
1127 1x return *this;
1128 }
1129
1130 capy::io_task<std::size_t>
1131 73841x openssl_stream::do_read_some(
1132 capy::detail::mutable_buffer_array<capy::detail::max_iovec_> buffers)
1133 {
1134 co_return co_await impl_->do_read_some(buffers);
1135 147682x }
1136
1137 capy::io_task<std::size_t>
1138 73919x openssl_stream::do_write_some(
1139 capy::detail::const_buffer_array<capy::detail::max_iovec_> buffers)
1140 {
1141 co_return co_await impl_->do_write_some(buffers);
1142 147838x }
1143
1144 capy::io_task<>
1145 1432x openssl_stream::handshake(handshake_type type)
1146 {
1147 co_return co_await impl_->do_handshake(type);
1148 2864x }
1149
1150 capy::io_task<>
1151 39x openssl_stream::shutdown()
1152 {
1153 co_return co_await impl_->do_shutdown();
1154 78x }
1155
1156 void
1157 16x openssl_stream::reset()
1158 {
1159 16x impl_->reset();
1160 16x }
1161
1162 std::string_view
1163 1x openssl_stream::name() const noexcept
1164 {
1165 1x return "openssl";
1166 }
1167
1168 std::string_view
1169 3x openssl_stream::alpn_protocol() const noexcept
1170 {
1171 3x return impl_->alpn_selected_;
1172 }
1173
1174 } // namespace boost::corosio
1175