Skip to content

TLS: OCSP stapling #313

Description

@mvandeberg

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.

  1. 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.
  2. 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

  1. 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.
  2. 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().
  3. 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

  • The memory-BIO probe retrieves the server's OCSP staple on the client.
  • require_ocsp_staple fails the handshake closed when no valid staple is
    presented, on OpenSSL and on capable WolfSSL builds.
  • Incapable WolfSSL builds fail closed (capability-gated), never silently
    skip the check.
  • Cross-backend test added; docs updated.

Pointers

  • src/openssl/src/openssl_stream.cpp:41 — BIO data-flow banner
  • src/openssl/src/openssl_stream.cppdo_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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions