src/openssl/src/detail/engine.cpp

94.8% Lines (441 / 465, 13 excl) 70.9% Branches (326 / 460, 8 excl) 100.0% Functions (35 / 35)
engine.cpp
f(x) Functions (35)
Function Calls Lines Branches Blocks
boost::corosio::(anonymous namespace)::tls_method_compat() :34 2049x 100.0% 50.0% 66.0% boost::corosio::(anonymous namespace)::apply_hostname_verification(ssl_st*, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>> const&) :44 2080x 93.3% 92.9% 94.0% boost::corosio::(anonymous namespace)::openssl_proto_version(boost::corosio::tls_version) :74 4096x 100.0% – 100.0% boost::corosio::(anonymous namespace)::build_alpn_wire(std::__1::vector<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, std::__1::allocator<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>>> const&) :83 4x 100.0% 60.0% 71.0% boost::corosio::(anonymous namespace)::openssl_category_impl::~openssl_category_impl() :97 4x 100.0% – 100.0% boost::corosio::(anonymous namespace)::openssl_category_impl::name() const :99 1x 100.0% – 100.0% boost::corosio::(anonymous namespace)::openssl_category_impl::message(int) const :104 2x 100.0% – 100.0% boost::corosio::(anonymous namespace)::make_openssl_error(unsigned long) :117 28x 66.7% 50.0% 75.0% boost::corosio::(anonymous namespace)::map_openssl_fatal() :132 25x 83.3% 50.0% 66.0% boost::corosio::openssl_category() :143 35x 100.0% 75.0% 100.0% boost::corosio::detail::password_callback(char*, int, int, void*) :158 2x 100.0% 66.7% 85.0% boost::corosio::detail::verify_callback_trampoline(int, x509_store_ctx_st*) :186 10x 88.9% 50.0% 70.0% boost::corosio::detail::alpn_select_cb(ssl_st*, unsigned char const**, unsigned char*, unsigned char const*, unsigned int, void*) :245 2x 93.8% 71.4% 88.0% boost::corosio::detail::sni_callback(ssl_st*, int*, void*) :284 12x 100.0% 75.0% 100.0% boost::corosio::detail::openssl_native_context::openssl_native_context(boost::corosio::detail::tls_context_data const&) :325 4098x 98.9% 67.3% 100.0% boost::corosio::detail::openssl_native_context::~openssl_native_context() :652 6147x 100.0% 75.0% 100.0% boost::corosio::detail::get_openssl_native_context(boost::corosio::detail::tls_context_data const&) :660 2069x 100.0% – 100.0% boost::corosio::detail::get_openssl_native_context(boost::corosio::detail::tls_context_data const&)::'lambda'()::operator()() const :663 2049x 100.0% 50.0% 50.0% boost::corosio::detail::openssl::engine::~engine() :673 5704x 100.0% 75.0% 100.0% boost::corosio::detail::openssl::engine::init(boost::corosio::tls_context const&) :682 2069x 66.7% 66.7% 75.0% boost::corosio::detail::openssl::engine::reset() :719 67x 77.8% 62.5% 66.0% boost::corosio::detail::openssl::engine::context_setup_failed() const :750 4147x 100.0% 100.0% 100.0% boost::corosio::detail::openssl::engine::check_context() const :758 4147x 100.0% 100.0% 100.0% boost::corosio::detail::openssl::engine::check_session() const :766 2102x 100.0% 100.0% 100.0% boost::corosio::detail::openssl::engine::prepare(boost::corosio::tls_context const&, boost::corosio::tls_role, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>> const&) :774 2100x 92.9% 92.9% 93.0% boost::corosio::detail::openssl::engine::apply_hostname(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>> const&) :812 2080x 100.0% – 100.0% boost::corosio::detail::openssl::engine::apply_alpn_offer() :818 1433x 100.0% 100.0% 100.0% boost::corosio::detail::openssl::engine::capture_alpn(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>&) const :834 924x 100.0% 75.0% 100.0% boost::corosio::detail::openssl::engine::perform(boost::corosio::detail::engine_op, void*, unsigned long) :844 282208x 92.2% 97.1% 87.0% boost::corosio::detail::openssl::engine::put_input(unsigned char const*, unsigned long) :958 59x 100.0% 50.0% 75.0% boost::corosio::detail::openssl::engine::input_area() :965 17779x 100.0% 75.0% 100.0% boost::corosio::detail::openssl::engine::input_committed(unsigned long) :979 16695x 83.3% 50.0% 75.0% boost::corosio::detail::openssl::engine::pending_output() const :990 630078x 100.0% – 100.0% boost::corosio::detail::openssl::engine::get_output(unsigned char*, unsigned long) :996 119562x 100.0% 100.0% 100.0% boost::corosio::detail::openssl::engine::received_shutdown() const :1003 1090x 100.0% – 100.0%
Line Branch TLA Hits Source Code
1 //
2 // Copyright (c) 2026 Steve Gerbino
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 "engine.hpp"
12
13 // openssl_category is declared alongside the public stream class
14 #include <boost/corosio/openssl_stream.hpp>
15 #include <boost/capy/error.hpp>
16
17 // Internal context implementation
18 #include "src/tls/detail/context_impl.hpp"
19
20 #include <openssl/ssl.h>
21 #include <openssl/err.h>
22 #include <openssl/bio.h>
23 #include <openssl/x509.h>
24 #include <openssl/pkcs12.h>
25
26 #include <cstring>
27 #include <vector>
28
29 namespace boost::corosio {
30
31 namespace {
32
33 inline SSL_METHOD const*
34 2049x tls_method_compat() noexcept
35 {
36 #if OPENSSL_VERSION_NUMBER >= 0x10100000L
37
1/2
✓ Branch 0 taken 2049 times.
✗ Branch 1 not taken.
2049x return TLS_method();
38 #else
39 return SSLv23_method();
40 #endif
41 }
42
43 inline bool
44 2080x apply_hostname_verification(SSL* ssl, std::string const& hostname)
45 {
46 // SSL_clear retains a previously applied name; an empty hostname
47 // must clear SNI and the verify-param host or a reset stream
48 // would leak the old peer's name into the next handshake
49
2/2
✓ Branch 0 taken 2065 times.
✓ Branch 1 taken 15 times.
2080x char const* name = hostname.empty() ? nullptr : hostname.c_str();
50
51 // RFC 6066 excludes IP literals from SNI, and a literal must match
52 // the certificate's iPAddress entries rather than its DNS names.
53 // The unused field is cleared so a reset stream cannot carry the
54 // previous target's matching rule.
55
2/2
✓ Branch 0 taken 2065 times.
✓ Branch 1 taken 15 times.
2080x bool const is_ip = name && detail::is_ip_literal(hostname);
56
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2078 times.
2080x char const* dns_name = is_ip ? nullptr : name;
57
58
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2080 times.
2080x if (SSL_set_tlsext_host_name(ssl, dns_name) != 1)
59 ✗ return false;
60
61 2080x auto* param = SSL_get0_param(ssl);
62
2/2
✓ Branch 0 taken 2079 times.
✓ Branch 1 taken 1 time.
2080x if (!param)
63 1x return name == nullptr;
64
65
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 2078 times.
2079x if (X509_VERIFY_PARAM_set1_host(param, dns_name, 0) != 1)
66 1x return false;
67
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2076 times.
2078x if (is_ip)
68 2x return X509_VERIFY_PARAM_set1_ip_asc(param, name) == 1;
69 2076x return X509_VERIFY_PARAM_set1_ip(param, nullptr, 0) == 1;
70 2080x }
71
72 // Map a portable protocol version to the OpenSSL version constant.
73 inline int
74 4096x openssl_proto_version(tls_version v) noexcept
75 {
76 4096x return v == tls_version::tls_1_3 ? TLS1_3_VERSION : TLS1_2_VERSION;
77 }
78
79 // Encode a protocol list into ALPN wire format: each entry is a
80 // one-byte length followed by that many bytes. Entries longer than 255
81 // bytes are skipped (invalid per RFC 7301).
82 inline std::string
83 4x build_alpn_wire(std::vector<std::string> const& protocols)
84 {
85 4x std::string wire;
86
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 4 times.
10x for (auto const& p : protocols)
87 {
88
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 6 times.
6x if (p.empty() ||
89 − p.size() > 255) // LCOV_EXCL_LINE set_alpn validates eagerly
90 − continue; // LCOV_EXCL_LINE set_alpn validates eagerly
91
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6x wire.push_back(static_cast<char>(p.size()));
92
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6x wire.append(p);
93 }
94 4x return wire;
95 4x }
96
97 class openssl_category_impl final : public std::error_category
98 {
99 1x char const* name() const noexcept override
100 {
101 1x return "corosio.openssl";
102 }
103
104 2x std::string message(int value) const override
105 {
106 char buf[256];
107 2x ::ERR_error_string_n(
108 2x static_cast<unsigned long>(value), buf, sizeof(buf));
109 2x return buf;
110 }
111 };
112
113 // Convert a packed OpenSSL error (from ERR_get_error) into an error_code.
114 // Codes from the ERR_LIB_SYS library carry a genuine errno reason and are
115 // reported with the system category; everything else uses openssl_category.
116 inline std::error_code
117 28x make_openssl_error(unsigned long err) noexcept
118 {
119
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 28 times.
28x if (ERR_GET_LIB(err) == ERR_LIB_SYS)
120 ✗ return std::error_code(
121 ✗ static_cast<int>(ERR_GET_REASON(err)), std::system_category());
122 28x return std::error_code(static_cast<int>(err), openssl_category());
123 28x }
124
125 // Maps a terminal engine failure (any path that isn't WANT_READ/WANT_WRITE/
126 // ZERO_RETURN) to an error_code. SYSCALL and a bare SSL_ERROR_SSL both leave
127 // the queue empty when the transport simply vanished rather than reporting
128 // a protocol failure; make_openssl_error(0) is falsy, which would otherwise
129 // turn that vanished transport into a false success, so an empty queue maps
130 // to stream_truncated instead.
131 inline std::error_code
132 25x map_openssl_fatal() noexcept
133 {
134
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
25x unsigned long ssl_err = ERR_get_error();
135
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
25x if (ssl_err == 0)
136 ✗ return make_error_code(capy::error::stream_truncated);
137 25x return make_openssl_error(ssl_err);
138 25x }
139
140 } // namespace
141
142 std::error_category const&
143 35x openssl_category() noexcept
144 {
145
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 33 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
35x static openssl_category_impl instance;
146 35x return instance;
147 }
148
149 //
150 // Native context caching
151 //
152
153 namespace detail {
154
155 static int sni_ctx_data_index = -1;
156
157 static int
158 2x password_callback(char* buf, int size, int rwflag, void* userdata)
159 {
160 2x auto* cd = static_cast<tls_context_data const*>(userdata);
161
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
2x if (!cd ||
162 − !cd->password_callback) // LCOV_EXCL_LINE installed only with a callback
163 − return 0; // LCOV_EXCL_LINE installed only with a callback
164
165 2x tls_password_purpose purpose = (rwflag == 0)
166 ? tls_password_purpose::for_reading
167 : tls_password_purpose::for_writing;
168
169 std::string password =
170 2x cd->password_callback(static_cast<std::size_t>(size), purpose);
171
172 2x int len = static_cast<int>(password.size());
173
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
2x if (len > size)
174 1x len = size;
175
176 2x std::memcpy(buf, password.data(), static_cast<std::size_t>(len));
177 2x return len;
178 2x }
179
180 // Trampoline installed via SSL_CTX_set_verify. Recovers the portable
181 // context data from the SSL_CTX ex_data (populated for every context)
182 // and applies, in order: the revocation policy's soft-fail downgrade,
183 // then the user's verify callback. Installed whenever a verify callback
184 // or a non-disabled revocation policy is configured.
185 static int
186 10x verify_callback_trampoline(int preverified, X509_STORE_CTX* store_ctx)
187 {
188 10x SSL* ssl = static_cast<SSL*>(X509_STORE_CTX_get_ex_data(
189 10x store_ctx, SSL_get_ex_data_X509_STORE_CTX_idx()));
190 − if (!ssl) // LCOV_EXCL_LINE ex-data set before verify runs
191 − return preverified; // LCOV_EXCL_LINE ex-data set before verify runs
192
193 10x auto* cd = static_cast<tls_context_data const*>(
194 10x SSL_CTX_get_ex_data(SSL_get_SSL_CTX(ssl), sni_ctx_data_index));
195 − if (!cd) // LCOV_EXCL_LINE set at context build
196 − return preverified; // LCOV_EXCL_LINE set at context build
197
198 10x bool ok = preverified != 0;
199
200 // Soft-fail revocation: accept certificates whose revocation status
201 // could not be determined (missing/expired CRL), but never downgrade
202 // an actual revocation. hard_fail leaves every CRL error fatal.
203
4/4
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 1 time.
10x if (!ok && cd->revocation == tls_revocation_policy::soft_fail)
204 {
205 1x int const err = X509_STORE_CTX_get_error(store_ctx);
206
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
1x if (err == X509_V_ERR_UNABLE_TO_GET_CRL ||
207
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ err == X509_V_ERR_UNABLE_TO_GET_CRL_ISSUER ||
208
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ err == X509_V_ERR_CRL_HAS_EXPIRED ||
209 ✗ err == X509_V_ERR_CRL_NOT_YET_VALID)
210 1x ok = true;
211 1x }
212
213
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 6 times.
10x if (cd->verify_callback)
214 {
215 // Expose the current certificate's DER so the callback can inspect
216 // it portably. i2d_X509 allocates; free it after the callback.
217 6x X509* cert = X509_STORE_CTX_get_current_cert(store_ctx);
218 6x unsigned char* der = nullptr;
219
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6x int der_len = cert ? i2d_X509(cert, &der) : 0;
220
221 6x verify_context vc(
222 6x store_ctx, der,
223
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6x der_len > 0 ? static_cast<std::size_t>(der_len) : 0);
224 6x ok = cd->verify_callback(ok, vc);
225
226
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6x if (der)
227 6x OPENSSL_free(der);
228 6x }
229
230 10x return ok ? 1 : 0;
231 10x }
232
233 // Server-side ALPN selection. Chooses the server's most-preferred
234 // protocol that the client also offered. On no overlap it sends a fatal
235 // no_application_protocol alert (RFC 7301 §3.2).
236 //
237 // `arg` points at the native context's build-time snapshot of the server
238 // preference list (a std::vector<std::string>), so client offer and server
239 // selection are both taken from the same immutable snapshot.
240 //
241 // The selected protocol pointer must stay valid until the callback runs
242 // again, so we point *out into the client list `in` (OpenSSL keeps it
243 // valid for the connection) rather than into a local buffer.
244 static int
245 2x alpn_select_cb(
246 SSL* /* ssl */,
247 unsigned char const** out,
248 unsigned char* outlen,
249 unsigned char const* in,
250 unsigned int inlen,
251 void* arg)
252 {
253 2x auto const* prefs = static_cast<std::vector<std::string> const*>(arg);
254
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
2x if (!prefs ||
255 − prefs->empty()) // LCOV_EXCL_LINE installed only with a non-empty list
256 − return SSL_TLSEXT_ERR_NOACK; // LCOV_EXCL_LINE installed only with a non-empty list
257
258 // Server preference order wins: for each server protocol, look for a
259 // matching entry in the client's offered list.
260
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 time.
3x for (auto const& pref : *prefs)
261 {
262
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 2 times.
3x for (unsigned int i = 0; i + 1 <= inlen;)
263 {
264 2x unsigned int len = in[i];
265
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2x if (i + 1 + len > inlen)
266 ✗ break; // malformed
267
3/4
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 time.
2x if (len == pref.size() &&
268 1x std::memcmp(in + i + 1, pref.data(), len) == 0)
269 {
270 1x *out = in + i + 1;
271 1x *outlen = static_cast<unsigned char>(len);
272 1x return SSL_TLSEXT_ERR_OK;
273 }
274 1x i += 1 + len;
275 }
276 }
277
278 // The server supports ALPN but shares no protocol with the client.
279 // RFC 7301 §3.2: fail the handshake with a fatal alert.
280 1x return SSL_TLSEXT_ERR_ALERT_FATAL;
281 2x }
282
283 static int
284 12x sni_callback(SSL* ssl, int* /* alert */, void* /* arg */)
285 {
286 12x char const* servername = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
287
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 4 times.
12x if (!servername)
288 4x return SSL_TLSEXT_ERR_NOACK;
289
290 8x SSL_CTX* ctx = SSL_get_SSL_CTX(ssl);
291 8x auto* cd = static_cast<tls_context_data const*>(
292 8x SSL_CTX_get_ex_data(ctx, sni_ctx_data_index));
293
294
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
8x if (cd && cd->servername_callback)
295 {
296
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 1 time.
8x if (!cd->servername_callback(servername))
297 1x return SSL_TLSEXT_ERR_ALERT_FATAL;
298 7x }
299
300 7x return SSL_TLSEXT_ERR_OK;
301 12x }
302
303 class openssl_native_context : public native_context_base
304 {
305 public:
306 SSL_CTX* ctx_;
307 tls_context_data const* cd_;
308 // Set when a requested configuration could not be applied: an inverted
309 // protocol window (min > max), a cipher list / suite the library
310 // rejected, a protocol-version bound that would not set, or a CRL that
311 // parsed as neither PEM nor DER. Silently proceeding would negotiate an
312 // unexpected version, ignore the requested ciphers, or weaken revocation
313 // (fail-open under soft_fail), so do_handshake refuses the handshake.
314 2049x bool setup_failed_ = false;
315 // ALPN offer in wire format (length-prefixed), encoded once from the
316 // immutable protocol list. The client sets it per-SSL each handshake;
317 // caching it here avoids re-encoding and re-allocating per connection.
318 std::string alpn_wire_;
319 // Server preference snapshot, captured at build time so the select
320 // callback matches against the same immutable list the client offers
321 // from (see alpn_select_cb). Its address is handed to OpenSSL as the
322 // callback arg, so it must outlive the SSL_CTX (it does — same object).
323 std::vector<std::string> alpn_snapshot_;
324
325 6147x explicit openssl_native_context(tls_context_data const& cd)
326 2049x : ctx_(nullptr)
327 2049x , cd_(&cd)
328 4098x {
329
1/2
✓ Branch 0 taken 2049 times.
✗ Branch 1 not taken.
2049x ctx_ = SSL_CTX_new(tls_method_compat());
330
2/2
✓ Branch 0 taken 2048 times.
✓ Branch 1 taken 1 time.
2049x if (!ctx_)
331 1x return;
332
333
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 2042 times.
2048x if (sni_ctx_data_index < 0)
334 6x sni_ctx_data_index =
335
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6x SSL_CTX_get_ex_new_index(0, nullptr, nullptr, nullptr, nullptr);
336
337
1/2
✓ Branch 0 taken 2048 times.
✗ Branch 1 not taken.
2048x SSL_CTX_set_ex_data(
338 2048x ctx_, sni_ctx_data_index, const_cast<tls_context_data*>(&cd));
339
340
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 2040 times.
2048x if (cd.servername_callback)
341
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8x SSL_CTX_set_tlsext_servername_callback(ctx_, sni_callback);
342
343 // ALPN server-side selection. The callback only fires when this
344 // context is used as a server; the client offer (encoded once here)
345 // is set per-SSL from alpn_wire_. Snapshot the preference list so the
346 // callback and the client offer share one immutable source.
347
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 2044 times.
2048x if (!cd.alpn_protocols.empty())
348 {
349
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4x alpn_snapshot_ = cd.alpn_protocols;
350
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4x SSL_CTX_set_alpn_select_cb(ctx_, alpn_select_cb, &alpn_snapshot_);
351
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4x alpn_wire_ = build_alpn_wire(cd.alpn_protocols);
352 4x }
353
354
1/2
✓ Branch 0 taken 2048 times.
✗ Branch 1 not taken.
2048x SSL_CTX_set_mode(ctx_, SSL_MODE_ENABLE_PARTIAL_WRITE);
355
1/2
✓ Branch 0 taken 2048 times.
✗ Branch 1 not taken.
2048x SSL_CTX_set_mode(ctx_, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
356 #if defined(SSL_MODE_RELEASE_BUFFERS)
357
1/2
✓ Branch 0 taken 2048 times.
✗ Branch 1 not taken.
2048x SSL_CTX_set_mode(ctx_, SSL_MODE_RELEASE_BUFFERS);
358 #endif
359
360 // Enforce the configured protocol version window (role-agnostic).
361 // An inverted window (min > max) admits no protocol; fail closed
362 // rather than silently negotiate an unexpected version.
363
2/2
✓ Branch 0 taken 2046 times.
✓ Branch 1 taken 2 times.
2048x if (cd.min_version > cd.max_version)
364 2x setup_failed_ = true;
365
2/4
✓ Branch 0 taken 2048 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2048 times.
✗ Branch 3 not taken.
2048x if (!SSL_CTX_set_min_proto_version(
366 ctx_, openssl_proto_version(cd.min_version)))
367 ✗ setup_failed_ = true;
368
2/4
✓ Branch 0 taken 2048 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2048 times.
✗ Branch 3 not taken.
2048x if (!SSL_CTX_set_max_proto_version(
369 ctx_, openssl_proto_version(cd.max_version)))
370 ✗ setup_failed_ = true;
371
372 2048x int verify_mode_flag = SSL_VERIFY_NONE;
373
2/2
✓ Branch 0 taken 1398 times.
✓ Branch 1 taken 650 times.
2048x if (cd.verification_mode == tls_verify_mode::peer)
374 1398x verify_mode_flag = SSL_VERIFY_PEER;
375
2/2
✓ Branch 0 taken 647 times.
✓ Branch 1 taken 3 times.
650x else if (cd.verification_mode == tls_verify_mode::require_peer)
376 3x verify_mode_flag =
377 SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
378 // The trampoline runs the revocation soft-fail downgrade and the
379 // user callback, so install it if either is configured.
380
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 2043 times.
2048x bool const need_trampoline = cd.verify_callback ||
381 2043x cd.revocation != tls_revocation_policy::disabled;
382
1/2
✓ Branch 0 taken 2048 times.
✗ Branch 1 not taken.
2048x SSL_CTX_set_verify(
383 2048x ctx_, verify_mode_flag,
384 2048x need_trampoline ? &verify_callback_trampoline : nullptr);
385
386 // PKCS#12 bundle: decode cert + key + chain directly into the
387 // context. This is an alternative credential source; the PEM/DER
388 // fields below are only consulted when no bundle is supplied.
389
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 2039 times.
2048x if (!cd.pkcs12_data.empty())
390 {
391 // A bundle that fails to decode or parse (wrong passphrase,
392 // malformed) must not leave the context silently credential-less:
393 // a client using PKCS#12 for mTLS would then fail open against a
394 // verify_mode::peer server. Fail closed like every other setup
395 // error.
396
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9x BIO* bio = BIO_new_mem_buf(
397 9x cd.pkcs12_data.data(), static_cast<int>(cd.pkcs12_data.size()));
398
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 1 time.
9x if (!bio)
399 1x setup_failed_ = true;
400 else
401 {
402
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8x PKCS12* p12 = d2i_PKCS12_bio(bio, nullptr);
403
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 2 times.
8x if (!p12)
404 2x setup_failed_ = true;
405 else
406 {
407 6x EVP_PKEY* pkey = nullptr;
408 6x X509* cert = nullptr;
409 6x STACK_OF(X509)* chain = nullptr;
410
3/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 2 times.
6x if (PKCS12_parse(
411 6x p12, cd.pkcs12_password.c_str(), &pkey, &cert,
412 &chain))
413 {
414
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4x if (cert)
415
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4x SSL_CTX_use_certificate(ctx_, cert);
416
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4x if (pkey)
417
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4x SSL_CTX_use_PrivateKey(ctx_, pkey);
418
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4x if (chain)
419
4/6
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 2 times.
4x for (int i = 0; i < sk_X509_num(chain); ++i)
420 {
421 // add_extra_chain_cert takes ownership of the
422 // dup only on success; free it (and fail
423 // closed) otherwise so a partial chain isn't
424 // sent silently.
425
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
2x X509* dup = X509_dup(sk_X509_value(chain, i));
426
3/4
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
3x if (!dup ||
427
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1x !SSL_CTX_add_extra_chain_cert(ctx_, dup))
428 {
429
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1x X509_free(dup);
430 1x setup_failed_ = true;
431 1x }
432 4x }
433 4x }
434 else
435 2x setup_failed_ = true;
436
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6x EVP_PKEY_free(pkey);
437
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6x X509_free(cert);
438
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 4 times.
6x if (chain)
439
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
2x sk_X509_pop_free(chain, X509_free);
440
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6x PKCS12_free(p12);
441 }
442
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8x ERR_clear_error();
443
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8x BIO_free(bio);
444 }
445 9x }
446
447
4/4
✓ Branch 0 taken 2039 times.
✓ Branch 1 taken 9 times.
✓ Branch 2 taken 638 times.
✓ Branch 3 taken 1401 times.
2048x if (cd.pkcs12_data.empty() && !cd.entity_certificate.empty())
448 {
449 // An entity certificate that fails to parse must not pass
450 // silently: the handshake would run without the identity the
451 // caller configured and fail remotely instead of at setup.
452
1/2
✓ Branch 0 taken 638 times.
✗ Branch 1 not taken.
638x BIO* bio = BIO_new_mem_buf(
453 638x cd.entity_certificate.data(),
454 638x static_cast<int>(cd.entity_certificate.size()));
455
2/2
✓ Branch 0 taken 637 times.
✓ Branch 1 taken 1 time.
638x if (bio)
456 {
457 637x X509* cert = nullptr;
458
2/2
✓ Branch 0 taken 635 times.
✓ Branch 1 taken 2 times.
637x if (cd.entity_cert_format == tls_file_format::pem)
459
1/2
✓ Branch 0 taken 635 times.
✗ Branch 1 not taken.
635x cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
460 else
461
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2x cert = d2i_X509_bio(bio, nullptr);
462
2/2
✓ Branch 0 taken 635 times.
✓ Branch 1 taken 2 times.
637x if (cert)
463 {
464
1/2
✓ Branch 0 taken 635 times.
✗ Branch 1 not taken.
635x SSL_CTX_use_certificate(ctx_, cert);
465
1/2
✓ Branch 0 taken 635 times.
✗ Branch 1 not taken.
635x X509_free(cert);
466 635x }
467 else
468 {
469 2x setup_failed_ = true;
470 }
471
1/2
✓ Branch 0 taken 637 times.
✗ Branch 1 not taken.
637x BIO_free(bio);
472 637x }
473 else
474 {
475 1x setup_failed_ = true;
476 }
477 638x }
478
479
4/4
✓ Branch 0 taken 2039 times.
✓ Branch 1 taken 9 times.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 2038 times.
2048x if (cd.pkcs12_data.empty() && !cd.certificate_chain.empty())
480 {
481
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1x BIO* bio = BIO_new_mem_buf(
482 1x cd.certificate_chain.data(),
483 1x static_cast<int>(cd.certificate_chain.size()));
484
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1x if (bio)
485 {
486 1x X509* entity =
487
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1x PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
488
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1x if (entity)
489 {
490
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1x SSL_CTX_use_certificate(ctx_, entity);
491
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1x X509_free(entity);
492 1x }
493
494 X509* cert;
495
5/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 1 time.
✓ Branch 5 taken 1 time.
2x while ((cert = PEM_read_bio_X509(
496 2x bio, nullptr, nullptr, nullptr)) != nullptr)
497 {
498
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1x SSL_CTX_add_extra_chain_cert(ctx_, cert);
499 }
500
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1x ERR_clear_error();
501
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1x BIO_free(bio);
502 1x }
503 1x }
504
505
4/4
✓ Branch 0 taken 2039 times.
✓ Branch 1 taken 9 times.
✓ Branch 2 taken 639 times.
✓ Branch 3 taken 1400 times.
2048x if (cd.pkcs12_data.empty() && !cd.private_key.empty())
506 {
507
1/2
✓ Branch 0 taken 639 times.
✗ Branch 1 not taken.
639x BIO* bio = BIO_new_mem_buf(
508 639x cd.private_key.data(), static_cast<int>(cd.private_key.size()));
509
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 638 times.
639x if (bio)
510 {
511 638x EVP_PKEY* pkey = nullptr;
512
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 637 times.
638x if (cd.private_key_format == tls_file_format::pem)
513 {
514
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 635 times.
637x if (cd.password_callback)
515
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2x pkey = PEM_read_bio_PrivateKey(
516 2x bio, nullptr, password_callback,
517 2x const_cast<tls_context_data*>(&cd));
518 else
519
1/2
✓ Branch 0 taken 635 times.
✗ Branch 1 not taken.
635x pkey = PEM_read_bio_PrivateKey(
520 635x bio, nullptr, nullptr, nullptr);
521 637x }
522 else
523
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1x pkey = d2i_PrivateKey_bio(bio, nullptr);
524 // A key that fails to parse or decrypt (wrong or missing
525 // password) must fail setup, not surface later as an
526 // inexplicable handshake error.
527
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 635 times.
638x if (pkey)
528 {
529
1/2
✓ Branch 0 taken 635 times.
✗ Branch 1 not taken.
635x SSL_CTX_use_PrivateKey(ctx_, pkey);
530
1/2
✓ Branch 0 taken 635 times.
✗ Branch 1 not taken.
635x EVP_PKEY_free(pkey);
531 635x }
532 else
533 {
534 3x setup_failed_ = true;
535 }
536
1/2
✓ Branch 0 taken 638 times.
✗ Branch 1 not taken.
638x BIO_free(bio);
537
1/2
✓ Branch 0 taken 638 times.
✗ Branch 1 not taken.
638x ERR_clear_error();
538 638x }
539 else
540 {
541 1x setup_failed_ = true;
542 }
543 639x }
544
545
1/2
✓ Branch 0 taken 2048 times.
✗ Branch 1 not taken.
2048x X509_STORE* store = SSL_CTX_get_cert_store(ctx_);
546
2/2
✓ Branch 0 taken 1418 times.
✓ Branch 1 taken 2048 times.
3466x for (auto const& ca : cd.ca_certificates)
547 {
548 // A trust anchor that fails to parse or add must not pass
549 // silently: the store would verify against fewer anchors than
550 // requested and reject a legitimate peer as untrusted. Fail
551 // closed instead, tolerating only a duplicate the store already
552 // holds. An entry may hold a bundle; it ends cleanly only when
553 // no further PEM block follows at least one certificate.
554
1/2
✓ Branch 0 taken 1418 times.
✗ Branch 1 not taken.
1418x BIO* bio = BIO_new_mem_buf(ca.data(), static_cast<int>(ca.size()));
555
2/2
✓ Branch 0 taken 1417 times.
✓ Branch 1 taken 1 time.
1418x if (!bio)
556 {
557 1x setup_failed_ = true;
558 1x continue;
559 }
560 2832x for (bool added = false;; added = true)
561 {
562
1/2
✓ Branch 0 taken 2832 times.
✗ Branch 1 not taken.
2832x X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
563
2/2
✓ Branch 0 taken 1417 times.
✓ Branch 1 taken 1415 times.
2832x if (!cert)
564 {
565
1/2
✓ Branch 0 taken 1417 times.
✗ Branch 1 not taken.
1417x unsigned long const err = ERR_peek_last_error();
566
6/8
✓ Branch 0 taken 1413 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 1413 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1413 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 time.
✓ Branch 7 taken 1412 times.
2830x if (!added || ERR_GET_LIB(err) != ERR_LIB_PEM ||
567
1/2
✓ Branch 0 taken 1413 times.
✗ Branch 1 not taken.
1413x ERR_GET_REASON(err) != PEM_R_NO_START_LINE)
568 5x setup_failed_ = true;
569 1417x break;
570 }
571
4/6
✓ Branch 0 taken 1415 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 1414 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 time.
1416x if (X509_STORE_add_cert(store, cert) != 1 &&
572
2/4
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
1x ERR_GET_REASON(ERR_peek_last_error()) !=
573 X509_R_CERT_ALREADY_IN_HASH_TABLE)
574 1x setup_failed_ = true;
575
1/2
✓ Branch 0 taken 1415 times.
✗ Branch 1 not taken.
1415x X509_free(cert);
576 1415x }
577
1/2
✓ Branch 0 taken 1417 times.
✗ Branch 1 not taken.
1417x ERR_clear_error();
578
1/2
✓ Branch 0 taken 1417 times.
✗ Branch 1 not taken.
1417x BIO_free(bio);
579 }
580
581 // Trust anchors from the system store and explicit directories.
582 // Failures leave the affected source unloaded rather than aborting
583 // context creation; the error queue is cleared so it does not leak
584 // into a later handshake.
585
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 2047 times.
2048x if (cd.use_default_verify_paths)
586
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1x SSL_CTX_set_default_verify_paths(ctx_);
587
2/2
✓ Branch 0 taken 2048 times.
✓ Branch 1 taken 1 time.
2049x for (auto const& path : cd.verify_paths)
588
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1x SSL_CTX_load_verify_locations(ctx_, nullptr, path.c_str());
589
1/2
✓ Branch 0 taken 2048 times.
✗ Branch 1 not taken.
2048x ERR_clear_error();
590
591 // Certificate revocation via CRLs. Load any supplied CRLs and, when
592 // a revocation policy is active, enable leaf CRL checking. soft_fail
593 // vs hard_fail is applied in the verify trampoline. CRL_CHECK (leaf
594 // only) is used so a missing CRL for a trusted root is not itself an
595 // error.
596
2/2
✓ Branch 0 taken 2042 times.
✓ Branch 1 taken 6 times.
2048x if (cd.revocation != tls_revocation_policy::disabled)
597 {
598
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 5 times.
11x for (auto const& crl_data : cd.crls)
599 {
600
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5x BIO* bio = BIO_new_mem_buf(
601 5x crl_data.data(), static_cast<int>(crl_data.size()));
602
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1 time.
5x if (!bio)
603 {
604 1x setup_failed_ = true;
605 1x continue;
606 }
607 // Accept PEM or DER (the documented contract). Try PEM first,
608 // then rewind and try DER.
609 4x X509_CRL* crl =
610
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4x PEM_read_bio_X509_CRL(bio, nullptr, nullptr, nullptr);
611
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 time.
4x if (!crl)
612 {
613
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3x BIO_reset(bio);
614
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3x crl = d2i_X509_CRL_bio(bio, nullptr);
615 3x }
616
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 3 times.
4x if (crl)
617 {
618
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1x X509_STORE_add_crl(store, crl);
619
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1x X509_CRL_free(crl);
620 1x }
621 else
622 {
623 // A supplied CRL that parses as neither PEM nor DER must
624 // not be silently dropped; record it so the handshake
625 // fails closed rather than weakening revocation.
626 3x setup_failed_ = true;
627 }
628
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4x BIO_free(bio);
629 }
630
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6x X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK);
631
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6x ERR_clear_error();
632 6x }
633
634
1/2
✓ Branch 0 taken 2048 times.
✗ Branch 1 not taken.
2048x SSL_CTX_set_verify_depth(ctx_, cd.verify_depth);
635
636 // Cipher configuration. TLS 1.2-and-below use the cipher list;
637 // TLS 1.3 uses the separate ciphersuites API. The security level
638 // is deliberately left at the library default: a weak cipher
639 // string should fail loudly rather than be silently permitted via
640 // a forced @SECLEVEL=0. Callers that genuinely need a lower level
641 // can express it in the cipher string (e.g. "...:@SECLEVEL=0").
642 // A cipher string the library rejects must not silently fall back to
643 // the default suites; fail closed instead.
644
4/4
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 2044 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 2 times.
2052x if (!cd.ciphersuites.empty() &&
645
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4x !SSL_CTX_set_cipher_list(ctx_, cd.ciphersuites.c_str()))
646 2x setup_failed_ = true;
647
4/4
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 2041 times.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 4 times.
2055x if (!cd.ciphersuites_tls13.empty() &&
648
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7x !SSL_CTX_set_ciphersuites(ctx_, cd.ciphersuites_tls13.c_str()))
649 3x setup_failed_ = true;
650 4098x }
651
652 6147x ~openssl_native_context() override
653 6147x {
654
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 2048 times.
2049x if (ctx_)
655
1/2
✓ Branch 0 taken 2048 times.
✗ Branch 1 not taken.
2048x SSL_CTX_free(ctx_);
656 6147x }
657 };
658
659 inline openssl_native_context*
660 2069x get_openssl_native_context(tls_context_data const& cd)
661 {
662 static char key;
663
1/2
✓ Branch 0 taken 2049 times.
✗ Branch 1 not taken.
4118x auto* p = cd.find(&key, [&] { return new openssl_native_context(cd); });
664 2069x return static_cast<openssl_native_context*>(p);
665 }
666
667 //
668 // engine
669 //
670
671 namespace openssl {
672
673 5704x engine::~engine()
674 2852x {
675
2/2
✓ Branch 0 taken 785 times.
✓ Branch 1 taken 2067 times.
2852x if (ext_bio_)
676
1/2
✓ Branch 0 taken 2067 times.
✗ Branch 1 not taken.
2067x BIO_free(ext_bio_);
677
2/2
✓ Branch 0 taken 2067 times.
✓ Branch 1 taken 785 times.
2852x if (ssl_)
678
1/2
✓ Branch 0 taken 2067 times.
✗ Branch 1 not taken.
2067x SSL_free(ssl_);
679 5704x }
680
681 std::error_code
682 2069x engine::init(tls_context const& ctx)
683 {
684 2069x auto& cd = get_tls_context_data(ctx);
685 2069x nc_ = get_openssl_native_context(cd);
686
2/2
✓ Branch 0 taken 2067 times.
✓ Branch 1 taken 2 times.
2069x if (!nc_->ctx_)
687 {
688 // The cache retains a failed context build permanently (it
689 // never retries), so a later construction can reach here
690 // with an already-drained error queue; make_openssl_error(0)
691 // is falsy and would let the caller treat this as success and
692 // dereference a null ssl_ on first use. Report unconditionally
693 // rather than trust ERR_get_error() to be nonzero.
694 2x return std::make_error_code(std::errc::not_enough_memory);
695 }
696
697 2067x ssl_ = SSL_new(nc_->ctx_);
698
1/2
✓ Branch 0 taken 2067 times.
✗ Branch 1 not taken.
2067x if (!ssl_)
699 {
700 ✗ unsigned long err = ERR_get_error();
701 ✗ return make_openssl_error(err);
702 }
703
704 2067x BIO* int_bio = nullptr;
705
1/2
✓ Branch 0 taken 2067 times.
✗ Branch 1 not taken.
2067x if (!BIO_new_bio_pair(&int_bio, 0, &ext_bio_, 0))
706 {
707 ✗ unsigned long err = ERR_get_error();
708 ✗ SSL_free(ssl_);
709 ✗ ssl_ = nullptr;
710 ✗ return make_openssl_error(err);
711 }
712
713 2067x SSL_set_bio(ssl_, int_bio, int_bio);
714
715 2067x return {};
716 2069x }
717
718 void
719 67x engine::reset()
720 {
721 − if (!ssl_) // LCOV_EXCL_LINE reset() runs only on a used stream
722 − return; // LCOV_EXCL_LINE reset() runs only on a used stream
723
724 // Preserves SSL* and BIO pair, releases session state
725
2/2
✓ Branch 0 taken 66 times.
✓ Branch 1 taken 1 time.
67x if (SSL_clear(ssl_) != 1)
726 1x clear_failed_ = true;
727
728 // SSL_clear() retains the negotiated session so a subsequent
729 // handshake on this SSL* can resume it. Resumed handshakes skip
730 // certificate/hostname re-verification, which would let a changed
731 // set_hostname() go unchecked after reset(); drop it to force a
732 // full handshake. A failure leaves the old session resumable,
733 // defeating that guarantee, so latch the same failure flag
734 // SSL_clear() uses above.
735
2/2
✓ Branch 0 taken 66 times.
✓ Branch 1 taken 1 time.
67x if (SSL_set_session(ssl_, nullptr) != 1)
736 1x clear_failed_ = true;
737
738 // Drain stale data from the external BIO. Mirrors the driver's
739 // flush guard: a read failure here would otherwise spin the loop
740 // forever since the pending count never advances.
741 char drain[1024];
742
1/2
✓ Branch 0 taken 67 times.
✗ Branch 1 not taken.
67x while (BIO_ctrl_pending(ext_bio_) > 0)
743 {
744
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (BIO_read(ext_bio_, drain, sizeof(drain)) <= 0)
745 ✗ break;
746 }
747 67x }
748
749 bool
750 4147x engine::context_setup_failed() const noexcept
751 {
752 // Before the deferred init runs there is no native context to
753 // judge; prepare() re-checks once it exists.
754
2/2
✓ Branch 0 taken 2035 times.
✓ Branch 1 taken 2112 times.
4147x return nc_ && nc_->setup_failed_;
755 }
756
757 std::error_code
758 4147x engine::check_context() const noexcept
759 {
760
2/2
✓ Branch 0 taken 30 times.
✓ Branch 1 taken 4117 times.
4147x if (context_setup_failed())
761 30x return std::make_error_code(std::errc::invalid_argument);
762 4117x return {};
763 4147x }
764
765 std::error_code
766 2102x engine::check_session() const noexcept
767 {
768
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2100 times.
2102x if (clear_failed_)
769 2x return std::make_error_code(std::errc::invalid_argument);
770 2100x return {};
771 2102x }
772
773 std::error_code
774 2100x engine::prepare(
775 tls_context const& ctx, tls_role role, std::string const& hostname)
776 {
777 // Session creation is deferred from construction so a setup
778 // failure reports through the handshake completion.
779
2/2
✓ Branch 0 taken 65 times.
✓ Branch 1 taken 2035 times.
2100x if (!ssl_)
780 {
781
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2033 times.
2035x if (auto ec = init(ctx))
782 2x return ec;
783 // The driver's check_context gate ran before this init could
784 // populate the native context; re-check so a rejected
785 // configuration still fails closed on the first handshake.
786
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 2015 times.
2033x if (auto cec = check_context())
787 18x return cec;
788 2015x }
789
790 // The hostname applies to client handshakes only; a server
791 // handshake clears any name left by a prior client-role
792 // handshake so client certificates are never hostname-matched.
793 2080x std::string const no_name;
794
4/4
✓ Branch 0 taken 645 times.
✓ Branch 1 taken 1435 times.
✓ Branch 2 taken 2078 times.
✓ Branch 3 taken 2 times.
2080x if (!apply_hostname(role == tls_role::client ? hostname : no_name))
795 {
796 // Fail closed rather than handshake without the requested
797 // name check.
798 2x return std::make_error_code(std::errc::invalid_argument);
799 }
800
801 // Client offers its ALPN protocol list; the server selects via
802 // the context callback. Role is only known here, so install the
803 // offer per-session; fail closed rather than negotiate nothing
804 // silently.
805
3/4
✓ Branch 0 taken 1433 times.
✓ Branch 1 taken 645 times.
✓ Branch 2 taken 1433 times.
✗ Branch 3 not taken.
2078x if (role == tls_role::client && !apply_alpn_offer())
806 ✗ return std::make_error_code(std::errc::invalid_argument);
807
808 2078x return {};
809 2100x }
810
811 bool
812 2080x engine::apply_hostname(std::string const& hostname)
813 {
814 2080x return apply_hostname_verification(ssl_, hostname);
815 }
816
817 bool
818 1433x engine::apply_alpn_offer()
819 {
820
2/2
✓ Branch 0 taken 1431 times.
✓ Branch 1 taken 2 times.
1433x if (nc_->alpn_wire_.empty())
821 1431x return true;
822
823 // SSL_set_alpn_protos uses the inverted convention: 0 = success.
824 // A non-zero return (allocation failure) means the offer was not
825 // installed; the caller fails closed rather than negotiate nothing
826 // silently.
827 4x return SSL_set_alpn_protos(
828 2x ssl_,
829 2x reinterpret_cast<unsigned char const*>(nc_->alpn_wire_.data()),
830 4x static_cast<unsigned int>(nc_->alpn_wire_.size())) == 0;
831 1433x }
832
833 void
834 924x engine::capture_alpn(std::string& out) const
835 {
836 924x unsigned char const* data = nullptr;
837 924x unsigned int len = 0;
838 924x SSL_get0_alpn_selected(ssl_, &data, &len);
839
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 922 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
924x if (data && len)
840 2x out.assign(reinterpret_cast<char const*>(data), len);
841 924x }
842
843 engine_result
844 282208x engine::perform(engine_op op, void* data, std::size_t len)
845 {
846 // No session exists until the first handshake's deferred init;
847 // report I/O attempted before then instead of crashing on a null
848 // SSL handle.
849
2/2
✓ Branch 0 taken 282205 times.
✓ Branch 1 taken 3 times.
282208x if (!ssl_)
850 6x return {
851 engine_want::done,
852 3x std::make_error_code(std::errc::invalid_argument), 0};
853
854 282205x ERR_clear_error();
855
856 282205x int ret = 0;
857
6/6
✓ Branch 0 taken 3327 times.
✓ Branch 1 taken 2164 times.
✓ Branch 2 taken 131602 times.
✓ Branch 3 taken 116448 times.
✓ Branch 4 taken 28338 times.
✓ Branch 5 taken 326 times.
282205x switch (op)
858 {
859 case engine_op::handshake_client:
860 3327x ret = SSL_connect(ssl_);
861 3327x break;
862 case engine_op::handshake_server:
863 2164x ret = SSL_accept(ssl_);
864 2164x break;
865 case engine_op::read:
866 131602x ret = SSL_read(ssl_, data, static_cast<int>(len));
867 131602x break;
868 case engine_op::write:
869 116448x ret = SSL_write(ssl_, data, static_cast<int>(len));
870 116448x break;
871 case engine_op::shutdown:
872 326x ret = SSL_shutdown(ssl_);
873 326x break;
874 }
875
876
2/2
✓ Branch 0 taken 103264 times.
✓ Branch 1 taken 122265 times.
282205x bool const transfer = op == engine_op::read || op == engine_op::write;
877
878 // SSL_shutdown returning 0 means our close_notify was queued but
879 // the peer's has not arrived yet: flush it, then read for it.
880
4/4
✓ Branch 0 taken 326 times.
✓ Branch 1 taken 225203 times.
✓ Branch 2 taken 232 times.
✓ Branch 3 taken 94 times.
225529x if (op == engine_op::shutdown && ret == 0)
881 282x return {
882 94x pending_output() > 0 ? engine_want::output_then_retry
883 : engine_want::input,
884 94x {},
885 0};
886
887
4/4
✓ Branch 0 taken 5723 times.
✓ Branch 1 taken 219712 times.
✓ Branch 2 taken 14169 times.
✓ Branch 3 taken 19892 times.
225435x if (transfer ? ret > 0 : ret == 1)
888 935524x return {
889 233881x pending_output() > 0 ? engine_want::output_then_done
890 : engine_want::done,
891 233881x {},
892
2/2
✓ Branch 0 taken 232835 times.
✓ Branch 1 taken 1046 times.
233881x transfer ? static_cast<std::size_t>(ret) : 0};
893
894 19892x int const err = SSL_get_error(ssl_, ret);
895
896
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 19891 times.
19892x if (err == SSL_ERROR_WANT_WRITE)
897 1x return {engine_want::output_then_retry, {}, 0};
898
899
2/2
✓ Branch 0 taken 19799 times.
✓ Branch 1 taken 92 times.
19891x if (err == SSL_ERROR_WANT_READ)
900 59397x return {
901 19799x pending_output() > 0 ? engine_want::output_then_retry
902 : engine_want::input,
903 19799x {},
904 0};
905
906
4/4
✓ Branch 0 taken 68 times.
✓ Branch 1 taken 24 times.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 64 times.
92x if (transfer && err == SSL_ERROR_ZERO_RETURN)
907 {
908 // ZERO_RETURN means the peer's close_notify WAS received (an
909 // announced close), unlike the terminal branch's empty-queue
910 // fallback below, which means an unannounced one; report eof,
911 // not stream_truncated. A received close_notify queues no
912 // output, so a plain done skips the flush.
913 64x return {engine_want::done, make_error_code(capy::error::eof), 0};
914 }
915
916 28x std::error_code ec;
917
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 25 times.
28x if (op == engine_op::shutdown)
918 {
919 3x unsigned long ssl_err = ERR_get_error();
920
3/4
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
3x if (ssl_err == 0 && err == SSL_ERROR_SYSCALL)
921 {
922 // The socket closed without an OpenSSL-level error, but
923 // that can mean either the peer's close_notify already
924 // arrived (this operation's fill, or a concurrent reader,
925 // consumed it) or the peer vanished mid-shutdown without
926 // ever sending one; only RECEIVED_SHUTDOWN tells them
927 // apart, and the documented contract promises
928 // stream_truncated for the latter, matching the read path
929 // and the driver's `map_fill_error` policy.
930 // The driver's map_fill_error reports the truncation
931 // before a BIO-pair engine can see SYSCALL.
932 ✗ ec =
933 − received_shutdown() // LCOV_EXCL_LINE driver maps truncation first
934 ✗ ? std::error_code{}
935 // LCOV_EXCL_LINE driver maps truncation first
936 ✗ : make_error_code(
937 capy::error::
938 stream_truncated); // LCOV_EXCL_LINE driver maps truncation first
939 ✗ }
940 else
941 {
942 3x ec = make_openssl_error(ssl_err);
943 }
944 3x }
945 else
946 {
947 // SYSCALL and every other terminal code map the same way;
948 // map_openssl_fatal() covers both.
949 25x ec = map_openssl_fatal();
950 }
951 84x return {
952 28x pending_output() > 0 ? engine_want::output_then_done
953 : engine_want::done,
954 28x ec, 0};
955 253870x }
956
957 std::size_t
958 59x engine::put_input(unsigned char const* data, std::size_t len)
959 {
960 59x int put = BIO_write(ext_bio_, data, static_cast<int>(len));
961
1/2
✓ Branch 0 taken 59 times.
✗ Branch 1 not taken.
59x return put > 0 ? static_cast<std::size_t>(put) : 0;
962 }
963
964 std::pair<unsigned char*, std::size_t>
965 17779x engine::input_area()
966 {
967 // A BIO pair hands out a pointer into its own buffer, so the
968 // transport reads ciphertext directly into the pair with no staging
969 // copy. The buffer is circular: this is only the contiguous run to
970 // the wrap, which the driver's read loop already tolerates.
971 17779x char* p = nullptr;
972 17779x int const cap = BIO_nwrite0(ext_bio_, &p);
973
3/4
✓ Branch 0 taken 17778 times.
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✓ Branch 3 taken 17778 times.
17779x if (cap <= 0 || !p)
974 1x return {nullptr, 0};
975 17778x return {reinterpret_cast<unsigned char*>(p), static_cast<std::size_t>(cap)};
976 17779x }
977
978 void
979 16695x engine::input_committed(std::size_t n)
980 {
981
1/2
✓ Branch 0 taken 16695 times.
✗ Branch 1 not taken.
16695x if (n == 0)
982 ✗ return;
983 // The bytes were written straight into the region BIO_nwrite0
984 // returned; advance the pair's write cursor to make them readable.
985 16695x char* p = nullptr;
986 16695x BIO_nwrite(ext_bio_, &p, static_cast<int>(n));
987 16695x }
988
989 std::size_t
990 630078x engine::pending_output() const
991 {
992 630078x return BIO_ctrl_pending(ext_bio_);
993 }
994
995 std::size_t
996 119562x engine::get_output(unsigned char* data, std::size_t len)
997 {
998 119562x int r = BIO_read(ext_bio_, data, static_cast<int>(len));
999
2/2
✓ Branch 0 taken 119559 times.
✓ Branch 1 taken 3 times.
119562x return r > 0 ? static_cast<std::size_t>(r) : 0;
1000 }
1001
1002 bool
1003 1090x engine::received_shutdown() const
1004 {
1005 1090x return (SSL_get_shutdown(ssl_) & SSL_RECEIVED_SHUTDOWN) != 0;
1006 }
1007
1008 } // namespace openssl
1009
1010 } // namespace detail
1011
1012 } // namespace boost::corosio
1013