Skip to content

Share CTX certificates and keys with SSL objects by reference count - #12021

Open
LinuxJedi wants to merge 7 commits into
wolfSSL:masterfrom
LinuxJedi:ossl-ctx-cert-ref
Open

LinuxJedi wants to merge 7 commits into
wolfSSL:masterfrom
LinuxJedi:ossl-ctx-cert-ref

Conversation

@LinuxJedi

@LinuxJedi LinuxJedi commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

Description

Two changes to how a WOLFSSL holds the certificate, chain and key of its WOLFSSL_CTX, one commit each. Based on master, not stacked on another PR.

  • Share the CTX certificates and keys by reference count. OPENSSL_ALL turns on WOLFSSL_COPY_CERT and WOLFSSL_COPY_KEY (Add new option to always copy cert buffer for each SSL object #7867, New option to always copy over key to SSL object #8005), so that an SSL keeps what its CTX had when the SSL was made even if the CTX replaces or frees them later, as OpenSSL does. That was done by copying the certificate, chain and key into every WOLFSSL, and with OPENSSL_EXTRA the copies stay for the whole connection. So per-connection server memory grew with the size of the chain: 0.9 KB with an ECC leaf, 4.7 KB with an RSA leaf and two intermediates.
    • Nothing changes a certificate, chain or key DerBuffer once it is on the CTX: every setter builds a new buffer and frees the old one. With the new WOLFSSL_DER_REFCOUNT, a DerBuffer carries a reference count and the SSL holds a reference to the CTX's buffers instead of a copy.
    • ShareDer() takes the reference, or makes a copy where the count is not compiled in. FreeDer() frees the buffer, and zeroes a private key, at the last release.
    • WOLFSSL_DER_REFCOUNT is on with WOLFSSL_COPY_CERT or WOLFSSL_COPY_KEY when atomic operations are available or the build is single-threaded. Otherwise each SSL still gets a copy.
    • With WOLFSSL_BLIND_PRIVATE_KEY the key is still copied: each SSL blinds it with its own mask and unblinds it in place.
    • The SSL owns its reference (weOwnCert, weOwnCertChain, weOwnKey). The free paths, the OCSP request cache and the SNI / cert setup callback check from Prevent cert modification in callbacks #11886 therefore behave as they did with the copy.
  • Drop the CTX OCSP request when the CTX certificate is replaced. The leaf OCSP request built for TLS 1.2 stapling is cached on the CTX and reused by every connection that shares the CTX certificate. It was only freed in wolfSSL_CTX_free(). So after the certificate was replaced on a live CTX, the server kept asking the responder about, and stapling the status of, the old certificate. FreeCtxOcspRequestCache() now drops it, under the CTX OCSP lock, whenever the CTX leaf is replaced. The cache is only in use when the SSL shares the CTX certificate (no WOLFSSL_COPY_CERT, or WOLFSSL_NO_COPY_CERT).
    • Connections used to check the CTX's own cached request outside the lock, since the responder lookup can block and takes the same lock itself. A certificate reload during that lookup would now free the request under them. So the cache is only touched under the lock: it stores a copy of the request a connection built, and hands each connection a copy of its own. That removes the ctxOwnsRequest flag, as every request a caller holds is now its own.

Review points

  • DerBuffer layout. DerBuffer is in asn_public.h. With WOLFSSL_DER_REFCOUNT it gains a wolfSSL_Ref (an atomic int, never a mutex): +4 bytes on 32-bit targets, and none on 64-bit, where it fits in padding. Default builds are unchanged. wc_AllocDer() / wc_FreeDer() callers see a count of 1 and behave as before.
  • Key zeroing. SSL_CtxResourceFree() and wolfSSL_UnloadCertsKeys() zeroed the key before FreeDer(), whatever its type. With a shared buffer that would wipe a key other connections still use. Every release of a key slot (ctx->privateKey, ctx->altPrivateKey, ssl->buffers.key, ssl->buffers.altKey) now goes through FreeKeyDer() instead. It zeroes the buffer whatever its type when the last holder lets go: a key slot can hold a public key when the private key is on a device. The new test checks that freeing a second SSL leaves the shared key intact.
  • wolfSSL_set_SSL_CTX(). It now calls SetSSL_CTX_CertsAndKeys() instead of keeping its own copy of it. That also takes the alternative key's id, label and device id from the new CTX; before, they kept the old CTX's values.
  • wolfSSL_get_certificate(). It compares the SSL's certificate buffer with the CTX's by pointer before the byte compare, and no longer dereferences a NULL CTX certificate.
  • Not covered. With WOLFSSL_DUAL_ALG_CERTS, the alternative key is still shared by plain pointer under WOLFSSL_COPY_KEY, as before this PR.
  • Chain OCSP cache. ctx->chainOcspRequest[] is never filled anywhere, so there is nothing to drop there. Its use in SendCertificateStatus() is dead code, left alone here.

Testing

  • test_tls_ssl_keeps_ctx_certs() (new). It makes an SSL from a CTX holding an RSA leaf, chain and key. The CTX then moves to an ECC chain and key, and the CTX and a second SSL made from it are freed. The SSL must still hold the same bytes and complete a TLS 1.2 and a TLS 1.3 handshake on them.
  • test_ocsp_ctx_request_cache_cert_change() (new). The CTX switches from server1 to server2 through the file loader, then back through wolfSSL_CTX_use_certificate() (the file loader without OPENSSL_EXTRA). The test checks which certificate the request sent to the responder, and the one left cached, are for. It fails on master.
  • test_wolfSSL_set_SSL_CTX() now checks ownership and pointer identity for both the shared and the copied case.
  • make check passed for:
    • --enable-opensslall, --enable-all, --enable-opensslextra and the default configure. The first two ran with ASan.
    • --enable-ocsp --enable-ocspstapling --enable-ocspstapling2 and --enable-all CPPFLAGS=-DWOLFSSL_NO_COPY_CERT, both with ASan.
  • The API tests also passed with --enable-opensslall plus WOLFSSL_NO_ATOMICS (copies) and plus WOLFSSL_BLIND_PRIVATE_KEY.

Results

STM32U585 (Cortex-M33, newlib-nano malloc), server side of a TLS 1.3 handshake over memory I/O, master -> this PR, bytes:

OPENSSL_ALL ECC leaf ECC leaf, 2 intermediates RSA leaf, 2 intermediates
wolfSSL_new() 4922 -> 4064 6500 -> 4064 8740 -> 4064
Per-connection resident 4468 -> 3610 6046 -> 3610 8286 -> 3610
wolfSSL_new() allocations 72 -> 70 73 -> 70 73 -> 70
wolfSSL_new() time 2237 -> 2181 us 2347 -> 2179 us 2492 -> 2176 us
  • Whole run: the newlib heap high-water mark goes from 57312 to 53424 bytes.
  • Client side: 8 to 16 bytes more, the count in each DerBuffer.
  • OPENSSL_EXTRA: unchanged (3876 B for wolfSSL_new(), 3426 B resident per connection).
  • Flash: 560 bytes smaller with OPENSSL_ALL and 608 bytes smaller with OPENSSL_EXTRA.

Host (macOS arm64), same harness, OPENSSL_ALL server wolfSSL_new(): 5482 / 7072 / 9312 -> 4600 bytes for the three chains.

Copilot AI balanced review requested due to automatic review settings November 5, 2026 18:36
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

MemBrowse Memory Report · Explore what changed →

gcc-arm-cortex-m0plus

  • FLASH: .text +56 B (+0.1%, 70,771 B / 262,144 B, total: 27% used)

gcc-arm-cortex-m3

  • FLASH: .text +44 B (+0.0%, 131,717 B / 262,144 B, total: 50% used)

gcc-arm-cortex-m4

  • FLASH: .text +64 B (+0.0%, 208,972 B / 262,144 B, total: 80% used)

gcc-arm-cortex-m4-crypto-only

  • FLASH: .text +64 B (+0.0%, 181,715 B / 262,144 B, total: 69% used)

gcc-arm-cortex-m4-min-ecc

  • FLASH: .text +64 B (+0.1%, 68,057 B / 262,144 B, total: 26% used)

gcc-arm-cortex-m4-openssl-compat

  • FLASH: .text -128 B (-0.0%, 800,420 B / 1,048,576 B, total: 76% used)

gcc-arm-cortex-m4-pkcs7

  • FLASH: .text +64 B (+0.0%, 222,804 B / 262,144 B, total: 85% used)

gcc-arm-cortex-m4-pq

  • FLASH: .text +64 B (+0.0%, 312,720 B / 1,048,576 B, total: 30% used)

gcc-arm-cortex-m4-rsa-only

  • FLASH: .text +64 B (+0.0%, 343,504 B / 1,048,576 B, total: 33% used)

gcc-arm-cortex-m4-sp-math

  • FLASH: .text +64 B (+0.1%, 68,057 B / 262,144 B, total: 26% used)

gcc-arm-cortex-m4-tls12

  • FLASH: .text +64 B (+0.0%, 132,497 B / 262,144 B, total: 51% used)

gcc-arm-cortex-m4-tls13

  • FLASH: .text +64 B (+0.0%, 249,442 B / 262,144 B, total: 95% used)

gcc-arm-cortex-m7-pq

  • FLASH: .text +64 B (+0.0%, 313,552 B / 1,048,576 B, total: 30% used)

gcc-arm-cortex-m7-tls13

  • FLASH: .text +64 B (+0.0%, 249,442 B / 262,144 B, total: 95% used)

No memory changes detected for:


Catch memory regressions in CI with MemBrowse. Free for open source.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The hand-built fakeChain test fixtures leave reference counts at zero, causing CTX teardown to leak their allocations when WOLFSSL_DER_REFCOUNT is enabled.

1 open issue
What changed in this PR

This PR reduces per-connection memory by sharing CTX certificate, chain, and key buffers through reference counts while preserving SSL ownership semantics. It also invalidates cached OCSP requests when the CTX certificate changes.

Changes:

  • Add reference-counted DER sharing, with copying retained for unsupported configurations and blinded private keys.
  • Reuse certificate/key setup during CTX switching and defer key zeroing until final release.
  • Add regression coverage for certificate retention, ownership, and OCSP cache invalidation.
File Description
wolfssl/​wolfcrypt/​settings.h Clarifies certificate and key retention semantics.
wolfssl/​wolfcrypt/​asn.h Declares ShareDer().
wolfssl/​wolfcrypt/​asn_public.h Adds conditional DER reference-count storage.
wolfssl/​internal.h Declares shared setup and OCSP cache helpers.
wolfcrypt/​src/​asn.c Implements DER reference acquisition and final-release cleanup.
tests/​api/​test_tls.h Registers the certificate-retention test.
tests/​api/​test_tls.c Tests retained credentials across CTX replacement and handshakes.
tests/​api/​test_ocsp.h Declares the OCSP certificate-change test.
tests/​api/​test_ocsp.c Tests cache invalidation after certificate rotation.
tests/​api.c Updates ownership assertions and registers OCSP coverage.
src/​ssl.c Reuses certificate/key setup when switching contexts.
src/​ssl_load.c Invalidates OCSP requests when replacing CTX certificates.
src/​ssl_api_cert.c Defers key zeroing and updates certificate comparisons.
src/​internal.c Shares credential buffers and implements OCSP cache cleanup.
.github/​workflows/​ocsp.yml Updates comments describing certificate ownership.

Review effort: Balanced

Comment thread wolfcrypt/src/asn.c Resolved

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fenrir Automated Review — PR #12021

Scan targets checked: wolfcrypt-src, wolfcrypt-bugs, wolfssl-src, wolfssl-bugs
Not reviewed (no scan target covers them): .github/workflows/ocsp.yml, tests/api/test_ocsp.h, tests/api/test_tls.h
Coverage: 12 of 12 in-scope changed file(s) opened by the reviewer

Findings: 1
1 finding(s) posted as inline comments (see file-level comments below)

This review was generated automatically by Fenrir. Reported findings require changes before merge.

Review tier: Max

Comment thread tests/api/test_tls.c Resolved
The leaf OCSP request that TLS 1.2 stapling builds is cached on the
WOLFSSL_CTX (certOcspRequest) and reused by every later connection that
shares the CTX certificate. Only wolfSSL_CTX_free() freed it, so after
the certificate was replaced on a live CTX the server kept asking the
responder about, and stapling the status of, the certificate it used to
have.

FreeCtxOcspRequestCache() takes the cached request off the CTX under the
CTX OCSP lock and frees it. ProcessBufferCertHandleDer(), which all the
CTX certificate and chain loaders go through, and
wolfSSL_CTX_use_certificate() call it when they replace the leaf. The
next connection that staples builds and caches a request for the new
certificate. GetCtxOcspLock() now takes the CTX so that both sides can
use it.

Only connections that share the CTX certificate use the cached request,
and replacing that certificate frees the buffer they share, so none of
them can still be using it. The lock orders the drop against a
connection publishing a request. The chain cache (chainOcspRequest[]) is
never filled, so there is nothing to drop there.

test_ocsp_ctx_request_cache_cert_change() switches the CTX from server1
to server2 with the file loader and back with
wolfSSL_CTX_use_certificate() (the file loader without OPENSSL_EXTRA),
and checks which certificate the request sent to the responder, and the
one left cached, are for.
OPENSSL_ALL turns on WOLFSSL_COPY_CERT and WOLFSSL_COPY_KEY so that an
SSL keeps the certificate, chain and key its CTX had when the SSL was
made, even if the CTX replaces or frees them later, as OpenSSL does.
SetSSL_CTX_CertsAndKeys() did that by copying all three into every
WOLFSSL, and with OPENSSL_EXTRA they stay for the life of the
connection. On a Cortex-M33 server that is 0.9 KB per connection with
an ECC leaf and 4.7 KB with an RSA leaf and two intermediates.

Nothing changes a certificate, chain or key DerBuffer once it is on the
CTX: every setter builds a new buffer and frees the old one. So with
WOLFSSL_DER_REFCOUNT a DerBuffer carries a reference count and the SSL
holds a reference to the CTX's buffers instead of a copy. ShareDer()
takes the reference, or makes a copy where the count is not compiled
in, and FreeDer() frees, and zeroes a private key, at the last release.
WOLFSSL_DER_REFCOUNT is on with WOLFSSL_COPY_CERT or WOLFSSL_COPY_KEY
when atomic operations are available or the build is single-threaded;
otherwise each SSL still gets a copy. With WOLFSSL_BLIND_PRIVATE_KEY the
key is still copied, as each SSL blinds it with its own mask and
unblinds it in place.

The SSL owns its reference (weOwnCert, weOwnCertChain, weOwnKey), so the
free paths, the OCSP request cache and the SNI and certificate setup
callback checks behave as they did with the copy. SSL_CtxResourceFree()
and wolfSSL_UnloadCertsKeys() no longer zero the key before FreeDer(),
which would wipe a key other connections still use; FreeDer() already
zeroes private keys. wolfSSL_set_SSL_CTX() now calls
SetSSL_CTX_CertsAndKeys() instead of keeping its own copy of it, which
also takes the alternative key's id, label and device id from the new
CTX. wolfSSL_get_certificate() compares the buffers by pointer first.

test_tls_ssl_keeps_ctx_certs() replaces the CTX certificate, chain and
key after wolfSSL_new(), frees the CTX and another SSL made from it, and
checks that the SSL still holds the same bytes and completes the
handshake on them.

Server wolfSSL_new() on an STM32U585 with OPENSSL_ALL, bytes:
  ECC leaf                     4922 -> 4064
  ECC leaf, 2 intermediates    6500 -> 4064
  RSA leaf, 2 intermediates    8740 -> 4064
OPENSSL_EXTRA is unchanged (3876).
test_wolfSSL_CTX_add_extra_chain_cert_counts() and
test_wolfSSL_add_to_chain_overflow() built a DerBuffer by hand, which
left its new reference count at zero. With WOLFSSL_DER_REFCOUNT, freeing
the CTX then took the count below zero without freeing the buffer, and
LeakSanitizer reported it. Both tests now allocate the buffer with
AllocDer(), and replace and free chains with FreeDer().

Also rename the refErr local in AllocDer() to err, which codespell
flagged.
SSL_CtxResourceFree() and wolfSSL_UnloadCertsKeys() zeroed the key
buffer before FreeDer() whatever its type. Sharing the CTX keys by
reference count dropped that, as it would wipe a key other holders still
use, and left the zeroing to FreeDer(), which only zeroes
PRIVATEKEY_TYPE and ALT_PRIVATEKEY_TYPE buffers. A key slot can also
hold a public key when the private key is on a device, which
test_wc_CryptoCb() does, and WOLFSSL_CHECK_MEM_ZERO caught that buffer
being freed without being zeroed.

FreeKeyDer() frees like FreeDer() but zeroes the buffer whatever its
type when the last holder lets go. Every release of ctx->privateKey,
ctx->altPrivateKey, ssl->buffers.key and ssl->buffers.altKey now uses
it, as with shared buffers any of those can be the last holder.
The check helper freed the saved copies of the certificate, chain and
key, but it runs inside ExpectIntEQ(), which skips it once an earlier
expectation has failed, so a failing run leaked them. The helper now
only compares, and the test frees the copies at the end.
check-api-guards.py requires !NO_FILESYSTEM and !NO_CERTS around calls
to wolfSSL_CTX_use_PrivateKey_file() and
wolfSSL_CTX_use_certificate_file(). The guards around the new OCSP cache
test and test_tls_ssl_keeps_ctx_certs() only implied them, so they now
name them.

The wolfSSL_UnloadCertsKeys() coverage in test_ssl_cert.c built a key
DerBuffer by hand, which left its reference count at zero, so with
WOLFSSL_DER_REFCOUNT it was never freed. It now uses AllocDer().

test_internal_nullguard_whitebox.c passes the SSL's CTX to
GetCtxOcspLock(), which now takes the CTX.

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fenrir Automated Review — PR #12021

Scan targets checked: wolfcrypt-src, wolfcrypt-bugs, wolfssl-src, wolfssl-bugs
Not reviewed (no scan target covers them): .github/workflows/ocsp.yml, tests/api/test_ocsp.h, tests/api/test_tls.h
Coverage: 6 of 11 in-scope changed file(s) opened by the reviewer; not opened: tests/api/test_ocsp.c, tests/api/test_ssl_cert.c, tests/api/test_tls.c, tests/unit-mcdc/test_internal_nullguard_whitebox.c, wolfssl/wolfcrypt/asn.h

Findings: 1
1 finding(s) posted as inline comments (see file-level comments below)

This review was generated automatically by Fenrir. Reported findings require changes before merge.

Review tier: Lite

Comment thread src/internal.c Resolved
GetCtxOcspRequest() read ctx->certOcspRequest under the CTX OCSP lock
and handed the CTX's own object to the connection, which then checked it
in CheckOcspRequest() without the lock. The connection that cached the
request did the same with it after publishing it. That check can block
on the responder, and it takes the same lock itself, so it cannot be
done under the lock. With FreeCtxOcspRequestCache() dropping the cache
when the CTX certificate is replaced, a reload during that check freed
the request under the connection.

The cache is now only touched under the lock. CreateOcspResponse()
caches a copy of the request it built, allocated from the CTX heap that
frees it, and GetCtxOcspRequest() returns a copy for the connection to
free. Every request a caller holds is now its own, so the ctxOwnsRequest
flag is gone. GetCtxOcspRequest() also skips the cache for a connection
with a certificate of its own, which it was not built from.

test_ocsp_ctx_request_cache_drop_in_use() drops the cache from inside
the responder lookup of a handshake whose request came from it, as a
certificate reload on another thread would, and FreeCtxOcspRequestCache()
is now WOLFSSL_TEST_VIS for that. Without this change ASan reports a use
after free in CompareOcspReqResp().

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fenrir Automated Review — PR #12021

Scan targets checked: wolfssl-src, wolfssl-bugs
Unchanged since last review (not re-run): wolfcrypt-src, wolfcrypt-bugs
Not reviewed (no scan target covers them): .github/workflows/ocsp.yml, tests/api/test_ocsp.h, tests/api/test_tls.h
Coverage: 2 of 5 in-scope changed file(s) opened by the reviewer; not opened: tests/api.c, tests/api/test_ocsp.c, wolfssl/internal.h

Fenrir result: Approved ✅

No new issues found in the changed files.

Advisory only — this automated result does not count as a GitHub approval.

Review tier: Lite

@wolfSSL-Fenrir-bot
wolfSSL-Fenrir-bot dismissed stale reviews from themself November 5, 2026 21:35

Fenrir's latest completed scan found no issues; clearing the prior automated change request.

@LinuxJedi LinuxJedi assigned wolfSSL-Bot and unassigned LinuxJedi Nov 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants