src/openssl/src/detail/engine.hpp

100.0% Lines (7/7) 100.0% List of functions (2/3) -% Branches (0/0)
engine.hpp
f(x) Functions (3)
Line TLA Hits Source Code
1 //
2 // Copyright (c) 2026 Steve Gerbino
3 //
4 // Distributed under the Boost Software License, Version 1.0. (See accompanying
5 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6 //
7 // Official repository: https://github.com/cppalliance/corosio
8 //
9
10 #ifndef SRC_OPENSSL_DETAIL_ENGINE_HPP
11 #define SRC_OPENSSL_DETAIL_ENGINE_HPP
12
13 #include <boost/corosio/detail/config.hpp>
14 #include <boost/corosio/tls_context.hpp>
15 #include <boost/corosio/tls_stream.hpp>
16
17 #include "src/tls/detail/engine_types.hpp"
18
19 #include <cstddef>
20 #include <string>
21 #include <system_error>
22 #include <utility>
23
24 // Opaque OpenSSL session handles, mirroring the vendor's own typedef
25 // targets (`typedef struct ssl_st SSL` / `typedef struct bio_st
26 // BIO`). This header stays vendor-free so a TU may hold both
27 // backends' engines: the real OpenSSL and WolfSSL headers cannot
28 // coexist (WolfSSL's compatibility layer clashes with genuine
29 // OpenSSL declarations).
30 struct ssl_st;
31 struct bio_st;
32
33 namespace boost::corosio {
34
35 namespace detail {
36
37 class openssl_native_context;
38
39 // Backend scope: both backends spell their engine `engine`, and both
40 // libraries (plus their unit tests) link into one binary, so each
41 // class needs a distinct qualified name.
42 namespace openssl {
43
44 /** Synchronous, transport-free OpenSSL record engine.
45
46 Owns the SSL session and its byte interface (a memory BIO pair)
47 and concentrates every SSL-result-to-error-code decision in one
48 mapping site (`perform`). The coroutine driver keeps the
49 transport, claims, and buffering; it only shuttles bytes through
50 `put_input` / `get_output` as directed by `engine_want` verdicts.
51
52 Data flow through the BIO pair:
53
54 App -> SSL_write -> int_bio -> get_output -> transport write
55 App <- SSL_read <- int_bio <- put_input <- transport read
56
57 @par Thread Safety
58 Distinct objects: Safe.@n
59 Shared objects: Unsafe.
60 */
61 // Exported so the transport-free engine unit tests can link against
62 // shared library builds (hidden visibility / DLL boundaries).
63 class BOOST_COROSIO_DECL engine
64 {
65 2122x ssl_st* ssl_ = nullptr;
66 2122x bio_st* ext_bio_ = nullptr;
67
68 // Cached at init; the per-context cache returns the same object
69 // for the stream's context every time, so one lookup suffices.
70 2122x openssl_native_context* nc_ = nullptr;
71
72 // Set when SSL_clear() or SSL_set_session() fails in reset().
73 // Neither has a documented partial-failure contract, so the SSL*
74 // is left in an unknown (or still-resumable) state; the driver
75 // refuses the next handshake instead of resuming on it. Engine-
76 // owned because only engine-internal operations can latch it.
77 2122x bool clear_failed_ = false;
78
79 public:
80 /** Whether a failed transport write keeps drained ciphertext.
81
82 Ciphertext the driver already drained from the engine when a
83 transport write fails is dropped; a later flush does not
84 resend it.
85 */
86
87 /// Destroy the engine, releasing the session and BIO pair.
88 ~engine();
89
90 6366x engine() = default;
91 engine(engine const&) = delete;
92 engine& operator=(engine const&) = delete;
93
94 /** Create the SSL session and BIO pair from a TLS context.
95
96 Called lazily by `prepare` on the first handshake so a setup
97 failure reports through the handshake completion. A context
98 whose native build failed is reported unconditionally: the
99 cache retains a failed build permanently and the error queue
100 may already be drained, so a queue-derived code could read as
101 success.
102
103 @param ctx The TLS context supplying the native `SSL_CTX`.
104
105 @return An error if the session could not be created.
106 */
107 std::error_code init(tls_context const& ctx);
108
109 /** Reset the session for a fresh handshake.
110
111 Preserves the `SSL*` and BIO pair, releases session state,
112 drops the negotiated session (a resumed handshake would skip
113 certificate/hostname re-verification), and drains stale bytes
114 from the output BIO. Failures latch `clear_failed()`.
115 */
116 void reset();
117
118 /// Check whether a prior `reset()` left the session unusable.
119 bool clear_failed() const noexcept
120 {
121 return clear_failed_;
122 }
123
124 /// Check whether the native context build rejected its configuration.
125 bool context_setup_failed() const noexcept;
126
127 /** Check that the native context can back a handshake.
128
129 A requested configuration could not be applied when the
130 native context was built (inverted protocol window, rejected
131 cipher/version, or an unparseable CRL); refuse the handshake
132 rather than proceed with weakened or unexpected settings.
133
134 @return An error when the context build rejected its
135 configuration.
136 */
137 std::error_code check_context() const noexcept;
138
139 /** Check that the session survived its last reset.
140
141 A failed `reset()` leaves the session in an unknown (or
142 still-resumable) state; refuse the next handshake rather than
143 resume on the unknown remainder of a failed clear.
144
145 @return An error when a prior `reset()` failed.
146 */
147 std::error_code check_session() const noexcept;
148
149 /** Prepare the session for a handshake in the given role.
150
151 Runs the deferred `init` on first use, then applies
152 SNI/hostname verification and installs the context's ALPN
153 offer, both for client handshakes only; a server handshake
154 clears any name left by a prior client-role handshake so
155 client certificates are never hostname-matched. Fails closed
156 rather than handshake without a requested check.
157
158 @param ctx The TLS context backing the deferred session build.
159 @param role Handshake role.
160 @param hostname Peer name for SNI/verification; empty for
161 none.
162
163 @return An error when a requested setting could not be
164 applied.
165 */
166 std::error_code
167 prepare(tls_context const& ctx, tls_role role, std::string const& hostname);
168
169 /** Apply SNI and hostname verification for the next handshake.
170
171 An empty hostname clears any previously applied name. IP
172 literals are excluded from SNI and matched against the
173 certificate's iPAddress entries instead of its DNS names.
174
175 @param hostname Peer name, or empty to clear.
176
177 @return `true` on success.
178 */
179 bool apply_hostname(std::string const& hostname);
180
181 /** Install the context's ALPN offer on the session.
182
183 No-op success when the context configured no protocols. Only
184 meaningful for client handshakes.
185
186 @return `true` when the offer (if any) was installed.
187 */
188 bool apply_alpn_offer();
189
190 /** Record the ALPN protocol selected during the handshake.
191
192 Assigns `out` only when a protocol was negotiated, leaving it
193 untouched otherwise.
194
195 @param out Receives the selected protocol.
196 */
197 void capture_alpn(std::string& out) const;
198
199 /** Run one synchronous engine step and map its outcome.
200
201 All error mapping lives here: a `done` verdict with a truthy
202 `ec` is terminal and already mapped. Output-first: when the
203 step leaves pending output, the verdict is
204 `output_then_retry` / `output_then_done` so ciphertext
205 reaches the peer before the driver parks on input. A received
206 close_notify (`read` / `write`) reports `eof` with a plain
207 `done`: it queues no output, so no flush precedes it.
208
209 @param op Which operation to advance.
210 @param data Application buffer (`read` / `write` only).
211 @param len Application buffer size in bytes.
212
213 @return The mapped verdict for this step.
214 */
215 engine_result perform(engine_op op, void* data, std::size_t len);
216
217 /** Stage transport bytes into the engine.
218
219 @param data Bytes received from the transport.
220 @param len Number of bytes offered.
221
222 @return The number of bytes accepted; may be zero when the
223 staging BIO is full.
224 */
225 std::size_t put_input(unsigned char const* data, std::size_t len);
226
227 /** Return the writable staging region for a zero-copy transport read.
228
229 The transport reads ciphertext directly into the returned span,
230 then reports how much landed via `input_committed`, avoiding the
231 copy a `put_input` deposit would incur. The region is the
232 contiguous run of the BIO pair's buffer, so its size may be less
233 than the total free space near the buffer's wrap.
234
235 @return Pointer and size of the contiguous writable region; the
236 size is zero when the staging BIO is full.
237 */
238 std::pair<unsigned char*, std::size_t> input_area();
239
240 /** Commit bytes the transport read into the `input_area` span.
241
242 @param n Number of bytes written into the region.
243 */
244 void input_committed(std::size_t n);
245
246 /// Return the number of ciphertext bytes awaiting transport write.
247 std::size_t pending_output() const;
248
249 /** Drain staged ciphertext for transport write.
250
251 @param data Destination buffer.
252 @param len Destination capacity in bytes.
253
254 @return The number of bytes drained; zero when nothing could
255 be read.
256 */
257 std::size_t get_output(unsigned char* data, std::size_t len);
258
259 /// Check whether the peer's close_notify has been received.
260 bool received_shutdown() const;
261
262 /// Return the underlying session handle (tests only).
263 3x ssl_st* native_handle() const noexcept
264 {
265 3x return ssl_;
266 }
267 };
268
269 } // namespace openssl
270
271 } // namespace detail
272
273 } // namespace boost::corosio
274
275 #endif
276