src/openssl/src/openssl_stream.cpp

89.5% Lines (317/354) 100.0% List of functions (40/40) 78.3% Branches (281/359)
openssl_stream.cpp
f(x) Functions (40)
Function Calls Lines Branches Blocks
boost::corosio::(anonymous namespace)::tls_method_compat() :68 2188x 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 2214x 100.0% 100.0% 100.0% boost::corosio::(anonymous namespace)::openssl_proto_version(boost::corosio::tls_version) :95 4376x 100.0% 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% 70.0% 82.6% boost::corosio::(anonymous namespace)::normalize_openssl_shutdown_read_error(std::error_code) :118 11x 85.7% 83.3% 95.2% 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% 100.0% 72.7% boost::corosio::(anonymous namespace)::make_openssl_error(unsigned long) :155 18x 75.0% 50.0% 71.4% boost::corosio::openssl_category() :166 21x 100.0% 75.0% 100.0% boost::corosio::detail::password_callback(char*, int, int, void*) :181 1x 84.6% 55.6% 81.2% boost::corosio::detail::verify_callback_trampoline(int, x509_store_ctx_st*) :208 10x 82.6% 60.7% 82.4% boost::corosio::detail::alpn_select_cb(ssl_st*, unsigned char const**, unsigned char*, unsigned char const*, unsigned int, void*) :267 2x 87.5% 72.2% 89.3% boost::corosio::detail::sni_callback(ssl_st*, int*, void*) :301 3x 100.0% 72.7% 94.4% boost::corosio::detail::openssl_native_context::openssl_native_context(boost::corosio::detail::tls_context_data const&) :342 2188x 91.1% 87.8% 89.6% boost::corosio::detail::openssl_native_context::~openssl_native_context() :628 4376x 100.0% 100.0% boost::corosio::detail::get_openssl_native_context(boost::corosio::detail::tls_context_data const&) :636 3712x 100.0% 100.0% boost::corosio::detail::get_openssl_native_context(boost::corosio::detail::tls_context_data const&)::{lambda()#1}::operator()() const :639 2188x 100.0% 50.0% 71.4% boost::corosio::detail::get_openssl_context(boost::corosio::detail::tls_context_data const&) :644 2194x 100.0% 100.0% boost::corosio::openssl_stream::impl::impl(boost::capy::any_stream&, boost::corosio::tls_context) :667 2194x 100.0% 100.0% 58.8% boost::corosio::openssl_stream::impl::~impl() :673 2194x 100.0% 50.0% 100.0% boost::corosio::openssl_stream::impl::reset() :681 20x 80.0% 62.5% 84.6% boost::corosio::openssl_stream::impl::capture_alpn() :703 782x 100.0% 83.3% 100.0% boost::corosio::openssl_stream::impl::flush_output() :712 65658x 100.0% 100.0% 43.8% boost::corosio::openssl_stream::impl::read_input() :743 33425x 100.0% 100.0% 43.8% boost::corosio::openssl_stream::impl::do_read_some(boost::capy::detail::buffer_array<16ull, false>) :764 31449x 100.0% 100.0% 42.1% boost::corosio::openssl_stream::impl::do_write_some(boost::capy::detail::buffer_array<16ull, true>) :847 31423x 100.0% 100.0% 42.1% boost::corosio::openssl_stream::impl::do_handshake(int) :907 1518x 100.0% 100.0% 43.8% boost::corosio::openssl_stream::impl::do_shutdown() :985 39x 100.0% 100.0% 43.8% boost::corosio::openssl_stream::impl::init_ssl() :1052 2194x 57.9% 53.3% 65.0% boost::corosio::openssl_stream::make_impl(boost::capy::any_stream&, boost::corosio::tls_context const&) :1087 2194x 71.4% 44.4% 52.4% boost::corosio::openssl_stream::~openssl_stream() :1101 2196x 0.0% 100.0% boost::corosio::openssl_stream::openssl_stream(boost::corosio::openssl_stream&&) :1106 2x 100.0% 50.0% 100.0% boost::corosio::openssl_stream::operator=(boost::corosio::openssl_stream&&) :1116 1x 100.0% 50.0% 100.0% boost::corosio::openssl_stream::do_read_some(boost::capy::detail::buffer_array<16ull, false>) :1131 31449x 100.0% 100.0% 42.1% boost::corosio::openssl_stream::do_write_some(boost::capy::detail::buffer_array<16ull, true>) :1138 31423x 100.0% 100.0% 42.1% boost::corosio::openssl_stream::handshake(boost::corosio::tls_stream::handshake_type) :1145 1518x 100.0% 100.0% 43.8% boost::corosio::openssl_stream::shutdown() :1151 39x 100.0% 100.0% 43.8% 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 Branch 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 2188x tls_method_compat() noexcept
69 {
70 #if OPENSSL_VERSION_NUMBER >= 0x10100000L
71 2188x return TLS_method();
72 #else
73 return SSLv23_method();
74 #endif
75 }
76
77 inline void
78 2214x apply_hostname_verification(SSL* ssl, std::string const& hostname)
79 {
80
2/2
✓ Branch 3 → 4 taken 2210 times.
✓ Branch 3 → 5 taken 4 times.
2214x if (hostname.empty())
81 2210x 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 4376x openssl_proto_version(tls_version v) noexcept
96 {
97
2/2
✓ Branch 2 → 3 taken 2195 times.
✓ Branch 2 → 4 taken 2181 times.
4376x 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
2/2
✓ Branch 19 → 5 taken 6 times.
✓ Branch 19 → 20 taken 4 times.
10x for (auto const& p : protocols)
108 {
109
3/6
✓ Branch 7 → 8 taken 6 times.
✗ Branch 7 → 10 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 6 times.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 6 times.
6x if (p.empty() || p.size() > 255)
110 continue;
111
1/1
✓ Branch 15 → 16 taken 6 times.
6x wire.push_back(static_cast<char>(p.size()));
112
1/1
✓ Branch 16 → 17 taken 6 times.
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
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 11 times.
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
4/4
✓ Branch 10 → 11 taken 3 times.
✓ Branch 10 → 17 taken 2 times.
✓ Branch 13 → 14 taken 2 times.
✓ Branch 13 → 17 taken 1 time.
19x if (ec == capy::cond::eof || ec == std::errc::connection_reset ||
127
5/6
✓ Branch 7 → 8 taken 5 times.
✓ Branch 7 → 17 taken 6 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 2 times.
✓ Branch 19 → 20 taken 9 times.
✓ Branch 19 → 21 taken 2 times.
19x 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
1/1
✓ Branch 2 → 3 taken 2 times.
2x ::ERR_error_string_n(
146 static_cast<unsigned long>(value), buf, sizeof(buf));
147
1/1
✓ Branch 5 → 6 taken 2 times.
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
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 18 times.
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
3/4
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 7 taken 20 times.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 7 not taken.
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
3/6
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 1 time.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 1 time.
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
1/1
✓ Branch 9 → 10 taken 1 time.
1x cd->password_callback(static_cast<std::size_t>(size), purpose);
193
194 1x int len = static_cast<int>(password.size());
195
1/2
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 1 time.
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
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 10 times.
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
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 10 times.
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
4/4
✓ Branch 10 → 11 taken 4 times.
✓ Branch 10 → 18 taken 6 times.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 18 taken 3 times.
10x if (!ok && cd->revocation == tls_revocation_policy::soft_fail)
226 {
227 1x int const err = X509_STORE_CTX_get_error(store_ctx);
228
1/4
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 17 taken 1 time.
✗ Branch 14 → 15 not taken.
✗ Branch 14 → 17 not taken.
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
2/2
✓ Branch 19 → 20 taken 6 times.
✓ Branch 19 → 30 taken 4 times.
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
1/1
✓ Branch 20 → 21 taken 6 times.
6x X509* cert = X509_STORE_CTX_get_current_cert(store_ctx);
240 6x unsigned char* der = nullptr;
241
2/3
✓ Branch 21 → 22 taken 6 times.
✗ Branch 21 → 24 not taken.
✓ Branch 22 → 23 taken 6 times.
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
1/1
✓ Branch 26 → 27 taken 6 times.
6x ok = cd->verify_callback(ok, vc);
247
248
1/2
✓ Branch 27 → 28 taken 6 times.
✗ Branch 27 → 29 not taken.
6x if (der)
249
1/1
✓ Branch 28 → 29 taken 6 times.
6x OPENSSL_free(der);
250 }
251
252
2/2
✓ Branch 30 → 31 taken 6 times.
✓ Branch 30 → 32 taken 4 times.
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
3/6
✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 2 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 2 times.
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
2/2
✓ Branch 27 → 11 taken 2 times.
✓ Branch 27 → 28 taken 1 time.
3x for (auto const& pref : *prefs)
278 {
279
2/2
✓ Branch 24 → 13 taken 2 times.
✓ Branch 24 → 25 taken 1 time.
3x for (unsigned int i = 0; i + 1 <= inlen;)
280 {
281 2x unsigned int len = in[i];
282
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 2 times.
2x if (i + 1 + len > inlen)
283 break; // malformed
284
4/4
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 20 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 23 taken 1 time.
3x if (len == pref.size() &&
285
1/2
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
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
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 2 times.
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
3/6
✓ Branch 7 → 8 taken 2 times.
✗ Branch 7 → 11 not taken.
✓ Branch 9 → 10 taken 2 times.
✗ Branch 9 → 11 not taken.
✓ Branch 12 → 13 taken 2 times.
✗ Branch 12 → 17 not taken.
2x if (cd && cd->servername_callback)
312 {
313
3/3
✓ Branch 14 → 15 taken 2 times.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 17 taken 1 time.
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 2188x explicit openssl_native_context(tls_context_data const& cd)
343 4376x : ctx_(nullptr)
344 2188x , cd_(&cd)
345 {
346
1/1
✓ Branch 6 → 7 taken 2188 times.
2188x ctx_ = SSL_CTX_new(tls_method_compat());
347
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 2188 times.
2188x if (!ctx_)
348 return;
349
350
2/2
✓ Branch 9 → 10 taken 4 times.
✓ Branch 9 → 12 taken 2184 times.
2188x if (sni_ctx_data_index < 0)
351 4x sni_ctx_data_index =
352
1/1
✓ Branch 10 → 11 taken 4 times.
4x SSL_CTX_get_ex_new_index(0, nullptr, nullptr, nullptr, nullptr);
353
354
1/1
✓ Branch 12 → 13 taken 2188 times.
2188x SSL_CTX_set_ex_data(
355 ctx_, sni_ctx_data_index, const_cast<tls_context_data*>(&cd));
356
357
2/2
✓ Branch 14 → 15 taken 3 times.
✓ Branch 14 → 16 taken 2185 times.
2188x if (cd.servername_callback)
358
1/1
✓ Branch 15 → 16 taken 3 times.
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
2/2
✓ Branch 17 → 18 taken 4 times.
✓ Branch 17 → 24 taken 2184 times.
2188x if (!cd.alpn_protocols.empty())
365 {
366
1/1
✓ Branch 18 → 19 taken 4 times.
4x alpn_snapshot_ = cd.alpn_protocols;
367
1/1
✓ Branch 19 → 20 taken 4 times.
4x SSL_CTX_set_alpn_select_cb(ctx_, alpn_select_cb, &alpn_snapshot_);
368
1/1
✓ Branch 20 → 21 taken 4 times.
4x alpn_wire_ = build_alpn_wire(cd.alpn_protocols);
369 }
370
371
1/1
✓ Branch 24 → 25 taken 2188 times.
2188x SSL_CTX_set_mode(ctx_, SSL_MODE_ENABLE_PARTIAL_WRITE);
372
1/1
✓ Branch 25 → 26 taken 2188 times.
2188x SSL_CTX_set_mode(ctx_, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
373 #if defined(SSL_MODE_RELEASE_BUFFERS)
374
1/1
✓ Branch 26 → 27 taken 2188 times.
2188x 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
2/2
✓ Branch 27 → 28 taken 1 time.
✓ Branch 27 → 29 taken 2187 times.
2188x if (cd.min_version > cd.max_version)
381 1x setup_failed_ = true;
382
2/3
✓ Branch 30 → 31 taken 2188 times.
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 2188 times.
2188x if (!SSL_CTX_set_min_proto_version(
383 ctx_, openssl_proto_version(cd.min_version)))
384 setup_failed_ = true;
385
2/3
✓ Branch 34 → 35 taken 2188 times.
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 37 taken 2188 times.
2188x if (!SSL_CTX_set_max_proto_version(
386 ctx_, openssl_proto_version(cd.max_version)))
387 setup_failed_ = true;
388
389 2188x int verify_mode_flag = SSL_VERIFY_NONE;
390
2/2
✓ Branch 37 → 38 taken 1091 times.
✓ Branch 37 → 39 taken 1097 times.
2188x if (cd.verification_mode == tls_verify_mode::peer)
391 1091x verify_mode_flag = SSL_VERIFY_PEER;
392
2/2
✓ Branch 39 → 40 taken 3 times.
✓ Branch 39 → 41 taken 1094 times.
1097x 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
2/2
✓ Branch 42 → 43 taken 2183 times.
✓ Branch 42 → 44 taken 5 times.
4371x cd.verify_callback ||
399
2/2
✓ Branch 43 → 44 taken 3 times.
✓ Branch 43 → 45 taken 2180 times.
4371x cd.revocation != tls_revocation_policy::disabled;
400
3/3
✓ Branch 46 → 47 taken 8 times.
✓ Branch 46 → 48 taken 2180 times.
✓ Branch 49 → 50 taken 2188 times.
2188x 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
2/2
✓ Branch 51 → 52 taken 5 times.
✓ Branch 51 → 95 taken 2183 times.
2188x 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
1/1
✓ Branch 54 → 55 taken 5 times.
5x BIO* bio = BIO_new_mem_buf(
415 5x cd.pkcs12_data.data(),
416 5x static_cast<int>(cd.pkcs12_data.size()));
417
1/2
✗ Branch 55 → 56 not taken.
✓ Branch 55 → 57 taken 5 times.
5x if (!bio)
418 setup_failed_ = true;
419 else
420 {
421
1/1
✓ Branch 57 → 58 taken 5 times.
5x PKCS12* p12 = d2i_PKCS12_bio(bio, nullptr);
422
1/2
✗ Branch 58 → 59 not taken.
✓ Branch 58 → 60 taken 5 times.
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
3/3
✓ Branch 61 → 62 taken 5 times.
✓ Branch 62 → 63 taken 3 times.
✓ Branch 62 → 84 taken 2 times.
5x if (PKCS12_parse(
430 p12, cd.pkcs12_password.c_str(), &pkey, &cert,
431 &chain))
432 {
433
1/2
✓ Branch 63 → 64 taken 3 times.
✗ Branch 63 → 65 not taken.
3x if (cert)
434
1/1
✓ Branch 64 → 65 taken 3 times.
3x SSL_CTX_use_certificate(ctx_, cert);
435
1/2
✓ Branch 65 → 66 taken 3 times.
✗ Branch 65 → 67 not taken.
3x if (pkey)
436
1/1
✓ Branch 66 → 67 taken 3 times.
3x SSL_CTX_use_PrivateKey(ctx_, pkey);
437
2/2
✓ Branch 67 → 68 taken 1 time.
✓ Branch 67 → 85 taken 2 times.
3x if (chain)
438
3/3
✓ Branch 82 → 83 taken 2 times.
✓ Branch 83 → 69 taken 1 time.
✓ Branch 83 → 85 taken 1 time.
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
2/2
✓ Branch 70 → 71 taken 1 time.
✓ Branch 71 → 72 taken 1 time.
1x X509* dup = X509_dup(sk_X509_value(chain, i));
445
2/4
✓ Branch 72 → 73 taken 1 time.
✗ Branch 72 → 75 not taken.
✗ Branch 77 → 78 not taken.
✓ Branch 77 → 80 taken 1 time.
2x if (!dup ||
446
2/3
✓ Branch 73 → 74 taken 1 time.
✗ Branch 74 → 75 not taken.
✓ Branch 74 → 76 taken 1 time.
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
1/1
✓ Branch 85 → 86 taken 5 times.
5x EVP_PKEY_free(pkey);
456
1/1
✓ Branch 86 → 87 taken 5 times.
5x X509_free(cert);
457
2/2
✓ Branch 87 → 88 taken 1 time.
✓ Branch 87 → 91 taken 4 times.
5x if (chain)
458
1/1
✓ Branch 90 → 91 taken 1 time.
1x sk_X509_pop_free(chain, X509_free);
459
1/1
✓ Branch 91 → 92 taken 5 times.
5x PKCS12_free(p12);
460 }
461
1/1
✓ Branch 93 → 94 taken 5 times.
5x ERR_clear_error();
462
1/1
✓ Branch 94 → 95 taken 5 times.
5x BIO_free(bio);
463 }
464 }
465
466
6/6
✓ Branch 96 → 97 taken 2183 times.
✓ Branch 96 → 100 taken 5 times.
✓ Branch 98 → 99 taken 1090 times.
✓ Branch 98 → 100 taken 1093 times.
✓ Branch 101 → 102 taken 1090 times.
✓ Branch 101 → 115 taken 1098 times.
2188x if (cd.pkcs12_data.empty() && !cd.entity_certificate.empty())
467 {
468
1/1
✓ Branch 104 → 105 taken 1090 times.
1090x BIO* bio = BIO_new_mem_buf(
469 1090x cd.entity_certificate.data(),
470 1090x static_cast<int>(cd.entity_certificate.size()));
471
1/2
✓ Branch 105 → 106 taken 1090 times.
✗ Branch 105 → 115 not taken.
1090x if (bio)
472 {
473 1090x X509* cert = nullptr;
474
1/2
✓ Branch 106 → 107 taken 1090 times.
✗ Branch 106 → 109 not taken.
1090x if (cd.entity_cert_format == tls_file_format::pem)
475
1/1
✓ Branch 107 → 108 taken 1090 times.
1090x cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
476 else
477 cert = d2i_X509_bio(bio, nullptr);
478
2/2
✓ Branch 111 → 112 taken 1089 times.
✓ Branch 111 → 114 taken 1 time.
1090x if (cert)
479 {
480
1/1
✓ Branch 112 → 113 taken 1089 times.
1089x SSL_CTX_use_certificate(ctx_, cert);
481
1/1
✓ Branch 113 → 114 taken 1089 times.
1089x X509_free(cert);
482 }
483
1/1
✓ Branch 114 → 115 taken 1090 times.
1090x BIO_free(bio);
484 }
485 }
486
487
6/6
✓ Branch 116 → 117 taken 2183 times.
✓ Branch 116 → 120 taken 5 times.
✓ Branch 118 → 119 taken 1 time.
✓ Branch 118 → 120 taken 2182 times.
✓ Branch 121 → 122 taken 1 time.
✓ Branch 121 → 136 taken 2187 times.
2188x if (cd.pkcs12_data.empty() && !cd.certificate_chain.empty())
488 {
489
1/1
✓ Branch 124 → 125 taken 1 time.
1x BIO* bio = BIO_new_mem_buf(
490 1x cd.certificate_chain.data(),
491 1x static_cast<int>(cd.certificate_chain.size()));
492
1/2
✓ Branch 125 → 126 taken 1 time.
✗ Branch 125 → 136 not taken.
1x if (bio)
493 {
494 X509* entity =
495
1/1
✓ Branch 126 → 127 taken 1 time.
1x PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
496
1/2
✓ Branch 127 → 128 taken 1 time.
✗ Branch 127 → 130 not taken.
1x if (entity)
497 {
498
1/1
✓ Branch 128 → 129 taken 1 time.
1x SSL_CTX_use_certificate(ctx_, entity);
499
1/1
✓ Branch 129 → 130 taken 1 time.
1x X509_free(entity);
500 }
501
502 X509* cert;
503
1/1
✓ Branch 132 → 133 taken 2 times.
2x while ((cert = PEM_read_bio_X509(
504
2/2
✓ Branch 133 → 131 taken 1 time.
✓ Branch 133 → 134 taken 1 time.
2x bio, nullptr, nullptr, nullptr)) != nullptr)
505 {
506
1/1
✓ Branch 131 → 132 taken 1 time.
1x SSL_CTX_add_extra_chain_cert(ctx_, cert);
507 }
508
1/1
✓ Branch 134 → 135 taken 1 time.
1x ERR_clear_error();
509
1/1
✓ Branch 135 → 136 taken 1 time.
1x BIO_free(bio);
510 }
511 }
512
513
6/6
✓ Branch 137 → 138 taken 2183 times.
✓ Branch 137 → 141 taken 5 times.
✓ Branch 139 → 140 taken 1091 times.
✓ Branch 139 → 141 taken 1092 times.
✓ Branch 142 → 143 taken 1091 times.
✓ Branch 142 → 160 taken 1097 times.
2188x if (cd.pkcs12_data.empty() && !cd.private_key.empty())
514 {
515
1/1
✓ Branch 145 → 146 taken 1091 times.
1091x BIO* bio = BIO_new_mem_buf(
516 1091x cd.private_key.data(), static_cast<int>(cd.private_key.size()));
517
1/2
✓ Branch 146 → 147 taken 1091 times.
✗ Branch 146 → 160 not taken.
1091x if (bio)
518 {
519 1091x EVP_PKEY* pkey = nullptr;
520
1/2
✓ Branch 147 → 148 taken 1091 times.
✗ Branch 147 → 154 not taken.
1091x if (cd.private_key_format == tls_file_format::pem)
521 {
522
2/2
✓ Branch 149 → 150 taken 1 time.
✓ Branch 149 → 152 taken 1090 times.
1091x if (cd.password_callback)
523
1/1
✓ Branch 150 → 151 taken 1 time.
1x pkey = PEM_read_bio_PrivateKey(
524 bio, nullptr, password_callback,
525 const_cast<tls_context_data*>(&cd));
526 else
527
1/1
✓ Branch 152 → 153 taken 1090 times.
1090x pkey = PEM_read_bio_PrivateKey(
528 bio, nullptr, nullptr, nullptr);
529 }
530 else
531 pkey = d2i_PrivateKey_bio(bio, nullptr);
532
2/2
✓ Branch 156 → 157 taken 1090 times.
✓ Branch 156 → 159 taken 1 time.
1091x if (pkey)
533 {
534
1/1
✓ Branch 157 → 158 taken 1090 times.
1090x SSL_CTX_use_PrivateKey(ctx_, pkey);
535
1/1
✓ Branch 158 → 159 taken 1090 times.
1090x EVP_PKEY_free(pkey);
536 }
537
1/1
✓ Branch 159 → 160 taken 1091 times.
1091x BIO_free(bio);
538 }
539 }
540
541
1/1
✓ Branch 160 → 161 taken 2188 times.
2188x X509_STORE* store = SSL_CTX_get_cert_store(ctx_);
542
2/2
✓ Branch 175 → 163 taken 1103 times.
✓ Branch 175 → 176 taken 2188 times.
3291x for (auto const& ca : cd.ca_certificates)
543 {
544
1/1
✓ Branch 166 → 167 taken 1103 times.
1103x BIO* bio = BIO_new_mem_buf(ca.data(), static_cast<int>(ca.size()));
545
1/2
✓ Branch 167 → 168 taken 1103 times.
✗ Branch 167 → 173 not taken.
1103x if (bio)
546 {
547
1/1
✓ Branch 168 → 169 taken 1103 times.
1103x X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
548
2/2
✓ Branch 169 → 170 taken 1100 times.
✓ Branch 169 → 172 taken 3 times.
1103x if (cert)
549 {
550
1/1
✓ Branch 170 → 171 taken 1100 times.
1100x X509_STORE_add_cert(store, cert);
551
1/1
✓ Branch 171 → 172 taken 1100 times.
1100x X509_free(cert);
552 }
553
1/1
✓ Branch 172 → 173 taken 1103 times.
1103x 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
2/2
✓ Branch 176 → 177 taken 1 time.
✓ Branch 176 → 178 taken 2187 times.
2188x if (cd.use_default_verify_paths)
562
1/1
✓ Branch 177 → 178 taken 1 time.
1x SSL_CTX_set_default_verify_paths(ctx_);
563
2/2
✓ Branch 185 → 180 taken 1 time.
✓ Branch 185 → 186 taken 2188 times.
2189x for (auto const& path : cd.verify_paths)
564
1/1
✓ Branch 182 → 183 taken 1 time.
1x SSL_CTX_load_verify_locations(ctx_, nullptr, path.c_str());
565
1/1
✓ Branch 186 → 187 taken 2188 times.
2188x 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
2/2
✓ Branch 187 → 188 taken 3 times.
✓ Branch 187 → 211 taken 2185 times.
2188x if (cd.revocation != tls_revocation_policy::disabled)
573 {
574
2/2
✓ Branch 208 → 190 taken 2 times.
✓ Branch 208 → 209 taken 3 times.
5x for (auto const& crl_data : cd.crls)
575 {
576
1/1
✓ Branch 193 → 194 taken 2 times.
2x BIO* bio = BIO_new_mem_buf(
577 2x crl_data.data(), static_cast<int>(crl_data.size()));
578
1/2
✗ Branch 194 → 195 not taken.
✓ Branch 194 → 196 taken 2 times.
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
1/1
✓ Branch 196 → 197 taken 2 times.
2x PEM_read_bio_X509_CRL(bio, nullptr, nullptr, nullptr);
587
2/2
✓ Branch 197 → 198 taken 1 time.
✓ Branch 197 → 201 taken 1 time.
2x if (!crl)
588 {
589
1/1
✓ Branch 198 → 199 taken 1 time.
1x BIO_reset(bio);
590
1/1
✓ Branch 199 → 200 taken 1 time.
1x crl = d2i_X509_CRL_bio(bio, nullptr);
591 }
592
2/2
✓ Branch 201 → 202 taken 1 time.
✓ Branch 201 → 204 taken 1 time.
2x if (crl)
593 {
594
1/1
✓ Branch 202 → 203 taken 1 time.
1x X509_STORE_add_crl(store, crl);
595
1/1
✓ Branch 203 → 205 taken 1 time.
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
1/1
✓ Branch 205 → 206 taken 2 times.
2x BIO_free(bio);
605 }
606
1/1
✓ Branch 209 → 210 taken 3 times.
3x X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK);
607
1/1
✓ Branch 210 → 211 taken 3 times.
3x ERR_clear_error();
608 }
609
610
1/1
✓ Branch 211 → 212 taken 2188 times.
2188x 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
3/4
✓ Branch 213 → 214 taken 4 times.
✓ Branch 213 → 218 taken 2184 times.
✗ Branch 219 → 220 not taken.
✓ Branch 219 → 221 taken 2188 times.
2192x if (!cd.ciphersuites.empty() &&
621
2/3
✓ Branch 215 → 216 taken 4 times.
✗ Branch 216 → 217 not taken.
✓ Branch 216 → 218 taken 4 times.
4x !SSL_CTX_set_cipher_list(ctx_, cd.ciphersuites.c_str()))
622 setup_failed_ = true;
623
4/4
✓ Branch 222 → 223 taken 5 times.
✓ Branch 222 → 227 taken 2183 times.
✓ Branch 228 → 229 taken 1 time.
✓ Branch 228 → 230 taken 2187 times.
2193x if (!cd.ciphersuites_tls13.empty() &&
624
3/3
✓ Branch 224 → 225 taken 5 times.
✓ Branch 225 → 226 taken 1 time.
✓ Branch 225 → 227 taken 4 times.
5x !SSL_CTX_set_ciphersuites(ctx_, cd.ciphersuites_tls13.c_str()))
625 1x setup_failed_ = true;
626 }
627
628 4376x ~openssl_native_context() override
629 2188x {
630
1/2
✓ Branch 2 → 3 taken 2188 times.
✗ Branch 2 → 4 not taken.
2188x if (ctx_)
631 2188x SSL_CTX_free(ctx_);
632 4376x }
633 };
634
635 inline openssl_native_context*
636 3712x get_openssl_native_context(tls_context_data const& cd)
637 {
638 static char key;
639
2/4
✓ Branch 2 → 3 taken 3712 times.
✓ Branch 3 → 4 taken 2188 times.
✗ Branch 6 → 7 not taken.
✗ Branch 6 → 8 not taken.
5900x auto* p = cd.find(&key, [&] { return new openssl_native_context(cd); });
640 3712x return static_cast<openssl_native_context*>(p);
641 }
642
643 SSL_CTX*
644 2194x get_openssl_context(tls_context_data const& cd)
645 {
646 2194x 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 2194x impl(capy::any_stream& s, tls_context ctx) : s_(&s), ctx_(std::move(ctx))
668 {
669
1/1
✓ Branch 8 → 9 taken 2194 times.
2194x in_buf_.resize(default_buffer_size);
670
1/1
✓ Branch 9 → 10 taken 2194 times.
2194x out_buf_.resize(default_buffer_size);
671 2194x }
672
673 2194x ~impl()
674 {
675
1/2
✓ Branch 2 → 3 taken 2194 times.
✗ Branch 2 → 4 not taken.
2194x if (ext_bio_)
676 2194x BIO_free(ext_bio_);
677
1/2
✓ Branch 4 → 5 taken 2194 times.
✗ Branch 4 → 6 not taken.
2194x if (ssl_)
678 2194x SSL_free(ssl_);
679 2194x }
680
681 20x void reset()
682 {
683
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 20 times.
20x if (!ssl_)
684 return;
685
686 // Preserves SSL* and BIO pair, releases session state
687
1/1
✓ Branch 4 → 5 taken 20 times.
20x SSL_clear(ssl_);
688
689 // Drain stale data from the external BIO
690 char drain[1024];
691
2/3
✓ Branch 7 → 8 taken 20 times.
✗ Branch 8 → 6 not taken.
✓ Branch 8 → 9 taken 20 times.
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
1/1
✓ Branch 10 → 11 taken 20 times.
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 782x void capture_alpn()
704 {
705 782x unsigned char const* data = nullptr;
706 782x unsigned int len = 0;
707
1/1
✓ Branch 2 → 3 taken 782 times.
782x SSL_get0_alpn_selected(ssl_, &data, &len);
708
3/4
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 6 taken 780 times.
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 6 not taken.
782x if (data && len)
709
1/1
✓ Branch 5 → 6 taken 2 times.
2x alpn_selected_.assign(reinterpret_cast<char const*>(data), len);
710 782x }
711
712
1/1
✓ Branch 5 → 6 taken 65658 times.
65658x 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 131316x }
742
743
1/1
✓ Branch 5 → 6 taken 33425 times.
33425x 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 66850x }
762
763 capy::io_task<std::size_t>
764
1/1
✓ Branch 6 → 7 taken 31449 times.
31449x 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 62898x }
845
846 capy::io_task<std::size_t>
847
1/1
✓ Branch 6 → 7 taken 31423 times.
31423x 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 62846x }
906
907
1/1
✓ Branch 5 → 6 taken 1518 times.
1518x 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 3036x }
984
985
1/1
✓ Branch 5 → 6 taken 39 times.
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 2194x std::error_code init_ssl()
1053 {
1054 2194x auto& cd = detail::get_tls_context_data(ctx_);
1055
1/1
✓ Branch 3 → 4 taken 2194 times.
2194x SSL_CTX* native_ctx = detail::get_openssl_context(cd);
1056
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 7 taken 2194 times.
2194x if (!native_ctx)
1057 {
1058 unsigned long err = ERR_get_error();
1059 return make_openssl_error(err);
1060 }
1061
1062
1/1
✓ Branch 7 → 8 taken 2194 times.
2194x ssl_ = SSL_new(native_ctx);
1063
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 11 taken 2194 times.
2194x if (!ssl_)
1064 {
1065 unsigned long err = ERR_get_error();
1066 return make_openssl_error(err);
1067 }
1068
1069 2194x BIO* int_bio = nullptr;
1070
2/3
✓ Branch 11 → 12 taken 2194 times.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 16 taken 2194 times.
2194x 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
1/1
✓ Branch 16 → 17 taken 2194 times.
2194x SSL_set_bio(ssl_, int_bio, int_bio);
1079
1080
1/1
✓ Branch 17 → 18 taken 2194 times.
2194x apply_hostname_verification(ssl_, cd.hostname);
1081
1082 2194x return {};
1083 }
1084 };
1085
1086 openssl_stream::impl*
1087 2194x openssl_stream::make_impl(capy::any_stream& stream, tls_context const& ctx)
1088 {
1089
2/4
✓ Branch 2 → 3 taken 2194 times.
✓ Branch 4 → 5 taken 2194 times.
✗ Branch 19 → 20 not taken.
✗ Branch 19 → 21 not taken.
2194x auto* p = new impl(stream, ctx);
1090
1091
1/1
✓ Branch 6 → 7 taken 2194 times.
2194x auto ec = p->init_ssl();
1092
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 13 taken 2194 times.
2194x if (ec)
1093 {
1094 delete p;
1095 return nullptr;
1096 }
1097
1098 2194x return p;
1099 }
1100
1101 2196x openssl_stream::~openssl_stream()
1102 {
1103
2/2
✓ Branch 2 → 3 taken 2193 times.
✓ Branch 2 → 5 taken 3 times.
2196x delete impl_;
1104 2196x }
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
1/2
✓ Branch 5 → 6 taken 2 times.
✗ Branch 5 → 7 not taken.
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
1/2
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 10 not taken.
1x if (this != &other)
1119 {
1120
1/2
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 6 not taken.
1x delete impl_;
1121 1x stream_ = std::move(other.stream_);
1122 1x impl_ = other.impl_;
1123 1x other.impl_ = nullptr;
1124
1/2
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 10 not taken.
1x if (impl_)
1125 1x impl_->s_ = &stream_;
1126 }
1127 1x return *this;
1128 }
1129
1130 capy::io_task<std::size_t>
1131
1/1
✓ Branch 6 → 7 taken 31449 times.
31449x 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 62898x }
1136
1137 capy::io_task<std::size_t>
1138
1/1
✓ Branch 6 → 7 taken 31423 times.
31423x 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 62846x }
1143
1144 capy::io_task<>
1145
1/1
✓ Branch 5 → 6 taken 1518 times.
1518x openssl_stream::handshake(handshake_type type)
1146 {
1147 co_return co_await impl_->do_handshake(type);
1148 3036x }
1149
1150 capy::io_task<>
1151
1/1
✓ Branch 5 → 6 taken 39 times.
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