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 3095x ssl_st* ssl_ = nullptr;
66 3095x 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 3095x 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 3095x 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 9285x 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
120 clear_failed() const noexcept
121 {
122 return clear_failed_;
123 }
124
125 /// Check whether the native context build rejected its configuration.
126 bool context_setup_failed() const noexcept;
127
128 /** Check that the native context can back a handshake.
129
130 A requested configuration could not be applied when the
131 native context was built (inverted protocol window, rejected
132 cipher/version, or an unparseable CRL); refuse the handshake
133 rather than proceed with weakened or unexpected settings.
134
135 @return An error when the context build rejected its
136 configuration.
137 */
138 std::error_code check_context() const noexcept;
139
140 /** Check that the session survived its last reset.
141
142 A failed `reset()` leaves the session in an unknown (or
143 still-resumable) state; refuse the next handshake rather than
144 resume on the unknown remainder of a failed clear.
145
146 @return An error when a prior `reset()` failed.
147 */
148 std::error_code check_session() const noexcept;
149
150 /** Prepare the session for a handshake in the given role.
151
152 Runs the deferred `init` on first use, then applies
153 SNI/hostname verification and installs the context's ALPN
154 offer, both for client handshakes only; a server handshake
155 clears any name left by a prior client-role handshake so
156 client certificates are never hostname-matched. Fails closed
157 rather than handshake without a requested check.
158
159 @param ctx The TLS context backing the deferred session build.
160 @param role Handshake role.
161 @param hostname Peer name for SNI/verification; empty for
162 none.
163
164 @return An error when a requested setting could not be
165 applied.
166 */
167 std::error_code prepare(
168 tls_context const& ctx, tls_role role, std::string const& hostname);
169
170 /** Apply SNI and hostname verification for the next handshake.
171
172 An empty hostname clears any previously applied name. IP
173 literals are excluded from SNI and matched against the
174 certificate's iPAddress entries instead of its DNS names.
175
176 @param hostname Peer name, or empty to clear.
177
178 @return `true` on success.
179 */
180 bool apply_hostname(std::string const& hostname);
181
182 /** Install the context's ALPN offer on the session.
183
184 No-op success when the context configured no protocols. Only
185 meaningful for client handshakes.
186
187 @return `true` when the offer (if any) was installed.
188 */
189 bool apply_alpn_offer();
190
191 /** Record the ALPN protocol selected during the handshake.
192
193 Assigns `out` only when a protocol was negotiated, leaving it
194 untouched otherwise.
195
196 @param out Receives the selected protocol.
197 */
198 void capture_alpn(std::string& out) const;
199
200 /** Run one synchronous engine step and map its outcome.
201
202 All error mapping lives here: a `done` verdict with a truthy
203 `ec` is terminal and already mapped. Output-first: when the
204 step leaves pending output, the verdict is
205 `output_then_retry` / `output_then_done` so ciphertext
206 reaches the peer before the driver parks on input. A received
207 close_notify (`read` / `write`) reports `eof` with a plain
208 `done`: it queues no output, so no flush precedes it.
209
210 @param op Which operation to advance.
211 @param data Application buffer (`read` / `write` only).
212 @param len Application buffer size in bytes.
213
214 @return The mapped verdict for this step.
215 */
216 engine_result perform(engine_op op, void* data, std::size_t len);
217
218 /** Stage transport bytes into the engine.
219
220 @param data Bytes received from the transport.
221 @param len Number of bytes offered.
222
223 @return The number of bytes accepted; may be zero when the
224 staging BIO is full.
225 */
226 std::size_t put_input(unsigned char const* data, std::size_t len);
227
228 /** Return the writable staging region for a zero-copy transport read.
229
230 The transport reads ciphertext directly into the returned span,
231 then reports how much landed via `input_committed`, avoiding the
232 copy a `put_input` deposit would incur. The region is the
233 contiguous run of the BIO pair's buffer, so its size may be less
234 than the total free space near the buffer's wrap.
235
236 @return Pointer and size of the contiguous writable region; the
237 size is zero when the staging BIO is full.
238 */
239 std::pair<unsigned char*, std::size_t> input_area();
240
241 /** Commit bytes the transport read into the `input_area` span.
242
243 @param n Number of bytes written into the region.
244 */
245 void input_committed(std::size_t n);
246
247 /// Return the number of ciphertext bytes awaiting transport write.
248 std::size_t pending_output() const;
249
250 /** Drain staged ciphertext for transport write.
251
252 @param data Destination buffer.
253 @param len Destination capacity in bytes.
254
255 @return The number of bytes drained; zero when nothing could
256 be read.
257 */
258 std::size_t get_output(unsigned char* data, std::size_t len);
259
260 /// Check whether the peer's close_notify has been received.
261 bool received_shutdown() const;
262
263 /// Return the underlying session handle (tests only).
264 ssl_st*
265 3x native_handle() const noexcept
266 {
267 3x return ssl_;
268 }
269 };
270
271 } // namespace openssl
272
273 } // namespace detail
274
275 } // namespace boost::corosio
276
277 #endif
278