wolfCert is designed to run on memory-constrained MCUs, but a few of the data structures it borrows from wolfSSL are large under wolfSSL's default desktop configuration. This document collects the knobs that control wolfCert's RAM footprint - both the wolfSSL build-time settings that dominate, and wolfCert's own tunables.
For the complementary topics - heap hints, static-memory pools, CryptoCb
offload - see ARCHITECTURE.md section 4 (MCU / CryptoCb integration
guide).
By default wolfCert's feature set lives in a generated wolfcert/options.h,
produced when you run wolfCert's CMake or autoconf step. MCU toolchains that
drive their own build (and never run that step) can instead supply the config
as a header, exactly like wolfSSL's WOLFSSL_USER_SETTINGS / user_settings.h:
- Copy
examples/user_settings.h.exampletouser_settings.hsomewhere on your include path and edit whichWOLFCERT_HAVE_*macros are defined. - Compile wolfCert (and your application) with
-DWOLFCERT_USER_SETTINGS.wolfcert/types.hthen pulls youruser_settings.hin place of the generatedoptions.h.
The macro set is small and closed - three protocol switches
(WOLFCERT_HAVE_EST / _SCEP / _SERVER) and five key algorithms
(WOLFCERT_HAVE_RSA / _ECC / _ED25519 / _ED448 / _MLDSA). Enable a
feature by defining its macro; disable it by leaving the line commented out -
never #define ... 0, because the code tests presence with #ifdef.
wolfcert/check_config.h (included automatically by types.h) validates the
result at compile time, so a contradictory or incomplete config fails with a
clear #error rather than a confusing downstream error. It enforces the same
rules the configure step does: at least one key algorithm, SCEP requires RSA
(RFC 8894), and the wolfSSL feature set wolfCert depends on (PKCS#7, cert
gen/req/ext, key gen, CryptoCb, base64 encode, OpenSSL-extra, alt names,
WOLFSSL_CERT_NAME_ALL, AES, SHA-256, and TLS 1.2 or 1.3). The header you copy
documents the matching wolfSSL configure flags. If wolfSSL itself is configured
through its own user_settings.h (so <wolfssl/options.h> does not reflect
its real feature set), define WOLFCERT_NO_WOLFSSL_FEATURE_CHECK to skip just
the wolfSSL half of the validation.
When you build wolfCert's own tree this way, the WOLFCERT_ENABLE_EST /
_SCEP / _SERVER options (CMake -DWOLFCERT_USER_SETTINGS=ON -DWOLFCERT_USER_SETTINGS_DIR=<dir>, or autoconf
--enable-user-settings --with-user-settings=<dir>) still select which source
files compile - keep them in sync with the macros in your header. External
build systems just compile the file set they want directly.
The header is called user_settings.h on purpose: it is the same basename
wolfSSL looks for under WOLFSSL_USER_SETTINGS. That is deliberate and safe,
because the macro namespaces are disjoint - wolfCert reads only
WOLFCERT_HAVE_*, wolfSSL reads HAVE_* / NO_* / WOLFSSL_*. The clean
pattern is therefore a single user_settings.h that carries both libraries'
config, with -DWOLFSSL_USER_SETTINGS -DWOLFCERT_USER_SETTINGS so both read
it.
The one thing to know: both <wolfssl/wolfcrypt/settings.h> and
<wolfcert/types.h> reach the file via a quoted #include "user_settings.h",
which searches the including header's own directory first and then the shared
-I path. So if you keep two separate files both named user_settings.h
on the include path, the first one found wins for both includes. Prefer one
combined file; if you must keep them apart, make sure wolfCert's directory is
the one its include resolves to. Either way a wrong pick fails loudly at compile
time - check_config.h will report a missing key algorithm rather than
miscompile.
| Consumer | Where it lives | Default size | Dominated by |
|---|---|---|---|
wolfSSL Cert (CSR / cert build) |
heap (wc_CertNew) |
~20+ KB | altNames[16384] |
wolfSSL DecodedCert (cert parse) |
stack, transient | several KB | parse scratch |
| HTTP request handling | stack (EST + client); heap (SCEP server) | 2-3 KB stack | request read buffer |
The good news: wolfCert never stack-allocates a Cert. Every CSR/cert
build path (src/csr.c, src/ca_issue.c, src/scep/scep_msg.c) obtains
it from wc_CertNew(heap), so its weight lands on your heap or static-
memory pool, not the call stack. The bad news: at wolfSSL defaults a single
Cert is ~20 KB, almost all of it the altNames array.
Cert embeds two CertName structures (issuer + subject), two raw copies
of them, and an alternate-names buffer. Two wolfSSL build-time macros set
the bulk of the size (defaults from wolfssl/wolfcrypt/asn_public.h):
| Macro | Default | Effect |
|---|---|---|
WC_CTC_MAX_ALT_SIZE |
16384 |
size of Cert.altNames[] - the encoded SAN extension. Single largest contributor. |
WC_CTC_NAME_SIZE |
64 |
size of every CertName string field (CN, O, OU, ...). CertName carries ~19 such fields under the WOLFSSL_CERT_NAME_ALL + WOLFSSL_CERT_EXT config wolfCert requires, ×2 for issuer+subject, plus raw copies. |
These are wolfSSL settings, not wolfCert ones - set them when you build
wolfSSL (via user_settings.h or CPPFLAGS), and wolfCert picks up
whatever wolfSSL provides. For example, to drop a Cert from ~20 KB to
~3 KB:
/* user_settings.h, when building wolfSSL */
#define WC_CTC_MAX_ALT_SIZE 1024 /* was 16384 */
#define WC_CTC_NAME_SIZE 32 /* was 64 */Trade-offs:
WC_CTC_MAX_ALT_SIZEbounds the total encoded size of all SANs on a certificate. 1024 bytes still holds a handful of DNS / IP / URI names; size it to your longest realistic SAN set.WC_CTC_NAME_SIZEbounds the length of each subject/issuer RDN value (CN, O, ...). wolfCert truncates over-long values to fit (copy_name()insrc/csr.c), so shrinking this silently caps how long a CN you can request.- Disabling
WOLFSSL_CERT_NAME_ALLand/orWOLFSSL_CERT_EXTin wolfSSL removes the less-commonCertNamefields entirely - but wolfCert's build requires both (seeCLAUDE.md/CMakeLists.txt), so prefer shrinkingWC_CTC_NAME_SIZEover dropping these.
The in-tree test server and HTTP client size a few request-handling
buffers with #ifndef-guarded macros in src/internal.h; override via -D
or your user_settings-style config header. The EST server and the client
place these on the stack; the SCEP server, whose GET PKIOperation read
buffer is by far the largest, allocates it on the heap (freed as soon as
the request completes) so it never counts against the stack budget.
| Macro | Default | Buffer |
|---|---|---|
WOLFCERT_HTTP_REQ_BUF_SZ |
2048 |
server request-header read buffer. The SCEP server adds WOLFCERT_HTTP_QUERY_SZ to it (so a base64 GET PKIOperation fits) and allocates the result on the heap; the EST server keeps its 2048-byte buffer on the stack (est_server.c, scep_server.c) |
WOLFCERT_HTTP_PATH_SZ |
512 |
EST server request path field (the SCEP server points path/query into its heap read buffer instead) |
WOLFCERT_HTTP_QUERY_SZ |
8192 |
sized to hold a base64 GET PKIOperation message; on the SCEP server it extends the heap read buffer (REQ_BUF_SZ + QUERY_SZ) that query points into |
WOLFCERT_HTTP_AUTH_BUF_SZ |
512 |
client Basic-auth header line (http.c) |
WOLFCERT_HTTP_MAX_PATH_LEN |
8192 |
client-side ceiling on a request URL's path+query (http.c) |
WOLFCERT_SCEP_MAX_GET_URL |
8192 |
client cap on a GET PKIOperation URL; a larger message is refused with WOLFCERT_ERR_UNSUPPORTED so the caller POSTs (internal.h) |
Shrinking WOLFCERT_HTTP_REQ_BUF_SZ lowers the largest request header
block the server accepts; WOLFCERT_HTTP_PATH_SZ / WOLFCERT_HTTP_QUERY_SZ
lower the longest request path / query; WOLFCERT_HTTP_AUTH_BUF_SZ lowers the
longest Basic-auth credential the client can send. A POST-only SCEP deployment
can trim WOLFCERT_HTTP_QUERY_SZ (and, on the client, WOLFCERT_SCEP_MAX_GET_URL
and WOLFCERT_HTTP_MAX_PATH_LEN) back down. Example:
#define WOLFCERT_HTTP_REQ_BUF_SZ 768
#define WOLFCERT_HTTP_PATH_SZ 128
#define WOLFCERT_HTTP_QUERY_SZ 256
#define WOLFCERT_HTTP_AUTH_BUF_SZ 128Two related buffers are intentionally not exposed as knobs:
- the ≤256-byte response-builder buffers (
hdr[256],body[192], ...) - too small to matter for a stack budget; - the SCEP
scratch[1024]insrc/scep/scep_msg.c, whose size is tied to PKCS#7 signed-attribute encoding; shrinking it risks breaking SCEP signing rather than saving meaningful RAM.
Encoding a SCEP SignedData pkiMessage allocates a one-shot heap buffer
sized envelope + signer-cert + WOLFCERT_SCEP_PKI_SLACK. The slack bounds
everything else in the message (signed attributes, signature, ASN.1
framing); see the WOLFCERT_SCEP_PKI_SLACK comment in src/internal.h.
| Macro | Default | Bounds |
|---|---|---|
WOLFCERT_SCEP_PKI_SLACK |
8192 |
signed-attribute set + signature + SignerInfo/ASN.1 framing on top of the envelope and signer cert |
A PENDING/FAILURE CertRep has no envelope, so its buffer is just
signer-cert + WOLFCERT_SCEP_PKI_SLACK. Override via -D or your
user_settings-style config header; ~4 KiB is the realistic floor for an
RSA-2048 signer (drop further only if you also bound the signer cert and
RSA key size). Example:
#define WOLFCERT_SCEP_PKI_SLACK (6 * 1024)On the decode side, wolfcert_scep_self_signed_rsa (the CSR) and
wolfcert_scep_deenvelop (the enveloped CertRep) each allocate a one-shot
buffer sized body + 4 KiB. WOLFCERT_SCEP_MAX_MSG_SZ caps the accepted
body so a malformed or hostile length cannot drive a huge allocation; an
over-large body is rejected with WOLFCERT_ERR_BAD_ARG before any malloc.
| Macro | Default | Bounds |
|---|---|---|
WOLFCERT_SCEP_MAX_MSG_SZ |
65536 |
largest CSR / enveloped CertRep accepted by the SCEP PKCS#7 helpers |
In the normal client/server flow these bodies already arrive bounded by the
HTTP body cap (WOLFCERT_HTTP_DEFAULT_MAX_BODY, also 64 KiB), so this is a
last-resort limit for direct callers. Real CSRs are < 4 KiB and a typical
RSA cert chain is < 16 KiB, so this can be trimmed well below the default on
constrained targets. Example:
#define WOLFCERT_SCEP_MAX_MSG_SZ (16 * 1024)Every wolfCert allocation carries a heap hint that threads through to
wolfSSL's XMALLOC. Build wolfSSL with WOLFSSL_STATIC_MEMORY and pass a
pool hint to bound total heap use; this is the primary mechanism for
keeping the large heap-allocated Cert off a general-purpose allocator.
See ARCHITECTURE.md section 4.1
for the three levels of hint granularity.
If your target is single-threaded, build wolfSSL with SINGLE_THREADED.
wolfCert holds no locks of its own (init/cleanup delegate refcounting to
wolfSSL_Init/wolfSSL_Cleanup), so there is nothing extra to configure.
Strip unused key algorithms and protocols at configure time so their code
and tables drop out entirely - see the WOLFCERT_HAVE_* /
WOLFCERT_ENABLE_* options in CMakeLists.txt / configure.ac and the
gating discussion in CLAUDE.md. SCEP is RSA-only; if you only need EST
with ECC or a PQC algorithm, disabling SCEP avoids pulling in RSA.
/* user_settings.h fragment for a constrained wolfCert target */
#define WC_CTC_MAX_ALT_SIZE 1024
#define WC_CTC_NAME_SIZE 32
#define WOLFSSL_STATIC_MEMORY
#define SINGLE_THREADED/* wolfCert side (compiler -D or your config header) */
#define WOLFCERT_HTTP_REQ_BUF_SZ 768
#define WOLFCERT_HTTP_PATH_SZ 128
#define WOLFCERT_HTTP_AUTH_BUF_SZ 128
#define WOLFCERT_SCEP_PKI_SLACK (6 * 1024)This drops the per-Cert heap cost by ~17 KB and the HTTP request stack
footprint by ~2 KB, while still supporting realistic device-certificate
subjects and SAN sets. Validate against your own longest expected subject
DN and SAN list before committing to the smaller sizes.