From 5e3a47354e30ebb727fd3a1ecc70579f9fb1c6f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 8 Oct 2026 11:13:45 +0200 Subject: [PATCH 01/10] Pass the static pool to wolfcert_init in the no-malloc tests test_static_mem_init() set wolfCert's default heap, then every test's wolfcert_init(NULL) reset it to NULL. The no-malloc tests still passed because wolfSSL falls back to its global heap hint, so wolfCert's own heap-hint plumbing was never exercised with a pool. The tests now hand the pool to wolfcert_init through test_heap_hint(). --- tests/test_static_mem.h | 7 ++++++- tests/unit/test_csr.c | 2 +- tests/unit/test_csr_attrs.c | 2 +- tests/unit/test_est.c | 2 +- tests/unit/test_keygen.c | 2 +- tests/unit/test_parse_negative.c | 2 +- tests/unit/test_scep_msg.c | 2 +- tests/unit/test_server_ca_store.c | 2 +- tests/unit/test_smoke.c | 2 +- tests/unit/test_store.c | 2 +- tests/unit/test_transport.c | 2 +- 11 files changed, 16 insertions(+), 11 deletions(-) diff --git a/tests/test_static_mem.h b/tests/test_static_mem.h index ae23ff7..40ceacf 100644 --- a/tests/test_static_mem.h +++ b/tests/test_static_mem.h @@ -41,13 +41,18 @@ static inline int test_static_mem_init(void) sizeof(g_test_static_pool), WOLFMEM_GENERAL, 1) != 0) return -1; wolfSSL_SetGlobalHeapHint(g_test_heap_hint); - wolfcert_set_default_heap(g_test_heap_hint); return 0; } +static inline void* test_heap_hint(void) +{ + return g_test_heap_hint; +} + #else static inline int test_static_mem_init(void) { return 0; } +static inline void* test_heap_hint(void) { return NULL; } #endif /* WOLFSSL_STATIC_MEMORY && WOLFSSL_NO_MALLOC */ diff --git a/tests/unit/test_csr.c b/tests/unit/test_csr.c index b130d93..02df008 100644 --- a/tests/unit/test_csr.c +++ b/tests/unit/test_csr.c @@ -844,7 +844,7 @@ static int renewal_size_limits(void) int main(void) { REQUIRE(test_static_mem_init() == 0); - REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); + REQUIRE(wolfcert_init(test_heap_hint()) == WOLFCERT_OK); #ifdef WOLFCERT_HAVE_SERVER if (subject_copy_rejects_oversized_rdn()) diff --git a/tests/unit/test_csr_attrs.c b/tests/unit/test_csr_attrs.c index b2bd61b..bd1d22c 100644 --- a/tests/unit/test_csr_attrs.c +++ b/tests/unit/test_csr_attrs.c @@ -64,7 +64,7 @@ static const uint8_t OID_SECP384R1[] = { 0x2B, 0x81, 0x04, 0x00, 0x22 }; int main(void) { REQUIRE(test_static_mem_init() == 0); - REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); + REQUIRE(wolfcert_init(test_heap_hint()) == WOLFCERT_OK); /* Empty input gives no items and zero hints. */ { diff --git a/tests/unit/test_est.c b/tests/unit/test_est.c index d7104e5..bed2567 100644 --- a/tests/unit/test_est.c +++ b/tests/unit/test_est.c @@ -564,7 +564,7 @@ int main(void) signal(SIGPIPE, SIG_IGN); REQUIRE(test_static_mem_init() == 0); - REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); + REQUIRE(wolfcert_init(test_heap_hint()) == WOLFCERT_OK); if (test_oid_to_dotted()) return 1; diff --git a/tests/unit/test_keygen.c b/tests/unit/test_keygen.c index b700cc7..91bd68a 100644 --- a/tests/unit/test_keygen.c +++ b/tests/unit/test_keygen.c @@ -69,7 +69,7 @@ static int roundtrip(WolfCertKeyType type, int param) int main(void) { REQUIRE(test_static_mem_init() == 0); - REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); + REQUIRE(wolfcert_init(test_heap_hint()) == WOLFCERT_OK); #ifdef WOLFCERT_HAVE_ECC if (roundtrip(WOLFCERT_KEY_ECC, 256)) return 1; diff --git a/tests/unit/test_parse_negative.c b/tests/unit/test_parse_negative.c index 06a6ce3..e4c4352 100644 --- a/tests/unit/test_parse_negative.c +++ b/tests/unit/test_parse_negative.c @@ -243,7 +243,7 @@ static int test_csr_pem(void) int main(void) { REQUIRE(test_static_mem_init() == 0); - REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); + REQUIRE(wolfcert_init(test_heap_hint()) == WOLFCERT_OK); if (test_url()) return 1; if (test_base64()) diff --git a/tests/unit/test_scep_msg.c b/tests/unit/test_scep_msg.c index e0fdf3c..d7b8c55 100644 --- a/tests/unit/test_scep_msg.c +++ b/tests/unit/test_scep_msg.c @@ -2306,7 +2306,7 @@ static int test_text_attrib_printable(void) int main(void) { REQUIRE(test_static_mem_init() == 0); - REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); + REQUIRE(wolfcert_init(test_heap_hint()) == WOLFCERT_OK); if (test_getca_url()) return 1; if (test_scep_rejects_est_cfg()) diff --git a/tests/unit/test_server_ca_store.c b/tests/unit/test_server_ca_store.c index 6ef4f91..27a1312 100644 --- a/tests/unit/test_server_ca_store.c +++ b/tests/unit/test_server_ca_store.c @@ -955,7 +955,7 @@ static int test_bad_bind_host_rejected(void) int main(void) { REQUIRE(test_static_mem_init() == 0); - REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); + REQUIRE(wolfcert_init(test_heap_hint()) == WOLFCERT_OK); #if CA_STORE_NEEDS_TLS REQUIRE(gen_server_identity(&srv_cert_pem, &srv_cert_pem_len, diff --git a/tests/unit/test_smoke.c b/tests/unit/test_smoke.c index d1eff5a..8cad76f 100644 --- a/tests/unit/test_smoke.c +++ b/tests/unit/test_smoke.c @@ -29,7 +29,7 @@ int main(void) fprintf(stderr, "static mem init failed\n"); return 1; } - if (wolfcert_init(NULL) != WOLFCERT_OK) { + if (wolfcert_init(test_heap_hint()) != WOLFCERT_OK) { fprintf(stderr, "wolfcert_init failed\n"); return 1; } diff --git a/tests/unit/test_store.c b/tests/unit/test_store.c index 10b59f6..2b513a7 100644 --- a/tests/unit/test_store.c +++ b/tests/unit/test_store.c @@ -109,7 +109,7 @@ static int test_memory(void) int main(void) { REQUIRE(test_static_mem_init() == 0); - REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); + REQUIRE(wolfcert_init(test_heap_hint()) == WOLFCERT_OK); #ifdef WOLFCERT_HAVE_POSIX_STORE if (test_posix()) return 1; diff --git a/tests/unit/test_transport.c b/tests/unit/test_transport.c index df5d34b..d425ca8 100644 --- a/tests/unit/test_transport.c +++ b/tests/unit/test_transport.c @@ -1075,7 +1075,7 @@ static int test_no_builtin_transport(void) int main(void) { REQUIRE(test_static_mem_init() == 0); - REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); + REQUIRE(wolfcert_init(test_heap_hint()) == WOLFCERT_OK); if (test_roundtrip()) return 1; if (test_byte_at_a_time()) From b9ad1f7d9a26eee96eaea26bf032f84f3f0e688f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 8 Oct 2026 11:13:45 +0200 Subject: [PATCH 02/10] Take the error state's storage class from wolfSSL The last-error state used __thread on GCC and Clang unless __STDC_NO_THREADS__ was defined. Apple clang defines it, so the state was global on macOS while the docs said it was per-thread. It now uses wolfSSL's THREAD_LS_T: per-thread when wolfSSL is built with HAVE_THREAD_LS, which its configure enables where the compiler supports it, and one global otherwise. wolfSSL also leaves THREAD_LS_T empty under NO_THREAD_LS and on FreeRTOS and Zephyr, and the docs say so. This drops WOLFCERT_NO_THREAD_LOCAL. On Zephyr the ~300-byte state therefore no longer sits in .tbss, which Zephyr copies onto every thread's stack (qemu_x86 sample: 340 to 44 bytes). The Kconfig no longer implies THREAD_LOCAL_STORAGE, and the README's idle-stack sizing note is gone. test_smoke checks that an error on a second thread does not show up on the first, wherever THREAD_LS_T is thread-local; it failed on macOS before this change. examples/user_settings.h.example notes HAVE_THREAD_LS for hosted targets whose wolfSSL settings share the file. --- AGENTS.md | 7 +-- CONTRIBUTING.md | 2 +- Makefile.am | 2 +- docs/ARCHITECTURE.md | 11 +++-- docs/EMBEDDED.md | 10 +++-- docs/MIGRATING-FROM-WOLFSCEP.md | 2 +- examples/user_settings.h.example | 4 ++ src/internal.c | 10 +---- tests/CMakeLists.txt | 4 +- tests/unit/test_smoke.c | 45 +++++++++++++++++++ wolfcert/status.h | 6 +-- zephyr/CMakeLists.txt | 5 --- zephyr/Kconfig | 1 - zephyr/README.md | 14 +----- .../boards/frdm_mcxn947_mcxn947_cpu0.conf | 2 - 15 files changed, 78 insertions(+), 47 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fb2cd70..bdcd01d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -188,7 +188,7 @@ wolfcert_cleanup(); | `wolfcert/types.h` | `WolfCertServerCfg`, `WolfCertKeyCfg`, `WolfCertCertMeta`, `WolfCertTransport`, `wolfcert_buffer_free` | | `wolfcert/keygen.h`, `wolfcert/csr.h` | Key generation, import / export, CSR building | | `wolfcert/store.h` | `WolfCertStoreOps` storage vtable, POSIX and in-memory backends | -| `wolfcert/errors.h`, `wolfcert/status.h`, `wolfcert/log.h` | Error codes, per-thread error detail, log callback | +| `wolfcert/errors.h`, `wolfcert/status.h`, `wolfcert/log.h` | Error codes, last-error detail, log callback | | `wolfcert/memory.h` | Default heap hint, allocation macros | | `examples/` | `enroll_est.c`, `enroll_scep.c`, `enroll_cryptocb.c`, `user_settings.h.example` | | `examples/certs/` | Test credentials; never ship them | @@ -229,7 +229,7 @@ contract, alone or in a group comment. `getnextca` / `getcert` (SCEP), which is handy for checking a production CA before writing code against it. - On Zephyr, the EST client sample runs against a host `wolfcert-server`; - `zephyr/README.md` has the steps and the per-thread stack sizing. + `zephyr/README.md` has the steps and the stack sizing. ## Gotchas @@ -257,7 +257,8 @@ contract, alone or in a group comment. `proto_opts.scep.content_cipher` where the network is untrusted. - **Error detail:** `wolfcert_strerror(rc)` names the code; `wolfcert_last_error_message()` and `wolfcert_last_wolfssl_err()` give the - per-thread detail behind it. + detail behind it, per thread when wolfSSL's `THREAD_LS_T` is thread-local + (`HAVE_THREAD_LS`, outside FreeRTOS and Zephyr). ## Further documentation diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d46b007..a04f4ec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -118,4 +118,4 @@ Email **support@wolfssl.com** instead, and please keep the issue private until a 3. Add the new files to `EXTRA_DIST` in `zephyr/include.am`. 4. Add a job to `.github/workflows/zephyr.yml` modelled on the `mcxn` job: the `zephyr-workspace` action with the board's SDK toolchain, twister `--build-only` for the board, then `scripts/ci/twister-assert-ran.py --built-only `. 5. Add a row to both board tables under "EST client sample" in `zephyr/README.md`. -6. Run it on the hardware: enroll against a host `wolfcert-server`, and check every thread's stack headroom with `CONFIG_THREAD_ANALYZER`, since the thread-local data takes room on each one ("Sizing" in `zephyr/README.md`). +6. Run it on the hardware: enroll against a host `wolfcert-server`, and check every thread's stack headroom with `CONFIG_THREAD_ANALYZER`. diff --git a/Makefile.am b/Makefile.am index 2bf6f70..395c68e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -112,7 +112,7 @@ check_PROGRAMS += test_net endif test_smoke_SOURCES = tests/unit/test_smoke.c -test_smoke_LDADD = libwolfcert.la $(WOLFSSL_LIBS) +test_smoke_LDADD = libwolfcert.la $(WOLFSSL_LIBS) -lpthread test_keygen_SOURCES = tests/unit/test_keygen.c test_keygen_LDADD = libwolfcert.la $(WOLFSSL_LIBS) test_csr_SOURCES = tests/unit/test_csr.c diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index cdea1f5..eb75cf7 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -55,7 +55,7 @@ A few constraints shape the whole library: disabled subsystems disappear from the link entirely. 7. **Minimal, explicit error reporting.** Every function returns a small negative `WOLFCERT_ERR_*` code; extended diagnostics (module, underlying - wolfSSL error, message) are recorded in thread-local state and retrieved + wolfSSL error, message) are recorded in last-error state and retrieved via `wolfcert_last_error_message()` / `wolfcert_last_wolfssl_err()`. ### What wolfCert is not @@ -418,8 +418,11 @@ wolfcert_set_log_cb(my_log, NULL); wolfcert_set_log_level(WOLFCERT_LOG_INFO); /* default is WARN */ ``` -Extended error state is thread-local. On metal without TLS support, define -`WOLFCERT_NO_THREAD_LOCAL` and it degrades to a global. After any failing call: +Extended error state is per-thread when wolfSSL's `THREAD_LS_T` is +thread-local: `HAVE_THREAD_LS`, which its configure enables where the compiler +supports it, without `NO_THREAD_LS`, and not on FreeRTOS or Zephyr, where +wolfSSL leaves `THREAD_LS_T` empty. Otherwise (RTOS and bare-metal builds) the +state is one global, so keep wolfCert calls on one thread. After any failing call: ```c int rc = wolfcert_est_simple_enroll(&srv, csr, csr_len, &cert); @@ -431,7 +434,7 @@ if (rc != WOLFCERT_OK) { } ``` -The message string is valid until the next wolfCert call on the same thread. +The message string is valid until the error state next changes. ### 4.6 Pluggable transport diff --git a/docs/EMBEDDED.md b/docs/EMBEDDED.md index 5840812..e05cb21 100644 --- a/docs/EMBEDDED.md +++ b/docs/EMBEDDED.md @@ -252,6 +252,12 @@ 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. +The error state behind `wolfcert_last_error_message()` is per-thread only +when wolfSSL's `THREAD_LS_T` is thread-local, which takes `HAVE_THREAD_LS` +and is never the case on FreeRTOS or Zephyr. Otherwise, as in most RTOS and +`user_settings.h` builds, it is one global, so make the wolfCert calls whose +errors you read from one thread. + ## 6. Algorithms & protocols Strip unused key algorithms and protocols at configure time so their code @@ -313,9 +319,7 @@ overwrites the idle thread and the image dies later in the interrupt handler rather than reporting a stack overflow. Build with `CONFIG_HW_STACK_PROTECTION=y` while tuning `CONFIG_MAIN_STACK_SIZE` so an overflow names itself. The x86 default `CONFIG_ISR_STACK_SIZE` (2048) is also -thin once the networking stack is in the image. Every thread's stack also -holds the thread-local data; -[`zephyr/README.md` (Sizing)](../zephyr/README.md#sizing) covers how much. +thin once the networking stack is in the image. **A clock is not optional.** Without a set wall clock every certificate looks not-yet-valid and the failure surfaces as a trust-anchor error, not a clock diff --git a/docs/MIGRATING-FROM-WOLFSCEP.md b/docs/MIGRATING-FROM-WOLFSCEP.md index b4dad5b..a62bebe 100644 --- a/docs/MIGRATING-FROM-WOLFSCEP.md +++ b/docs/MIGRATING-FROM-WOLFSCEP.md @@ -152,7 +152,7 @@ srv.proto_opts.scep.content_cipher = WOLFCERT_SCEP_CIPHER_AES256; **Fingerprint verification is now the library's job.** Delete your helper and call `wolfcert_scep_verify_ca_fingerprint`, which hashes the DER certificate and compares in constant time. -**Error codes are a different, smaller set.** wolfSCEP's `WS_*` codes map onto `wolfcert/errors.h`: `WS_BAD_ARGUMENT` to `WOLFCERT_ERR_BAD_ARG`, `WS_MEMORY_E` to `WOLFCERT_ERR_MEMORY`, `WS_PARSE_E` to `WOLFCERT_ERR_PARSE`, the `WS_CBIO_ERR_*` family to `WOLFCERT_ERR_IO` and, in non-blocking mode only, to `WOLFCERT_ERR_WANT_READ` and `WOLFCERT_ERR_WANT_WRITE`. There is no per-call error string; use `wolfcert_strerror` for the code and `wolfcert_last_error_message` for the per-thread detail. +**Error codes are a different, smaller set.** wolfSCEP's `WS_*` codes map onto `wolfcert/errors.h`: `WS_BAD_ARGUMENT` to `WOLFCERT_ERR_BAD_ARG`, `WS_MEMORY_E` to `WOLFCERT_ERR_MEMORY`, `WS_PARSE_E` to `WOLFCERT_ERR_PARSE`, the `WS_CBIO_ERR_*` family to `WOLFCERT_ERR_IO` and, in non-blocking mode only, to `WOLFCERT_ERR_WANT_READ` and `WOLFCERT_ERR_WANT_WRITE`. There is no per-call error string; use `wolfcert_strerror` for the code and `wolfcert_last_error_message` for the detail. **TLS is optional for SCEP and mandatory for EST.** SCEP authenticates inside the pkiMessage, so a plaintext `http://` endpoint is accepted. An `https://` endpoint requires `srv.verify_server`: every SCEP entry point, one-shot and session alike, refuses to run unverified rather than completing a silent anonymous handshake, because `verify_server` is the only peer-verification switch in the transport. diff --git a/examples/user_settings.h.example b/examples/user_settings.h.example index 0cb0e72..abbf035 100644 --- a/examples/user_settings.h.example +++ b/examples/user_settings.h.example @@ -50,4 +50,8 @@ #define WOLFCERT_HAVE_ED448 1 #define WOLFCERT_HAVE_MLDSA 1 +/* wolfSSL setting for a hosted target sharing this file: makes the error + * state per-thread (wolfSSL ignores it on FreeRTOS and Zephyr). */ +/* #define HAVE_THREAD_LS */ + #endif /* WOLFCERT_USER_SETTINGS_H */ diff --git a/src/internal.c b/src/internal.c index fe4268e..ed1d1e6 100644 --- a/src/internal.c +++ b/src/internal.c @@ -32,14 +32,6 @@ static WolfCertLogCb g_log_cb = NULL; static void* g_log_ctx = NULL; static WolfCertLogLevel g_log_level = WOLFCERT_LOG_WARN; -#if defined(__STDC_NO_THREADS__) || defined(WOLFCERT_NO_THREAD_LOCAL) -# define WOLFCERT_TLS /* empty */ -#elif defined(__GNUC__) || defined(__clang__) -# define WOLFCERT_TLS __thread -#else -# define WOLFCERT_TLS /* empty */ -#endif - typedef struct { int wolfcert_rc; int wolfssl_rc; @@ -47,7 +39,7 @@ typedef struct { char message[256]; } WolfCertErrState; -static WOLFCERT_TLS WolfCertErrState g_err; +static THREAD_LS_T WolfCertErrState g_err; void wolfcert_set_default_heap(void* heap) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8edf576..567debc 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,7 +1,8 @@ # SPDX-License-Identifier: GPL-3.0-or-later +find_package(Threads REQUIRED) add_executable(test_smoke unit/test_smoke.c) -target_link_libraries(test_smoke PRIVATE wolfcert) +target_link_libraries(test_smoke PRIVATE wolfcert Threads::Threads) add_test(NAME smoke COMMAND test_smoke) add_executable(test_keygen unit/test_keygen.c) @@ -24,7 +25,6 @@ if(WOLFCERT_ENABLE_BUILTIN_TRANSPORT) add_test(NAME net COMMAND test_net) endif() -find_package(Threads REQUIRED) add_executable(test_transport unit/test_transport.c) target_include_directories(test_transport PRIVATE ${CMAKE_SOURCE_DIR}/src) target_link_libraries(test_transport PRIVATE wolfcert) diff --git a/tests/unit/test_smoke.c b/tests/unit/test_smoke.c index 8cad76f..af6317a 100644 --- a/tests/unit/test_smoke.c +++ b/tests/unit/test_smoke.c @@ -23,6 +23,45 @@ #include #include +/* wolfSSL leaves THREAD_LS_T empty on FreeRTOS and Zephyr. */ +#if defined(HAVE_THREAD_LS) && !defined(NO_THREAD_LS) && \ + !defined(FREERTOS) && !defined(FREERTOS_TCP) && !defined(WOLFSSL_ZEPHYR) +#define TEST_ERR_PER_THREAD +#endif + +#ifdef TEST_ERR_PER_THREAD +#include + +static void* fail_keygen(void* arg) +{ + WolfCertKeyCfg cfg = { .type = (WolfCertKeyType)999 }; + WolfCertKey* key = NULL; + int* ok = (int*)arg; + + *ok = wolfcert_key_generate(&cfg, &key) == WOLFCERT_ERR_UNSUPPORTED && + wolfcert_last_error_message()[0] != '\0'; + return NULL; +} + +/* An error on another thread leaves this thread's error state alone. */ +static int error_state_is_per_thread(void) +{ + pthread_t t; + int ok = 0; + + wolfcert_clear_error(); + if (pthread_create(&t, NULL, fail_keygen, &ok) != 0 || + pthread_join(t, NULL) != 0 || !ok) + return 1; + if (wolfcert_last_error_message()[0] != '\0') { + fprintf(stderr, "error state leaked across threads: %s\n", + wolfcert_last_error_message()); + return 1; + } + return 0; +} +#endif + int main(void) { if (test_static_mem_init() != 0) { @@ -42,6 +81,12 @@ int main(void) wolfcert_cleanup(); return 1; } +#ifdef TEST_ERR_PER_THREAD + if (error_state_is_per_thread() != 0) { + wolfcert_cleanup(); + return 1; + } +#endif printf("wolfCert %s\n", v); wolfcert_cleanup(); return 0; diff --git a/wolfcert/status.h b/wolfcert/status.h index 8207242..a7ca001 100644 --- a/wolfcert/status.h +++ b/wolfcert/status.h @@ -19,9 +19,9 @@ /** * @file status.h - * Detail behind the last WOLFCERT_ERR_* code. The state is per-thread on GCC - * and Clang unless __STDC_NO_THREADS__ or WOLFCERT_NO_THREAD_LOCAL is - * defined, and global otherwise. + * Detail behind the last WOLFCERT_ERR_* code. The state is per-thread when + * wolfSSL's THREAD_LS_T is thread-local (HAVE_THREAD_LS without NO_THREAD_LS, + * outside FreeRTOS and Zephyr), and global otherwise. */ #ifndef WOLFCERT_STATUS_H diff --git a/zephyr/CMakeLists.txt b/zephyr/CMakeLists.txt index 77b9d8b..3c048ff 100644 --- a/zephyr/CMakeLists.txt +++ b/zephyr/CMakeLists.txt @@ -82,11 +82,6 @@ if(CONFIG_WOLFCERT) # CONFIG_APP_LINK_WITH_WOLFSSL being left at its default. target_link_libraries(wolfCert INTERFACE wolfSSL) - # Without thread-local storage the last-error slot is shared by all threads. - if(NOT CONFIG_THREAD_LOCAL_STORAGE) - zephyr_library_compile_definitions(WOLFCERT_NO_THREAD_LOCAL) - endif() - if(CONFIG_WOLFCERT_BUILD_TESTING) target_compile_definitions(wolfCert INTERFACE WOLFCERT_BUILD_TESTING) zephyr_library_compile_definitions(WOLFCERT_BUILD_TESTING) diff --git a/zephyr/Kconfig b/zephyr/Kconfig index c53394b..67f2b6e 100644 --- a/zephyr/Kconfig +++ b/zephyr/Kconfig @@ -15,7 +15,6 @@ menuconfig WOLFCERT depends on WOLFSSL select NETWORKING select POSIX_API - imply THREAD_LOCAL_STORAGE help Build wolfCert as a Zephyr module. See the top-level README.md for what the library does and zephyr/README.md for the integration. diff --git a/zephyr/README.md b/zephyr/README.md index af1dc98..ada0aed 100644 --- a/zephyr/README.md +++ b/zephyr/README.md @@ -79,15 +79,6 @@ See `docs/EMBEDDED.md` section 8. The short version: build with `CONFIG_HW_STACK_PROTECTION=y` while tuning `CONFIG_MAIN_STACK_SIZE`, because a stack overflow here is reported as something else entirely. -**Every thread's stack also holds the thread-local data.** Zephyr copies all -of the image's `__thread` variables onto the top of each thread's stack when -the thread starts, and does not check that they fit. wolfCert's per-thread -error state brings that copy to ~340 bytes, more than the default 320-byte -`CONFIG_IDLE_STACK_SIZE`, and the image hard-faults before the boot banner. -Raise the idle stack to at least 1024 and leave the same ~340 bytes of room on -every small-stack thread — for example `CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE`, -and `CONFIG_LOG_PROCESS_THREAD_STACK_SIZE` in a `CONFIG_LOG` build. - ## Networking on qemu_x86 A networked QEMU image defaults to SLIP and waits forever on `/tmp/slip.sock`. @@ -204,14 +195,13 @@ needs changing. `prj.conf` leaves these to the board: - **An entropy source.** The devicetree's `zephyr,entropy` usually provides one, as the SoC's TRNG does on FRDM-MCXN947. `CONFIG_TEST_RANDOM_GENERATOR` is for emulators only: keys generated from it are predictable. -- **Stack and heap sizes.** Start from the FRDM-MCXN947 file, and leave room on - every thread's stack for the thread-local data (see [Sizing](#sizing)). +- **Stack and heap sizes.** Start from the FRDM-MCXN947 file (see + [Sizing](#sizing)). ### Troubleshooting | Symptom | Cause | |---|---| -| `HARD FAULT` repeated before the boot banner | A thread's stack cannot hold the thread-local data; see [Sizing](#sizing) | | `peer ip address mismatch` or `peer subject name mismatch` | The server certificate's SAN does not name the host in the URL; issue one with `gen-server-cert.sh` | | `enroll failed: TLS error ()`, or `ASN date error, current date is before start of validity` | The device clock is before a certificate's start date; the clock is the build time, so rebuild with `-p` | | `ASN no signer error to confirm failure` | `CONFIG_WOLFCERT_SAMPLE_CA_CERT` is not the CA that issued the server's certificate; see [step 2](#2-configure-the-sample) | diff --git a/zephyr/samples/wolfcert_est_client/boards/frdm_mcxn947_mcxn947_cpu0.conf b/zephyr/samples/wolfcert_est_client/boards/frdm_mcxn947_mcxn947_cpu0.conf index 347ba3b..4275b29 100644 --- a/zephyr/samples/wolfcert_est_client/boards/frdm_mcxn947_mcxn947_cpu0.conf +++ b/zephyr/samples/wolfcert_est_client/boards/frdm_mcxn947_mcxn947_cpu0.conf @@ -5,6 +5,4 @@ CONFIG_NET_DHCPV4=y CONFIG_MAIN_STACK_SIZE=8192 CONFIG_HEAP_MEM_POOL_SIZE=16384 -# Room for the thread-local data; see zephyr/README.md, Sizing. -CONFIG_IDLE_STACK_SIZE=1024 CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048 From ee058198177ec4c7a131fafc641c79d655f95978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 8 Oct 2026 11:13:45 +0200 Subject: [PATCH 03/10] Scrub the Basic credential in the EST test server check_basic_auth() left the plaintext user:pass in a stack buffer and freed the base64 copy without zeroing it. The request's Authorization value is now zeroed before it is freed, and so is the stack buffer parse_request() reads the header block into. --- src/est/est_server.c | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/est/est_server.c b/src/est/est_server.c index b6aee7d..568f015 100644 --- a/src/est/est_server.c +++ b/src/est/est_server.c @@ -116,6 +116,8 @@ typedef struct { static void free_req(EstRequest* r) { + if (r->auth_header != NULL) + wc_ForceZero(r->auth_header, (word32)strlen(r->auth_header)); WOLFCERT_XFREE(r->auth_header, r->heap); WOLFCERT_XFREE(r->body, r->heap); memset(r, 0, sizeof(*r)); @@ -235,14 +237,14 @@ static int hdr_is(const char* line, size_t llen, const char* name) wolfcert_ascii_ncasecmp(line, name, n) == 0; } -static int parse_request(WolfCertServer* s, int fd, EstRequest* out, void* heap) +static int parse_request_in(WolfCertServer* s, int fd, EstRequest* out, + void* heap, char* buf, size_t buf_sz) { memset(out, 0, sizeof(*out)); out->heap = heap; - char buf[WOLFCERT_HTTP_REQ_BUF_SZ]; size_t n = 0; - while (n < sizeof(buf) - 1) { - ssize_t r = wolfcert_io_recv(s, fd, buf + n, sizeof(buf) - 1 - n); + while (n < buf_sz - 1) { + ssize_t r = wolfcert_io_recv(s, fd, buf + n, buf_sz - 1 - n); if (r <= 0) return WOLFCERT_ERR_IO; @@ -454,6 +456,16 @@ static int parse_request(WolfCertServer* s, int fd, EstRequest* out, void* heap) return WOLFCERT_OK; } +/* The header block can carry Basic credentials. */ +static int parse_request(WolfCertServer* s, int fd, EstRequest* out, void* heap) +{ + char buf[WOLFCERT_HTTP_REQ_BUF_SZ]; + int rc = parse_request_in(s, fd, out, heap, buf, sizeof(buf)); + + wc_ForceZero(buf, (word32)sizeof(buf)); + return rc; +} + static void send_all(WolfCertServer* s, int fd, const void* buf, size_t len) { const uint8_t* p = buf; @@ -546,7 +558,9 @@ static int check_basic_auth(const WolfCertServer* s, const char* auth_header) memcpy(raw + ul + 1, s->cfg_basic_pass, pl); WolfCertBuffer enc = { 0 }; - if (wolfcert_base64_encode(raw, ul + 1 + pl, &enc, s->heap) != WOLFCERT_OK) + int erc = wolfcert_base64_encode(raw, ul + 1 + pl, &enc, s->heap); + wc_ForceZero(raw, (word32)sizeof(raw)); + if (erc != WOLFCERT_OK) return 0; /* Constant-time compare of the base64 credential. */ @@ -555,7 +569,7 @@ static int check_basic_auth(const WolfCertServer* s, const char* auth_header) if (ok) ok = (wc_ConstantCompare((const byte*)tok, enc.data, (int)enc.len) == 0); - wolfcert_buffer_free(&enc); + wolfcert_buffer_free_secure(&enc); return ok; } From ab8a84a12413d88c9b07958f357f735e81e93d27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 8 Oct 2026 11:13:45 +0200 Subject: [PATCH 04/10] Bound the EST server's chunk copy by subtraction The decode pass checked ri + csz and body_sz + csz, which can wrap on a 32-bit size_t. chunked_body_complete() already rejects any chunk that does not fit before the decode pass runs, so this was not reachable, but the check should not depend on that. The client decoder in http.c already uses this form. --- src/est/est_server.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/est/est_server.c b/src/est/est_server.c index 568f015..0946de2 100644 --- a/src/est/est_server.c +++ b/src/est/est_server.c @@ -394,7 +394,7 @@ static int parse_request_in(WolfCertServer* s, int fd, EstRequest* out, if (csz == 0) break; - if (ri + csz > raw_len || body_sz + csz > BODY_CAP) { + if (csz > raw_len - ri || csz > BODY_CAP - body_sz) { WOLFCERT_XFREE(body, heap); WOLFCERT_XFREE(raw, heap); return WOLFCERT_ERR_PROTOCOL; From ccce575b994eba86e981d70896a13ee1d9424b97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 8 Oct 2026 11:13:45 +0200 Subject: [PATCH 05/10] Pin the CA in the SCEP example GetCACert is unauthenticated over plain HTTP, and the example went straight from it to PKCSReq. It now requires the CA's SHA-256 fingerprint as its third argument and checks it with wolfcert_scep_verify_ca_fingerprint before enrolling. The migration guide no longer describes the example as lacking the check. --- docs/MIGRATING-FROM-WOLFSCEP.md | 2 +- examples/enroll_scep.c | 52 ++++++++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/docs/MIGRATING-FROM-WOLFSCEP.md b/docs/MIGRATING-FROM-WOLFSCEP.md index a62bebe..27a1116 100644 --- a/docs/MIGRATING-FROM-WOLFSCEP.md +++ b/docs/MIGRATING-FROM-WOLFSCEP.md @@ -75,7 +75,7 @@ wolfSCEP_response(scep, answer, &answerSz, 0); if (wolfSCEP_reply_status(scep) == WS_PKI_SUCCESS) { /* ... */ } ``` -The same enrollment in wolfCert, which is `examples/enroll_scep.c` with the fingerprint check added: +The same enrollment in wolfCert; `examples/enroll_scep.c` is a runnable version of it: ```c WolfCertServerCfg srv = { diff --git a/examples/enroll_scep.c b/examples/enroll_scep.c index 521e953..c002761 100644 --- a/examples/enroll_scep.c +++ b/examples/enroll_scep.c @@ -21,10 +21,11 @@ * Minimal SCEP enrollment example. * * Usage: - * enroll_scep + * enroll_scep * - * Fetches the CA cert via GetCACert, then PKCSReq-enrolls a fresh 2048-bit - * RSA key. Writes the issued cert in PEM to stdout. + * Fetches the CA cert via GetCACert, checks it against the SHA-256 + * fingerprint, then PKCSReq-enrolls a fresh 2048-bit RSA key. Writes the + * issued cert in PEM to stdout. */ #include @@ -41,10 +42,38 @@ /* SCEP is RSA-only (RFC 8894); other key types enroll over EST. */ +static int hex_decode(const char* hex, uint8_t* out, size_t out_len) +{ + size_t i; + + if (strlen(hex) != 2 * out_len) + return -1; + + for (i = 0; i < 2 * out_len; i++) { + char c = hex[i]; + int v = (c >= '0' && c <= '9') ? c - '0' + : (c >= 'a' && c <= 'f') ? c - 'a' + 10 + : (c >= 'A' && c <= 'F') ? c - 'A' + 10 : -1; + if (v < 0) + return -1; + if (i % 2 == 0) + out[i / 2] = (uint8_t)(v << 4); + else + out[i / 2] |= (uint8_t)v; + } + return 0; +} + int main(int argc, char** argv) { - if (argc < 3) { - fprintf(stderr, "usage: %s URL SUBJECT\n", argv[0]); + uint8_t fp[32]; + + if (argc < 4) { + fprintf(stderr, "usage: %s URL SUBJECT CA_SHA256_HEX\n", argv[0]); + return 1; + } + if (hex_decode(argv[3], fp, sizeof(fp)) != 0) { + fprintf(stderr, "CA fingerprint must be 64 hex digits\n"); return 1; } @@ -63,6 +92,15 @@ int main(int argc, char** argv) if (wc_PemToDer(ca_pem.data, (long)ca_pem.len, CERT_TYPE, &ca_der, NULL, NULL, NULL) != 0) return 2; + /* GetCACert is unauthenticated over plain HTTP, so pin the CA. */ + int rc = wolfcert_scep_verify_ca_fingerprint(ca_der->buffer, + ca_der->length, fp, sizeof(fp), WOLFCERT_SCEP_FP_SHA256); + if (rc != WOLFCERT_OK) { + fprintf(stderr, "CA fingerprint check failed: %s\n", + wolfcert_strerror(rc)); + return 2; + } + /* 2) Optionally query capabilities. */ WolfCertScepCaps caps = { 0 }; wolfcert_scep_get_ca_caps(&srv, &caps); @@ -81,8 +119,8 @@ int main(int argc, char** argv) /* 4) PKCSReq. */ WolfCertBuffer cert = { 0 }; - int rc = wolfcert_scep_pkcs_req(&srv, &caps, ca_der->buffer, ca_der->length, - key, csr.data, csr.len, &cert); + rc = wolfcert_scep_pkcs_req(&srv, &caps, ca_der->buffer, ca_der->length, + key, csr.data, csr.len, &cert); if (rc != WOLFCERT_OK) { fprintf(stderr, "PKCSReq failed: %s\n", wolfcert_strerror(rc)); return 2; From d3a18535852035a79c321841c006ed2ab2071974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 8 Oct 2026 11:13:45 +0200 Subject: [PATCH 06/10] Check the issued subject in the EST TLS roundtrip The assert on the issued certificate's subject was ORed with issued.len > 0 and so could not fail. It now decodes the certificate and compares the subject CN. --- tests/integration/test_est_tls_roundtrip.c | 38 +++++++++++++++++----- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/tests/integration/test_est_tls_roundtrip.c b/tests/integration/test_est_tls_roundtrip.c index 992a296..51c8483 100644 --- a/tests/integration/test_est_tls_roundtrip.c +++ b/tests/integration/test_est_tls_roundtrip.c @@ -55,6 +55,32 @@ static void* server_thread(void* arg) { wolfcert_server_run((WolfCertServer*)arg); return NULL; } +static int cn_is(const DecodedCert* dc, const char* cn) +{ + return dc->subjectCN != NULL && dc->subjectCNLen == (int)strlen(cn) && + memcmp(dc->subjectCN, cn, strlen(cn)) == 0; +} + +/* The issued cert's subject CN must equal cn. */ +static int check_subject_cn(const WolfCertBuffer* issued, const char* cn) +{ + uint8_t der[4096]; + DecodedCert dc; + int der_len; + int match; + + der_len = wc_CertPemToDer(issued->data, (int)issued->len, der, + (int)sizeof(der), CERT_TYPE); + REQUIRE(der_len > 0); + + wc_InitDecodedCert(&dc, der, (word32)der_len, NULL); + match = wc_ParseCert(&dc, CERT_TYPE, NO_VERIFY, NULL) == 0 && + cn_is(&dc, cn); + wc_FreeDecodedCert(&dc); + REQUIRE(match); + return 0; +} + #ifdef KEEP_PEER_CERT static int impostor_customize(void* wolfssl_cert, void* ctx) { @@ -80,12 +106,9 @@ static int check_renewed_identity(const WolfCertBuffer* issued) REQUIRE(der_len > 0); wc_InitDecodedCert(&dc, der, (word32)der_len, NULL); - REQUIRE(wc_ParseCert(&dc, CERT_TYPE, NO_VERIFY, NULL) == 0); - REQUIRE(dc.subjectCN != NULL); - REQUIRE(dc.subjectCNLen == (int)strlen("reenroll-device")); - REQUIRE(memcmp(dc.subjectCN, "reenroll-device", - strlen("reenroll-device")) == 0); - found = has_alt(dc.altNames, ASN_IP_TYPE, (const char*)ip, + found = wc_ParseCert(&dc, CERT_TYPE, NO_VERIFY, NULL) == 0 && + cn_is(&dc, "reenroll-device") && + has_alt(dc.altNames, ASN_IP_TYPE, (const char*)ip, (int)sizeof(ip)); wc_FreeDecodedCert(&dc); REQUIRE(found); @@ -375,8 +398,7 @@ int main(void) fprintf(stderr, "enroll rc=%d (%s)\n", rc, wolfcert_strerror(rc)); REQUIRE(rc == WOLFCERT_OK); REQUIRE(memmem(issued.data, issued.len, "BEGIN CERTIFICATE", 17) != NULL); - REQUIRE(memmem(issued.data, issued.len, "tls-est-client", 14) != NULL || - issued.len > 0); /* PEM carries subject in DER, not ASCII */ + REQUIRE(check_subject_cn(&issued, "tls-est-client") == 0); /* The same trust anchor in DER form. */ DerBuffer* ta_der = NULL; From 2fe68362cbdf2c4f58af325ae99a43c115354167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 8 Oct 2026 11:13:45 +0200 Subject: [PATCH 07/10] Remove WolfCertCertMeta.csr_attributes_der wolfcert_csr_build never put it into the CSR, so callers setting it had their attributes dropped without an error. wolfSSL's CSR encoder has no way to pass raw attributes through; the customize callback and wolfcert_csr_attrs_apply cover what it was meant for. This changes the layout of WolfCertCertMeta. --- wolfcert/types.h | 6 ------ 1 file changed, 6 deletions(-) diff --git a/wolfcert/types.h b/wolfcert/types.h index cde97c4..2617a3a 100644 --- a/wolfcert/types.h +++ b/wolfcert/types.h @@ -112,12 +112,6 @@ typedef struct { const char* key_usage; /* comma-separated */ const char* extended_key_usage; /* comma-separated EKU names or OIDs */ - /* Raw CSR attribute DER. wolfcert_csr_build does not add it to the CSR - * yet; customize can set other attributes on the wolfSSL Cert, and - * wolfcert_csr_attrs_apply maps the typed /csrattrs hints. */ - const uint8_t* csr_attributes_der; - size_t csr_attributes_der_len; - /* Optional PKCS#9 challengePassword CSR attribute, used by SCEP (RFC 8894 * section 2.4); NULL emits none. At most CTC_NAME_SIZE - 1 chars. */ const char* challenge_password; From 4bf4d724a6539a5d76d439d6b62c21b7501f7481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 8 Oct 2026 11:13:45 +0200 Subject: [PATCH 08/10] Fix the WolfCertKeyCfg param comment For ECC it is the curve size in bits (256, 384, 521), not a curve id. --- wolfcert/types.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wolfcert/types.h b/wolfcert/types.h index 2617a3a..26e8c63 100644 --- a/wolfcert/types.h +++ b/wolfcert/types.h @@ -83,7 +83,7 @@ typedef enum { typedef struct { WolfCertKeyType type; - int param; /* RSA bits (2048/3072/4096) or ECC curve id */ + int param; /* RSA 2048/3072/4096 or ECC 256/384/521 bits */ int dev_id; /* wolfSSL CryptoCb devId; -1 for software */ const char* key_label; /* optional backend identifier for persistent keys */ void* heap; /* optional heap hint; NULL = default */ From 07a50d8253a485cda4b29d1e056eb083ae376643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 8 Oct 2026 11:13:45 +0200 Subject: [PATCH 09/10] Note the missing tls-unique binding in the README RFC 7030 section 3.5 has the client put tls-unique into the CSR and the server check it. wolfCert does neither, and TLS 1.3 does not define tls-unique. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 65f5b71..5465fe3 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,8 @@ All `WolfCertBuffer` outputs remember which heap they came from, so the same build). Keys run in software or through any wolfSSL CryptoCb backend. - **SCEP is RSA-only** (per RFC 8894); use EST for the other key types. - **EST endpoints:** `/cacerts`, `/simpleenroll`, `/simplereenroll`, - `/csrattrs`. + `/csrattrs`. The RFC 7030 section 3.5 tls-unique binding is neither sent + nor checked; TLS 1.3 does not define tls-unique. - **SCEP operations:** `GetCACaps`, `GetCACert`, `GetNextCACert`, `PKCSReq`, `RenewalReq`, `GetCert`, plus `pkiStatus=PENDING` + `GetCertInitial` polling. - **Transport:** HTTP/1.1 over wolfSSL TLS 1.3/1.2 (trust anchors, SNI, From e9d407a87109778915d3799f88ccd5525bb90844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 8 Oct 2026 11:13:45 +0200 Subject: [PATCH 10/10] Stop build-wolfssl.sh on an unknown config resolve_flags ran in a process substitution, so its exit 2 only ended the subshell and the script went on to build with no configure flags. --- scripts/ci/build-wolfssl.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/ci/build-wolfssl.sh b/scripts/ci/build-wolfssl.sh index abcab84..1072bd6 100755 --- a/scripts/ci/build-wolfssl.sh +++ b/scripts/ci/build-wolfssl.sh @@ -182,10 +182,11 @@ done # One flag per line keeps the spaces inside CPPFLAGS. No mapfile: macOS # runners run this under /bin/bash 3.2 on a cache miss. +_flags=$(resolve_flags "$CONFIG") || exit 2 CONFIGURE_FLAGS=() while IFS= read -r _flag; do CONFIGURE_FLAGS+=("$_flag") -done < <(resolve_flags "$CONFIG") +done <<< "$_flags" : "${PREFIX:=$PWD/.wolfssl-install/$CONFIG}" : "${JOBS:=$( (command -v nproc >/dev/null && nproc) || sysctl -n hw.ncpu 2>/dev/null || echo 2)}"