Repository navigation
Conversation
|
There was a problem hiding this comment.
🟡 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
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
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
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.
3243bc7 to
f09206b
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
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
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
left a comment
There was a problem hiding this comment.
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
Fenrir's latest completed scan found no issues; clearing the prior automated change request.

Description
Two changes to how a
WOLFSSLholds the certificate, chain and key of itsWOLFSSL_CTX, one commit each. Based on master, not stacked on another PR.OPENSSL_ALLturns onWOLFSSL_COPY_CERTandWOLFSSL_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 everyWOLFSSL, and withOPENSSL_EXTRAthe 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.DerBufferonce it is on the CTX: every setter builds a new buffer and frees the old one. With the newWOLFSSL_DER_REFCOUNT, aDerBuffercarries 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_REFCOUNTis on withWOLFSSL_COPY_CERTorWOLFSSL_COPY_KEYwhen atomic operations are available or the build is single-threaded. Otherwise each SSL still gets a copy.WOLFSSL_BLIND_PRIVATE_KEYthe key is still copied: each SSL blinds it with its own mask and unblinds it in place.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.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 (noWOLFSSL_COPY_CERT, orWOLFSSL_NO_COPY_CERT).ctxOwnsRequestflag, as every request a caller holds is now its own.Review points
DerBufferlayout.DerBufferis inasn_public.h. WithWOLFSSL_DER_REFCOUNTit gains awolfSSL_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.SSL_CtxResourceFree()andwolfSSL_UnloadCertsKeys()zeroed the key beforeFreeDer(), 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 throughFreeKeyDer()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 callsSetSSL_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.WOLFSSL_DUAL_ALG_CERTS, the alternative key is still shared by plain pointer underWOLFSSL_COPY_KEY, as before this PR.ctx->chainOcspRequest[]is never filled anywhere, so there is nothing to drop there. Its use inSendCertificateStatus()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 throughwolfSSL_CTX_use_certificate()(the file loader withoutOPENSSL_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 checkpassed for:--enable-opensslall,--enable-all,--enable-opensslextraand the default configure. The first two ran with ASan.--enable-ocsp --enable-ocspstapling --enable-ocspstapling2and--enable-all CPPFLAGS=-DWOLFSSL_NO_COPY_CERT, both with ASan.--enable-opensslallplusWOLFSSL_NO_ATOMICS(copies) and plusWOLFSSL_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_ALLwolfSSL_new()wolfSSL_new()allocationswolfSSL_new()timeDerBuffer.OPENSSL_EXTRA: unchanged (3876 B forwolfSSL_new(), 3426 B resident per connection).OPENSSL_ALLand 608 bytes smaller withOPENSSL_EXTRA.Host (macOS arm64), same harness,
OPENSSL_ALLserverwolfSSL_new(): 5482 / 7072 / 9312 -> 4600 bytes for the three chains.