Files
wolfssl/.github/workflows/ocsp.yml
T
Tobias Frauenschläger 1d15e1f27f Make OCSP stapling request ownership explicit
The OcspRequest carried a "void* ssl" back-pointer that the stapling
paths wrote just before handing the request to the OCSP layer. For the
request cached on the WOLFSSL_CTX that field is shared by every
connection using it, so concurrent handshakes raced on it. Drop the
field and pass the connection to CheckOcspRequest() and
CheckOcspResponse() as an argument instead, which is the only thing it
was ever read for.

Ownership of the cached request was equally implicit. Publication moves
out of CreateOcspRequest() into CreateOcspResponse(), and callers now
learn whether the CTX took ownership from a "ctxOwnsRequest" flag rather
than by comparing pointers against ssl->ctx->certOcspRequest, which was
read without the lock that guards it. The flag and the request are
handed back together on success and both left untouched on failure, so a
caller never decides ownership against a request it is not holding.

The cache is a field of the WOLFSSL_CTX, so serialize it with a lock
scoped to the CTX. SSL_CM(ssl) can resolve to a per-SSL cert manager
when WOLFSSL_LOCAL_X509_STORE is defined, which left two connections on
one CTX taking different locks for a check-then-set on the same pointer.
GetCtxOcspLock() keys off ssl->ctx->cm for both the reader and the
publisher, and a failure to take it is logged instead of silently
disabling the cache.

CheckOcspRequest() also loses its heap argument. It was only ever the
hint for the response buffer it hands back, which the caller frees
against the connection, so take it from the connection rather than from
a parameter every caller had to keep in step with its own free.

Smaller fixes in the same paths: zero the caller's response buffer
before the argument check can return, since SendCertificateStatus()
frees it without checking the return code; fold the ocsp_stapling NULL
check into the single early skip so the later uses need no guard;
gate the SetupOcspResp() free on success like the other two callers;
split the three differently owned requests in the
WOLFSSL_CSR2_OCSP_MULTI case into separate variables; and let that
case's allocation failures fall through to its shared cleanup instead of
returning, which leaked an already built leaf response.

Add test_ocsp_ctx_request_cache, which runs three handshakes over one
CTX pair and checks that the later ones reuse the cached request rather
than building another. The responder callback answers with a canned good
response, so stapling runs all the way through and the ownership
decision each connection makes is actually acted on: a connection that
freed the shared request shows up as a use after free on the next pass
and a double free at CTX teardown. The cached request is marked before
the last pass and the encoded request the callback sees is compared,
since a request rebuilt from the same certificate would otherwise be
identical byte for byte. The test is gated on !WOLFSSL_COPY_CERT:
OPENSSL_ALL implies it, and it gives every WOLFSSL its own certificate
copy, which takes the cache out of play. A new ocsp.yml job covers the
plain stapling build, an --enable-all build with the copy turned back
off, and an ASan build.

Also gate test_tls13_pha_status_request on KEEP_PEER_CERT. It checks the
received client certificate with wolfSSL_get_peer_certificate(), which is
only built when that macro is defined, so a post-handshake auth build with
stapling but without the OpenSSL compatibility layer failed to link
tests/unit.test.

Fixes F-7230 and F-7231.
2026-07-31 22:02:42 +02:00

151 lines
7.0 KiB
YAML

