TLS: session resumption not observable through the memory-BIO transport
Labels: tls, enhancement, blocked
Component: src/openssl, src/wolfssl, TLS stream transport
Blocked by: #312
Summary
corosio has no session-resumption API. An explicit, opaque tls_session
handle plus session() / set_session() / session_reused() accessors were
designed and prototyped, but resumption could not be observed through
corosio's in-memory BIO transport: the server's NewSessionTicket (and, for
TLS 1.2, the session it establishes) never became resumable on the client, so
the API was deferred and removed rather than shipped non-functional. This
issue records the behavior, how to confirm it, and how to move forward.
Session resumption lets a client skip the full handshake on reconnect by
reusing a previously negotiated session (TLS 1.2 session IDs / TLS 1.3
tickets). It is a performance optimization, not a security feature — a
backend that cannot resume simply does a full handshake, so the policy is
best-effort, never fail-closed.
Current state
- No public API. No
tls_session, session(), set_session(), or
session_reused() in include/boost/corosio/.
- A full design exists and is still valid for re-introduction:
~/.claude/plans/create-a-plan-to-keen-lamport.md (opaque tls_session
handle over a shared_ptr<session_holder>; three non-pure virtuals on
tls_stream defaulting to empty/false; client-side first, server accept
layered later; in-memory only; TDD pinned to TLS 1.2).
Why it is blocked
Same root cause as OCSP stapling (see tasks/tls/ocsp-stapling.md): corosio
drives OpenSSL through a memory-BIO pair rather than a socket, and records
the server emits at/after the tail of the handshake are not pumped to the
client before the handshake coroutine returns.
Observed behavior:
-
TLS 1.3: after a full handshake the client's SSL_get1_session(ssl)
yields a session with SSL_SESSION_is_resumable() == 0. The TLS 1.3
NewSessionTicket is a post-handshake message — it arrives only once the
client performs a read after the handshake, but do_handshake returns
immediately on SSL_connect == 1 without that read:
// src/openssl/src/openssl_stream.cpp (do_handshake, ~line 903)
if (ret == 1) { used_ = true; capture_alpn();
ec = co_await flush_output(); co_return {ec}; } // no read
-
TLS 1.2: even though NewSessionTicket is part of the server's final
flight, a second connection reusing the captured session reported
SSL_session_reused(ssl) == 0 when both peers ran over the memory-BIO pair.
Over a real socket pair the same OpenSSL configuration resumes correctly
(round 1 SSL_SESSION_is_resumable() == 1; round 2 SSL_session_reused() == 1), which localizes the fault to the transport, not the SSL setup.
How to confirm (reproduce both directions)
Two standalone probes against the same OpenSSL corosio links; also worth
repeating for WolfSSL with wolfSSL_get_session / wolfSSL_set_session /
wolfSSL_session_reused.
-
Socket-pair baseline (expected: resumes). Give the server a session
id context (SSL_CTX_set_session_id_context) and a session cache; connect
two SSL objects over socketpair with SSL_set_fd. Round 1: full
handshake, then sess = SSL_get1_session(client) and assert
SSL_SESSION_is_resumable(sess) == 1 (for TLS 1.3, do a short
SSL_read/SSL_peek on the client first so the ticket is processed).
Round 2: new SSL objects from the same SSL_CTXs,
SSL_set_session(client, sess), handshake, assert
SSL_session_reused(client) == 1.
-
Memory-BIO reproduction (expected: does not resume). Same logic, but
wire each SSL to a BIO_new_bio_pair and shuttle bytes by hand (mirror
flush_output / read_input). Round 1 yields a non-resumable session;
round 2 reports SSL_session_reused == 0.
A prior investigation ran both and saw exactly this split.
Backend capability (verified on the linked libraries)
- OpenSSL: full —
SSL_get1_session (refcounted), SSL_set_session,
SSL_session_reused, TLS 1.2 IDs and TLS 1.3 tickets, cache modes. Server
accept needs a fixed SSL_CTX_set_session_id_context.
- WolfSSL (CI/vcpkg build has
HAVE_SESSION_TICKET; the distro/minimal
build does not, and OPENSSL_EXTRA is off there):
- native
wolfSSL_get_session / wolfSSL_set_session /
wolfSSL_session_reused are available → TLS 1.2 session-ID resumption
works and is observable;
- no TLS 1.3 tickets without
HAVE_SESSION_TICKET;
wolfSSL_get1_session / owning-dup and session serialization need
OPENSSL_EXTRA (absent in the minimal build), so a WolfSSL tls_session
is non-owning and valid only for its originating tls_context's
lifetime.
Way forward
- Root-cause the transport first (shared with OCSP stapling). Confirm the
hypothesis by adding a bounded post-handshake read_input() pump (so TLS
1.3 NewSessionTicket records are processed) and ensuring coupled streams
drain each other's trailing flight. Re-run the memory-BIO probe until
round-2 SSL_session_reused == 1.
- Re-introduce the API per the plan file: opaque
tls_session,
session() / set_session() / session_reused() (base defaults =
empty/false so any backend compiles honestly), OpenSSL owning holder
(SSL_SESSION_free) + server id-context, WolfSSL non-owning holder gated
by a wolfssl_supports_session_resumption() capability query. Best-effort:
never fail closed.
- Test across backends, TLS 1.2 pinned (deterministic and the only mode
the minimal WolfSSL can resume): round 1 captures a session, round 2 with
set_session reports session_reused() and transfers data. Base-class
negatives: empty/false before a handshake and for a fresh unshared
handshake.
Deferred sub-items (call out; do not silently skip)
- TLS 1.3 client ticket capture timing (
SSL_CTX_sess_set_new_cb).
- Server accept-resumption tuning (cache size/mode, TLS 1.3 server tickets).
- Serializable sessions (
i2d/d2i, OpenSSL-only) for cross-process caches.
Acceptance criteria
Pointers
~/.claude/plans/create-a-plan-to-keen-lamport.md — full design/plan
src/openssl/src/openssl_stream.cpp:41 — BIO data-flow banner
src/openssl/src/openssl_stream.cpp — do_handshake (~863, the ret == 1
branch is the suspect), flush_output (~668), read_input (~699)
tasks/tls/ocsp-stapling.md — same transport root cause
TLS: session resumption not observable through the memory-BIO transport
Labels: tls, enhancement, blocked
Component:
src/openssl,src/wolfssl, TLS stream transportBlocked by: #312
Summary
corosio has no session-resumption API. An explicit, opaque
tls_sessionhandle plus
session()/set_session()/session_reused()accessors weredesigned and prototyped, but resumption could not be observed through
corosio's in-memory BIO transport: the server's
NewSessionTicket(and, forTLS 1.2, the session it establishes) never became resumable on the client, so
the API was deferred and removed rather than shipped non-functional. This
issue records the behavior, how to confirm it, and how to move forward.
Session resumption lets a client skip the full handshake on reconnect by
reusing a previously negotiated session (TLS 1.2 session IDs / TLS 1.3
tickets). It is a performance optimization, not a security feature — a
backend that cannot resume simply does a full handshake, so the policy is
best-effort, never fail-closed.
Current state
tls_session,session(),set_session(), orsession_reused()ininclude/boost/corosio/.~/.claude/plans/create-a-plan-to-keen-lamport.md(opaquetls_sessionhandle over a
shared_ptr<session_holder>; three non-pure virtuals ontls_streamdefaulting to empty/false; client-side first, server acceptlayered later; in-memory only; TDD pinned to TLS 1.2).
Why it is blocked
Same root cause as OCSP stapling (see
tasks/tls/ocsp-stapling.md): corosiodrives OpenSSL through a memory-BIO pair rather than a socket, and records
the server emits at/after the tail of the handshake are not pumped to the
client before the handshake coroutine returns.
Observed behavior:
TLS 1.3: after a full handshake the client's
SSL_get1_session(ssl)yields a session with
SSL_SESSION_is_resumable() == 0. The TLS 1.3NewSessionTicketis a post-handshake message — it arrives only once theclient performs a read after the handshake, but
do_handshakereturnsimmediately on
SSL_connect == 1without that read:TLS 1.2: even though
NewSessionTicketis part of the server's finalflight, a second connection reusing the captured session reported
SSL_session_reused(ssl) == 0when both peers ran over the memory-BIO pair.Over a real socket pair the same OpenSSL configuration resumes correctly
(round 1
SSL_SESSION_is_resumable() == 1; round 2SSL_session_reused() == 1), which localizes the fault to the transport, not the SSL setup.How to confirm (reproduce both directions)
Two standalone probes against the same OpenSSL corosio links; also worth
repeating for WolfSSL with
wolfSSL_get_session/wolfSSL_set_session/wolfSSL_session_reused.Socket-pair baseline (expected: resumes). Give the server a session
id context (
SSL_CTX_set_session_id_context) and a session cache; connecttwo
SSLobjects oversocketpairwithSSL_set_fd. Round 1: fullhandshake, then
sess = SSL_get1_session(client)and assertSSL_SESSION_is_resumable(sess) == 1(for TLS 1.3, do a shortSSL_read/SSL_peekon the client first so the ticket is processed).Round 2: new
SSLobjects from the sameSSL_CTXs,SSL_set_session(client, sess), handshake, assertSSL_session_reused(client) == 1.Memory-BIO reproduction (expected: does not resume). Same logic, but
wire each
SSLto aBIO_new_bio_pairand shuttle bytes by hand (mirrorflush_output/read_input). Round 1 yields a non-resumable session;round 2 reports
SSL_session_reused == 0.A prior investigation ran both and saw exactly this split.
Backend capability (verified on the linked libraries)
SSL_get1_session(refcounted),SSL_set_session,SSL_session_reused, TLS 1.2 IDs and TLS 1.3 tickets, cache modes. Serveraccept needs a fixed
SSL_CTX_set_session_id_context.HAVE_SESSION_TICKET; the distro/minimalbuild does not, and
OPENSSL_EXTRAis off there):wolfSSL_get_session/wolfSSL_set_session/wolfSSL_session_reusedare available → TLS 1.2 session-ID resumptionworks and is observable;
HAVE_SESSION_TICKET;wolfSSL_get1_session/ owning-dup and session serialization needOPENSSL_EXTRA(absent in the minimal build), so a WolfSSLtls_sessionis non-owning and valid only for its originating
tls_context'slifetime.
Way forward
hypothesis by adding a bounded post-handshake
read_input()pump (so TLS1.3
NewSessionTicketrecords are processed) and ensuring coupled streamsdrain each other's trailing flight. Re-run the memory-BIO probe until
round-2
SSL_session_reused == 1.tls_session,session()/set_session()/session_reused()(base defaults =empty/false so any backend compiles honestly), OpenSSL owning holder
(
SSL_SESSION_free) + server id-context, WolfSSL non-owning holder gatedby a
wolfssl_supports_session_resumption()capability query. Best-effort:never fail closed.
the minimal WolfSSL can resume): round 1 captures a session, round 2 with
set_sessionreportssession_reused()and transfers data. Base-classnegatives: empty/false before a handshake and for a fresh unshared
handshake.
Deferred sub-items (call out; do not silently skip)
SSL_CTX_sess_set_new_cb).i2d/d2i, OpenSSL-only) for cross-process caches.Acceptance criteria
SSL_session_reused == 1) onOpenSSL and on a capable WolfSSL (TLS 1.2).
tls_session+ accessors re-introduced; best-effort, no fail-closed.constraint.
Pointers
~/.claude/plans/create-a-plan-to-keen-lamport.md— full design/plansrc/openssl/src/openssl_stream.cpp:41— BIO data-flow bannersrc/openssl/src/openssl_stream.cpp—do_handshake(~863, theret == 1branch is the suspect),
flush_output(~668),read_input(~699)tasks/tls/ocsp-stapling.md— same transport root cause