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