name: OCSP Test
# START OF COMMON SECTION
on:
push:
branches: [ 'release/**' ]
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches: [ '*' ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
# END OF COMMON SECTION
jobs:
ocsp_stapling:
name: ocsp stapling
if: ${{ (github.repository_owner == 'wolfssl') && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Checkout wolfSSL
uses: actions/checkout@v5
- name: Build wolfSSL
run: autoreconf -ivf && ./configure --enable-ocsp --enable-ocspstapling && make
- name: Start OCSP responder 1
run: openssl ocsp -port 22221 -ndays 1000 -index certs/ocsp/index-intermediate1-ca-issued-certs.txt -rsigner certs/ocsp/ocsp-responder-int1-cert.pem -rkey certs/ocsp/ocsp-responder-int1-key.pem -CA certs/ocsp/intermediate1-ca-cert.pem &
- name: Start OCSP responder 2
run: openssl ocsp -port 22220 -ndays 1000 -index certs/ocsp/index-ca-and-intermediate-cas.txt -rsigner certs/ocsp/ocsp-responder-cert.pem -rkey certs/ocsp/ocsp-responder-key.pem -CA certs/ocsp/root-ca-cert.pem &
- name: Start TLS server
run: ./examples/server/server -p 11111 -c ./certs/ocsp/server1-cert.pem -k ./certs/ocsp/server1-key.pem -d &
- name: Test Look Up
run: ./examples/client/client -A ./certs/ocsp/root-ca-cert.pem -o
ocsp_ssrf_screen:
name: ocsp responder SSRF screening
if: ${{ (github.repository_owner == 'wolfssl') && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Checkout wolfSSL
uses: actions/checkout@v5
# Build with the opt-in OCSP responder destination screening enabled
# (WOLFSSL_OCSP_SCREEN_RESPONDER). This guards against a certificate AIA
# OCSP URL driving an outbound request to an internal address (SSRF,
# CWE-918). The screening is off by default, so it is not exercised by
# the ocsp_stapling job above (which uses localhost responders).
- name: Build wolfSSL with OCSP responder screening enabled
run: autoreconf -ivf && ./configure --enable-ocsp CPPFLAGS=-DWOLFSSL_OCSP_SCREEN_RESPONDER && make
# Run only the boundary unit test, not the localhost OCSP test scripts:
# with screening on, 127.0.0.1 responders are (correctly) rejected, so
# the stapling scripts do not apply to this build. Assert the test
# actually ran (passed) rather than being compiled out and skipped, so a
# future build-define change cannot turn this into a false-green signal.
- name: Run OCSP destination screening boundary tests
run: |
./tests/unit.test -test_wolfIO_OcspDestAllowed | tee out.txt
grep -Eq 'test_wolfIO_OcspDestAllowed[^_].*: passed' out.txt
# The leaf OCSP request built for stapling is cached on the WOLFSSL_CTX and
# reused by every later connection on it, with the CTX owning it. None of the
# jobs above reach that cache: it is only populated when the SSL shares the
# CTX certificate buffer (ssl->buffers.weOwnCert == 0), and OPENSSL_ALL
# implies WOLFSSL_COPY_CERT, which gives every SSL its own copy instead.
ocsp_ctx_request_cache:
name: ocsp ctx request cache (${{ matrix.name }})
if: ${{ (github.repository_owner == 'wolfssl') && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
runs-on: ubuntu-24.04
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
# Plain stapling build: no OPENSSL_ALL, so no WOLFSSL_COPY_CERT and
# the cache is live.
- name: default
config: --enable-ocsp --enable-ocspstapling --enable-ocspstapling2
# The same cache under --enable-all, which pulls in OPENSSL_ALL and
# with it the compatibility-layer code paths around the cert manager.
# OPENSSL_ALL would otherwise force WOLFSSL_COPY_CERT and take the
# cache out of play entirely, so that is turned back off explicitly -
# which is what this entry is really here to prove.
- name: all, no cert copy
config: --enable-all CPPFLAGS=-DWOLFSSL_NO_COPY_CERT
# The cache hands one OcspRequest to many connections, so the failure
# mode of an ownership mistake is a double free or a use after free at
# CTX teardown rather than a wrong answer. ASan is what turns that into
# a test failure.
- name: asan
config: --enable-ocsp --enable-ocspstapling --enable-ocspstapling2 CFLAGS='-fsanitize=address -g' LDFLAGS='-fsanitize=address'
steps:
- name: workaround high-entropy ASLR
# Needed for the ASan build on this runner image; harmless for the rest.
run: sudo sysctl vm.mmap_rnd_bits=28
- name: Checkout wolfSSL
uses: actions/checkout@v5
- name: Build wolfSSL
run: autoreconf -ivf && ./configure ${{ matrix.config }} && make
# Assert on the counters rather than grepping the test name for "passed":
# the handshake under test logs to the same stream and splits the name and
# the result across lines. Running the one test on its own makes 0/0/1/1
# exact, and a build where the cache is compiled out reports 0/1/0/1
# instead - so a config change that quietly disables this cannot pass as
# green.
#
# Leak detection is off because wolfSSL's own unit.test has no verified
# clean LSan baseline; the double free and use after free this is here to
# catch are reported either way.
- name: Run the CTX OCSP request cache test
env:
ASAN_OPTIONS: detect_leaks=0
run: |
set -o pipefail
./tests/unit.test -test_ocsp_ctx_request_cache | tee out.txt
grep -Eq 'Failed/Skipped/Passed/All: 0/0/1/1' out.txt
ocsp_ssrf_screen_fallback:
name: ocsp responder SSRF screening (gethostbyname fallback)
if: ${{ (github.repository_owner == 'wolfssl') && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Checkout wolfSSL
uses: actions/checkout@v5
# Force the gethostbyname() resolver fallback (ac_cv_func_getaddrinfo=no)
# so the otherwise-untested fallback path of wolfIO_OcspDestAllowed is
# exercised. The fallback is IPv4-only and relies on glibc parsing
# numeric IPv4 literals locally.
- name: Build wolfSSL forcing the gethostbyname resolver fallback
run: autoreconf -ivf && ./configure --enable-ocsp ac_cv_func_getaddrinfo=no CPPFLAGS=-DWOLFSSL_OCSP_SCREEN_RESPONDER && make
- name: Run OCSP destination screening fallback boundary tests
run: |
./tests/unit.test -test_wolfIO_OcspDestAllowed_fallback | tee out.txt
grep -Eq 'test_wolfIO_OcspDestAllowed_fallback.*: passed' out.txt