Repository navigation
Conversation
With WOLFSSL_DYNAMIC_PEER_CERT a connection now keeps only the DER of the peer's leaf certificate when it arrives, in ssl->peerCertDer. GetPeerCert() builds the WOLFSSL_X509 from it the first time it is used, for example by wolfSSL_get_peer_certificate(), and frees the encoding as the object has its own copy. A connection whose application never asks for the certificate no longer holds the decoded object, its two names and an ASN1_STRING per name entry. - SetPeerCertDer() keeps the encoding (without the flag it is SetPeerCert()) and is used when the leaf is kept in ProcessPeerCerts() and when the certificate is restored from a session ticket. - When a verify callback runs for the leaf, the object is built from the decoded leaf as before, so the callback's current certificate is still the kept object. - The object is built as d2i_X509() does. When memory runs out the encoding is kept for a later call. - GetPeerCertDer() gives the encoding without building the object; session ticket creation and wolfSSL_cmp_peer_cert_to_file() use it. - Only a leaf that parsed without error is kept as its encoding alone. One accepted despite an error, such as keyCertSign without being a CA cleared by the chain verify callback, is copied from the decoded leaf as before, as decoding it again would fail the same way. Established client connection with an ECC leaf on an STM32U585 (Cortex-M33, 160 MHz): resident heap 9409 -> 5697 bytes with OPENSSL_EXTRA and 11846 -> 6652 bytes with OPENSSL_ALL (3445 without the compat layer); handshake peak 14822 -> 12925 and 16304 -> 13294 bytes. Unchanged when a verify callback is set or once the certificate has been asked for; the first wolfSSL_get_peer_certificate() then takes about 1.2 ms.
|
There was a problem hiding this comment.
🟡 Changes recommended
The tests do not exercise wolfSSL_clear() while peerCert is NULL and peerCertDer is non-NULL, leaving DER-only cleanup unverified.
1 open issue
What changed in this PR
Reduces per-connection memory use in dynamic peer-certificate builds by retaining only the certificate’s DER encoding until an X509 object is requested.
Changes:
- Adds lazy X509 construction, with retry support after allocation failure.
- Uses DER directly for certificate comparison and session tickets while preserving verification-callback behavior.
- Adds tests for on-demand decoding, allocation failure, callback identity, and certificates accepted despite decode errors.
| File | Description |
|---|---|
wolfssl/internal.h |
Adds DER storage, decode-error tracking, and internal accessors. |
tests/api/test_tls13.c |
Checks that ticket re-verification leaves no retained DER. |
tests/api/test_ssl_cert.h |
Registers the new peer-certificate tests. |
tests/api/test_ssl_cert.c |
Tests lazy construction and accepted decode-error handling. |
src/ssl_api_cert.c |
Compares DER directly and adapts chain helpers to lazy construction. |
src/internal.c |
Implements DER retention, lazy decoding, cleanup, and handshake/ticket integration. |
Review effort: Balanced
check-api-guards.py cannot see that TEST_PEER_CERT_COPY_OOM is only defined with USE_WOLFSSL_MEMORY, so the out-of-memory part of test_wolfSSL_peer_cert_on_demand now names it next to the macro.
test_wolfSSL_peer_cert_on_demand cleared the connection only after the certificate object had been built from the encoding. A further handshake now clears it while only the encoding is kept, and checks that both the object and the encoding are gone.
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #12020
Scan targets checked: wolfssl-src, wolfssl-bugs
Not reviewed (no scan target covers them): tests/api/test_ssl_cert.h
Coverage: 5 of 5 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
In test_wolfSSL_peer_cert_kept_decode_err, wc_InitCert() is skipped when an earlier expectation fails, and with WOLFSSL_CERT_GEN_CACHE wc_SetCert_Free() would then free an uninitialized Cert. A certInit flag now guards it, as keyInit and rngInit do for the key and RNG.
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #12020
Scan targets checked: wolfssl-src, wolfssl-bugs
Not reviewed (no scan target covers them): tests/api/test_ssl_cert.h
Coverage: 1 of 1 in-scope changed file(s) opened by the reviewer
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
With
WOLFSSL_DYNAMIC_PEER_CERT(#11990, on by default withWOLFSSL_SMALL_STACK), a connection now keeps only the DER of the peer's leaf certificate when it arrives. TheWOLFSSL_X509is built from it the first time something asks for it, for examplewolfSSL_get_peer_certificate(). This follows #11914, #11991, #11992 and #11990 (all merged), is based on master, and is not stacked on another PR.On an established compat-layer client connection, most of the heap above a non-compat build was the decoded peer certificate kept by
KEEP_PEER_CERT. Host harness, ECC leaf,OPENSSL_EXTRAminus no compat layer = 7128 bytes, of which 5320 is the peer certificate:WOLFSSL_X509struct, with its two embedded names: 2936ASN1_STRINGper name entry, 14 of them: 1456OPENSSL_ALLadds a larger struct (4032), a stack node per name entry and anASN1_OBJECT. An application that never looks at the peer certificate paid all of this for the life of the connection.Changes
ssl->peerCertDerholds the leaf's encoding untilssl->peerCertis built from it, then is freed: the object has its own copy. At most one of the two is set.SetPeerCertDer()keeps the encoding. It is used whenProcessPeerCerts()keeps the leaf and when the certificate is restored from a session ticket. WithoutWOLFSSL_DYNAMIC_PEER_CERTit isSetPeerCert(), so those builds are unchanged.GetPeerCert()is now a function in dynamic builds. When only the encoding is kept it builds the object asd2i_X509()does (NO_VERIFY, names added straight to the X509) and keeps it in its place. When memory runs out the encoding is kept so a later call can try again. A copy that fails part way is still kept, asSetPeerCert()does.GetPeerCertDer()returns the encoding without building the object. Session ticket creation andwolfSSL_cmp_peer_cert_to_file()use it.SetupStoreCtxCallback()builds the object from the already decoded leaf, not from the encoding. The callback's current certificate is still the kept object, so pointer identity and ex_data set in the callback behave as before.ParseCertRelative()returned no error is kept as its encoding alone (newProcPeerCertArgs.leafDecodeErr). Some errors come back even withNO_VERIFY, such asKEYUSAGE_Efor keyCertSign on a non-CA certificate, and the handshake can still accept such a leaf: the chain verify callback clears content errors,ProcessPeerCertCbcan override the result, and a verify callback can accept it in builds withoutOPENSSL_EXTRA. Such a leaf is copied from the decoded certificate as before, since decoding it again would fail the same way.PushPeerCertToChain()andCreatePeerCertChain()take a non-constWOLFSSL*, since getting the peer certificate can now build it.No public structure changes.
WOLFSSLgains one pointer.Results
STM32U585 (Cortex-M33 at 160 MHz, newlib-nano malloc), TLS 1.3 client and server over memory I/O, bytes unless noted, master -> this PR. Heap kept by the established client connection (its CTX and CA included), no verify callback:
OPENSSL_EXTRAOPENSSL_ALLFor the ECC leaf the compat layer's extra cost goes from 5964 to 2252 bytes (
OPENSSL_EXTRA) and from 8401 to 3207 (OPENSSL_ALL).OPENSSL_EXTRAOPENSSL_ALLwolfSSL_get_peer_certificate()With a verify callback set, or once the certificate has been asked for, the numbers are the same as before (+8 bytes for the pointer).
Host (macOS arm64), same harness, ECC leaf, no verify callback: resident 11201 -> 6601 (
OPENSSL_EXTRA) and 14558 -> 7724 (OPENSSL_ALL); handshake peak 14134 -> 10498 and 17352 -> 11482.Not in this PR
An application that sets a verify callback or asks for the peer certificate still holds the whole
WOLFSSL_X509, about 4 KB on the M33 withOPENSSL_EXTRA. The remaining large parts are each name's 330-byte one-line string, the fixedentry[MAX_NAME_ENTRIES]array and a heapASN1_STRINGper name entry. Shrinking those touches much ofx509.cand is left for a separate change.Testing
New tests:
test_wolfSSL_peer_cert_on_demand(TLS 1.3 and 1.2, dynamic builds):cmp_peer_cert_to_file()builds nothing;get_peer_certificate()builds an object with the same DER, names and contents as the certificate sent, and the encoding is dropped;wolfSSL_clear()drops the encoding;test_wolfSSL_peer_cert_kept_decode_err: a generated leaf with keyCertSign but not a CA, accepted by the chain verify callback, is kept as an object. It fails with the decode-error handling disabled.test_wolfSSL_get_peer_certificate_refno longer callsGetPeerCert()from the test, as it is now a hidden symbol in dynamic builds. The ticket re-verify test also checks that no encoding is kept.make checkpasses on macOS (clang):--enable-opensslextra,--enable-opensslall,--enable-all,--enable-opensslextra=x509small, default;--enable-opensslextra,--enable-opensslalland--enable-allwith-DWOLFSSL_DYNAMIC_PEER_CERT,--enable-opensslextra=x509smallwith-DKEEP_PEER_CERT -DWOLFSSL_DYNAMIC_PEER_CERT,--enable-opensslextra --enable-smallstack, and--enable-opensslextra --enable-asn=originalwith-DWOLFSSL_DYNAMIC_PEER_CERT.Also:
--enable-all -DWOLFSSL_DYNAMIC_PEER_CERTbuilds with-Wall -Wextra -Wdeclaration-after-statement -Werror, andmake checkpasses for--enable-opensslextra --enable-smallstack.--enable-all -DWOLFSSL_DYNAMIC_PEER_CERT: the whole unit test passes. The only UBSan report is a misaligned load inwolfssl/test.h, which this PR does not touch.