src/openssl/src/detail/engine.cpp

88.9% Lines (362 / 407, 13 excl) 100.0% Functions (34 / 34)
engine.cpp
f(x) Functions (34)
Function Calls Lines Blocks
boost::corosio::(anonymous namespace)::tls_method_compat() :34 1712x 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&) :44 1737x 78.6% 89.0% boost::corosio::(anonymous namespace)::openssl_proto_version(boost::corosio::tls_version) :74 3424x 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&) :83 4x 87.5% 83.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% 73.0% boost::corosio::(anonymous namespace)::make_openssl_error(unsigned long) :117 28x 60.0% 70.0% boost::corosio::(anonymous namespace)::map_openssl_fatal() :132 25x 80.0% 80.0% boost::corosio::openssl_category() :143 35x 100.0% 100.0% boost::corosio::detail::password_callback(char*, int, int, void*) :158 2x 100.0% 88.0% boost::corosio::detail::verify_callback_trampoline(int, x509_store_ctx_st*) :186 10x 89.5% 82.0% boost::corosio::detail::alpn_select_cb(ssl_st*, unsigned char const**, unsigned char*, unsigned char const*, unsigned int, void*) :245 2x 93.3% 89.0% boost::corosio::detail::sni_callback(ssl_st*, int*, void*) :284 12x 100.0% 94.0% boost::corosio::detail::openssl_native_context::openssl_native_context(boost::corosio::detail::tls_context_data const&) :325 1712x 90.7% 90.0% boost::corosio::detail::openssl_native_context::~openssl_native_context() :652 3424x 100.0% 100.0% boost::corosio::detail::get_openssl_native_context(boost::corosio::detail::tls_context_data const&) :660 1717x 100.0% 100.0% boost::corosio::detail::get_openssl_native_context(boost::corosio::detail::tls_context_data const&)::{lambda()#1}::operator()() const :663 1712x 100.0% 71.0% boost::corosio::detail::openssl::engine::~engine() :673 2328x 100.0% 100.0% boost::corosio::detail::openssl::engine::init(boost::corosio::tls_context const&) :682 1717x 58.8% 70.0% boost::corosio::detail::openssl::engine::reset() :719 65x 50.0% 61.0% boost::corosio::detail::openssl::engine::context_setup_failed() const :750 3443x 100.0% 100.0% boost::corosio::detail::openssl::engine::check_context() const :758 3443x 100.0% 100.0% boost::corosio::detail::openssl::engine::check_session() const :766 1748x 75.0% 83.0% boost::corosio::detail::openssl::engine::prepare(boost::corosio::tls_context const&, boost::corosio::tls_role, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) :774 1748x 76.9% 79.0% boost::corosio::detail::openssl::engine::apply_hostname(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) :812 1737x 100.0% 100.0% boost::corosio::detail::openssl::engine::apply_alpn_offer() :818 1173x 100.0% 100.0% boost::corosio::detail::openssl::engine::capture_alpn(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&) const :834 766x 100.0% 100.0% boost::corosio::detail::openssl::engine::perform(boost::corosio::detail::engine_op, void*, unsigned long) :844 80136x 93.6% 90.0% boost::corosio::detail::openssl::engine::put_input(unsigned char const*, unsigned long) :958 59x 100.0% 100.0% boost::corosio::detail::openssl::engine::input_area() :965 3650x 83.3% 75.0% boost::corosio::detail::openssl::engine::input_committed(unsigned long) :979 2756x 80.0% 83.0% boost::corosio::detail::openssl::engine::pending_output() const :990 202231x 100.0% 100.0% boost::corosio::detail::openssl::engine::get_output(unsigned char*, unsigned long) :996 39571x 100.0% 100.0% boost::corosio::detail::openssl::engine::received_shutdown() const :1003 901x 100.0% 100.0%
Line 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 1712x tls_method_compat() noexcept
35 {
36 #if OPENSSL_VERSION_NUMBER >= 0x10100000L
37 1712x return TLS_method();
38 #else
39 return SSLv23_method();
40 #endif
41 }
42
43 inline bool
44 1737x 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 1737x 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 1737x bool const is_ip = name && detail::is_ip_literal(hostname);
56 1737x char const* dns_name = is_ip ? nullptr : name;
57
58 1737x if (SSL_set_tlsext_host_name(ssl, dns_name) != 1)
59 ✗ return false;
60
61 1737x auto* param = SSL_get0_param(ssl);
62 1737x if (!param)
63 ✗ return name == nullptr;
64
65 1737x if (X509_VERIFY_PARAM_set1_host(param, dns_name, 0) != 1)
66 ✗ return false;
67 1737x if (is_ip)
68 2x return X509_VERIFY_PARAM_set1_ip_asc(param, name) == 1;
69 1735x return X509_VERIFY_PARAM_set1_ip(param, nullptr, 0) == 1;
70 }
71
72 // Map a portable protocol version to the OpenSSL version constant.
73 inline int
74 3424x openssl_proto_version(tls_version v) noexcept
75 {
76 3424x 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 10x for (auto const& p : protocols)
87 {
88 12x if (p.empty() ||
89 − p.size() > 255) // LCOV_EXCL_LINE set_alpn validates eagerly
90 − continue; // LCOV_EXCL_LINE set_alpn validates eagerly
91 6x wire.push_back(static_cast<char>(p.size()));
92 6x wire.append(p);
93 }
94 4x return wire;
95 ✗ }
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 static_cast<unsigned long>(value), buf, sizeof(buf));
109 4x 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 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 }
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 25x unsigned long ssl_err = ERR_get_error();
135 25x if (ssl_err == 0)
136 ✗ return make_error_code(capy::error::stream_truncated);
137 25x return make_openssl_error(ssl_err);
138 }
139
140 } // namespace
141
142 std::error_category const&
143 35x openssl_category() noexcept
144 {
145 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 4x 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 2x ? 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 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 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 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 10x if (!ok && cd->revocation == tls_revocation_policy::soft_fail)
204 {
205 1x int const err = X509_STORE_CTX_get_error(store_ctx);
206 1x if (err == X509_V_ERR_UNABLE_TO_GET_CRL ||
207 ✗ err == X509_V_ERR_UNABLE_TO_GET_CRL_ISSUER ||
208 ✗ err == X509_V_ERR_CRL_HAS_EXPIRED ||
209 err == X509_V_ERR_CRL_NOT_YET_VALID)
210 1x ok = true;
211 }
212
213 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 6x int der_len = cert ? i2d_X509(cert, &der) : 0;
220
221 verify_context vc(
222 store_ctx, der,
223 6x der_len > 0 ? static_cast<std::size_t>(der_len) : 0);
224 6x ok = cd->verify_callback(ok, vc);
225
226 6x if (der)
227 6x OPENSSL_free(der);
228 }
229
230 10x return ok ? 1 : 0;
231 }
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 4x 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 3x for (auto const& pref : *prefs)
261 {
262 3x for (unsigned int i = 0; i + 1 <= inlen;)
263 {
264 2x unsigned int len = in[i];
265 2x if (i + 1 + len > inlen)
266 ✗ break; // malformed
267 3x 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 }
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 12x if (!servername)
288 4x return SSL_TLSEXT_ERR_NOACK;
289
290 8x SSL_CTX* ctx = SSL_get_SSL_CTX(ssl);
291 auto* cd = static_cast<tls_context_data const*>(
292 8x SSL_CTX_get_ex_data(ctx, sni_ctx_data_index));
293
294 8x if (cd && cd->servername_callback)
295 {
296 8x if (!cd->servername_callback(servername))
297 1x return SSL_TLSEXT_ERR_ALERT_FATAL;
298 }
299
300 7x return SSL_TLSEXT_ERR_OK;
301 }
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 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 1712x explicit openssl_native_context(tls_context_data const& cd)
326 3424x : ctx_(nullptr)
327 1712x , cd_(&cd)
328 {
329 1712x ctx_ = SSL_CTX_new(tls_method_compat());
330 1712x if (!ctx_)
331 ✗ return;
332
333 1712x if (sni_ctx_data_index < 0)
334 3x sni_ctx_data_index =
335 3x SSL_CTX_get_ex_new_index(0, nullptr, nullptr, nullptr, nullptr);
336
337 1712x SSL_CTX_set_ex_data(
338 ctx_, sni_ctx_data_index, const_cast<tls_context_data*>(&cd));
339
340 1712x if (cd.servername_callback)
341 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 1712x if (!cd.alpn_protocols.empty())
348 {
349 4x alpn_snapshot_ = cd.alpn_protocols;
350 4x SSL_CTX_set_alpn_select_cb(ctx_, alpn_select_cb, &alpn_snapshot_);
351 4x alpn_wire_ = build_alpn_wire(cd.alpn_protocols);
352 }
353
354 1712x SSL_CTX_set_mode(ctx_, SSL_MODE_ENABLE_PARTIAL_WRITE);
355 1712x SSL_CTX_set_mode(ctx_, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
356 #if defined(SSL_MODE_RELEASE_BUFFERS)
357 1712x 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 1712x if (cd.min_version > cd.max_version)
364 2x setup_failed_ = true;
365 1712x if (!SSL_CTX_set_min_proto_version(
366 ctx_, openssl_proto_version(cd.min_version)))
367 ✗ setup_failed_ = true;
368 1712x if (!SSL_CTX_set_max_proto_version(
369 ctx_, openssl_proto_version(cd.max_version)))
370 ✗ setup_failed_ = true;
371
372 1712x int verify_mode_flag = SSL_VERIFY_NONE;
373 1712x if (cd.verification_mode == tls_verify_mode::peer)
374 1150x verify_mode_flag = SSL_VERIFY_PEER;
375 562x 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 3419x bool const need_trampoline = cd.verify_callback ||
381 3419x cd.revocation != tls_revocation_policy::disabled;
382 1712x SSL_CTX_set_verify(
383 ctx_, verify_mode_flag,
384 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 1712x 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 7x BIO* bio = BIO_new_mem_buf(
397 7x cd.pkcs12_data.data(), static_cast<int>(cd.pkcs12_data.size()));
398 7x if (!bio)
399 ✗ setup_failed_ = true;
400 else
401 {
402 7x PKCS12* p12 = d2i_PKCS12_bio(bio, nullptr);
403 7x if (!p12)
404 2x setup_failed_ = true;
405 else
406 {
407 5x EVP_PKEY* pkey = nullptr;
408 5x X509* cert = nullptr;
409 5x STACK_OF(X509)* chain = nullptr;
410 5x if (PKCS12_parse(
411 p12, cd.pkcs12_password.c_str(), &pkey, &cert,
412 &chain))
413 {
414 3x if (cert)
415 3x SSL_CTX_use_certificate(ctx_, cert);
416 3x if (pkey)
417 3x SSL_CTX_use_PrivateKey(ctx_, pkey);
418 3x if (chain)
419 2x 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 1x X509* dup = X509_dup(sk_X509_value(chain, i));
426 2x if (!dup ||
427 1x !SSL_CTX_add_extra_chain_cert(ctx_, dup))
428 {
429 ✗ X509_free(dup);
430 ✗ setup_failed_ = true;
431 }
432 }
433 }
434 else
435 2x setup_failed_ = true;
436 5x EVP_PKEY_free(pkey);
437 5x X509_free(cert);
438 5x if (chain)
439 1x sk_X509_pop_free(chain, X509_free);
440 5x PKCS12_free(p12);
441 }
442 7x ERR_clear_error();
443 7x BIO_free(bio);
444 }
445 }
446
447 1712x 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 552x BIO* bio = BIO_new_mem_buf(
453 552x cd.entity_certificate.data(),
454 552x static_cast<int>(cd.entity_certificate.size()));
455 552x if (bio)
456 {
457 552x X509* cert = nullptr;
458 552x if (cd.entity_cert_format == tls_file_format::pem)
459 550x cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
460 else
461 2x cert = d2i_X509_bio(bio, nullptr);
462 552x if (cert)
463 {
464 550x SSL_CTX_use_certificate(ctx_, cert);
465 550x X509_free(cert);
466 }
467 else
468 {
469 2x setup_failed_ = true;
470 }
471 552x BIO_free(bio);
472 }
473 else
474 {
475 ✗ setup_failed_ = true;
476 }
477 }
478
479 1712x if (cd.pkcs12_data.empty() && !cd.certificate_chain.empty())
480 {
481 1x BIO* bio = BIO_new_mem_buf(
482 1x cd.certificate_chain.data(),
483 1x static_cast<int>(cd.certificate_chain.size()));
484 1x if (bio)
485 {
486 X509* entity =
487 1x PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
488 1x if (entity)
489 {
490 1x SSL_CTX_use_certificate(ctx_, entity);
491 1x X509_free(entity);
492 }
493
494 X509* cert;
495 2x while ((cert = PEM_read_bio_X509(
496 2x bio, nullptr, nullptr, nullptr)) != nullptr)
497 {
498 1x SSL_CTX_add_extra_chain_cert(ctx_, cert);
499 }
500 1x ERR_clear_error();
501 1x BIO_free(bio);
502 }
503 }
504
505 1712x if (cd.pkcs12_data.empty() && !cd.private_key.empty())
506 {
507 553x BIO* bio = BIO_new_mem_buf(
508 553x cd.private_key.data(), static_cast<int>(cd.private_key.size()));
509 553x if (bio)
510 {
511 553x EVP_PKEY* pkey = nullptr;
512 553x if (cd.private_key_format == tls_file_format::pem)
513 {
514 552x if (cd.password_callback)
515 2x pkey = PEM_read_bio_PrivateKey(
516 bio, nullptr, password_callback,
517 const_cast<tls_context_data*>(&cd));
518 else
519 550x pkey = PEM_read_bio_PrivateKey(
520 bio, nullptr, nullptr, nullptr);
521 }
522 else
523 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 553x if (pkey)
528 {
529 550x SSL_CTX_use_PrivateKey(ctx_, pkey);
530 550x EVP_PKEY_free(pkey);
531 }
532 else
533 {
534 3x setup_failed_ = true;
535 }
536 553x BIO_free(bio);
537 553x ERR_clear_error();
538 }
539 else
540 {
541 ✗ setup_failed_ = true;
542 }
543 }
544
545 1712x X509_STORE* store = SSL_CTX_get_cert_store(ctx_);
546 2880x 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 1168x BIO* bio = BIO_new_mem_buf(ca.data(), static_cast<int>(ca.size()));
555 1168x if (!bio)
556 {
557 ✗ setup_failed_ = true;
558 ✗ continue;
559 }
560 1168x for (bool added = false;; added = true)
561 {
562 2334x X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
563 2334x if (!cert)
564 {
565 1168x unsigned long const err = ERR_peek_last_error();
566 2332x if (!added || ERR_GET_LIB(err) != ERR_LIB_PEM ||
567 1164x ERR_GET_REASON(err) != PEM_R_NO_START_LINE)
568 5x setup_failed_ = true;
569 1168x break;
570 }
571 1166x if (X509_STORE_add_cert(store, cert) != 1 &&
572 ✗ ERR_GET_REASON(ERR_peek_last_error()) !=
573 X509_R_CERT_ALREADY_IN_HASH_TABLE)
574 ✗ setup_failed_ = true;
575 1166x X509_free(cert);
576 1166x }
577 1168x ERR_clear_error();
578 1168x 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 1712x if (cd.use_default_verify_paths)
586 1x SSL_CTX_set_default_verify_paths(ctx_);
587 1713x for (auto const& path : cd.verify_paths)
588 1x SSL_CTX_load_verify_locations(ctx_, nullptr, path.c_str());
589 1712x 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 1712x if (cd.revocation != tls_revocation_policy::disabled)
597 {
598 9x for (auto const& crl_data : cd.crls)
599 {
600 4x BIO* bio = BIO_new_mem_buf(
601 4x crl_data.data(), static_cast<int>(crl_data.size()));
602 4x if (!bio)
603 {
604 ✗ setup_failed_ = true;
605 ✗ continue;
606 }
607 // Accept PEM or DER (the documented contract). Try PEM first,
608 // then rewind and try DER.
609 X509_CRL* crl =
610 4x PEM_read_bio_X509_CRL(bio, nullptr, nullptr, nullptr);
611 4x if (!crl)
612 {
613 3x BIO_reset(bio);
614 3x crl = d2i_X509_CRL_bio(bio, nullptr);
615 }
616 4x if (crl)
617 {
618 1x X509_STORE_add_crl(store, crl);
619 1x X509_CRL_free(crl);
620 }
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 4x BIO_free(bio);
629 }
630 5x X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK);
631 5x ERR_clear_error();
632 }
633
634 1712x 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 1716x if (!cd.ciphersuites.empty() &&
645 4x !SSL_CTX_set_cipher_list(ctx_, cd.ciphersuites.c_str()))
646 2x setup_failed_ = true;
647 1719x if (!cd.ciphersuites_tls13.empty() &&
648 7x !SSL_CTX_set_ciphersuites(ctx_, cd.ciphersuites_tls13.c_str()))
649 3x setup_failed_ = true;
650 ✗ }
651
652 3424x ~openssl_native_context() override
653 1712x {
654 1712x if (ctx_)
655 1712x SSL_CTX_free(ctx_);
656 3424x }
657 };
658
659 inline openssl_native_context*
660 1717x get_openssl_native_context(tls_context_data const& cd)
661 {
662 static char key;
663 3429x auto* p = cd.find(&key, [&] { return new openssl_native_context(cd); });
664 1717x return static_cast<openssl_native_context*>(p);
665 }
666
667 //
668 // engine
669 //
670
671 namespace openssl {
672
673 2328x engine::~engine()
674 {
675 2328x if (ext_bio_)
676 1717x BIO_free(ext_bio_);
677 2328x if (ssl_)
678 1717x SSL_free(ssl_);
679 2328x }
680
681 std::error_code
682 1717x engine::init(tls_context const& ctx)
683 {
684 1717x auto& cd = get_tls_context_data(ctx);
685 1717x nc_ = get_openssl_native_context(cd);
686 1717x 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 ✗ return std::make_error_code(std::errc::not_enough_memory);
695 }
696
697 1717x ssl_ = SSL_new(nc_->ctx_);
698 1717x if (!ssl_)
699 {
700 ✗ unsigned long err = ERR_get_error();
701 ✗ return make_openssl_error(err);
702 }
703
704 1717x BIO* int_bio = nullptr;
705 1717x 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 1717x SSL_set_bio(ssl_, int_bio, int_bio);
714
715 1717x return {};
716 }
717
718 void
719 65x 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 65x if (SSL_clear(ssl_) != 1)
726 ✗ 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 65x if (SSL_set_session(ssl_, nullptr) != 1)
736 ✗ 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 65x while (BIO_ctrl_pending(ext_bio_) > 0)
743 {
744 ✗ if (BIO_read(ext_bio_, drain, sizeof(drain)) <= 0)
745 ✗ break;
746 }
747 }
748
749 bool
750 3443x 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 3443x return nc_ && nc_->setup_failed_;
755 }
756
757 std::error_code
758 3443x engine::check_context() const noexcept
759 {
760 3443x if (context_setup_failed())
761 23x return std::make_error_code(std::errc::invalid_argument);
762 3420x return {};
763 }
764
765 std::error_code
766 1748x engine::check_session() const noexcept
767 {
768 1748x if (clear_failed_)
769 ✗ return std::make_error_code(std::errc::invalid_argument);
770 1748x return {};
771 }
772
773 std::error_code
774 1748x 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 1748x if (!ssl_)
780 {
781 1683x if (auto ec = init(ctx))
782 ✗ 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 1683x if (auto cec = check_context())
787 11x return cec;
788 }
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 1737x std::string const no_name;
794 1737x if (!apply_hostname(role == tls_role::client ? hostname : no_name))
795 {
796 // Fail closed rather than handshake without the requested
797 // name check.
798 ✗ 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 1737x if (role == tls_role::client && !apply_alpn_offer())
806 ✗ return std::make_error_code(std::errc::invalid_argument);
807
808 1737x return {};
809 1737x }
810
811 bool
812 1737x engine::apply_hostname(std::string const& hostname)
813 {
814 1737x return apply_hostname_verification(ssl_, hostname);
815 }
816
817 bool
818 1173x engine::apply_alpn_offer()
819 {
820 1173x if (nc_->alpn_wire_.empty())
821 1171x 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 2x return SSL_set_alpn_protos(
828 ssl_,
829 2x reinterpret_cast<unsigned char const*>(nc_->alpn_wire_.data()),
830 4x static_cast<unsigned int>(nc_->alpn_wire_.size())) == 0;
831 }
832
833 void
834 766x engine::capture_alpn(std::string& out) const
835 {
836 766x unsigned char const* data = nullptr;
837 766x unsigned int len = 0;
838 766x SSL_get0_alpn_selected(ssl_, &data, &len);
839 766x if (data && len)
840 2x out.assign(reinterpret_cast<char const*>(data), len);
841 766x }
842
843 engine_result
844 80136x 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 80136x if (!ssl_)
850 return {
851 engine_want::done,
852 3x std::make_error_code(std::errc::invalid_argument), 0};
853
854 80133x ERR_clear_error();
855
856 80133x int ret = 0;
857 80133x switch (op)
858 {
859 2728x case engine_op::handshake_client:
860 2728x ret = SSL_connect(ssl_);
861 2728x break;
862 1849x case engine_op::handshake_server:
863 1849x ret = SSL_accept(ssl_);
864 1849x break;
865 38280x case engine_op::read:
866 38280x ret = SSL_read(ssl_, data, static_cast<int>(len));
867 38280x break;
868 36947x case engine_op::write:
869 36947x ret = SSL_write(ssl_, data, static_cast<int>(len));
870 36947x break;
871 329x case engine_op::shutdown:
872 329x ret = SSL_shutdown(ssl_);
873 329x break;
874 }
875
876 80133x 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 80133x if (op == engine_op::shutdown && ret == 0)
881 return {
882 94x pending_output() > 0 ? engine_want::output_then_retry
883 : engine_want::input,
884 {},
885 94x 0};
886
887 80039x if (transfer ? ret > 0 : ret == 1)
888 return {
889 74609x pending_output() > 0 ? engine_want::output_then_done
890 : engine_want::done,
891 {},
892 74609x transfer ? static_cast<std::size_t>(ret) : 0};
893
894 5430x int const err = SSL_get_error(ssl_, ret);
895
896 5430x if (err == SSL_ERROR_WANT_WRITE)
897 1x return {engine_want::output_then_retry, {}, 0};
898
899 5429x if (err == SSL_ERROR_WANT_READ)
900 return {
901 5334x pending_output() > 0 ? engine_want::output_then_retry
902 : engine_want::input,
903 {},
904 5334x 0};
905
906 95x 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 67x return {engine_want::done, make_error_code(capy::error::eof), 0};
914 }
915
916 28x std::error_code ec;
917 28x if (op == engine_op::shutdown)
918 {
919 3x unsigned long ssl_err = ERR_get_error();
920 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 }
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 return {
952 28x pending_output() > 0 ? engine_want::output_then_done
953 : engine_want::done,
954 28x ec, 0};
955 }
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 59x return put > 0 ? static_cast<std::size_t>(put) : 0;
962 }
963
964 std::pair<unsigned char*, std::size_t>
965 3650x 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 3650x char* p = nullptr;
972 3650x int const cap = BIO_nwrite0(ext_bio_, &p);
973 3650x if (cap <= 0 || !p)
974 ✗ return {nullptr, 0};
975 3650x return {reinterpret_cast<unsigned char*>(p), static_cast<std::size_t>(cap)};
976 }
977
978 void
979 2756x engine::input_committed(std::size_t n)
980 {
981 2756x 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 2756x char* p = nullptr;
986 2756x BIO_nwrite(ext_bio_, &p, static_cast<int>(n));
987 }
988
989 std::size_t
990 202231x engine::pending_output() const
991 {
992 202231x return BIO_ctrl_pending(ext_bio_);
993 }
994
995 std::size_t
996 39571x engine::get_output(unsigned char* data, std::size_t len)
997 {
998 39571x int r = BIO_read(ext_bio_, data, static_cast<int>(len));
999 39571x return r > 0 ? static_cast<std::size_t>(r) : 0;
1000 }
1001
1002 bool
1003 901x engine::received_shutdown() const
1004 {
1005 901x return (SSL_get_shutdown(ssl_) & SSL_RECEIVED_SHUTDOWN) != 0;
1006 }
1007
1008 } // namespace openssl
1009
1010 } // namespace detail
1011
1012 } // namespace boost::corosio
1013