TLS: OCSP stapling not implementable through the memory-BIO transport
Labels: tls, enhancement, blocked
Component: src/openssl, src/wolfssl, TLS stream transport
Blocked by: #312
Summary
corosio has no OCSP stapling API. An earlier implementation added
set_ocsp_staple() / set_require_ocsp_staple() on tls_context, but the
client could never retrieve the server's stapled response through corosio's
in-memory BIO transport, so the API was removed rather than shipped as a
silent no-op. This issue records what was observed, how to confirm it, and
how to move forward.
OCSP stapling lets the server attach a signed, time-stamped OCSP response to
its certificate during the handshake (TLS status_request extension +
CertificateStatus message), so the client learns revocation status without
contacting the CA. It is the modern alternative to CRLs.
Current state
- No public API. There is no
set_ocsp_staple / set_require_ocsp_staple
/ OCSP accessor anywhere in include/boost/corosio/.
- Revocation today is CRL-only (
add_crl() / add_crl_file() /
set_revocation_policy()); see tls_context.hpp.
- A stale docstring reference remains at
include/boost/corosio/tls_context.hpp:975 ("OCSP-based revocation is not
available") — harmless, can be dropped when this lands.
Why it is blocked
corosio does not give OpenSSL a socket. Each openssl_stream drives the SSL
state machine through an OpenSSL memory BIO pair and shuttles bytes to the
underlying stream itself:
App -> SSL_write -> int_bio -> BIO_read(ext_bio_) -> out_buf_ -> s_->write_some -> Network
App <- SSL_read <- int_bio <- BIO_write(ext_bio_) <- in_buf_ <- s_->read_some <- Network
(see the banner comment at src/openssl/src/openssl_stream.cpp:41 and
init_ssl's BIO_new_bio_pair at the SSL setup.)
Observed behavior: with a server that staples correctly, the client's
SSL_get_tlsext_status_ocsp_resp(ssl, &resp) returns -1 / no response
when the stream runs over the memory-BIO pair, even though the identical
OpenSSL configuration returns a valid response length when the two SSL
objects are connected by a real socket pair.
Leading hypothesis (needs confirmation). The handshake loop returns as
soon as SSL_connect / SSL_accept reports success and only flushes — it
never does a trailing read:
// src/openssl/src/openssl_stream.cpp (do_handshake, ~line 903)
if (ret == 1) {
used_ = true;
capture_alpn();
ec = co_await flush_output(); // <-- flush only, no final read_input()
co_return {ec};
}
Records the server emits at/after the tail of the handshake (the
CertificateStatus staple, and — see the session-resumption issue — the
NewSessionTicket) may still be sitting unread in the peer's buffer / the
ext_bio_ when the coroutine returns. Because both peers here are corosio
coroutines driving coupled buffers, neither side necessarily pumps the
other's trailing flight once its own SSL_* call has reported success.
How to confirm (reproduce both directions)
Two standalone probes, no corosio needed. Compile against the same OpenSSL
corosio links.
-
Socket-pair baseline (expected: works). Create SSL_CTXs, enable
stapling on the server:
- server:
SSL_CTX_set_tlsext_status_cb(cb) returning SSL_TLSEXT_ERR_OK
and setting a DER OCSP response via SSL_set_tlsext_status_ocsp_resp;
- client:
SSL_set_tlsext_status_type(ssl, TLSEXT_STATUSTYPE_ocsp).
Connect the two SSL objects over socketpair(AF_UNIX, SOCK_STREAM) with
SSL_set_fd, drive SSL_connect/SSL_accept to completion, then on the
client call SSL_get_tlsext_status_ocsp_resp(ssl, &resp) — expect a
positive length and a non-null pointer.
-
Memory-BIO reproduction (expected: fails). Same config, but wire each
SSL to a BIO_new_bio_pair and shuttle bytes between the two external
BIOs by hand (mirror flush_output / read_input in
openssl_stream.cpp). After the handshake, the client's
SSL_get_tlsext_status_ocsp_resp returns -1.
The delta between (1) and (2) is the transport bug. A prior investigation ran
exactly these two probes and saw the split.
Backend capability
- OpenSSL: full stapling support (
status_request, CertificateStatus,
SSL_get_tlsext_status_ocsp_resp, SSL_CTX_set_tlsext_status_cb). The only
blocker is the transport.
- WolfSSL: requires a build with
HAVE_OCSP +
HAVE_CERTIFICATE_STATUS_REQUEST (the vcpkg port used in CI enables OCSP;
the distro/minimal build may not). Same transport blocker applies; add a
wolfssl_supports_ocsp() capability gate mirroring
wolfssl_supports_crl() and fail closed for require_ocsp_staple on
incapable builds.
Way forward
- Root-cause the transport first (this unblocks session resumption too —
see tasks/tls/session-resumption.md, same underlying bug). Confirm the
hypothesis above by adding a bounded final read_input() pump after
SSL_connect/SSL_accept succeeds, and/or ensuring both coupled streams
drain each other's trailing records before the handshake coroutine
returns. Verify the memory-BIO probe then returns the staple.
- Re-introduce the API once retrieval works:
set_ocsp_staple(response)
(server) and set_require_ocsp_staple(bool) (client, fail closed when no
valid staple is presented). Wire the OpenSSL callbacks in the native
context; gate WolfSSL behind wolfssl_supports_ocsp().
- Test across backends with a generic test (mirror
testCrlRevocation):
a stapling server + a client that requires the staple completes; a client
that requires it against a non-stapling server fails closed.
Acceptance criteria
Pointers
src/openssl/src/openssl_stream.cpp:41 — BIO data-flow banner
src/openssl/src/openssl_stream.cpp — do_handshake (~863), flush_output
(~668), read_input (~699), init_ssl / BIO_new_bio_pair
include/boost/corosio/tls_context.hpp:975 — stale OCSP docstring to remove
TLS: OCSP stapling not implementable through the memory-BIO transport
Labels: tls, enhancement, blocked
Component:
src/openssl,src/wolfssl, TLS stream transportBlocked by: #312
Summary
corosio has no OCSP stapling API. An earlier implementation added
set_ocsp_staple()/set_require_ocsp_staple()ontls_context, but theclient could never retrieve the server's stapled response through corosio's
in-memory BIO transport, so the API was removed rather than shipped as a
silent no-op. This issue records what was observed, how to confirm it, and
how to move forward.
OCSP stapling lets the server attach a signed, time-stamped OCSP response to
its certificate during the handshake (TLS
status_requestextension +CertificateStatusmessage), so the client learns revocation status withoutcontacting the CA. It is the modern alternative to CRLs.
Current state
set_ocsp_staple/set_require_ocsp_staple/ OCSP accessor anywhere in
include/boost/corosio/.add_crl()/add_crl_file()/set_revocation_policy()); seetls_context.hpp.include/boost/corosio/tls_context.hpp:975("OCSP-based revocation is notavailable") — harmless, can be dropped when this lands.
Why it is blocked
corosio does not give OpenSSL a socket. Each
openssl_streamdrives the SSLstate machine through an OpenSSL memory BIO pair and shuttles bytes to the
underlying stream itself:
(see the banner comment at
src/openssl/src/openssl_stream.cpp:41andinit_ssl'sBIO_new_bio_pairat the SSL setup.)Observed behavior: with a server that staples correctly, the client's
SSL_get_tlsext_status_ocsp_resp(ssl, &resp)returns -1 / no responsewhen the stream runs over the memory-BIO pair, even though the identical
OpenSSL configuration returns a valid response length when the two
SSLobjects are connected by a real socket pair.
Leading hypothesis (needs confirmation). The handshake loop returns as
soon as
SSL_connect/SSL_acceptreports success and only flushes — itnever does a trailing read:
Records the server emits at/after the tail of the handshake (the
CertificateStatusstaple, and — see the session-resumption issue — theNewSessionTicket) may still be sitting unread in the peer's buffer / theext_bio_when the coroutine returns. Because both peers here are corosiocoroutines driving coupled buffers, neither side necessarily pumps the
other's trailing flight once its own
SSL_*call has reported success.How to confirm (reproduce both directions)
Two standalone probes, no corosio needed. Compile against the same OpenSSL
corosio links.
Socket-pair baseline (expected: works). Create
SSL_CTXs, enablestapling on the server:
SSL_CTX_set_tlsext_status_cb(cb)returningSSL_TLSEXT_ERR_OKand setting a DER OCSP response via
SSL_set_tlsext_status_ocsp_resp;SSL_set_tlsext_status_type(ssl, TLSEXT_STATUSTYPE_ocsp).Connect the two
SSLobjects oversocketpair(AF_UNIX, SOCK_STREAM)withSSL_set_fd, driveSSL_connect/SSL_acceptto completion, then on theclient call
SSL_get_tlsext_status_ocsp_resp(ssl, &resp)— expect apositive length and a non-null pointer.
Memory-BIO reproduction (expected: fails). Same config, but wire each
SSLto aBIO_new_bio_pairand shuttle bytes between the two externalBIOs by hand (mirror
flush_output/read_inputinopenssl_stream.cpp). After the handshake, the client'sSSL_get_tlsext_status_ocsp_respreturns -1.The delta between (1) and (2) is the transport bug. A prior investigation ran
exactly these two probes and saw the split.
Backend capability
status_request,CertificateStatus,SSL_get_tlsext_status_ocsp_resp,SSL_CTX_set_tlsext_status_cb). The onlyblocker is the transport.
HAVE_OCSP+HAVE_CERTIFICATE_STATUS_REQUEST(the vcpkg port used in CI enables OCSP;the distro/minimal build may not). Same transport blocker applies; add a
wolfssl_supports_ocsp()capability gate mirroringwolfssl_supports_crl()and fail closed forrequire_ocsp_stapleonincapable builds.
Way forward
see
tasks/tls/session-resumption.md, same underlying bug). Confirm thehypothesis above by adding a bounded final
read_input()pump afterSSL_connect/SSL_acceptsucceeds, and/or ensuring both coupled streamsdrain each other's trailing records before the handshake coroutine
returns. Verify the memory-BIO probe then returns the staple.
set_ocsp_staple(response)(server) and
set_require_ocsp_staple(bool)(client, fail closed when novalid staple is presented). Wire the OpenSSL callbacks in the native
context; gate WolfSSL behind
wolfssl_supports_ocsp().testCrlRevocation):a stapling server + a client that requires the staple completes; a client
that requires it against a non-stapling server fails closed.
Acceptance criteria
require_ocsp_staplefails the handshake closed when no valid staple ispresented, on OpenSSL and on capable WolfSSL builds.
skip the check.
Pointers
src/openssl/src/openssl_stream.cpp:41— BIO data-flow bannersrc/openssl/src/openssl_stream.cpp—do_handshake(~863),flush_output(~668),
read_input(~699),init_ssl/BIO_new_bio_pairinclude/boost/corosio/tls_context.hpp:975— stale OCSP docstring to remove