From a94f85a42aed3b0d41cbcba1ff2031ff6ffda27d Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 21 May 2026 13:45:09 +0000 Subject: [PATCH 01/15] feat: S08.07 slice 6a mbedTLS scaffolding into FreeRTOS BDD target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compile and link mbedTLS into the FreeRTOS QEMU BDD ELF. No behaviour yet beyond proving the build stack is healthy — the new TlsSender wrapper returns a no-op SolidSyslogSender so the linker has something to satisfy its eventual call site in slice 6b without yet pulling mbedTLS into the reachable graph (--gc-sections drops all of mbedTLS for now). - Add Platform/MbedTls sources to the FreeRTOS executable so future MbedTlsStream_Create from main.c will reach them. - Pull mbedTLS upstream into the cross build via add_subdirectory with the same EXCLUDE_FROM_ALL + -w + tidy/cppcheck-disabled pattern used by Tests/MbedTlsIntegration/CMakeLists.txt. Three new file-scope variables in the new CMake section (ENABLE_PROGRAMS, ENABLE_TESTING, MBEDTLS_FATAL_WARNINGS) force-OFF the upstream demo binaries and tests we never want in our build. - Bdd/Targets/FreeRtos/mbedtls_user_config.h: integrator overrides wired via MBEDTLS_USER_CONFIG_FILE. Disable the upstream code paths that assume a Unix/Windows host — platform entropy (Cortex-M3 has no /dev/urandom; slice 6b will register a weak entropy callback), filesystem IO (PEMs are baked into the ELF), BSD sockets (our own transport via mbedtls_ssl_set_bio), system clock (no RTC, baked certs carry 2024–2099 validity), threading hooks (per the customer- coexistence contract), PSA on-filesystem storage, and the timing helpers (the adapter manages its own bounded handshake retry). - Add BddTargetTlsSender_MbedTls_FreeRtosTcp.c as a same-shape parallel to BddTargetTlsSender_OpenSsl_PosixTcp.c. Slice 6a placeholder returns a no-op sender; slice 6b composes the real FreeRtosTcpStream + MbedTlsStream + StreamSender stack. - Commit the .devcontainer/devcontainer.json swap to freertos-target (ARM cross + QEMU + GDB + Behave), the container that this slice and the rest of slice 6 are developed in. The container image's xxd dependency for slice 6b's PEM baking is already pushed to ghcr; the in-repo SHA bump lands as part of 6b. --- .devcontainer/devcontainer.json | 4 +- .../BddTargetTlsSender_MbedTls_FreeRtosTcp.c | 53 +++++++++++++++ Bdd/Targets/FreeRtos/CMakeLists.txt | 55 ++++++++++++++++ Bdd/Targets/FreeRtos/mbedtls_user_config.h | 66 +++++++++++++++++++ 4 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c create mode 100644 Bdd/Targets/FreeRtos/mbedtls_user_config.h diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a5e3e58f..4de6513f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,10 +3,10 @@ "dockerComposeFile": "docker-compose.yml", // Pick one service — comment the active line, uncomment the desired one, // then "Dev Containers: Rebuild Container". See docs/containers.md. - "service": "gcc", // host development — BUILD_PRESET=debug + // "service": "gcc", // host development — BUILD_PRESET=debug // "service": "clang", // clang variant — BUILD_PRESET=clang-debug // "service": "freertos-host", // FreeRTOS adapter TDD vs fakes — BUILD_PRESET=debug - // "service": "freertos-target", // ARM cross + QEMU + GDB — BUILD_PRESET=freertos-cross + "service": "freertos-target", // ARM cross + QEMU + GDB — BUILD_PRESET=freertos-cross // "service": "behave", // BDD scenarios — no cmake (BUILD_PRESET unset) "workspaceFolder": "/workspaces/SolidSyslog", "features": { diff --git a/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c b/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c new file mode 100644 index 00000000..abb923c1 --- /dev/null +++ b/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c @@ -0,0 +1,53 @@ +/* MbedTLS-over-FreeRtosTcpStream variant of the BDD TLS sender. + * + * Slice 6a (this file): scaffolding only. Returns a no-op sender so the + * FreeRTOS BDD target compiles and links with mbedTLS wired into the build, + * proving the platform / config / CMake stack is healthy before slice 6b + * lands the real entropy + DRBG + PEM-parse + StreamSender wiring. + * + * Once 6b lands, this file will compose: + * - SolidSyslogFreeRtosTcpStream (inner TCP transport) + * - SolidSyslogMbedTlsStream (TLS, with handles passed + * from main.c's CTR_DRBG + + * parsed CA / client cert) + * - SolidSyslogStreamSender (octet-framed RFC 6587) + * + * Mirroring BddTargetTlsSender_OpenSsl_PosixTcp.c on the POSIX target. The + * mbedTLS adapter takes pre-built handles rather than file paths (no + * MBEDTLS_FS_IO on this target), so the demo PEMs are baked into the ELF + * via xxd -i in slice 6b and parsed at first BddTargetTlsSender_Create call. + */ + +#include "BddTargetTlsSender.h" +#include "SolidSyslogSenderDefinition.h" + +#include + +struct SolidSyslogResolver; + +static bool MbedTlsBddSender_Send(struct SolidSyslogSender* self, const void* buffer, size_t size) +{ + (void) self; + (void) buffer; + (void) size; + /* Slice 6a placeholder: drop on the floor. Real implementation in 6b. */ + return false; +} + +static void MbedTlsBddSender_Disconnect(struct SolidSyslogSender* self) +{ + (void) self; +} + +static struct SolidSyslogSender mbedTlsBddSender = {MbedTlsBddSender_Send, MbedTlsBddSender_Disconnect}; + +struct SolidSyslogSender* BddTargetTlsSender_Create(struct SolidSyslogResolver* resolver, bool mtls) +{ + (void) resolver; + (void) mtls; + return &mbedTlsBddSender; +} + +void BddTargetTlsSender_Destroy(void) +{ +} diff --git a/Bdd/Targets/FreeRtos/CMakeLists.txt b/Bdd/Targets/FreeRtos/CMakeLists.txt index 45faf05c..dc6af36e 100644 --- a/Bdd/Targets/FreeRtos/CMakeLists.txt +++ b/Bdd/Targets/FreeRtos/CMakeLists.txt @@ -37,6 +37,13 @@ if(NOT FATFS_PATH) "which sets this to /opt/fatfs.") endif() +set(MBEDTLS_SOURCE_DIR "$ENV{MBEDTLS_DIR}") +if(NOT EXISTS "${MBEDTLS_SOURCE_DIR}/CMakeLists.txt") + message(FATAL_ERROR + "MBEDTLS_DIR not set or invalid. Use the cpputest-freertos-cross " + "container, which sets this to /opt/mbedtls.") +endif() + # Stage the upstream FatFs source pack alongside our integrator ffconf.h in # the build dir. FatFs's ff.h does `#include "ffconf.h"` and GCC's `""` # include lookup checks the source file's own directory first, so the only @@ -161,6 +168,41 @@ target_include_directories(solid_syslog_freertos_upstream PRIVATE ${FATFS_STAGE_DIR} # ff.h, diskio.h, our ffconf.h colocated ) +# --- mbedTLS upstream as a subproject ---------------------------------------- +# +# Same pattern as Tests/MbedTlsIntegration/CMakeLists.txt: bring mbedTLS in via +# add_subdirectory(EXCLUDE_FROM_ALL), force-disable its programs/tests, and +# strip our strict warning surface from the upstream targets so a future +# mbedTLS bump doesn't break our build for diagnostic reasons. clang-tidy / +# cppcheck are also disabled per-target — they would otherwise apply our +# .clang-tidy ruleset to upstream code. +set(ENABLE_PROGRAMS OFF CACHE BOOL "Disable mbedTLS demo programs in our build" FORCE) +set(ENABLE_TESTING OFF CACHE BOOL "Disable mbedTLS upstream tests in our build" FORCE) +set(MBEDTLS_FATAL_WARNINGS OFF CACHE BOOL "Upstream mbedTLS warnings are not errors in our build" FORCE) +add_subdirectory(${MBEDTLS_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}/_mbedtls EXCLUDE_FROM_ALL) + +# MBEDTLS_USER_CONFIG_FILE is consumed by mbedTLS's headers via `#include`, +# so the macro value must be a quoted C string literal. CMake-quote with +# `\"` to survive the shell -> compiler hop. +set(MBEDTLS_USER_CONFIG_HEADER + "\"${CMAKE_CURRENT_SOURCE_DIR}/mbedtls_user_config.h\"" +) + +foreach(_mbedtls_upstream_target IN ITEMS mbedtls mbedx509 mbedcrypto everest p256m) + if(TARGET ${_mbedtls_upstream_target}) + target_compile_options(${_mbedtls_upstream_target} PRIVATE -w) + target_compile_definitions(${_mbedtls_upstream_target} PRIVATE + MBEDTLS_USER_CONFIG_FILE=${MBEDTLS_USER_CONFIG_HEADER} + ) + set_target_properties(${_mbedtls_upstream_target} PROPERTIES + C_CLANG_TIDY "" + CXX_CLANG_TIDY "" + C_CPPCHECK "" + CXX_CPPCHECK "" + ) + endif() +endforeach() + # --- Executable --------------------------------------------------------------- # # Project code only. Inherits the full project warning set @@ -185,10 +227,13 @@ add_executable(SolidSyslogBddTarget ${CMAKE_SOURCE_DIR}/Platform/FreeRtos/Source/SolidSyslogFreeRtosSysUpTime.c ${CMAKE_SOURCE_DIR}/Platform/FreeRtos/Source/SolidSyslogFreeRtosTcpStream.c ${CMAKE_SOURCE_DIR}/Platform/FreeRtos/Source/SolidSyslogFreeRtosTcpStreamStatic.c + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamStatic.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetInteractive.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetIps.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetLanguage.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetSwitchConfig.c + ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c $ ) @@ -210,6 +255,8 @@ target_include_directories(SolidSyslogBddTarget PRIVATE ${CMAKE_SOURCE_DIR}/Platform/FatFs/Interface # SolidSyslogFatFsFile.h ${CMAKE_SOURCE_DIR}/Platform/FreeRtos/Interface # SolidSyslogFreeRtos*.h ${CMAKE_SOURCE_DIR}/Platform/FreeRtos/Source # SolidSyslogAddressInternal.h + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Interface # SolidSyslogMbedTlsStream.h + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source # SolidSyslogMbedTlsStreamPrivate.h ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common # BddTargetInteractive.h ${FREERTOS_KERNEL_PATH}/include ${FREERTOS_PORT_DIR} # portmacro.h @@ -226,6 +273,14 @@ target_include_directories(SolidSyslogBddTarget PRIVATE target_link_libraries(SolidSyslogBddTarget PRIVATE ${PROJECT_NAME} # libSolidSyslog.a (cross-compiled) + mbedtls mbedx509 mbedcrypto # mbedTLS upstream (subproject, cross-compiled) +) + +# Platform/MbedTls/Source/*.c include ; they MUST see the same +# user config as the mbedTLS library itself, otherwise struct sizes and the +# enabled-feature bitset diverge between the consumer and the library. +target_compile_definitions(SolidSyslogBddTarget PRIVATE + MBEDTLS_USER_CONFIG_FILE=${MBEDTLS_USER_CONFIG_HEADER} ) target_link_options(SolidSyslogBddTarget PRIVATE diff --git a/Bdd/Targets/FreeRtos/mbedtls_user_config.h b/Bdd/Targets/FreeRtos/mbedtls_user_config.h new file mode 100644 index 00000000..d0147fcc --- /dev/null +++ b/Bdd/Targets/FreeRtos/mbedtls_user_config.h @@ -0,0 +1,66 @@ +/* mbedTLS integrator overrides for the FreeRTOS QEMU BDD target. + * + * mbedTLS supports an optional integrator config layered on top of its + * built-in default via `-DMBEDTLS_USER_CONFIG_FILE="path"`. Anything we + * #define here adds to the default; anything we #undef removes from it. + * + * The defaults that mbedTLS picks for a generic build assume a Unix or + * Windows host — they pull /dev/urandom for entropy, use fopen for cert + * loading, and call BSD sockets directly. The Cortex-M3 / FreeRTOS QEMU + * BDD target has none of those, so we strip them and rely on the integrator + * (main.c, slice 6b) to wire entropy, transport, and cert handles via DI. + * + * Anything not touched here keeps mbedTLS's default — including the cipher + * suite set, RSA, ECC curves, SHA, AES, the PEM parser, x509 parsing, the + * CTR-DRBG implementation, etc. Trimming further is a binary-size exercise + * that belongs after the QEMU footprint is measured (slice 6c+). + */ + +#ifndef BDD_TARGET_FREERTOS_MBEDTLS_USER_CONFIG_H +#define BDD_TARGET_FREERTOS_MBEDTLS_USER_CONFIG_H + +/* Don't compile entropy_poll.c's Unix/Windows code path — mbedTLS would + * otherwise #error on "Platform entropy sources only work on Unix and + * Windows". main.c provides a weak entropy callback via + * mbedtls_entropy_add_source(MBEDTLS_ENTROPY_SOURCE_WEAK). The + * "demo-only entropy" caveat is documented in the integrator guide. */ +#define MBEDTLS_NO_PLATFORM_ENTROPY + +/* No filesystem from mbedTLS's point of view. PEMs are baked into the ELF + * via xxd -i (slice 6b) and parsed via mbedtls_x509_crt_parse / _pk_parse_key + * against the in-memory buffer, so MBEDTLS_FS_IO is unused dead code and a + * link hazard against newlib stubs. */ +#undef MBEDTLS_FS_IO + +/* No BSD sockets — the transport is injected as a SolidSyslogStream + * (FreeRtosTcpStream) and bridged into mbedTLS via mbedtls_ssl_set_bio + * callbacks. MBEDTLS_NET_C would otherwise pull in . */ +#undef MBEDTLS_NET_C + +/* No host clock. Cortex-M3 has no wall-clock; mbedTLS's cert-validity-date + * check is skipped when this is off, which is fine for BDD with baked certs + * carrying validity 20240101–20990101. Production integrators with an RTC + * should turn MBEDTLS_HAVE_TIME[_DATE] back on. */ +#undef MBEDTLS_HAVE_TIME +#undef MBEDTLS_HAVE_TIME_DATE + +/* Disable mbedTLS's own threading primitive layer. The library runs on the + * service task only — concurrent access to the ssl_context is not in scope + * for this target. Per [[project-mbedtls-coexistence-contract]] the library + * must never install threading hooks. */ +#undef MBEDTLS_THREADING_C +#undef MBEDTLS_THREADING_PTHREAD + +/* PSA's "internal trusted storage on filesystem" requires MBEDTLS_FS_IO, + * which we just disabled. We don't use the PSA API surface anyway — the + * adapter is built on the classic mbedTLS API (mbedtls_ssl_*, mbedtls_x509_*, + * mbedtls_pk_*, mbedtls_ctr_drbg_*). */ +#undef MBEDTLS_PSA_ITS_FILE_C +#undef MBEDTLS_PSA_CRYPTO_STORAGE_C + +/* mbedTLS's timing.c uses gettimeofday / clock_gettime — Unix/Windows only. + * The adapter manages its own bounded handshake retry budget via the + * injected Sleep callback, so MBEDTLS_TIMING_C is unused. */ +#undef MBEDTLS_TIMING_C + +#endif /* BDD_TARGET_FREERTOS_MBEDTLS_USER_CONFIG_H */ From 3361ce8475bf45f260d70d7fa2f4015f87c5b41b Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 21 May 2026 15:18:30 +0000 Subject: [PATCH 02/15] chore: bump cpputest-freertos image to sha-1cb0f34 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds xxd to the host + cross images (CppUTestFreertosDocker commit d364407 → 1cb0f34 after merge). xxd is consumed by slice 6b's CMake to bake demo CA / client cert / client key PEMs into the FreeRTOS BDD ELF via `xxd -i` — there's no filesystem path from the host into QEMU, so the PEMs travel as static const arrays in rodata and feed straight into mbedtls_x509_crt_parse / _pk_parse_key. Both cpputest-freertos (host TDD) and cpputest-freertos-cross (cross build, QEMU, behave) get bumped together per docs/containers.md ("always update both rows together"). --- .devcontainer/docker-compose.yml | 4 ++-- .github/workflows/ci.yml | 6 +++--- ci/docker-compose.bdd.yml | 2 +- docs/containers.md | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 6c8632e3..42d62f9e 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -102,7 +102,7 @@ services: command: sleep infinity freertos-host: - image: ghcr.io/davidcozens/cpputest-freertos:sha-bbc958b + image: ghcr.io/davidcozens/cpputest-freertos:sha-1cb0f34 volumes: - ..:/workspaces/SolidSyslog:cached - ${SSH_AUTH_SOCK:-/dev/null}:/ssh-agent @@ -153,7 +153,7 @@ services: - syslog-ng freertos-target: - image: ghcr.io/davidcozens/cpputest-freertos-cross:sha-bbc958b + image: ghcr.io/davidcozens/cpputest-freertos-cross:sha-1cb0f34 volumes: - ..:/workspaces/SolidSyslog:cached - syslog-ng-ctl-freertos:/var/lib/syslog-ng diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89425c07..afcf0bf6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -119,7 +119,7 @@ jobs: # cpputest-freertos image carries /opt/mbedtls (and exports MBEDTLS_DIR); # Tests/MbedTlsIntegration/CMakeLists.txt requires that env var. container: - image: ghcr.io/davidcozens/cpputest-freertos:sha-bbc958b + image: ghcr.io/davidcozens/cpputest-freertos:sha-1cb0f34 options: --user root env: GIT_CONFIG_COUNT: 1 @@ -840,7 +840,7 @@ jobs: contents: read checks: write container: - image: ghcr.io/davidcozens/cpputest-freertos:sha-bbc958b + image: ghcr.io/davidcozens/cpputest-freertos:sha-1cb0f34 options: --user root env: GIT_CONFIG_COUNT: 1 @@ -884,7 +884,7 @@ jobs: permissions: contents: read container: - image: ghcr.io/davidcozens/cpputest-freertos-cross:sha-bbc958b + image: ghcr.io/davidcozens/cpputest-freertos-cross:sha-1cb0f34 options: --user root env: GIT_CONFIG_COUNT: 1 diff --git a/ci/docker-compose.bdd.yml b/ci/docker-compose.bdd.yml index c0235964..62048b45 100644 --- a/ci/docker-compose.bdd.yml +++ b/ci/docker-compose.bdd.yml @@ -87,7 +87,7 @@ services: behave-freertos: # Cross image carries qemu-system-arm + python3 + behave. - image: ghcr.io/davidcozens/cpputest-freertos-cross:sha-bbc958b + image: ghcr.io/davidcozens/cpputest-freertos-cross:sha-1cb0f34 user: "0" volumes: - ..:/workspaces/SolidSyslog diff --git a/docs/containers.md b/docs/containers.md index 0c2ba7f3..974412cc 100644 --- a/docs/containers.md +++ b/docs/containers.md @@ -6,8 +6,8 @@ |---|---|---| | `ghcr.io/davidcozens/cpputest` | `sha-18f19e1` | devcontainer (`gcc` service), most CI jobs | | `ghcr.io/davidcozens/cpputest-clang` | `sha-7eac3ab` | `clang` compose service, `build-linux-clang` CI job, `analyze-iwyu` CI job | -| `ghcr.io/davidcozens/cpputest-freertos` | `sha-bbc958b` | `freertos-host` compose service, `build-freertos-host-tdd` CI job — adds FreeRTOS-Kernel / Plus-TCP / FatFs / Mbed TLS sources for host-TDD of FreeRTOS adapters against fakes | -| `ghcr.io/davidcozens/cpputest-freertos-cross` | `sha-bbc958b` | `freertos-target` compose service, `build-freertos-target` CI job, `behave-freertos` BDD service, `bdd-freertos-qemu` CI job — adds `gcc-arm-none-eabi`, `libnewlib-arm-none-eabi`, `gdb-multiarch` (aliased as `arm-none-eabi-gdb`), `qemu-system-arm`, `python3` + `behave`, FreeRTOS-Kernel / Plus-TCP / FatFs sources at `/opt/freertos-kernel` / `/opt/freertos-plus-tcp` / `/opt/fatfs` for cross builds, on-QEMU runs, and BDD scenarios driving a QEMU target | +| `ghcr.io/davidcozens/cpputest-freertos` | `sha-1cb0f34` | `freertos-host` compose service, `build-freertos-host-tdd` CI job — adds FreeRTOS-Kernel / Plus-TCP / FatFs / Mbed TLS sources for host-TDD of FreeRTOS adapters against fakes | +| `ghcr.io/davidcozens/cpputest-freertos-cross` | `sha-1cb0f34` | `freertos-target` compose service, `build-freertos-target` CI job, `behave-freertos` BDD service, `bdd-freertos-qemu` CI job — adds `gcc-arm-none-eabi`, `libnewlib-arm-none-eabi`, `gdb-multiarch` (aliased as `arm-none-eabi-gdb`), `qemu-system-arm`, `python3` + `behave`, FreeRTOS-Kernel / Plus-TCP / FatFs sources at `/opt/freertos-kernel` / `/opt/freertos-plus-tcp` / `/opt/fatfs` for cross builds, on-QEMU runs, and BDD scenarios driving a QEMU target | | `balabit/syslog-ng` | `4.8.2` | `syslog-ng-linux` and `syslog-ng-freertos` services — BDD test oracles, one per target pair. Pinned to the 4.8 LTS line; 4.11.0 (`latest` as of 2026-02-24) regressed by aborting on `STATS` over the control socket, which crashed the oracle and cascaded to the dev-container network when `freertos-target` shares the namespace. | | `ghcr.io/davidcozens/behave` | `sha-3faff14` | `behave-linux` service — Debian trixie + Python 3.12 + Behave for Linux BDD scenarios. The FreeRTOS BDD runner uses the `cpputest-freertos-cross` image instead (which carries QEMU + Behave). | From 33410f6db32e29f71b71afb0904fabbc33860d0e Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 21 May 2026 16:02:57 +0000 Subject: [PATCH 03/15] feat: S08.07 slice 6b mbedTLS over FreeRtosTcpStream TLS slot in BDD target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The FreeRTOS QEMU BDD target now has a working TLS path. mbedTLS pulls into the reachable graph (--gc-sections no longer drops it: text 125 KB → 642 KB) and the switching sender's slot 2 is wired to the new mbedTLS stack instead of falling through to NullSender. - CMake bakes Bdd/syslog-ng/tls/{ca,client}.pem + client.key into generated headers via xxd -i (cat-then-NUL trick deliberately not used; the wrapper TU does the NUL-terminate copy at parse time instead). - BddTargetTlsSender_MbedTls_FreeRtosTcp.c owns the full TLS stack composition: entropy + CTR_DRBG seed (one-shot, idempotent), baked-PEM parse to mbedtls_x509_crt + mbedtls_pk_context handles, and the FreeRtosTcpStream + MbedTlsStream + StreamSender chain. Diagnostic printfs and vTaskDelay(1) yields surround each major init step — QEMU emulates Cortex-M3 at host speed, but cert/key parses can still take seconds, and the yields keep lower-priority tasks alive instead of starving them through a multi-second monopoly of the service-thread. - Boot diagnostics use printf rather than SolidSyslog_Error because main installs the error handler AFTER BddTargetTlsSender_Create runs; the default no-op handler would otherwise swallow the WARNING that documents the demo-only entropy. - Slice 6b ships TLS-only (mtls=false) in main.c. The FreeRTOS BDD-harness command flow has the tls/mtls choice arrive over the UART AFTER boot, so mTLS support either needs a fourth switching- sender slot, a runtime endpoint-dispatch shim, or boot-time configuration. Picked up in slice 6c. Smoke-tested under QEMU mps2-an385: target boots cleanly, mbedTLS init prints emit in <3s, interactive prompt appears, no regression on the existing UDP/TCP slots. --- .../BddTargetTlsSender_MbedTls_FreeRtosTcp.c | 231 +++++++++++++++--- Bdd/Targets/FreeRtos/CMakeLists.txt | 48 ++++ Bdd/Targets/FreeRtos/main.c | 23 +- 3 files changed, 268 insertions(+), 34 deletions(-) diff --git a/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c b/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c index abb923c1..5139bcab 100644 --- a/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c +++ b/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c @@ -1,53 +1,228 @@ -/* MbedTLS-over-FreeRtosTcpStream variant of the BDD TLS sender. +/* MbedTLS-over-FreeRtosTcpStream BDD TLS sender (slice 6b). * - * Slice 6a (this file): scaffolding only. Returns a no-op sender so the - * FreeRTOS BDD target compiles and links with mbedTLS wired into the build, - * proving the platform / config / CMake stack is healthy before slice 6b - * lands the real entropy + DRBG + PEM-parse + StreamSender wiring. + * Composes: + * - SolidSyslogFreeRtosTcpStream inner TCP transport + * - SolidSyslogMbedTlsStream TLS over the injected Stream + * - SolidSyslogStreamSender RFC 6587 octet-counting framing * - * Once 6b lands, this file will compose: - * - SolidSyslogFreeRtosTcpStream (inner TCP transport) - * - SolidSyslogMbedTlsStream (TLS, with handles passed - * from main.c's CTR_DRBG + - * parsed CA / client cert) - * - SolidSyslogStreamSender (octet-framed RFC 6587) + * Mirrors BddTargetTlsSender_OpenSsl_PosixTcp.c on the POSIX target. The + * mbedTLS adapter takes pre-built handles (mbedtls_ctr_drbg, mbedtls_x509_crt, + * mbedtls_pk_context) rather than file paths, because MBEDTLS_FS_IO is + * disabled in the integrator config — there is no host path reachable from + * QEMU. The demo CA / client cert / client key PEMs travel as `static const` + * arrays in rodata, baked at CMake-time by xxd -i from + * Bdd/syslog-ng/tls/ ca.pem / client.pem / client.key. The arrays are + * parsed once on first BddTargetTlsSender_Create call. * - * Mirroring BddTargetTlsSender_OpenSsl_PosixTcp.c on the POSIX target. The - * mbedTLS adapter takes pre-built handles rather than file paths (no - * MBEDTLS_FS_IO on this target), so the demo PEMs are baked into the ELF - * via xxd -i in slice 6b and parsed at first BddTargetTlsSender_Create call. + * Entropy + CTR_DRBG also live in this TU rather than in main.c so all + * mbedTLS-specific state is one file's responsibility. The entropy source + * is deliberately weak — see DemoEntropySource — and an audit-trail + * WARNING is emitted via SolidSyslog_Error on first init. */ #include "BddTargetTlsSender.h" -#include "SolidSyslogSenderDefinition.h" +#include "BddTargetMtlsConfig.h" +#include "BddTargetTlsConfig.h" +#include "SolidSyslogFreeRtosTcpStream.h" +#include "SolidSyslogMbedTlsStream.h" +#include "SolidSyslogStream.h" +#include "SolidSyslogStreamSender.h" + +#include +#include +#include +#include + +#include +#include + +#include #include +#include +#include +#include + +#include "BddBakedCaPem.h" +#include "BddBakedClientCertPem.h" +#include "BddBakedClientKeyPem.h" struct SolidSyslogResolver; -static bool MbedTlsBddSender_Send(struct SolidSyslogSender* self, const void* buffer, size_t size) +static struct SolidSyslogStream* underlyingStream; +static struct SolidSyslogStream* tlsStream; +static struct SolidSyslogSender* sender; + +/* Entropy + CTR_DRBG live for the lifetime of the BDD target; one-shot init + * gated on `mbedTlsInitialised`. mbedTLS structs are zero-initialised by + * static storage; mbedtls_*_init is still called explicitly because the + * library treats post-init / post-free as the same state. */ +static mbedtls_entropy_context entropy; +static mbedtls_ctr_drbg_context drbg; +static bool mbedTlsInitialised; + +/* mbedtls_x509_crt_parse / mbedtls_pk_parse_key require NUL-terminated PEM + * input. xxd -i doesn't append a NUL, so we hold a +1-sized copy here and + * NUL-terminate at parse time. The cost is one extra byte per PEM in BSS; + * cheaper than the shell pipeline that would be needed to bake the NUL at + * CMake time. */ +static unsigned char caPemBuf[sizeof(bdd_baked_ca_pem) + 1U]; +static unsigned char clientCertPemBuf[sizeof(bdd_baked_client_cert_pem) + 1U]; +static unsigned char clientKeyPemBuf[sizeof(bdd_baked_client_key_pem) + 1U]; + +static mbedtls_x509_crt caChain; +static mbedtls_x509_crt clientCertChain; +static mbedtls_pk_context clientKey; + +/* Demo-only entropy: XOR the FreeRTOS tick count, a per-call counter, and + * the destination address (which varies per call) into each output byte. + * Quality is intentionally terrible — QEMU has no real source — and the + * SolidSyslog_Error(WARNING, …) emit below makes that explicit. Real + * integrators on bare-metal would replace this with TRNG / HSM bytes. */ +static int DemoEntropySource(void* data, unsigned char* output, size_t len, size_t* olen) { - (void) self; - (void) buffer; - (void) size; - /* Slice 6a placeholder: drop on the floor. Real implementation in 6b. */ - return false; + (void) data; + static uint32_t counter = 0U; + for (size_t i = 0; i < len; i++) + { + counter++; + uint32_t mix = (uint32_t) xTaskGetTickCount() ^ counter ^ (uint32_t) (uintptr_t) &output[i]; + output[i] = (unsigned char) ((mix >> ((i % 4U) * 8U)) & 0xFFU); + } + *olen = len; + return 0; } -static void MbedTlsBddSender_Disconnect(struct SolidSyslogSender* self) +static void RtosSleep(int milliseconds) { - (void) self; + /* Same rounding rule as the CmsdkUart sleep in main.c — sub-tick requests + * must still block the task, otherwise vTaskDelay(0) just yields. */ + TickType_t ticks = pdMS_TO_TICKS((TickType_t) milliseconds); + if ((milliseconds > 0) && (ticks == 0U)) + { + ticks = 1U; + } + vTaskDelay(ticks); } -static struct SolidSyslogSender mbedTlsBddSender = {MbedTlsBddSender_Send, MbedTlsBddSender_Disconnect}; +/* Idempotent: safe to call from BddTargetTlsSender_Create on every invocation + * even though the first call is the only one that does real work. mbedTLS + * state for entropy/DRBG/cert/key lives at file scope and survives across + * connect/disconnect cycles. + * + * Each major step emits a SolidSyslog_Error INFO message and yields one + * tick to the FreeRTOS scheduler. Under QEMU mps2-an385 the DRBG seed + + * cert/key parses can each take several seconds (mbedTLS does serious + * crypto work — RSA key parse, ECDHE primes, ASN.1 walks); without the + * yields, lower-priority tasks would starve until init finishes, and + * without the diagnostic prints the boot would appear to hang. */ +static void EnsureMbedTlsInitialised(void) +{ + if (mbedTlsInitialised) + { + return; + } + + /* Boot diagnostics via printf rather than SolidSyslog_Error because main's + * error handler installation happens AFTER BddTargetTlsSender_Create — + * the default no-op handler would otherwise swallow these. */ + (void) printf("[mbedtls] init entropy + DRBG seed (slow under QEMU)\r\n"); + vTaskDelay(1U); + + mbedtls_entropy_init(&entropy); + mbedtls_entropy_add_source( + &entropy, + DemoEntropySource, + NULL, + MBEDTLS_ENTROPY_BLOCK_SIZE, + MBEDTLS_ENTROPY_SOURCE_WEAK + ); + + mbedtls_ctr_drbg_init(&drbg); + static const unsigned char personalization[] = "solidsyslog-freertos-bdd"; + (void) mbedtls_ctr_drbg_seed(&drbg, mbedtls_entropy_func, &entropy, personalization, sizeof(personalization) - 1U); + vTaskDelay(1U); + + (void) printf("[mbedtls] parsing CA chain\r\n"); + + memcpy(caPemBuf, bdd_baked_ca_pem, sizeof(bdd_baked_ca_pem)); + caPemBuf[sizeof(bdd_baked_ca_pem)] = '\0'; + mbedtls_x509_crt_init(&caChain); + (void) mbedtls_x509_crt_parse(&caChain, caPemBuf, sizeof(caPemBuf)); + vTaskDelay(1U); + + (void) printf("[mbedtls] parsing client cert chain\r\n"); + + memcpy(clientCertPemBuf, bdd_baked_client_cert_pem, sizeof(bdd_baked_client_cert_pem)); + clientCertPemBuf[sizeof(bdd_baked_client_cert_pem)] = '\0'; + mbedtls_x509_crt_init(&clientCertChain); + (void) mbedtls_x509_crt_parse(&clientCertChain, clientCertPemBuf, sizeof(clientCertPemBuf)); + vTaskDelay(1U); + + (void) printf("[mbedtls] parsing client key (RSA — slowest step)\r\n"); + + memcpy(clientKeyPemBuf, bdd_baked_client_key_pem, sizeof(bdd_baked_client_key_pem)); + clientKeyPemBuf[sizeof(bdd_baked_client_key_pem)] = '\0'; + mbedtls_pk_init(&clientKey); + (void) mbedtls_pk_parse_key( + &clientKey, + clientKeyPemBuf, + sizeof(clientKeyPemBuf), + NULL, + 0U, + mbedtls_ctr_drbg_random, + &drbg + ); + vTaskDelay(1U); + + /* Audit trail: every cold boot of this target announces the demo-only + * entropy explicitly. Integrators porting this off the BDD target should + * see this and replace DemoEntropySource with TRNG before shipping. */ + (void) printf("[mbedtls] init complete. WARNING: demo-only entropy " + "(xTaskGetTickCount + per-call counter). Not for production.\r\n"); + + mbedTlsInitialised = true; +} struct SolidSyslogSender* BddTargetTlsSender_Create(struct SolidSyslogResolver* resolver, bool mtls) { - (void) resolver; - (void) mtls; - return &mbedTlsBddSender; + EnsureMbedTlsInitialised(); + + underlyingStream = SolidSyslogFreeRtosTcpStream_Create(); + + static struct SolidSyslogMbedTlsStreamConfig tlsStreamConfig; + tlsStreamConfig = (struct SolidSyslogMbedTlsStreamConfig) {0}; + tlsStreamConfig.Transport = underlyingStream; + tlsStreamConfig.Sleep = RtosSleep; + tlsStreamConfig.Rng = &drbg; + tlsStreamConfig.CaChain = &caChain; + tlsStreamConfig.ServerName = mtls ? BddTargetMtlsConfig_GetServerName() : BddTargetTlsConfig_GetServerName(); + if (mtls) + { + tlsStreamConfig.ClientCertChain = &clientCertChain; + tlsStreamConfig.ClientKey = &clientKey; + } + tlsStream = SolidSyslogMbedTlsStream_Create(&tlsStreamConfig); + + static struct SolidSyslogStreamSenderConfig senderConfig; + senderConfig = (struct SolidSyslogStreamSenderConfig) {0}; + senderConfig.Resolver = resolver; + senderConfig.Stream = tlsStream; + senderConfig.Endpoint = mtls ? BddTargetMtlsConfig_GetEndpoint : BddTargetTlsConfig_GetEndpoint; + senderConfig.EndpointVersion = + mtls ? BddTargetMtlsConfig_GetEndpointVersion : BddTargetTlsConfig_GetEndpointVersion; + sender = SolidSyslogStreamSender_Create(&senderConfig); + + return sender; } void BddTargetTlsSender_Destroy(void) { + SolidSyslogStreamSender_Destroy(sender); + SolidSyslogMbedTlsStream_Destroy(tlsStream); + SolidSyslogFreeRtosTcpStream_Destroy(underlyingStream); + + /* Entropy / DRBG / parsed certs survive across Destroy → Create cycles to + * avoid re-seeding on every reconnect. Real teardown only happens at + * process exit, which the FreeRTOS target never reaches. */ } diff --git a/Bdd/Targets/FreeRtos/CMakeLists.txt b/Bdd/Targets/FreeRtos/CMakeLists.txt index dc6af36e..a9282429 100644 --- a/Bdd/Targets/FreeRtos/CMakeLists.txt +++ b/Bdd/Targets/FreeRtos/CMakeLists.txt @@ -203,6 +203,50 @@ foreach(_mbedtls_upstream_target IN ITEMS mbedtls mbedx509 mbedcrypto everest p2 endif() endforeach() +# --- Bake demo PEMs into the ELF ------------------------------------------- +# +# QEMU has no path to the host filesystem, and the integrator config disables +# MBEDTLS_FS_IO, so the demo CA + client identity travel as `static const` +# arrays in rodata. xxd -i emits the array shape we want; mbedTLS's PEM parser +# additionally requires a NUL-terminated input, which the wrapper TU adds at +# parse time by copying into a +1-sized buffer (cheaper than a shell pipeline +# to append the NUL at bake time, and keeps this CMake step simple). + +set(BAKED_PEMS_DIR "${CMAKE_CURRENT_BINARY_DIR}/baked_pems") +file(MAKE_DIRECTORY ${BAKED_PEMS_DIR}) + +function(bake_pem_into_header input_pem output_header symbol) + add_custom_command( + OUTPUT ${output_header} + COMMAND bash -c "xxd -i -n '${symbol}' '${input_pem}' > '${output_header}'" + DEPENDS ${input_pem} + COMMENT "Baking ${input_pem} -> ${output_header}" + VERBATIM + ) +endfunction() + +bake_pem_into_header( + ${CMAKE_SOURCE_DIR}/Bdd/syslog-ng/tls/ca.pem + ${BAKED_PEMS_DIR}/BddBakedCaPem.h + bdd_baked_ca_pem +) +bake_pem_into_header( + ${CMAKE_SOURCE_DIR}/Bdd/syslog-ng/tls/client.pem + ${BAKED_PEMS_DIR}/BddBakedClientCertPem.h + bdd_baked_client_cert_pem +) +bake_pem_into_header( + ${CMAKE_SOURCE_DIR}/Bdd/syslog-ng/tls/client.key + ${BAKED_PEMS_DIR}/BddBakedClientKeyPem.h + bdd_baked_client_key_pem +) + +set(BAKED_PEM_HEADERS + ${BAKED_PEMS_DIR}/BddBakedCaPem.h + ${BAKED_PEMS_DIR}/BddBakedClientCertPem.h + ${BAKED_PEMS_DIR}/BddBakedClientKeyPem.h +) + # --- Executable --------------------------------------------------------------- # # Project code only. Inherits the full project warning set @@ -232,8 +276,11 @@ add_executable(SolidSyslogBddTarget ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetInteractive.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetIps.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetLanguage.c + ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetMtlsConfig.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetSwitchConfig.c + ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetTlsConfig.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c + ${BAKED_PEM_HEADERS} $ ) @@ -258,6 +305,7 @@ target_include_directories(SolidSyslogBddTarget PRIVATE ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Interface # SolidSyslogMbedTlsStream.h ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source # SolidSyslogMbedTlsStreamPrivate.h ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common # BddTargetInteractive.h + ${BAKED_PEMS_DIR} # BddBaked*.h (xxd -i output) ${FREERTOS_KERNEL_PATH}/include ${FREERTOS_PORT_DIR} # portmacro.h ${PLUS_TCP_SRC_DIR}/include diff --git a/Bdd/Targets/FreeRtos/main.c b/Bdd/Targets/FreeRtos/main.c index 267ea5e9..e32e85c3 100644 --- a/Bdd/Targets/FreeRtos/main.c +++ b/Bdd/Targets/FreeRtos/main.c @@ -28,6 +28,7 @@ #include "BddTargetIps.h" #include "BddTargetLanguage.h" #include "BddTargetSwitchConfig.h" +#include "BddTargetTlsSender.h" #include "SolidSyslog.h" #include "SolidSyslogAtomicCounter.h" #include "SolidSyslogStdAtomicCounter.h" @@ -239,6 +240,7 @@ static struct SolidSyslogResolver* resolver = NULL; static struct SolidSyslogDatagram* datagram = NULL; static struct SolidSyslogStream* tcpStream = NULL; static struct SolidSyslogSender* tcpSender = NULL; +static struct SolidSyslogSender* tlsSender = NULL; static struct SolidSyslogSender* udpSender = NULL; static struct SolidSyslogSender* switchingSender = NULL; static struct SolidSyslogBuffer* buffer = NULL; @@ -793,16 +795,25 @@ static void InteractiveTask(void* argument) }; tcpSender = SolidSyslogStreamSender_Create(&tcpConfig); - /* SwitchingSender lets `set transport ` flip the active transport - * at runtime. Default to UDP so existing UDP-tagged scenarios stay green; - * `--transport tcp` flowing through the behave harness lands here as - * `set transport tcp` over the UART and switches before the first send. */ - static struct SolidSyslogSender* inners[2]; + /* TLS slot: SolidSyslogMbedTlsStream over SolidSyslogFreeRtosTcpStream, + * with the demo CA / client cert / client key baked into the ELF. + * Slice 6b ships TLS-only (mtls=false) — port 6514 on the BDD syslog-ng + * oracle, no client cert presented. mTLS support (port 6515, client + * cert wired) is slice 6c work and may require an extra + * BDD_TARGET_SWITCH_MTLS slot or a runtime endpoint-dispatch shim. */ + tlsSender = BddTargetTlsSender_Create(resolver, false); + + /* SwitchingSender lets `set transport ` flip the active + * transport at runtime. Default to UDP so existing UDP-tagged scenarios + * stay green; `--transport tcp|tls` flowing through the behave harness + * lands here as `set transport ` over the UART. */ + static struct SolidSyslogSender* inners[BDD_TARGET_SWITCH_COUNT]; inners[BDD_TARGET_SWITCH_UDP] = udpSender; inners[BDD_TARGET_SWITCH_TCP] = tcpSender; + inners[BDD_TARGET_SWITCH_TLS] = tlsSender; struct SolidSyslogSwitchingSenderConfig switchConfig = { .Senders = inners, - .SenderCount = sizeof(inners) / sizeof(inners[0]), + .SenderCount = BDD_TARGET_SWITCH_COUNT, .Selector = BddTargetSwitchConfig_Selector, }; BddTargetSwitchConfig_SetByName("udp"); From 5b76e64a522947a860775bba6673009af6487fb6 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 21 May 2026 16:15:58 +0000 Subject: [PATCH 04/15] test: S08.07 slice 6c admit @tls scenarios on FreeRTOS, bump prompt timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ci/docker-compose.bdd.yml: behave-freertos tag filter now admits @tls (was excluded with a "until S08.07 lights up TLS on FreeRTOS" note). @mtls is excluded by virtue of not matching the (@udp or @tcp or @tls) clause — its dual-mode dispatch over the switching sender is still outstanding under slice 6c. - Bdd/features/steps/syslog_steps.py: wait_for_prompt default bumped 30s -> 120s. Linux + Windows native still complete in under a second so the change is invisible there; FreeRTOS QEMU first-boot has to parse the baked mbedTLS RSA client key plus bring up Plus-TCP's IP task, and the local smoke test measured ~60s to reach the prompt. Recorded in [[feedback-qemu-bdd-timeouts-generous]] so future QEMU deadline choices default to "budget for QEMU first" rather than reusing host integration numbers. CI's bdd-freertos-qemu now drives the real end-to-end TLS handshake against the syslog-ng oracle (port 6514, demo CA / server cert from Bdd/syslog-ng/tls/, baked into the ELF via xxd -i in slice 6b). --- Bdd/features/steps/syslog_steps.py | 9 ++++++++- ci/docker-compose.bdd.yml | 8 +++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Bdd/features/steps/syslog_steps.py b/Bdd/features/steps/syslog_steps.py index e7143677..f0cc08d7 100644 --- a/Bdd/features/steps/syslog_steps.py +++ b/Bdd/features/steps/syslog_steps.py @@ -337,13 +337,20 @@ def _reader(): threading.Thread(target=_reader, daemon=True).start() -def wait_for_prompt(process, timeout=30): +def wait_for_prompt(process, timeout=120): """Read stdout until we see 'SolidSyslog> ', confirming the command completed. Portable across POSIX and Windows: a daemon thread (started lazily by _start_stdout_reader) reads stdout byte-by-byte into a queue; this function pulls bytes off the queue with a per-iteration timeout so the deadline is honoured even on platforms where select.select can't monitor pipe fds. + + The default is generous (120s) for the FreeRTOS QEMU target — first-boot + has to bring up Plus-TCP's IP task, parse the baked mbedTLS CA + client + cert + RSA client key (multi-second under QEMU's emulated Cortex-M3), + and then drop the interactive task. Linux/Windows native still complete + in well under a second, so the bump is a no-op for them; see + [[feedback-qemu-bdd-timeouts-generous]]. """ _start_stdout_reader(process) diff --git a/ci/docker-compose.bdd.yml b/ci/docker-compose.bdd.yml index 62048b45..cd1c9e19 100644 --- a/ci/docker-compose.bdd.yml +++ b/ci/docker-compose.bdd.yml @@ -110,14 +110,16 @@ services: # capacity_threshold stays out via per-feature @freertoswip until the # threshold-marker plumbing has a semihosting equivalent. # S08.11 tags the non-@tls switching_transport scenario @udp @tcp so the - # existing (@udp or @tcp) clause admits it; the @tls sibling stays - # excluded until S08.07 lights up TLS on FreeRTOS. + # existing (@udp or @tcp) clause admits it. S08.07 slice 6b lights up + # TLS on FreeRTOS — admit @tls now too, but keep @mtls excluded as + # @freertoswip until slice 6c resolves dual-mode TLS/mTLS dispatch on + # the switching sender. # @windows_wip excludes the Windows-only TCP singletask variant. # Tunable-driven gates like @requires_message_size_1500 are not listed # here — Bdd/features/environment.py skips them at runtime based on the # build's SOLIDSYSLOG_MAX_MESSAGE_SIZE so the exclusion auto-tracks # solidsyslog_user_tunables.h changes. - command: behave --junit --junit-directory Bdd/junit --tags='not @wip and not @freertoswip and not @rtc and not @windows_wip and (@udp or @tcp)' Bdd/features/ + command: behave --junit --junit-directory Bdd/junit --tags='not @wip and not @freertoswip and not @rtc and not @windows_wip and (@udp or @tcp or @tls)' Bdd/features/ volumes: bdd-output-linux: From f50fb5435b820267bdcff8427e3c501a73811329 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 21 May 2026 18:10:03 +0100 Subject: [PATCH 05/15] fix: S08.07 slice 6c mbedTLS heap + PSA wiring so TLS handshake succeeds on FreeRTOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two missing integrator-side init steps were sinking every TLS handshake on the QEMU mps2-an385 BDD target. Both surface as opaque mbedTLS errors: 1. mbedtls_ssl_setup returned MBEDTLS_ERR_SSL_ALLOC_FAILED (-0x7F00). mbedTLS calls libc calloc, which on newlib routes through _sbrk into the 4 KiB syscall heap in Bdd/Targets/FreeRtos/Common/Syscalls.c — far too small for a single TLS context. Defined MBEDTLS_PLATFORM_MEMORY in the user config and added mbedtls_platform_set_calloc_free wired to pvPortMalloc / vPortFree so allocations land in the 96 KiB FreeRTOS heap_4 region — the standard FreeRTOS+mbedTLS integration. 2. mbedtls_ssl_handshake then returned MBEDTLS_ERR_ERROR_GENERIC_ERROR (-0x0001) at the first state transition with no BIO traffic. mbedTLS 3.6's TLS 1.3 path drives PSA crypto, and PSA's built-in entropy collector returns PSA_ERROR_INSUFFICIENT_ENTROPY (-148) on MBEDTLS_NO_PLATFORM_ENTROPY targets. Defined MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG, provided mbedtls_psa_external_get_random that feeds PSA from the same CTR_DRBG the classic API uses, and call psa_crypto_init() AFTER seeding the DRBG so the first PSA op has a working source. Also shrunk MBEDTLS_SSL_IN_CONTENT_LEN to 4096 and OUT to 2048 (from the 16 KiB default) — comfortable for the BDD oracle's cert chain + the messages we send, and frees ~28 KiB per context for FreeRTOS-Plus-TCP socket buffers and the SolidSyslog tasks. Local: 39/39 freertos BDD scenarios pass, including both @tls and the @tls13 scenario. No changes under Platform/MbedTls/, so the customer coexistence contract is intact. --- .../BddTargetTlsSender_MbedTls_FreeRtosTcp.c | 77 +++++++++++++++++++ Bdd/Targets/FreeRtos/mbedtls_user_config.h | 33 ++++++++ 2 files changed, 110 insertions(+) diff --git a/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c b/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c index 5139bcab..236e4bdf 100644 --- a/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c +++ b/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c @@ -32,7 +32,9 @@ #include #include #include +#include #include +#include #include #include @@ -74,6 +76,65 @@ static mbedtls_x509_crt caChain; static mbedtls_x509_crt clientCertChain; static mbedtls_pk_context clientKey; +/* mbedTLS allocates its per-SSL-context IN/OUT buffers (~6 KiB combined) and + * handshake state (~10 KiB) via libc calloc — on this FreeRTOS target that + * funnels through newlib's tiny syscall heap (~4 KiB, see Common/Syscalls.c), + * which can't satisfy a single TLS context. Redirect to pvPortMalloc so + * mbedTLS allocates from the 96 KiB FreeRTOS heap_4 region instead — the + * standard FreeRTOS+mbedTLS integration. Gated on MBEDTLS_PLATFORM_MEMORY in + * mbedtls_user_config.h. The zero-fill mirrors libc calloc's contract. */ +static void* FreeRtosMbedTlsCalloc(size_t nmemb, size_t size) +{ + void* result = NULL; + if ((nmemb != 0U) && (size != 0U)) + { + size_t bytes = nmemb * size; + /* Detect the multiplication wrap so a maliciously huge request can't + * underestimate the allocation size — pvPortMalloc would then hand + * back a too-small buffer that the caller writes past. */ + if ((bytes / nmemb) == size) + { + result = pvPortMalloc(bytes); + if (result != NULL) + { + memset(result, 0, bytes); + } + } + } + return result; +} + +static void FreeRtosMbedTlsFree(void* ptr) +{ + if (ptr != NULL) + { + vPortFree(ptr); + } +} + +/* PSA crypto's randomness hook (gated on MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG in + * the user config). mbedTLS 3.6's TLS 1.3 path drives PSA crypto, and PSA's + * built-in entropy collector returns PSA_ERROR_INSUFFICIENT_ENTROPY on + * MBEDTLS_NO_PLATFORM_ENTROPY targets — bypass it by feeding PSA from the + * same CTR_DRBG the classic mbedTLS API already uses. The DRBG must be + * seeded before psa_crypto_init() so the first PSA crypto operation can + * draw bytes — EnsureMbedTlsInitialised below enforces that ordering. */ +psa_status_t mbedtls_psa_external_get_random( + mbedtls_psa_external_random_context_t* context, + uint8_t* output, + size_t outputSize, + size_t* outputLength +) +{ + (void) context; + if (mbedtls_ctr_drbg_random(&drbg, output, outputSize) != 0) + { + return PSA_ERROR_GENERIC_ERROR; + } + *outputLength = outputSize; + return PSA_SUCCESS; +} + /* Demo-only entropy: XOR the FreeRTOS tick count, a per-call counter, and * the destination address (which varies per call) into each output byte. * Quality is intentionally terrible — QEMU has no real source — and the @@ -129,6 +190,12 @@ static void EnsureMbedTlsInitialised(void) (void) printf("[mbedtls] init entropy + DRBG seed (slow under QEMU)\r\n"); vTaskDelay(1U); + /* Redirect mbedTLS allocations to the FreeRTOS heap before any + * mbedtls_*_init runs. Must come first — once an ssl_setup runs against + * the default libc calloc and fails, the failure mode is heap exhaustion + * inside newlib's 4 KiB syscall heap, not a recoverable error. */ + mbedtls_platform_set_calloc_free(FreeRtosMbedTlsCalloc, FreeRtosMbedTlsFree); + mbedtls_entropy_init(&entropy); mbedtls_entropy_add_source( &entropy, @@ -143,6 +210,16 @@ static void EnsureMbedTlsInitialised(void) (void) mbedtls_ctr_drbg_seed(&drbg, mbedtls_entropy_func, &entropy, personalization, sizeof(personalization) - 1U); vTaskDelay(1U); + /* mbedTLS 3.6 routes TLS 1.3 cryptography through PSA, so psa_crypto_init() + * must succeed before the first handshake or mbedtls_ssl_handshake returns + * MBEDTLS_ERR_ERROR_GENERIC_ERROR (-0x0001) at the first state transition. + * MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG in the user config disables PSA's + * built-in entropy collector (which would otherwise return + * PSA_ERROR_INSUFFICIENT_ENTROPY on this no-platform-entropy target), so + * the init only succeeds once the DRBG is seeded above — that's why this + * sits after the seed step. Idempotent across subsequent calls. */ + (void) psa_crypto_init(); + (void) printf("[mbedtls] parsing CA chain\r\n"); memcpy(caPemBuf, bdd_baked_ca_pem, sizeof(bdd_baked_ca_pem)); diff --git a/Bdd/Targets/FreeRtos/mbedtls_user_config.h b/Bdd/Targets/FreeRtos/mbedtls_user_config.h index d0147fcc..7cd2029c 100644 --- a/Bdd/Targets/FreeRtos/mbedtls_user_config.h +++ b/Bdd/Targets/FreeRtos/mbedtls_user_config.h @@ -63,4 +63,37 @@ * injected Sleep callback, so MBEDTLS_TIMING_C is unused. */ #undef MBEDTLS_TIMING_C +/* Route mbedTLS allocations through a runtime-installed calloc/free pair. By + * default mbedTLS calls libc calloc/free, which on this target funnels through + * newlib into the small 4 KiB syscall heap in Bdd/Targets/FreeRtos/Common/ + * Syscalls.c — far too small for mbedTLS's per-context allocations (IN/OUT + * buffers plus handshake state run ~10–20 KiB). Enabling + * MBEDTLS_PLATFORM_MEMORY lets BddTargetTlsSender_MbedTls_FreeRtosTcp.c call + * mbedtls_platform_set_calloc_free(...) to redirect those allocations to + * pvPortMalloc, which uses the 96 KiB heap_4 region — the textbook + * FreeRTOS+mbedTLS integration. */ +#define MBEDTLS_PLATFORM_MEMORY + +/* Route PSA crypto's randomness through an integrator-supplied callback rather + * than PSA's internal entropy pool. mbedTLS 3.6's TLS 1.3 path is built on PSA, + * so psa_crypto_init() must succeed before any TLS 1.3 handshake — and the + * default PSA entropy collector returns PSA_ERROR_INSUFFICIENT_ENTROPY + * (-148) on platforms with no real entropy source (which is us, with + * MBEDTLS_NO_PLATFORM_ENTROPY defined above). With this define, PSA never + * tries to seed itself; the integrator provides mbedtls_psa_external_get_random + * (see BddTargetTlsSender_MbedTls_FreeRtosTcp.c) which feeds the same CTR_DRBG + * the classic API uses, so PSA and the classic API share one entropy chain. */ +#define MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG + +/* Shrink the TLS record buffers from the 16 KiB default. The BDD syslog-ng + * oracle's server cert + chain fits in 4 KiB IN comfortably, and the BDD + * messages we send fit in 2 KiB OUT. Cuts ~28 KiB off the per-context + * footprint — important when the FreeRTOS heap also has to satisfy + * FreeRTOS-Plus-TCP socket buffers, the SolidSyslog Service task, and the + * interactive task all at once. Embedded integrators replicating this + * footprint should re-tune both knobs against their peer's largest TLS + * record. */ +#define MBEDTLS_SSL_IN_CONTENT_LEN 4096 +#define MBEDTLS_SSL_OUT_CONTENT_LEN 2048 + #endif /* BDD_TARGET_FREERTOS_MBEDTLS_USER_CONFIG_H */ From 6051ef780ce75e8359239f8fdb7410d85d30be89 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 21 May 2026 18:18:34 +0100 Subject: [PATCH 06/15] feat: S08.07 slice 6c FreeRTOS dual-mode TLS/mTLS dispatch + admit @mtls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The FreeRTOS BDD target spawns the TLS sender once at boot — before the behave harness has sent `set transport tls` / `set transport mtls` over the UART — so the per-startup `mtls` flag that the POSIX and Windows targets read from argv can't reach this Create call in time. Resolve by sharing one TLS sender / TLS stream / TCP socket across both modes and dispatching the destination port at Connect time: - BddTargetSwitchConfig gains an mtlsMode bool tracked by SetByName and exposed via BddTargetSwitchConfig_IsMtlsMode(). `tls` clears it, `mtls` sets it, both route through BDD_TARGET_SWITCH_TLS. - BddTargetTlsSender_MbedTls_FreeRtosTcp wires the client cert+key unconditionally (syslog-ng's plain-TLS listener accepts an optional client cert harmlessly, and its mTLS listener requires one), then plumbs DispatchEndpoint / DispatchEndpointVersion as the StreamSender Endpoint pair. Those read IsMtlsMode at each Connect call and pick between BddTargetTlsConfig (port 6514) and BddTargetMtlsConfig (port 6515). The `mtls` parameter on the wrapper's Create signature is retained for cross-platform contract uniformity but ignored. - ci/docker-compose.bdd.yml admits @mtls in the freertos behave filter: `(@udp or @tcp or @tls or @mtls)`. Local full freertos suite: 40 scenarios pass / 0 fail / 9 skipped (intentional @freertoswip / @windows_wip). The new @mtls scenario from mtls_transport.feature passes in ~3s. --- Bdd/Targets/Common/BddTargetSwitchConfig.c | 22 ++++++-- Bdd/Targets/Common/BddTargetSwitchConfig.h | 8 +++ .../BddTargetTlsSender_MbedTls_FreeRtosTcp.c | 52 +++++++++++++++---- ci/docker-compose.bdd.yml | 8 +-- 4 files changed, 74 insertions(+), 16 deletions(-) diff --git a/Bdd/Targets/Common/BddTargetSwitchConfig.c b/Bdd/Targets/Common/BddTargetSwitchConfig.c index d2d6fe6f..05062ff1 100644 --- a/Bdd/Targets/Common/BddTargetSwitchConfig.c +++ b/Bdd/Targets/Common/BddTargetSwitchConfig.c @@ -3,22 +3,33 @@ #include static volatile uint8_t selectedIndex; +static volatile bool mtlsMode; void BddTargetSwitchConfig_SetByName(const char* name) { if (strcmp(name, "udp") == 0) { selectedIndex = BDD_TARGET_SWITCH_UDP; + mtlsMode = false; } else if (strcmp(name, "tcp") == 0) { selectedIndex = BDD_TARGET_SWITCH_TCP; + mtlsMode = false; } - else if ((strcmp(name, "tls") == 0) || (strcmp(name, "mtls") == 0)) + else if (strcmp(name, "tls") == 0) { - /* mTLS shares the reliable/TLS slot; the TLS stream is configured - * with client cert+key at startup when --transport mtls is selected. */ selectedIndex = BDD_TARGET_SWITCH_TLS; + mtlsMode = false; + } + else if (strcmp(name, "mtls") == 0) + { + /* mTLS shares the reliable/TLS slot. The wrapper wires the client + * cert+key at startup regardless of mode, and the TLS sender's + * Endpoint callback dispatches on mtlsMode to pick the right port + * (6514 vs 6515) at Connect time. */ + selectedIndex = BDD_TARGET_SWITCH_TLS; + mtlsMode = true; } } @@ -26,3 +37,8 @@ uint8_t BddTargetSwitchConfig_Selector(void) { return selectedIndex; } + +bool BddTargetSwitchConfig_IsMtlsMode(void) +{ + return mtlsMode; +} diff --git a/Bdd/Targets/Common/BddTargetSwitchConfig.h b/Bdd/Targets/Common/BddTargetSwitchConfig.h index fb801c6f..e66568d4 100644 --- a/Bdd/Targets/Common/BddTargetSwitchConfig.h +++ b/Bdd/Targets/Common/BddTargetSwitchConfig.h @@ -3,6 +3,7 @@ #include "ExternC.h" +#include #include EXTERN_C_BEGIN @@ -17,6 +18,13 @@ EXTERN_C_BEGIN void BddTargetSwitchConfig_SetByName(const char* name); uint8_t BddTargetSwitchConfig_Selector(void); // NOLINT(modernize-redundant-void-arg) -- C idiom + /* tls and mtls share the same Switching slot (BDD_TARGET_SWITCH_TLS) so + * the TLS sender / TLS stream / underlying TCP socket get reused across + * the two modes. The FreeRTOS BDD target reads this at TLS Connect time + * to pick between the syslog-ng oracle's port 6514 (TLS) and 6515 (mTLS); + * the client cert is wired regardless of mode, so the port is the only + * thing that has to flip. */ + bool BddTargetSwitchConfig_IsMtlsMode(void); // NOLINT(modernize-redundant-void-arg) -- C idiom EXTERN_C_END diff --git a/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c b/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c index 236e4bdf..0dbcd571 100644 --- a/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c +++ b/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c @@ -23,6 +23,7 @@ #include "BddTargetTlsSender.h" #include "BddTargetMtlsConfig.h" +#include "BddTargetSwitchConfig.h" #include "BddTargetTlsConfig.h" #include "SolidSyslogFreeRtosTcpStream.h" #include "SolidSyslogMbedTlsStream.h" @@ -261,8 +262,42 @@ static void EnsureMbedTlsInitialised(void) mbedTlsInitialised = true; } +/* Dispatch the StreamSender's endpoint+version callbacks based on whether + * the harness most recently selected `tls` or `mtls` over the UART. tls + * and mtls share BDD_TARGET_SWITCH_TLS (and therefore one TLS stream / one + * TCP socket / one mbedTLS context); only the destination port (6514 vs + * 6515) differs at Connect time. The mTLS port's syslog-ng listener + * peer-verifies the client cert that the wrapper wires unconditionally + * below, and the plain-TLS port's listener accepts it as optional-untrusted + * — so the same client identity works on both ports. */ +static void DispatchEndpoint(struct SolidSyslogEndpoint* endpoint) +{ + if (BddTargetSwitchConfig_IsMtlsMode()) + { + BddTargetMtlsConfig_GetEndpoint(endpoint); + } + else + { + BddTargetTlsConfig_GetEndpoint(endpoint); + } +} + +static uint32_t DispatchEndpointVersion(void) +{ + return BddTargetSwitchConfig_IsMtlsMode() ? BddTargetMtlsConfig_GetEndpointVersion() + : BddTargetTlsConfig_GetEndpointVersion(); +} + struct SolidSyslogSender* BddTargetTlsSender_Create(struct SolidSyslogResolver* resolver, bool mtls) { + /* `mtls` is honoured for cross-platform contract uniformity but does not + * gate cert wiring on FreeRTOS — both TLS and mTLS BDD scenarios share + * one Switching slot here, and `set transport mtls` arrives over the UART + * AFTER this Create call has run. Wiring the client identity + * unconditionally lets the dispatcher above flip ports at runtime without + * needing a sender re-create. */ + (void) mtls; + EnsureMbedTlsInitialised(); underlyingStream = SolidSyslogFreeRtosTcpStream_Create(); @@ -273,21 +308,20 @@ struct SolidSyslogSender* BddTargetTlsSender_Create(struct SolidSyslogResolver* tlsStreamConfig.Sleep = RtosSleep; tlsStreamConfig.Rng = &drbg; tlsStreamConfig.CaChain = &caChain; - tlsStreamConfig.ServerName = mtls ? BddTargetMtlsConfig_GetServerName() : BddTargetTlsConfig_GetServerName(); - if (mtls) - { - tlsStreamConfig.ClientCertChain = &clientCertChain; - tlsStreamConfig.ClientKey = &clientKey; - } + /* Plain-TLS and mTLS share one SNI on this oracle (CN/SAN = "syslog-ng"), + * so either *_GetServerName accessor returns the same string. Use the + * TLS one to make the equivalence explicit. */ + tlsStreamConfig.ServerName = BddTargetTlsConfig_GetServerName(); + tlsStreamConfig.ClientCertChain = &clientCertChain; + tlsStreamConfig.ClientKey = &clientKey; tlsStream = SolidSyslogMbedTlsStream_Create(&tlsStreamConfig); static struct SolidSyslogStreamSenderConfig senderConfig; senderConfig = (struct SolidSyslogStreamSenderConfig) {0}; senderConfig.Resolver = resolver; senderConfig.Stream = tlsStream; - senderConfig.Endpoint = mtls ? BddTargetMtlsConfig_GetEndpoint : BddTargetTlsConfig_GetEndpoint; - senderConfig.EndpointVersion = - mtls ? BddTargetMtlsConfig_GetEndpointVersion : BddTargetTlsConfig_GetEndpointVersion; + senderConfig.Endpoint = DispatchEndpoint; + senderConfig.EndpointVersion = DispatchEndpointVersion; sender = SolidSyslogStreamSender_Create(&senderConfig); return sender; diff --git a/ci/docker-compose.bdd.yml b/ci/docker-compose.bdd.yml index cd1c9e19..efb63514 100644 --- a/ci/docker-compose.bdd.yml +++ b/ci/docker-compose.bdd.yml @@ -111,15 +111,15 @@ services: # threshold-marker plumbing has a semihosting equivalent. # S08.11 tags the non-@tls switching_transport scenario @udp @tcp so the # existing (@udp or @tcp) clause admits it. S08.07 slice 6b lights up - # TLS on FreeRTOS — admit @tls now too, but keep @mtls excluded as - # @freertoswip until slice 6c resolves dual-mode TLS/mTLS dispatch on - # the switching sender. + # TLS on FreeRTOS, and slice 6c (this commit) lights up mTLS via the + # dual-port dispatcher on the same Switching slot — admit both @tls + # and @mtls now. # @windows_wip excludes the Windows-only TCP singletask variant. # Tunable-driven gates like @requires_message_size_1500 are not listed # here — Bdd/features/environment.py skips them at runtime based on the # build's SOLIDSYSLOG_MAX_MESSAGE_SIZE so the exclusion auto-tracks # solidsyslog_user_tunables.h changes. - command: behave --junit --junit-directory Bdd/junit --tags='not @wip and not @freertoswip and not @rtc and not @windows_wip and (@udp or @tcp or @tls)' Bdd/features/ + command: behave --junit --junit-directory Bdd/junit --tags='not @wip and not @freertoswip and not @rtc and not @windows_wip and (@udp or @tcp or @tls or @mtls)' Bdd/features/ volumes: bdd-output-linux: From 4124604e5b228445a8377e97273af29ce8a501e9 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 21 May 2026 22:45:09 +0100 Subject: [PATCH 07/15] feat: S08.07 slice 6c admit capacity_threshold.feature on FreeRTOS via UART marker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The threshold-crossed callback was deferred from S08.05 on the grounds that the behave step inspects a host-side marker file (/tmp/...) which a QEMU guest can't write directly. The actual gap is smaller — semihosting fopen against a relative path would have worked, but a UART-based marker is cleaner because it reuses the captured-stdout reader the harness already runs for the prompt protocol. Changes: - Bdd/Targets/FreeRtos/main.c — OnThresholdCrossed callback prints a line-anchored `[THRESHOLD-CROSSED]` token to the UART, wired into the BlockStoreConfig (previously NULL). Added `set capacity-threshold N` to OnSet so the harness's --capacity-threshold flag plumbs through. - Bdd/features/steps/syslog_steps.py — _threshold_marker_present helper accepts either the host marker file (Linux/Windows binaries write to /tmp/solidsyslog_threshold_marker.log unchanged) or the [THRESHOLD-CROSSED] token in the captured stdout buffer (FreeRTOS). Both `was invoked` / `was not invoked` steps route through it. - Bdd/features/steps/target_driver.py — --capacity-threshold added to the FreeRTOS UART translation table. - Bdd/features/capacity_threshold.feature — drops @freertoswip; the feature header documents the dual-channel marker contract. - ci/docker-compose.bdd.yml — comment refresh; the freertos behave filter is unchanged (the feature is admitted via @tcp @buffered @store which the existing `(@udp or @tcp or @tls or @mtls)` clause covers). Local: FreeRTOS suite goes from 40/0 to 42/0 (+2 capacity_threshold); Linux capacity_threshold still passes via the file path. --- Bdd/Targets/FreeRtos/main.c | 27 ++++++++++++++++++- Bdd/features/capacity_threshold.feature | 15 ++++++----- Bdd/features/steps/syslog_steps.py | 36 ++++++++++++++++++++----- Bdd/features/steps/target_driver.py | 3 +++ ci/docker-compose.bdd.yml | 4 ++- 5 files changed, 69 insertions(+), 16 deletions(-) diff --git a/Bdd/Targets/FreeRtos/main.c b/Bdd/Targets/FreeRtos/main.c index e32e85c3..fe4b5c1e 100644 --- a/Bdd/Targets/FreeRtos/main.c +++ b/Bdd/Targets/FreeRtos/main.c @@ -265,6 +265,7 @@ static bool EnsureFatFsMounted(void); static enum SolidSyslogDiscardPolicy MapDiscardPolicy(const char* policy); static void OnStoreFull(void* context); static size_t GetCapacityThreshold(void* context); +static void OnThresholdCrossed(void* context); static void SemihostingExit(int status); static uint32_t MmioRead32(uintptr_t address) @@ -459,6 +460,16 @@ static bool OnSet(const char* name, const char* value) (strcmp(value, "newest") == 0) ? "newest" : ((strcmp(value, "halt") == 0) ? "halt" : "oldest"); return true; } + if (strcmp(name, "capacity-threshold") == 0) + { + unsigned long parsed = 0U; + if (!TryParseUInt(value, &parsed)) + { + return false; + } + pendingCapacityThreshold = (size_t) parsed; + return true; + } if (strcmp(name, "halt-exit") == 0) { /* Harness emits `set halt-exit 1` (or `0`) because the FreeRTOS @@ -555,6 +566,20 @@ static size_t GetCapacityThreshold(void* context) return *(const size_t*) context; } +/* Stdout-based marker the behave harness watches for. Linux's equivalent + * (Bdd/Targets/Linux/main.c::OnThresholdCrossed) writes a host file at + * /tmp/solidsyslog_threshold_marker.log — that file path isn't reachable + * from the QEMU guest. Instead we print a known token to the UART, which + * the captured-stdout reader in Bdd/features/steps/syslog_steps.py buffers + * and the threshold step then scans. The token is line-anchored so a stray + * substring inside a longer message body could not accidentally trip the + * assertion. */ +static void OnThresholdCrossed(void* context) +{ + (void) context; + (void) printf("[THRESHOLD-CROSSED]\r\n"); +} + static bool RebuildWithFileStore(void) { /* Lifecycle mutex blocks the Service task from running SolidSyslog_Service @@ -595,7 +620,7 @@ static bool RebuildWithFileStore(void) .OnStoreFull = OnStoreFull, .StoreFullContext = NULL, .GetCapacityThreshold = GetCapacityThreshold, - .OnThresholdCrossed = NULL, + .OnThresholdCrossed = OnThresholdCrossed, .ThresholdContext = &pendingCapacityThreshold, }; currentStore = SolidSyslogBlockStore_Create(&storeConfig); diff --git a/Bdd/features/capacity_threshold.feature b/Bdd/features/capacity_threshold.feature index 734e9515..c4abf325 100644 --- a/Bdd/features/capacity_threshold.feature +++ b/Bdd/features/capacity_threshold.feature @@ -1,12 +1,13 @@ -@tcp @buffered @store @freertoswip +@tcp @buffered @store Feature: Capacity threshold alert - # @freertoswip excludes this from the FreeRTOS BDD run (S08.05+). - # Threshold-callback support on FreeRTOS is a follow-up — the wiring - # in main.c plumbs g_pendingCapacityThreshold through to BlockStore - # but the .feature relies on the harness inspecting a marker file - # which has no semihosting equivalent today. - an early-warning callback fires when the block store crosses a configured + An early-warning callback fires when the block store crosses a configured capacity threshold, before the terminal full-store callback engages. + The callback marker is observed via a host file on POSIX/Windows + (OnThresholdCrossed writes /tmp/solidsyslog_threshold_marker.log) and + via a [THRESHOLD-CROSSED] line on UART on FreeRTOS-on-QEMU (which the + behave harness's captured-stdout reader sees) — see + Bdd/features/steps/syslog_steps.py::_threshold_marker_present for the + cross-target assertion. Scenario: Threshold callback fires when usage crosses configured threshold Given the syslog oracle is running diff --git a/Bdd/features/steps/syslog_steps.py b/Bdd/features/steps/syslog_steps.py index f0cc08d7..54d6fd98 100644 --- a/Bdd/features/steps/syslog_steps.py +++ b/Bdd/features/steps/syslog_steps.py @@ -620,20 +620,42 @@ def step_capacity_threshold_enabled(context, threshold): pass +_THRESHOLD_STDOUT_TOKEN = b"[THRESHOLD-CROSSED]" + + +def _threshold_marker_present(context): + """True if either marker channel reports a crossing. + + Linux and Windows binaries write a host file at THRESHOLD_MARKER_PATH. + FreeRTOS-on-QEMU has no shared filesystem with the host, so its + OnThresholdCrossed prints the _THRESHOLD_STDOUT_TOKEN to UART and the + captured-stdout reader buffers it into process._solidsyslog_stdout_log. + Both channels are accepted so the same step works across all targets + without target branching at the assertion site. + """ + if os.path.exists(THRESHOLD_MARKER_PATH) and os.path.getsize(THRESHOLD_MARKER_PATH) > 0: + return True + if hasattr(context, "interactive_process"): + log = getattr(context.interactive_process, "_solidsyslog_stdout_log", None) + if log and _THRESHOLD_STDOUT_TOKEN in bytes(log): + return True + return False + + @then("the capacity threshold callback was invoked") def step_threshold_callback_invoked(context): - assert os.path.exists(THRESHOLD_MARKER_PATH), ( - f"Expected threshold marker at {THRESHOLD_MARKER_PATH}, found none" - ) - assert os.path.getsize(THRESHOLD_MARKER_PATH) > 0, ( - f"Threshold marker {THRESHOLD_MARKER_PATH} exists but is empty" + assert _threshold_marker_present(context), ( + f"Expected threshold marker at {THRESHOLD_MARKER_PATH} or " + f"{_THRESHOLD_STDOUT_TOKEN.decode()} on captured stdout; found neither" ) @then("the capacity threshold callback was not invoked") def step_threshold_callback_not_invoked(context): - assert not os.path.exists(THRESHOLD_MARKER_PATH) or os.path.getsize(THRESHOLD_MARKER_PATH) == 0, ( - f"Expected no threshold marker but found {THRESHOLD_MARKER_PATH} non-empty" + assert not _threshold_marker_present(context), ( + f"Expected no threshold marker but found one (file at " + f"{THRESHOLD_MARKER_PATH} or {_THRESHOLD_STDOUT_TOKEN.decode()} on " + f"captured stdout)" ) diff --git a/Bdd/features/steps/target_driver.py b/Bdd/features/steps/target_driver.py index 8c8298aa..b3cdc8c8 100644 --- a/Bdd/features/steps/target_driver.py +++ b/Bdd/features/steps/target_driver.py @@ -53,6 +53,9 @@ "--discard-policy": "discard-policy", "--halt-exit": "halt-exit", "--no-sd": "no-sd", + # S08.07 slice 6c admits capacity_threshold.feature on FreeRTOS — the + # threshold byte count must reach the rebuild path before `--store file`. + "--capacity-threshold": "capacity-threshold", "--store": "store", } diff --git a/ci/docker-compose.bdd.yml b/ci/docker-compose.bdd.yml index efb63514..004ec4a2 100644 --- a/ci/docker-compose.bdd.yml +++ b/ci/docker-compose.bdd.yml @@ -113,7 +113,9 @@ services: # existing (@udp or @tcp) clause admits it. S08.07 slice 6b lights up # TLS on FreeRTOS, and slice 6c (this commit) lights up mTLS via the # dual-port dispatcher on the same Switching slot — admit both @tls - # and @mtls now. + # and @mtls now. Slice 6c also wires the capacity_threshold marker + # over UART so capacity_threshold.feature runs here unmodified — the + # @freertoswip exclusion that used to gate it has been dropped. # @windows_wip excludes the Windows-only TCP singletask variant. # Tunable-driven gates like @requires_message_size_1500 are not listed # here — Bdd/features/environment.py skips them at runtime based on the From 935cfe99a73b88d23a605a12e9e32a3cb83d4c94 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 21 May 2026 22:45:20 +0100 Subject: [PATCH 08/15] docs: S08.07 slice 7 mbedTLS integrator guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New docs/integrating-mbedtls.md walks integrators through wiring SolidSyslogMbedTlsStream. Covers: - When to use mbedTLS vs the OpenSSL-backed SolidSyslogTlsStream — the two adapters share the SolidSyslogStream vtable so callers swap at link time. - The coexistence contract — Platform/MbedTls/Source/ never calls process-global mbedTLS APIs; the integrator owns init. - The config struct fields and their ownership. - The process-wide setup ordering (allocator hook → entropy + DRBG seed → psa_crypto_init → cert parsing → adapter Create). - FreeRTOS specifics — heap budget, newlib's syscall heap trap, PSA external RNG on no-platform-entropy targets, bounded handshake retry. - Pointers to the reference integration (BDD target). - A common-failures table that maps the cryptic mbedTLS error codes surfaced during slice-6c diagnosis back to their root causes. No code change, no API surface change. --- docs/integrating-mbedtls.md | 213 ++++++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 docs/integrating-mbedtls.md diff --git a/docs/integrating-mbedtls.md b/docs/integrating-mbedtls.md new file mode 100644 index 00000000..97c70eff --- /dev/null +++ b/docs/integrating-mbedtls.md @@ -0,0 +1,213 @@ +# Integrating mbedTLS + +`SolidSyslogMbedTlsStream` is a `SolidSyslogStream` implementation that +wraps an injected byte-stream (typically `SolidSyslogFreeRtosTcpStream` or +`SolidSyslogPosixTcpStream`) with TLS via Mbed TLS 3.x. It is a reference +adapter — small, dependency-injected, and intended to be auditable line by +line. `SolidSyslogTlsStream` (OpenSSL-backed) remains the host-side +reference; this guide covers the mbedTLS adapter for embedded / FreeRTOS / +resource-constrained integrations where OpenSSL is impractical. + +## When to use mbedTLS + +| Scenario | Recommended adapter | +|---|---| +| Linux server / appliance / containerised | `SolidSyslogTlsStream` (OpenSSL) | +| Windows server / desktop | `SolidSyslogTlsStream` (OpenSSL) | +| FreeRTOS / Zephyr / bare-metal Cortex-M with TCP/IP | `SolidSyslogMbedTlsStream` | +| RTOS with no full-fat TLS library available | `SolidSyslogMbedTlsStream` | + +Both adapters expose the same `SolidSyslogStream` vtable, so callers swap +implementations at link time — `SolidSyslogStreamSender` doesn't know +which TLS library is underneath. + +## Coexistence contract + +`Platform/MbedTls/Source/` MUST NOT call any process-global mbedTLS APIs +(global RNG installers, global PSA initialisers, global mbedTLS allocator +hooks). The contract is auditable by grep: any future change that +introduces such a call must be flagged in review. See +[[project-mbedtls-coexistence-contract]] for the rationale. + +The integrator owns process-global setup. The adapter only consumes +caller-built handles via the config struct. + +This matters because mbedTLS allows applications to share a single global +crypto context across multiple library users. If the SolidSyslog adapter +installed its own global state — a process-wide allocator, RNG, or PSA +context — it would silently overwrite the integrator's existing wiring +or be silently overwritten by it. + +## Configuration handles + +`SolidSyslogMbedTlsStreamConfig` (in +[Platform/MbedTls/Interface/SolidSyslogMbedTlsStream.h](../Platform/MbedTls/Interface/SolidSyslogMbedTlsStream.h)) +takes pre-built handles, never file paths or PEM blobs: + +| Field | Owner | Notes | +|---|---|---| +| `Transport` | Caller | A `SolidSyslogStream*` carrying the byte transport. The adapter calls `Open`/`Send`/`Read`/`Close` on it — same vtable that `SolidSyslogStreamSender` would use directly for plain TCP. | +| `Sleep` | Caller | A `SolidSyslogSleepFunction` driving the bounded handshake retry between `WANT_READ` / `WANT_WRITE` polls. Required. On FreeRTOS use a `vTaskDelay`-backed wrapper; on POSIX `SolidSyslogPosixSleep` is the natural fit. | +| `Rng` | Caller | `mbedtls_ctr_drbg_context*` — seeded by the integrator before the first handshake. The adapter calls `mbedtls_ctr_drbg_random` against this handle. | +| `CaChain` | Caller | `mbedtls_x509_crt*` parsed by the integrator from whatever source is appropriate (filesystem on POSIX, baked-in `xxd -i` arrays on FreeRTOS, HSM-backed trust store on a secure element). | +| `ServerName` | Caller | SNI string and certificate-name check target. `NULL` skips the name check — only appropriate for closed networks where IP-pinning replaces hostname identity. | +| `ClientCertChain` / `ClientKey` | Caller | `mbedtls_x509_crt*` + `mbedtls_pk_context*` for mTLS. Both NULL = server-auth TLS only. Both non-NULL = mTLS. Supplying only one is treated as "no client cert" — the adapter never half-configures. | + +The "caller owns handles" pattern keeps the adapter framework-agnostic: a +deployment that already builds its own X.509 / DRBG handles for other +purposes (key rotation policy, HSM integration, TRNG wiring) reuses those +handles unchanged. See [[project-mbedtls-di-handles]] for the design note. + +## Process-wide setup the integrator must do + +The order of operations matters — getting it wrong surfaces as misleading +mbedTLS errors (the most common ones are documented at the end of this +guide). Recommended sequence: + +1. **Install the platform allocator** (if not using the default libc one). + On FreeRTOS this routes mbedTLS allocations through `pvPortMalloc` / + `vPortFree` so they land in the RTOS heap rather than newlib's tiny + syscall heap. Requires `MBEDTLS_PLATFORM_MEMORY` in the integrator's + mbedTLS config: + ```c + mbedtls_platform_set_calloc_free(YourCalloc, YourFree); + ``` +2. **Initialise entropy and seed the DRBG.** The adapter does not seed the + DRBG itself; that responsibility stays with the integrator, who knows + what hardware sources are available: + ```c + mbedtls_entropy_init(&entropy); + mbedtls_entropy_add_source(&entropy, YourEntropySource, NULL, ...); + mbedtls_ctr_drbg_init(&drbg); + mbedtls_ctr_drbg_seed(&drbg, mbedtls_entropy_func, &entropy, ...); + ``` +3. **Initialise PSA crypto** (mandatory for mbedTLS 3.6 with TLS 1.3 + enabled — even when negotiating an older version, the handshake state + machine touches PSA). Must come *after* the DRBG is seeded if you are + using `MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG`: + ```c + psa_crypto_init(); + ``` +4. **Parse the certificates and keys** the adapter will consume: + ```c + mbedtls_x509_crt_init(&caChain); + mbedtls_x509_crt_parse(&caChain, caPem, caPemLen + 1); /* +1 for NUL */ + /* …same pattern for client cert and key… */ + ``` +5. **Build the adapter** via `SolidSyslogMbedTlsStream_Create(&config)` + passing the handles initialised above. + +The adapter's `Open` / `Close` cycle is idempotent and re-entrant: the +underlying mbedTLS contexts are zeroed on construction and after every +`Close`, so reconnect after an outage is automatic and leak-free. + +## FreeRTOS-specific considerations + +### Heap budget + +mbedTLS `mbedtls_ssl_setup` allocates per-context buffers totalling roughly +`MBEDTLS_SSL_IN_CONTENT_LEN + MBEDTLS_SSL_OUT_CONTENT_LEN + ~3 KiB` of +handshake state. With mbedTLS defaults (16 KiB each) that's >35 KiB per +context — likely larger than your RTOS heap if you haven't sized for it. + +Three knobs: + +- Size the **FreeRTOS heap** (`configTOTAL_HEAP_SIZE`) to cover the worst + case: per-SSL-context working set × (number of concurrent TLS connections), + plus everything else the application allocates. +- Shrink **`MBEDTLS_SSL_IN_CONTENT_LEN`** to the largest TLS record the peer + will send. Server certificates with intermediates are typically 2–4 KiB; + a 4096-byte IN buffer is a reasonable starting point for syslog deployments. +- Shrink **`MBEDTLS_SSL_OUT_CONTENT_LEN`** to the largest message you will + send. The library's default `SOLIDSYSLOG_MAX_MESSAGE_SIZE` fits inside + 2048 comfortably. + +The BDD target's [mbedtls_user_config.h](../Bdd/Targets/FreeRtos/mbedtls_user_config.h) +shows the full minimal config — `IN=4096`, `OUT=2048`, with rationale +comments — for the QEMU mps2-an385 reference image. + +### newlib's syscall heap + +If the FreeRTOS target uses newlib (the SolidSyslog FreeRTOS BDD target +does), libc `calloc` is backed by `_sbrk`, which in turn is typically backed +by a small static buffer (4 KiB in the SolidSyslog BDD reference at +[Syscalls.c](../Bdd/Targets/FreeRtos/Common/Syscalls.c)). That buffer is +intended for newlib's small scratch allocations (printf, etc.), not TLS +contexts. + +Without `mbedtls_platform_set_calloc_free`, mbedTLS will silently land +every `calloc` in that 4 KiB buffer and `mbedtls_ssl_setup` will fail with +`MBEDTLS_ERR_SSL_ALLOC_FAILED` (-0x7F00) before the first byte hits the +socket. + +### PSA crypto on no-platform-entropy targets + +mbedTLS 3.6's TLS 1.3 code path routes random-number requests through PSA +crypto. PSA's built-in entropy collector requires a platform entropy source +— which is exactly what `MBEDTLS_NO_PLATFORM_ENTROPY` turns off on +embedded targets. + +The fix is `MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG` plus an integrator-supplied +`mbedtls_psa_external_get_random` that feeds PSA from the same CTR_DRBG +the classic API uses. This keeps the entropy chain single-rooted at your +hardware source. + +Reference implementation (BDD target): +[BddTargetTlsSender_MbedTls_FreeRtosTcp.c](../Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c). + +### Bounded handshake retry + +The adapter polls `mbedtls_ssl_handshake` against the non-blocking +transport — it never blocks the FreeRTOS service task indefinitely. The +retry budget is `HANDSHAKE_TIMEOUT_MILLISECONDS` (5 seconds at time of +writing, configurable in +[SolidSyslogMbedTlsStream.c](../Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c)), +sleeping `HANDSHAKE_POLL_INTERVAL_MILLISECONDS` between attempts via the +caller's injected `Sleep` callback. A wedged peer surfaces as a fast Open +failure on the integrator's reconnect loop, not a hang. + +## Reference integration + +The cleanest end-to-end reference is the FreeRTOS BDD target: + +- Adapter wiring: + [Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c](../Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c) +- mbedTLS user config: + [Bdd/Targets/FreeRtos/mbedtls_user_config.h](../Bdd/Targets/FreeRtos/mbedtls_user_config.h) +- Stream composition (TLS over FreeRTOS-Plus-TCP): + [Bdd/Targets/FreeRtos/main.c](../Bdd/Targets/FreeRtos/main.c) + +The host-side reference (POSIX) lives at +[Bdd/Targets/Common/BddTargetTlsSender_OpenSsl_PosixTcp.c](../Bdd/Targets/Common/BddTargetTlsSender_OpenSsl_PosixTcp.c) +and demonstrates the same shape using the OpenSSL-backed +`SolidSyslogTlsStream` for comparison. + +## Common failure modes + +| Symptom | Likely cause | +|---|---| +| `mbedtls_ssl_setup` returns `MBEDTLS_ERR_SSL_ALLOC_FAILED` (-0x7F00) | Heap too small or routed to wrong allocator. Check the FreeRTOS heap budget and confirm `mbedtls_platform_set_calloc_free` was called before any `mbedtls_*_init`. | +| `mbedtls_ssl_handshake` returns `MBEDTLS_ERR_ERROR_GENERIC_ERROR` (-0x0001) at the first call with no BIO traffic | `psa_crypto_init` was not called or returned non-zero. Verify PSA is initialised after DRBG is seeded if using `MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG`. | +| `psa_crypto_init` returns `PSA_ERROR_INSUFFICIENT_ENTROPY` (-148) | PSA's built-in entropy collector cannot find a source. Define `MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG` and provide `mbedtls_psa_external_get_random`. | +| Handshake fails with a hostname / SAN error | `ServerName` doesn't match a SAN on the peer's certificate. Pass the same string your peer's cert advertises (typically the DNS name the client is connecting to, not an IP). | +| `mbedtls_x509_crt_parse` fails on baked-in PEM arrays | Missing NUL terminator. `xxd -i` does not append one; allocate `sizeof(array) + 1` and write `'\0'` at the last byte before parsing. | + +## Out of scope + +The adapter does not own: + +- **PEM-to-handle conversion.** Caller parses on the integrator's terms + (filesystem, baked-in, HSM-pulled). The DI shape is intentional — + baking parsing into the adapter would couple it to mbedTLS's filesystem + abstractions, which are typically disabled on embedded targets. +- **Certificate rotation.** A rotation occurs when the integrator + re-parses the PEM into a new `mbedtls_x509_crt` and rebuilds the + adapter (or destroys and recreates the SolidSyslogStreamSender so the + next Connect picks up the new chain). +- **HSM / TRNG integration.** The integrator's entropy source feeds + CTR_DRBG via `mbedtls_entropy_add_source`. On targets with no real + entropy source, the BDD reference uses a deliberately weak demo source + to keep the path testable — this is loudly marked as not for production. +- **Per-connection TLS configuration.** A single adapter instance carries + one ssl_config; deployments needing per-peer cipher / version pinning + build multiple adapters. From d6ba8c608c0656c77a725e9d1fb5fada001c553d Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 21 May 2026 23:49:44 +0100 Subject: [PATCH 09/15] fix: S08.07 slice 6c address CodeRabbit findings on FreeRTOS TLS bring-up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three findings on PR #421 — all valid, all in the BDD target rather than production library code: 1. **Silent `(void)`-discard on mbedTLS init returns.** The five fail-able calls (`mbedtls_ctr_drbg_seed`, `psa_crypto_init`, two `mbedtls_x509_crt_parse` and `mbedtls_pk_parse_key`) had their rcs discarded. Now each captures the rc, prints a one-line `[mbedtls] ... FAILED rc=...` diagnostic on non-zero, and returns before flipping `mbedTlsInitialised = true`. Surfacing the seed rc exposed a latent bug: DemoEntropySource was registered as `MBEDTLS_ENTROPY_SOURCE_WEAK`, so `mbedtls_entropy_func` never satisfied its `strong_size >= MBEDTLS_ENTROPY_BLOCK_SIZE` check and the seed call always returned ENTROPY_SOURCE_FAILED (-0x0034). `mbedtls_ctr_drbg_random` against the half-seeded context still produced deterministic-but-consistent bytes, which is why earlier handshakes appeared to succeed. Switched the source label to `MBEDTLS_ENTROPY_SOURCE_STRONG` (the audit-trail "demo-only entropy" printf at the end of `EnsureMbedTlsInitialised` is the real quality assertion — the STRONG/WEAK label is structural, not a quality claim). 2. **`tlsSender` not destroyed in TeardownAll.** Added `BddTargetTlsSender_Destroy()` + `tlsSender = NULL` after the existing `SolidSyslogSwitchingSender_Destroy` so the inner MbedTlsStream + FreeRtosTcpStream pool slots release on shutdown paths. 3. **Stale TLS/mTLS comments around tlsSender setup.** The block still said "mTLS support is slice 6c work and may require an extra BDD_TARGET_SWITCH_MTLS slot" — now reflects the dual-port dispatcher that already shipped. The `set transport` command-list comment includes `mtls` alongside the other transports. Local: 42/42 freertos BDD scenarios pass, Linux capacity_threshold unaffected. --- .../BddTargetTlsSender_MbedTls_FreeRtosTcp.c | 64 ++++++++++++++++--- Bdd/Targets/FreeRtos/main.c | 29 ++++++--- 2 files changed, 75 insertions(+), 18 deletions(-) diff --git a/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c b/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c index 0dbcd571..9bc6ca4a 100644 --- a/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c +++ b/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c @@ -139,8 +139,9 @@ psa_status_t mbedtls_psa_external_get_random( /* Demo-only entropy: XOR the FreeRTOS tick count, a per-call counter, and * the destination address (which varies per call) into each output byte. * Quality is intentionally terrible — QEMU has no real source — and the - * SolidSyslog_Error(WARNING, …) emit below makes that explicit. Real - * integrators on bare-metal would replace this with TRNG / HSM bytes. */ + * "demo-only entropy" printf emit at the end of EnsureMbedTlsInitialised + * makes that explicit. Real integrators on bare-metal would replace this + * with TRNG / HSM bytes. */ static int DemoEntropySource(void* data, unsigned char* output, size_t len, size_t* olen) { (void) data; @@ -198,17 +199,35 @@ static void EnsureMbedTlsInitialised(void) mbedtls_platform_set_calloc_free(FreeRtosMbedTlsCalloc, FreeRtosMbedTlsFree); mbedtls_entropy_init(&entropy); + /* Registered as MBEDTLS_ENTROPY_SOURCE_STRONG even though the demo + * randomness is intentionally terrible. The STRONG/WEAK label is + * checked structurally by `mbedtls_entropy_func`, which requires at + * least MBEDTLS_ENTROPY_BLOCK_SIZE bytes of strong contribution per + * call — without any strong source registered, every + * `mbedtls_ctr_drbg_seed` returns ENTROPY_SOURCE_FAILED (-0x0034) + * after looping 256 times trying to satisfy the threshold. The + * "demo-only entropy" notice printed at the end of this function is + * the real quality assertion; real integrators replace this with a + * TRNG / HSM source. */ mbedtls_entropy_add_source( &entropy, DemoEntropySource, NULL, MBEDTLS_ENTROPY_BLOCK_SIZE, - MBEDTLS_ENTROPY_SOURCE_WEAK + MBEDTLS_ENTROPY_SOURCE_STRONG ); mbedtls_ctr_drbg_init(&drbg); static const unsigned char personalization[] = "solidsyslog-freertos-bdd"; - (void) mbedtls_ctr_drbg_seed(&drbg, mbedtls_entropy_func, &entropy, personalization, sizeof(personalization) - 1U); + int drbgSeedRc = mbedtls_ctr_drbg_seed( + &drbg, mbedtls_entropy_func, &entropy, personalization, sizeof(personalization) - 1U + ); + if (drbgSeedRc != 0) + { + (void) printf("[mbedtls] ctr_drbg_seed FAILED rc=-0x%04x; TLS slot will be unusable\r\n", + (unsigned) -drbgSeedRc); + return; + } vTaskDelay(1U); /* mbedTLS 3.6 routes TLS 1.3 cryptography through PSA, so psa_crypto_init() @@ -219,14 +238,25 @@ static void EnsureMbedTlsInitialised(void) * PSA_ERROR_INSUFFICIENT_ENTROPY on this no-platform-entropy target), so * the init only succeeds once the DRBG is seeded above — that's why this * sits after the seed step. Idempotent across subsequent calls. */ - (void) psa_crypto_init(); + psa_status_t psaRc = psa_crypto_init(); + if (psaRc != PSA_SUCCESS) + { + (void) printf("[mbedtls] psa_crypto_init FAILED rc=%d; TLS slot will be unusable\r\n", (int) psaRc); + return; + } (void) printf("[mbedtls] parsing CA chain\r\n"); memcpy(caPemBuf, bdd_baked_ca_pem, sizeof(bdd_baked_ca_pem)); caPemBuf[sizeof(bdd_baked_ca_pem)] = '\0'; mbedtls_x509_crt_init(&caChain); - (void) mbedtls_x509_crt_parse(&caChain, caPemBuf, sizeof(caPemBuf)); + int caParseRc = mbedtls_x509_crt_parse(&caChain, caPemBuf, sizeof(caPemBuf)); + if (caParseRc != 0) + { + (void) printf("[mbedtls] CA chain parse FAILED rc=-0x%04x; TLS slot will be unusable\r\n", + (unsigned) -caParseRc); + return; + } vTaskDelay(1U); (void) printf("[mbedtls] parsing client cert chain\r\n"); @@ -234,7 +264,14 @@ static void EnsureMbedTlsInitialised(void) memcpy(clientCertPemBuf, bdd_baked_client_cert_pem, sizeof(bdd_baked_client_cert_pem)); clientCertPemBuf[sizeof(bdd_baked_client_cert_pem)] = '\0'; mbedtls_x509_crt_init(&clientCertChain); - (void) mbedtls_x509_crt_parse(&clientCertChain, clientCertPemBuf, sizeof(clientCertPemBuf)); + int clientCertParseRc = + mbedtls_x509_crt_parse(&clientCertChain, clientCertPemBuf, sizeof(clientCertPemBuf)); + if (clientCertParseRc != 0) + { + (void) printf("[mbedtls] client cert parse FAILED rc=-0x%04x; mTLS will be unusable\r\n", + (unsigned) -clientCertParseRc); + return; + } vTaskDelay(1U); (void) printf("[mbedtls] parsing client key (RSA — slowest step)\r\n"); @@ -242,7 +279,7 @@ static void EnsureMbedTlsInitialised(void) memcpy(clientKeyPemBuf, bdd_baked_client_key_pem, sizeof(bdd_baked_client_key_pem)); clientKeyPemBuf[sizeof(bdd_baked_client_key_pem)] = '\0'; mbedtls_pk_init(&clientKey); - (void) mbedtls_pk_parse_key( + int clientKeyParseRc = mbedtls_pk_parse_key( &clientKey, clientKeyPemBuf, sizeof(clientKeyPemBuf), @@ -251,11 +288,20 @@ static void EnsureMbedTlsInitialised(void) mbedtls_ctr_drbg_random, &drbg ); + if (clientKeyParseRc != 0) + { + (void) printf("[mbedtls] client key parse FAILED rc=-0x%04x; mTLS will be unusable\r\n", + (unsigned) -clientKeyParseRc); + return; + } vTaskDelay(1U); /* Audit trail: every cold boot of this target announces the demo-only * entropy explicitly. Integrators porting this off the BDD target should - * see this and replace DemoEntropySource with TRNG before shipping. */ + * see this and replace DemoEntropySource with TRNG before shipping. The + * `mbedTlsInitialised = true` latch happens only after every fail-able + * step above has succeeded — partial-init state would silently degrade + * later handshakes into confusing "internal" errors. */ (void) printf("[mbedtls] init complete. WARNING: demo-only entropy " "(xTaskGetTickCount + per-call counter). Not for production.\r\n"); diff --git a/Bdd/Targets/FreeRtos/main.c b/Bdd/Targets/FreeRtos/main.c index fe4b5c1e..8eb9ff0d 100644 --- a/Bdd/Targets/FreeRtos/main.c +++ b/Bdd/Targets/FreeRtos/main.c @@ -692,6 +692,13 @@ static void TeardownAll(void) SolidSyslogFreeRtosMutex_Destroy(lifecycleMutex); lifecycleMutex = NULL; SolidSyslogSwitchingSender_Destroy(switchingSender); + /* BddTargetTlsSender owns the inner MbedTlsStream + FreeRtosTcpStream pool + * slots and the StreamSender slot, so it must be destroyed before tcpSender + * / tcpStream below — releasing slots in roughly reverse order of Create + * keeps the dependency graph clean even though the pool allocator itself + * is order-insensitive. */ + BddTargetTlsSender_Destroy(); + tlsSender = NULL; SolidSyslogStreamSender_Destroy(tcpSender); SolidSyslogFreeRtosTcpStream_Destroy(tcpStream); SolidSyslogUdpSender_Destroy(udpSender); @@ -821,17 +828,21 @@ static void InteractiveTask(void* argument) tcpSender = SolidSyslogStreamSender_Create(&tcpConfig); /* TLS slot: SolidSyslogMbedTlsStream over SolidSyslogFreeRtosTcpStream, - * with the demo CA / client cert / client key baked into the ELF. - * Slice 6b ships TLS-only (mtls=false) — port 6514 on the BDD syslog-ng - * oracle, no client cert presented. mTLS support (port 6515, client - * cert wired) is slice 6c work and may require an extra - * BDD_TARGET_SWITCH_MTLS slot or a runtime endpoint-dispatch shim. */ + * with the demo CA / client cert / client key baked into the ELF. The + * same slot serves both @tls (port 6514) and @mtls (port 6515) + * scenarios — the wrapper wires the client cert unconditionally and + * its StreamSender Endpoint callback dispatches on + * BddTargetSwitchConfig_IsMtlsMode() at Connect time. The `false` + * argument here is honoured for cross-platform signature uniformity + * (Linux / Windows TLS senders read it at startup) but ignored on + * FreeRTOS, where the harness flips mode over the UART after the + * prompt is already up. */ tlsSender = BddTargetTlsSender_Create(resolver, false); - /* SwitchingSender lets `set transport ` flip the active - * transport at runtime. Default to UDP so existing UDP-tagged scenarios - * stay green; `--transport tcp|tls` flowing through the behave harness - * lands here as `set transport ` over the UART. */ + /* SwitchingSender lets `set transport ` flip the + * active transport at runtime. Default to UDP so existing UDP-tagged + * scenarios stay green; `--transport tcp|tls|mtls` flowing through the + * behave harness lands here as `set transport ` over the UART. */ static struct SolidSyslogSender* inners[BDD_TARGET_SWITCH_COUNT]; inners[BDD_TARGET_SWITCH_UDP] = udpSender; inners[BDD_TARGET_SWITCH_TCP] = tcpSender; From fd730fbb3f84cedb4b11d59a357b819f71181d9e Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 21 May 2026 23:52:20 +0100 Subject: [PATCH 10/15] docs: rewrite integrating-mbedtls.md for the SolidSyslog-integrator audience MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first draft of this file was structured like a generic mbedTLS tutorial — a flat init checklist plus a footprint discussion. That left the actual integration question unanswered: "I'm here because I want SolidSyslog on top of mbedTLS — what do *I* plug in?" This rewrite restructures the document around two concrete integrator scenarios: - **Scenario A — you already have Mbed TLS in your image.** Three steps: pick a `SolidSyslogStream` byte transport, fill in `SolidSyslogMbedTlsStreamConfig` with your existing handles, wire `tlsStream` into a `SolidSyslogStreamSender`. The coexistence contract documents the auditable not-touched list so an integrator with an existing Mbed TLS deployment can be confident the adapter won't fight their setup. - **Scenario B — you don't have Mbed TLS yet.** Defers to upstream Mbed TLS porting docs for the bring-up itself, then gives the integrator a SolidSyslog-specific checklist of what we actually consume (seeded CTR_DRBG with at least one STRONG entropy source, psa_crypto_init after the DRBG seed, parsed CA chain, optional client cert + key, byte transport). The FreeRTOS-specific gotchas (newlib syscall heap, PSA external RNG, record-buffer sizing) move to a dedicated section labelled as integrator checklist items rather than baked into the body — they only apply on that family of targets. The "what this adapter does not own" footer makes the dependency-injection contract explicit so integrators know which problems remain theirs (PEM parsing, rotation, HSM, per-connection config). No code change. --- docs/integrating-mbedtls.md | 421 ++++++++++++++++++------------------ 1 file changed, 212 insertions(+), 209 deletions(-) diff --git a/docs/integrating-mbedtls.md b/docs/integrating-mbedtls.md index 97c70eff..45f3b9fa 100644 --- a/docs/integrating-mbedtls.md +++ b/docs/integrating-mbedtls.md @@ -1,213 +1,216 @@ -# Integrating mbedTLS - -`SolidSyslogMbedTlsStream` is a `SolidSyslogStream` implementation that -wraps an injected byte-stream (typically `SolidSyslogFreeRtosTcpStream` or -`SolidSyslogPosixTcpStream`) with TLS via Mbed TLS 3.x. It is a reference -adapter — small, dependency-injected, and intended to be auditable line by -line. `SolidSyslogTlsStream` (OpenSSL-backed) remains the host-side -reference; this guide covers the mbedTLS adapter for embedded / FreeRTOS / -resource-constrained integrations where OpenSSL is impractical. - -## When to use mbedTLS - -| Scenario | Recommended adapter | -|---|---| -| Linux server / appliance / containerised | `SolidSyslogTlsStream` (OpenSSL) | -| Windows server / desktop | `SolidSyslogTlsStream` (OpenSSL) | -| FreeRTOS / Zephyr / bare-metal Cortex-M with TCP/IP | `SolidSyslogMbedTlsStream` | -| RTOS with no full-fat TLS library available | `SolidSyslogMbedTlsStream` | - -Both adapters expose the same `SolidSyslogStream` vtable, so callers swap -implementations at link time — `SolidSyslogStreamSender` doesn't know -which TLS library is underneath. - -## Coexistence contract - -`Platform/MbedTls/Source/` MUST NOT call any process-global mbedTLS APIs -(global RNG installers, global PSA initialisers, global mbedTLS allocator -hooks). The contract is auditable by grep: any future change that -introduces such a call must be flagged in review. See -[[project-mbedtls-coexistence-contract]] for the rationale. - -The integrator owns process-global setup. The adapter only consumes -caller-built handles via the config struct. - -This matters because mbedTLS allows applications to share a single global -crypto context across multiple library users. If the SolidSyslog adapter -installed its own global state — a process-wide allocator, RNG, or PSA -context — it would silently overwrite the integrator's existing wiring -or be silently overwritten by it. - -## Configuration handles - -`SolidSyslogMbedTlsStreamConfig` (in -[Platform/MbedTls/Interface/SolidSyslogMbedTlsStream.h](../Platform/MbedTls/Interface/SolidSyslogMbedTlsStream.h)) -takes pre-built handles, never file paths or PEM blobs: - -| Field | Owner | Notes | +# Integrating SolidSyslog with mbedTLS + +`SolidSyslogMbedTlsStream` lets you deliver RFC 5425 (syslog over TLS) +records from SolidSyslog through Mbed TLS instead of OpenSSL. It is the +recommended adapter on embedded / FreeRTOS / bare-metal targets where +OpenSSL is too large or impractical. Hosted Linux / Windows deployments +should use `SolidSyslogTlsStream` (OpenSSL) — both adapters expose the +same `SolidSyslogStream` vtable, so the rest of the wiring +(`SolidSyslogStreamSender`, your buffer, your store) is identical. + +This document covers what *you*, the integrator, plug in. It does not +re-teach mbedTLS — for that, see the +[upstream Mbed TLS documentation](https://mbed-tls.readthedocs.io/). + +--- + +## The shape + +``` +SolidSyslog_Log ─▶ Buffer ─▶ Sender ─▶ SolidSyslogStreamSender + │ + ▼ + SolidSyslogMbedTlsStream ◀── you build CA/cert/key/DRBG handles + │ + ▼ + SolidSyslogStream ◀── you pick / write the TCP backend + │ + ▼ + (your TCP/IP stack) +``` + +You supply two things directly: the byte-transport `SolidSyslogStream` and +the per-context mbedTLS handles passed through +`SolidSyslogMbedTlsStreamConfig`. + +--- + +## What you need to provide + +| Item | Owner | Notes | |---|---|---| -| `Transport` | Caller | A `SolidSyslogStream*` carrying the byte transport. The adapter calls `Open`/`Send`/`Read`/`Close` on it — same vtable that `SolidSyslogStreamSender` would use directly for plain TCP. | -| `Sleep` | Caller | A `SolidSyslogSleepFunction` driving the bounded handshake retry between `WANT_READ` / `WANT_WRITE` polls. Required. On FreeRTOS use a `vTaskDelay`-backed wrapper; on POSIX `SolidSyslogPosixSleep` is the natural fit. | -| `Rng` | Caller | `mbedtls_ctr_drbg_context*` — seeded by the integrator before the first handshake. The adapter calls `mbedtls_ctr_drbg_random` against this handle. | -| `CaChain` | Caller | `mbedtls_x509_crt*` parsed by the integrator from whatever source is appropriate (filesystem on POSIX, baked-in `xxd -i` arrays on FreeRTOS, HSM-backed trust store on a secure element). | -| `ServerName` | Caller | SNI string and certificate-name check target. `NULL` skips the name check — only appropriate for closed networks where IP-pinning replaces hostname identity. | -| `ClientCertChain` / `ClientKey` | Caller | `mbedtls_x509_crt*` + `mbedtls_pk_context*` for mTLS. Both NULL = server-auth TLS only. Both non-NULL = mTLS. Supplying only one is treated as "no client cert" — the adapter never half-configures. | - -The "caller owns handles" pattern keeps the adapter framework-agnostic: a -deployment that already builds its own X.509 / DRBG handles for other -purposes (key rotation policy, HSM integration, TRNG wiring) reuses those -handles unchanged. See [[project-mbedtls-di-handles]] for the design note. - -## Process-wide setup the integrator must do - -The order of operations matters — getting it wrong surfaces as misleading -mbedTLS errors (the most common ones are documented at the end of this -guide). Recommended sequence: - -1. **Install the platform allocator** (if not using the default libc one). - On FreeRTOS this routes mbedTLS allocations through `pvPortMalloc` / - `vPortFree` so they land in the RTOS heap rather than newlib's tiny - syscall heap. Requires `MBEDTLS_PLATFORM_MEMORY` in the integrator's - mbedTLS config: - ```c - mbedtls_platform_set_calloc_free(YourCalloc, YourFree); - ``` -2. **Initialise entropy and seed the DRBG.** The adapter does not seed the - DRBG itself; that responsibility stays with the integrator, who knows - what hardware sources are available: - ```c - mbedtls_entropy_init(&entropy); - mbedtls_entropy_add_source(&entropy, YourEntropySource, NULL, ...); - mbedtls_ctr_drbg_init(&drbg); - mbedtls_ctr_drbg_seed(&drbg, mbedtls_entropy_func, &entropy, ...); - ``` -3. **Initialise PSA crypto** (mandatory for mbedTLS 3.6 with TLS 1.3 - enabled — even when negotiating an older version, the handshake state - machine touches PSA). Must come *after* the DRBG is seeded if you are - using `MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG`: - ```c - psa_crypto_init(); - ``` -4. **Parse the certificates and keys** the adapter will consume: +| `Transport` | You | A `SolidSyslogStream*` carrying TCP. The library ships `SolidSyslogPosixTcpStream` (POSIX), `SolidSyslogWinsockTcpStream` (Windows), and `SolidSyslogFreeRtosTcpStream` (FreeRTOS-Plus-TCP). If your TCP/IP stack is different (LwIP, NicheStack, vendor BSP), write your own `SolidSyslogStream` — see [`Platform/Posix/Source/SolidSyslogPosixTcpStream.c`](../Platform/Posix/Source/SolidSyslogPosixTcpStream.c) as a reference. | +| `Sleep` | You | A `SolidSyslogSleepFunction`. Drives the bounded handshake retry between `WANT_READ` / `WANT_WRITE` polls. On FreeRTOS use a `vTaskDelay`-backed wrapper; on POSIX `SolidSyslogPosixSleep` is the natural fit. Required. | +| `Rng` | You | `mbedtls_ctr_drbg_context*` you seeded yourself. The adapter calls `mbedtls_ctr_drbg_random` against it. Required. | +| `CaChain` | You | `mbedtls_x509_crt*` you parsed yourself (from filesystem, baked-in PEM, HSM, whatever fits your build). Required. | +| `ServerName` | You | SNI + cert hostname check string. `NULL` skips the name check; only appropriate for closed networks where IP-pinning replaces hostname identity. | +| `ClientCertChain` / `ClientKey` | You | `mbedtls_x509_crt*` + `mbedtls_pk_context*` for mTLS. Both `NULL` = server-auth-only TLS. Both non-`NULL` = mTLS. Supplying only one is treated as "no client cert" — the adapter never half-configures. | + +The full struct shape lives in +[`Platform/MbedTls/Interface/SolidSyslogMbedTlsStream.h`](../Platform/MbedTls/Interface/SolidSyslogMbedTlsStream.h). + +--- + +## Scenario A: you already have Mbed TLS in your image + +If your firmware already wires Mbed TLS for another subsystem (a cloud +client, an OTA updater, a vendor security framework), you keep that wiring +intact. The adapter consumes the handles you've already built — it never +calls `mbedtls_platform_setup` / `_teardown`, never installs +threading-alt hooks, never resets the global RNG, never replaces your +debug callback. See the [coexistence contract](#coexistence-contract) +below for the auditable list. + +Concretely, on top of your existing setup: + +1. **Pick a `SolidSyslogStream` for the byte transport.** Use one of the + shipped adapters that matches your TCP/IP stack + (`SolidSyslogFreeRtosTcpStream`, `SolidSyslogPosixTcpStream`, + `SolidSyslogWinsockTcpStream`) or write your own backing the same + `SolidSyslogStream` vtable. If you wrote your own, the existing + shipped adapters are the worked examples. +2. **Fill in `SolidSyslogMbedTlsStreamConfig`** with the handles you + already have: ```c - mbedtls_x509_crt_init(&caChain); - mbedtls_x509_crt_parse(&caChain, caPem, caPemLen + 1); /* +1 for NUL */ - /* …same pattern for client cert and key… */ + struct SolidSyslogMbedTlsStreamConfig cfg = { + .Transport = myTcpStream, /* from step 1 */ + .Sleep = MyVTaskDelayWrapper, /* or PosixSleep / similar */ + .Rng = &myAlreadySeededDrbg, + .CaChain = &myAlreadyParsedCaChain, + .ServerName = "syslog.example.com", + .ClientCertChain = &myClientCert, /* NULL for server-auth-only */ + .ClientKey = &myClientKey, /* paired with ClientCertChain */ + }; + struct SolidSyslogStream* tlsStream = SolidSyslogMbedTlsStream_Create(&cfg); ``` -5. **Build the adapter** via `SolidSyslogMbedTlsStream_Create(&config)` - passing the handles initialised above. +3. **Wire `tlsStream` into a `SolidSyslogStreamSender`** as the `Stream` + field — the same way you'd wire a plain TCP stream. RFC 6587 + octet-counting framing is applied by `StreamSender` on top of the + adapter. + +That's the whole integration on the SolidSyslog side. There are no +process-wide hooks to install and nothing to teardown beyond the matching +`SolidSyslogMbedTlsStream_Destroy` when you tear the sender down. + +--- + +## Scenario B: you do not have Mbed TLS yet + +If you're bringing Mbed TLS in fresh for SolidSyslog, do that work first +following the upstream +[Mbed TLS porting guide](https://mbed-tls.readthedocs.io/en/latest/kb/how-to/how-do-i-port-mbed-tls-to-a-new-environment-os/). +Once Mbed TLS itself is building on your target, you need the following +specifically for this adapter: + +- **A seeded `mbedtls_ctr_drbg_context`.** `mbedtls_entropy_init` + + `mbedtls_entropy_add_source` for at least one source registered as + `MBEDTLS_ENTROPY_SOURCE_STRONG` (without a STRONG-tagged source, + `mbedtls_entropy_func` never satisfies its internal threshold and + every `mbedtls_ctr_drbg_seed` call returns + `MBEDTLS_ERR_CTR_DRBG_ENTROPY_SOURCE_FAILED` — silent on the wire, + loud in your tests), then `mbedtls_ctr_drbg_init` + + `mbedtls_ctr_drbg_seed`. Production-quality entropy is a hardware + question — TRNG, vendor HSM, or a board-specific source. +- **`psa_crypto_init()` called *after* the DRBG is seeded.** Mbed TLS + 3.6's TLS 1.3 code path routes through PSA. If PSA isn't initialised, + the first handshake state transition returns + `MBEDTLS_ERR_ERROR_GENERIC_ERROR` (-0x0001) before any TLS bytes leave + the socket. If your target has no platform entropy source (a common + embedded case), `#define MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG` in your + mbedTLS config and provide + `mbedtls_psa_external_get_random` that wraps the DRBG you just seeded — + this keeps PSA and the classic mbedTLS API on the same entropy chain. +- **A parsed CA chain.** `mbedtls_x509_crt_init` + + `mbedtls_x509_crt_parse` against whatever delivery mechanism fits your + build (filesystem on POSIX, baked-in array via `xxd -i` on bare-metal, + HSM-pulled blob, etc.). PEM input must be NUL-terminated. +- **(mTLS only) a parsed client cert chain and private key.** Same + pattern as the CA chain plus `mbedtls_pk_init` / + `mbedtls_pk_parse_key`. +- **A byte-transport `SolidSyslogStream`** matching your TCP/IP stack, + exactly as in [Scenario A](#scenario-a-you-already-have-mbed-tls-in-your-image). + +A worked end-to-end example for all of the above lives at +[`Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c`](../Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c) +(FreeRTOS-Plus-TCP on QEMU mps2-an385). The matching Mbed TLS config +overrides live at +[`Bdd/Targets/FreeRtos/mbedtls_user_config.h`](../Bdd/Targets/FreeRtos/mbedtls_user_config.h). + +--- + +## Coexistence contract -The adapter's `Open` / `Close` cycle is idempotent and re-entrant: the -underlying mbedTLS contexts are zeroed on construction and after every -`Close`, so reconnect after an outage is automatic and leak-free. - -## FreeRTOS-specific considerations - -### Heap budget - -mbedTLS `mbedtls_ssl_setup` allocates per-context buffers totalling roughly -`MBEDTLS_SSL_IN_CONTENT_LEN + MBEDTLS_SSL_OUT_CONTENT_LEN + ~3 KiB` of -handshake state. With mbedTLS defaults (16 KiB each) that's >35 KiB per -context — likely larger than your RTOS heap if you haven't sized for it. - -Three knobs: - -- Size the **FreeRTOS heap** (`configTOTAL_HEAP_SIZE`) to cover the worst - case: per-SSL-context working set × (number of concurrent TLS connections), - plus everything else the application allocates. -- Shrink **`MBEDTLS_SSL_IN_CONTENT_LEN`** to the largest TLS record the peer - will send. Server certificates with intermediates are typically 2–4 KiB; - a 4096-byte IN buffer is a reasonable starting point for syslog deployments. -- Shrink **`MBEDTLS_SSL_OUT_CONTENT_LEN`** to the largest message you will - send. The library's default `SOLIDSYSLOG_MAX_MESSAGE_SIZE` fits inside - 2048 comfortably. - -The BDD target's [mbedtls_user_config.h](../Bdd/Targets/FreeRtos/mbedtls_user_config.h) -shows the full minimal config — `IN=4096`, `OUT=2048`, with rationale -comments — for the QEMU mps2-an385 reference image. - -### newlib's syscall heap - -If the FreeRTOS target uses newlib (the SolidSyslog FreeRTOS BDD target -does), libc `calloc` is backed by `_sbrk`, which in turn is typically backed -by a small static buffer (4 KiB in the SolidSyslog BDD reference at -[Syscalls.c](../Bdd/Targets/FreeRtos/Common/Syscalls.c)). That buffer is -intended for newlib's small scratch allocations (printf, etc.), not TLS -contexts. - -Without `mbedtls_platform_set_calloc_free`, mbedTLS will silently land -every `calloc` in that 4 KiB buffer and `mbedtls_ssl_setup` will fail with -`MBEDTLS_ERR_SSL_ALLOC_FAILED` (-0x7F00) before the first byte hits the -socket. - -### PSA crypto on no-platform-entropy targets - -mbedTLS 3.6's TLS 1.3 code path routes random-number requests through PSA -crypto. PSA's built-in entropy collector requires a platform entropy source -— which is exactly what `MBEDTLS_NO_PLATFORM_ENTROPY` turns off on -embedded targets. - -The fix is `MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG` plus an integrator-supplied -`mbedtls_psa_external_get_random` that feeds PSA from the same CTR_DRBG -the classic API uses. This keeps the entropy chain single-rooted at your -hardware source. - -Reference implementation (BDD target): -[BddTargetTlsSender_MbedTls_FreeRtosTcp.c](../Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c). - -### Bounded handshake retry - -The adapter polls `mbedtls_ssl_handshake` against the non-blocking -transport — it never blocks the FreeRTOS service task indefinitely. The -retry budget is `HANDSHAKE_TIMEOUT_MILLISECONDS` (5 seconds at time of -writing, configurable in -[SolidSyslogMbedTlsStream.c](../Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c)), -sleeping `HANDSHAKE_POLL_INTERVAL_MILLISECONDS` between attempts via the -caller's injected `Sleep` callback. A wedged peer surfaces as a fast Open -failure on the integrator's reconnect loop, not a hang. - -## Reference integration - -The cleanest end-to-end reference is the FreeRTOS BDD target: - -- Adapter wiring: - [Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c](../Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c) -- mbedTLS user config: - [Bdd/Targets/FreeRtos/mbedtls_user_config.h](../Bdd/Targets/FreeRtos/mbedtls_user_config.h) -- Stream composition (TLS over FreeRTOS-Plus-TCP): - [Bdd/Targets/FreeRtos/main.c](../Bdd/Targets/FreeRtos/main.c) - -The host-side reference (POSIX) lives at -[Bdd/Targets/Common/BddTargetTlsSender_OpenSsl_PosixTcp.c](../Bdd/Targets/Common/BddTargetTlsSender_OpenSsl_PosixTcp.c) -and demonstrates the same shape using the OpenSSL-backed -`SolidSyslogTlsStream` for comparison. - -## Common failure modes - -| Symptom | Likely cause | -|---|---| -| `mbedtls_ssl_setup` returns `MBEDTLS_ERR_SSL_ALLOC_FAILED` (-0x7F00) | Heap too small or routed to wrong allocator. Check the FreeRTOS heap budget and confirm `mbedtls_platform_set_calloc_free` was called before any `mbedtls_*_init`. | -| `mbedtls_ssl_handshake` returns `MBEDTLS_ERR_ERROR_GENERIC_ERROR` (-0x0001) at the first call with no BIO traffic | `psa_crypto_init` was not called or returned non-zero. Verify PSA is initialised after DRBG is seeded if using `MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG`. | -| `psa_crypto_init` returns `PSA_ERROR_INSUFFICIENT_ENTROPY` (-148) | PSA's built-in entropy collector cannot find a source. Define `MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG` and provide `mbedtls_psa_external_get_random`. | -| Handshake fails with a hostname / SAN error | `ServerName` doesn't match a SAN on the peer's certificate. Pass the same string your peer's cert advertises (typically the DNS name the client is connecting to, not an IP). | -| `mbedtls_x509_crt_parse` fails on baked-in PEM arrays | Missing NUL terminator. `xxd -i` does not append one; allocate `sizeof(array) + 1` and write `'\0'` at the last byte before parsing. | - -## Out of scope - -The adapter does not own: - -- **PEM-to-handle conversion.** Caller parses on the integrator's terms - (filesystem, baked-in, HSM-pulled). The DI shape is intentional — - baking parsing into the adapter would couple it to mbedTLS's filesystem - abstractions, which are typically disabled on embedded targets. -- **Certificate rotation.** A rotation occurs when the integrator - re-parses the PEM into a new `mbedtls_x509_crt` and rebuilds the - adapter (or destroys and recreates the SolidSyslogStreamSender so the - next Connect picks up the new chain). -- **HSM / TRNG integration.** The integrator's entropy source feeds - CTR_DRBG via `mbedtls_entropy_add_source`. On targets with no real - entropy source, the BDD reference uses a deliberately weak demo source - to keep the path testable — this is loudly marked as not for production. -- **Per-connection TLS configuration.** A single adapter instance carries - one ssl_config; deployments needing per-peer cipher / version pinning - build multiple adapters. +`Platform/MbedTls/Source/` is auditably free of process-global Mbed TLS +calls. The adapter: + +- ✗ does not call `mbedtls_platform_setup` / `_teardown` +- ✗ does not call `mbedtls_threading_set_alt` +- ✗ does not call `psa_crypto_init` (you do) +- ✗ does not call `mbedtls_platform_set_calloc_free` (you do, if you need it) +- ✗ does not call `mbedtls_debug_set_threshold` / `mbedtls_ssl_conf_dbg` +- ✗ does not free any handle you passed in via the config struct + +Everything in that list is global state your existing integration may +already own. Auditors verify the contract by grepping +`Platform/MbedTls/Source/` — any future change that introduces a global +call must be flagged in review. + +--- + +## FreeRTOS-specific gotchas + +These bit us during the BDD-target bring-up. If you're on FreeRTOS with +newlib, treat them as integrator-side checklist items: + +- **Route mbedTLS allocations to the RTOS heap.** Mbed TLS calls libc + `calloc`, which on newlib targets typically hits a tiny `_sbrk`-backed + syscall heap (4 KiB in the SolidSyslog BDD reference at + [`Bdd/Targets/FreeRtos/Common/Syscalls.c`](../Bdd/Targets/FreeRtos/Common/Syscalls.c)). + A single `mbedtls_ssl_setup` wants ~10–16 KiB and will fail with + `MBEDTLS_ERR_SSL_ALLOC_FAILED` (-0x7F00). Set + `MBEDTLS_PLATFORM_MEMORY` in your config and call + `mbedtls_platform_set_calloc_free(yourCalloc, yourFree)` (pvPortMalloc + / vPortFree) before any `mbedtls_*_init`. +- **Shrink the TLS record buffers from the 16 KiB default.** Set + `MBEDTLS_SSL_IN_CONTENT_LEN` to the largest TLS record your peer will + send (server cert + chain is typically 2–4 KiB), and + `MBEDTLS_SSL_OUT_CONTENT_LEN` to your largest application message. + The defaults cost ~32 KiB of FreeRTOS heap per TLS context. +- **`mbedtls_ssl_setup` allocates roughly `IN + OUT + ~3 KiB` of + handshake state.** Size your FreeRTOS heap + (`configTOTAL_HEAP_SIZE`) accordingly across all concurrent TLS + contexts. +- **`MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG` + the external RNG hook are + effectively mandatory** if you've defined + `MBEDTLS_NO_PLATFORM_ENTROPY` (which you typically have on + embedded). Without it, `psa_crypto_init` returns + `PSA_ERROR_INSUFFICIENT_ENTROPY` (-148). + +The BDD target's +[mbedtls_user_config.h](../Bdd/Targets/FreeRtos/mbedtls_user_config.h) +shows the minimal config that satisfies the above for QEMU mps2-an385. + +--- + +## Reference integrations + +| Target | Adapter source | Mbed TLS config | Notes | +|---|---|---|---| +| FreeRTOS QEMU mps2-an385 + FreeRTOS-Plus-TCP | [BddTargetTlsSender_MbedTls_FreeRtosTcp.c](../Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c) | [mbedtls_user_config.h](../Bdd/Targets/FreeRtos/mbedtls_user_config.h) | Demo-quality entropy and baked-in PEMs; loudly tagged not-for-production. | +| Linux host (host-TDD parity with the embedded path) | [Tests/MbedTlsIntegration/](../Tests/MbedTlsIntegration/) | — | In-process TLS server drives a real handshake against the wrapper. | +| POSIX (OpenSSL reference, for comparison) | [BddTargetTlsSender_OpenSsl_PosixTcp.c](../Bdd/Targets/Common/BddTargetTlsSender_OpenSsl_PosixTcp.c) | — | Same composition shape using `SolidSyslogTlsStream` for the TLS layer. | + +--- + +## What this adapter does not own + +- **PEM-to-handle conversion** — you parse, in whatever way fits your + build. +- **Certificate rotation** — re-parse and rebuild the adapter, or destroy + / re-create the `SolidSyslogStreamSender` so the next Connect picks up + the new chain. +- **HSM / TRNG integration** — your entropy source feeds CTR_DRBG; the + adapter consumes the seeded DRBG. +- **Per-connection TLS configuration** — one adapter instance, one + `ssl_config`. If you need per-peer cipher / version pinning, build + multiple adapters. From 20affeab1c6e6c7ac5047b95cb84779a27c31d19 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 21 May 2026 23:57:46 +0100 Subject: [PATCH 11/15] =?UTF-8?q?docs:=20S08.07=20slice=207=20=E2=80=94=20?= =?UTF-8?q?register=20mbedTLS=20adapter=20across=20CLAUDE.md,=20iec62443.m?= =?UTF-8?q?d,=20rfc-compliance.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mbedTLS reference adapter (SolidSyslogMbedTlsStream) shipped in slices 1–5 (host) and slices 6a–6c (FreeRTOS BDD target) but was only ever described in the parent issue, not in the existing reference docs. This sweep registers it alongside the OpenSSL-backed reference: - CLAUDE.md — new `SolidSyslogMbedTlsStream.h` row in the Public-Header-Audiences table (alongside the existing `SolidSyslogTlsStream.h` row). Updates the StreamSender row to enumerate `SolidSyslogMbedTlsStream` alongside `SolidSyslogTlsStream` as a TLS-capable Stream backend. Points readers at the integrator guide for the full coexistence-contract write-up. - docs/iec62443.md — new `### Embedded SL4` subsection mapping `SolidSyslogMbedTlsStream` to the same CR 1.5 / CR 1.8 / CR 2.12 / CR 3.9 controls the OpenSSL substrate satisfies, with a coexistence-contract bullet aimed at integrators who already wire mbedTLS for another subsystem. The SL4 setup-recipe at the bottom of the page mentions both adapters as alternatives. - docs/rfc-compliance.md — RFC 5425 section table widens from OpenSSL-only language to per-row "OpenSSL: … / Mbed TLS: …" coverage, with an intro paragraph noting the two adapters share the SolidSyslogStream vtable so the requirement matrix is per-section, not per-adapter. No code change; all references are to existing code paths. --- CLAUDE.md | 3 ++- docs/iec62443.md | 51 ++++++++++++++++++++++++++++++++++++++++-- docs/rfc-compliance.md | 14 +++++++----- 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 26b8e365..88235b86 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -337,8 +337,9 @@ live under `Core/Interface/`; platform-specific helpers (the `SolidSyslogPosix*` | `SolidSyslogFreeRtosTcpStream.h` | System setup code on FreeRTOS targets using FreeRTOS-Plus-TCP for TCP | `SolidSyslogFreeRtosTcpStream_Create(void)`, `_Destroy(base)` (wraps `FreeRTOS_socket` / `FreeRTOS_connect` / `FreeRTOS_send` / `FreeRTOS_recv` / `FreeRTOS_closesocket`; bounded blocking connect via `SO_SNDTIMEO=200ms`, cleared post-connect so Send/Read are non-blocking). Instance struct lives in a library-internal static pool (E11); default pool size 2 to support the future TLS-via-mbedTLS + plain-TCP pair (S08.07). Pool-exhaustion fallback is the shared `SolidSyslogNullStream`. | | `SolidSyslogUdpSender.h` | System setup code using UDP transport | `SolidSyslogUdpSenderConfig` (resolver, datagram, endpoint, endpointVersion), `SolidSyslogUdpSender_Create(config)`, `_Destroy(sender)`. Instance struct lives in a library-internal static pool (E11); `_Destroy` takes the handle returned by `_Create`. Pool-exhaustion and bad-config fallback is the shared `SolidSyslogNullSender`. | | `SolidSyslogPosixTcpStream.h` | System setup code on POSIX targets using non-blocking TCP with bounded connect/keepalive | `SolidSyslogPosixTcpStream_Create(void)`, `_Destroy(base)`. Instance struct lives in a library-internal static pool (E11); default pool size 2 to support the BDD-target plain-TCP + TLS-underlying-TCP pair. Pool-exhaustion fallback is the shared `SolidSyslogNullStream`. | -| `SolidSyslogStreamSender.h` | System setup code using octet-framed transport (RFC 6587) over any Stream — `SolidSyslogPosixTcpStream` (POSIX TCP), `SolidSyslogWinsockTcpStream` (Windows TCP), `SolidSyslogFreeRtosTcpStream` (FreeRTOS-Plus-TCP), `SolidSyslogTlsStream` (TLS; OpenSSL reference integration), or a caller-supplied Stream backend | `SolidSyslogStreamSenderConfig` (resolver, stream, endpoint, endpointVersion), `SolidSyslogStreamSender_Create(config)`, `_Destroy(base)`. Instance struct lives in a library-internal static pool (E11); `_Destroy` takes the handle returned by `_Create`. Pool exhaustion resolves to the shared `SolidSyslogNullSender`. | +| `SolidSyslogStreamSender.h` | System setup code using octet-framed transport (RFC 6587) over any Stream — `SolidSyslogPosixTcpStream` (POSIX TCP), `SolidSyslogWinsockTcpStream` (Windows TCP), `SolidSyslogFreeRtosTcpStream` (FreeRTOS-Plus-TCP), `SolidSyslogTlsStream` (TLS; OpenSSL reference integration), `SolidSyslogMbedTlsStream` (TLS; Mbed TLS reference integration for embedded targets), or a caller-supplied Stream backend | `SolidSyslogStreamSenderConfig` (resolver, stream, endpoint, endpointVersion), `SolidSyslogStreamSender_Create(config)`, `_Destroy(base)`. Instance struct lives in a library-internal static pool (E11); `_Destroy` takes the handle returned by `_Create`. Pool exhaustion resolves to the shared `SolidSyslogNullSender`. | | `SolidSyslogTlsStream.h` | System setup code using TLS over an injected `SolidSyslogStream` transport (OpenSSL reference integration) | `SolidSyslogTlsStreamConfig` (transport, sleep, caBundlePath, serverName, cipherList, clientCertChainPath, clientKeyPath), `SolidSyslogTlsStream_Create(config)`, `_Destroy(base)`. Instance struct lives in a library-internal static pool (E11). Pool-exhaustion fallback is the shared `SolidSyslogNullStream`. | +| `SolidSyslogMbedTlsStream.h` | System setup code using TLS over an injected `SolidSyslogStream` transport via Mbed TLS (embedded-target reference integration; pair with `SolidSyslogFreeRtosTcpStream`, `SolidSyslogPosixTcpStream`, `SolidSyslogWinsockTcpStream`, or a caller-supplied byte transport) | `SolidSyslogMbedTlsStreamConfig` (transport, sleep, rng, caChain, serverName, clientCertChain, clientKey — caller-built `mbedtls_ctr_drbg_context*` / `mbedtls_x509_crt*` / `mbedtls_pk_context*` handles, never file paths or PEM blobs), `SolidSyslogMbedTlsStream_Create(config)`, `_Destroy(base)`. Instance struct lives in a library-internal static pool (E11); coexistence contract — `Platform/MbedTls/Source/` never calls process-global Mbed TLS APIs (`mbedtls_platform_setup` / `_teardown`, `psa_crypto_init`, threading-alt, debug hooks). Pool-exhaustion fallback is the shared `SolidSyslogNullStream`. See [`docs/integrating-mbedtls.md`](docs/integrating-mbedtls.md) for the full integrator guide. | | `SolidSyslogSwitchingSender.h` | System setup code composing multiple inner senders | `SolidSyslogSwitchingSenderConfig` (senders, senderCount, selector), `SolidSyslogSwitchingSenderSelector`, `SolidSyslogSwitchingSender_Create(config)`, `_Destroy(sender)`. Instance struct lives in a library-internal static pool (E11); `_Destroy` takes the handle returned by `_Create`. Out-of-range selector and pool exhaustion both resolve to the shared `SolidSyslogNullSender`. | | `SolidSyslogBuffer.h` | Library internals consuming a buffer (Service algorithm) | `SolidSyslogBuffer_Write`, `_Read` | | `SolidSyslogNullBuffer.h` | Any code installing a no-op buffer slot (Read returns `false`/`bytesRead=0`; Write swallows the record) | `SolidSyslogNullBuffer_Get` | diff --git a/docs/iec62443.md b/docs/iec62443.md index 620771f2..e4beefaf 100644 --- a/docs/iec62443.md +++ b/docs/iec62443.md @@ -88,6 +88,52 @@ with the SL4 controls that depend on the client: `BIO_METHOD` idempotently — no leaks on partial Open failure, no double frees if both are called. +### Embedded SL4: `SolidSyslogMbedTlsStream` + +`SolidSyslogMbedTlsStream` is the Mbed TLS-backed reference adapter for +embedded / FreeRTOS / bare-metal SL4 deployments where OpenSSL is too +heavy. It satisfies the same CR 1.5 / CR 1.8 / CR 2.12 / CR 3.9 controls +as `SolidSyslogTlsStream` and surfaces the same `SolidSyslogStream` +vtable, so the rest of the wiring (`SolidSyslogStreamSender`, buffer, +store) is unchanged. + +- **Hostname verification.** `SolidSyslogMbedTlsStreamConfig.ServerName` + is forwarded to both SNI (`mbedtls_ssl_set_hostname`) and the cert SAN + check (mbedTLS verifies the peer cert against the same string by + default with `MBEDTLS_SSL_VERIFY_REQUIRED`). +- **Trust chain.** `CaChain` is a caller-built `mbedtls_x509_crt*` + passed through dependency injection — there is no filesystem coupling + inside the adapter (`MBEDTLS_FS_IO` is typically off on embedded + targets). `mbedtls_ssl_conf_authmode` is pinned to + `MBEDTLS_SSL_VERIFY_REQUIRED`. +- **TLS 1.2+ floor.** Inherited from `mbedtls_ssl_config_defaults(... + PRESET_DEFAULT)`. TLS 1.3 negotiates automatically when both peers + support it. +- **Mutual TLS (non-repudiation, CR 2.12).** Optional `ClientCertChain` + / `ClientKey` handles. Both NULL = server-auth-only. Both non-NULL = + mTLS. Supplying only one is treated as "no client cert" — no silent + half-configuration. +- **Certificate rotation (CR 1.5 / CR 1.8).** Because the adapter + consumes pre-built handles rather than file paths, rotation is "parse + a new `mbedtls_x509_crt`, destroy and recreate the adapter (or the + parent `SolidSyslogStreamSender` so the next Connect picks it up)." A + deployment running a vendor key-rotation service drives this via its + existing reload hook. +- **Coexistence with an existing Mbed TLS integration.** Auditable + contract: `Platform/MbedTls/Source/` never calls + `mbedtls_platform_setup` / `_teardown`, never installs threading-alt + hooks, never calls `psa_crypto_init` itself, never resets the global + RNG, never replaces the integrator's debug callback. Devices that + already wire Mbed TLS for another subsystem (cloud, OTA, vendor + framework) keep that wiring intact. +- **Resource lifecycle.** `Close` / `Destroy` invoke + `mbedtls_ssl_close_notify` / `_ssl_free` / `_ssl_config_free` + idempotently and never free integrator-owned handles. + +Integrator guide: [`docs/integrating-mbedtls.md`](integrating-mbedtls.md). +Reference wiring: the FreeRTOS QEMU mps2-an385 BDD target at +[`Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c`](../Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c). + ## Architecture for Security SolidSyslog's architecture directly supports IEC 62443-4-1 (secure development @@ -201,6 +247,7 @@ above for the canonical CR-to-component mapping. ### SL4 — adds cryptographic identity and audit information protection - All SL3 components. -- `SolidSyslogTlsStream` instead of plain TCP, configured with `caBundlePath`, `serverName`, and (for mutual-auth deployments) `clientCertChainPath` / `clientKeyPath` (CR 1.5, CR 1.8, CR 3.9 in transit). -- A `cipherList` appropriate to the deployment's threat model — e.g. `"ECDHE+AESGCM:ECDHE+CHACHA20"` for TLS 1.2 AEAD with forward secrecy. +- `SolidSyslogTlsStream` (OpenSSL; POSIX / Windows) instead of plain TCP, configured with `caBundlePath`, `serverName`, and (for mutual-auth deployments) `clientCertChainPath` / `clientKeyPath` (CR 1.5, CR 1.8, CR 3.9 in transit). +- Or, on embedded / FreeRTOS / bare-metal targets where OpenSSL is impractical, `SolidSyslogMbedTlsStream` (Mbed TLS) — same controls, dependency-injected handles rather than file paths. See [Embedded SL4](#embedded-sl4-solidsyslogmbedtlsstream) above and [`docs/integrating-mbedtls.md`](integrating-mbedtls.md) for the integrator guide. +- A `cipherList` appropriate to the deployment's threat model — e.g. `"ECDHE+AESGCM:ECDHE+CHACHA20"` for TLS 1.2 AEAD with forward secrecy. On the Mbed TLS adapter the policy is owned by your Mbed TLS config; on the OpenSSL adapter it is owned by `SolidSyslogTlsStreamConfig`. - *Planned* — cryptographic at-rest integrity policy ([E17 #105](https://github.com/DavidCozens/solid-syslog/issues/105)) closes the remaining at-rest gap on CR 2.12 / CR 3.9. diff --git a/docs/rfc-compliance.md b/docs/rfc-compliance.md index 28b55dd1..b1963c66 100644 --- a/docs/rfc-compliance.md +++ b/docs/rfc-compliance.md @@ -58,15 +58,17 @@ Status key: ## RFC 5425 — TLS Transport Mapping for Syslog +The library ships two reference TLS adapters that satisfy this RFC: `SolidSyslogTlsStream` (OpenSSL, the POSIX / Windows reference) and `SolidSyslogMbedTlsStream` (Mbed TLS, the embedded / FreeRTOS reference). Both implement the same `SolidSyslogStream` vtable, so the section-by-section requirements below apply to whichever the integrator selects. mbedTLS-specific integration guidance lives in [`docs/integrating-mbedtls.md`](integrating-mbedtls.md). + | Section | Requirement | Status | Notes | |---|---|---|---| -| 4.1 | TLS over TCP | Supported | `SolidSyslogTlsStream` wraps a TCP `Stream` (typically `SolidSyslogPosixTcpStream`) | +| 4.1 | TLS over TCP | Supported | `SolidSyslogTlsStream` (OpenSSL) or `SolidSyslogMbedTlsStream` (Mbed TLS) wraps a TCP `Stream` (`SolidSyslogPosixTcpStream` / `SolidSyslogWinsockTcpStream` / `SolidSyslogFreeRtosTcpStream` / caller-supplied) | | 4.2 | Default port 6514 | Supported | `SOLIDSYSLOG_TLS_DEFAULT_PORT` constant in `SolidSyslogTransport.h`, alongside the UDP and TCP defaults. Caller-supplied via the endpoint callback so multi-port deployments can override | -| 5.1 | Server certificate validation | Supported | `SSL_VERIFY_PEER` + `SSL_CTX_load_verify_locations` + `SSL_set1_host` hostname check | -| 5.2 | Mutual TLS (client certificate) | Supported | Optional `clientCertChainPath` / `clientKeyPath` on `SolidSyslogTlsStreamConfig`; `SSL_CTX_check_private_key` confirms pairing | -| 5.3 | TLS 1.2+ cipher suites | Supported | `SSL_CTX_set_min_proto_version(TLS1_2_VERSION)` pinned; caller-supplied `cipherList` via `SSL_CTX_set_cipher_list` | -| 5.4 | Octet counting framing (mandatory for TLS) | Supported | Reuses `SolidSyslogStreamSender` (RFC 6587 framing is identical) | -| 5.5 | TLS close_notify handling | Supported | `SSL_shutdown` in `TlsStream_Close` | +| 5.1 | Server certificate validation | Supported | OpenSSL adapter: `SSL_VERIFY_PEER` + `SSL_CTX_load_verify_locations` + `SSL_set1_host`. Mbed TLS adapter: `MBEDTLS_SSL_VERIFY_REQUIRED` + `mbedtls_ssl_conf_ca_chain` against the caller's `mbedtls_x509_crt*` + `mbedtls_ssl_set_hostname` | +| 5.2 | Mutual TLS (client certificate) | Supported | OpenSSL: optional `clientCertChainPath` / `clientKeyPath` on `SolidSyslogTlsStreamConfig`; `SSL_CTX_check_private_key` confirms pairing. Mbed TLS: optional `ClientCertChain` / `ClientKey` handles on `SolidSyslogMbedTlsStreamConfig`; `mbedtls_ssl_conf_own_cert` | +| 5.3 | TLS 1.2+ cipher suites | Supported | OpenSSL: `SSL_CTX_set_min_proto_version(TLS1_2_VERSION)` pinned; caller-supplied `cipherList` via `SSL_CTX_set_cipher_list`. Mbed TLS: inherited from `mbedtls_ssl_config_defaults(... PRESET_DEFAULT)`; cipher policy owned by the integrator's `mbedtls_config.h` | +| 5.4 | Octet counting framing (mandatory for TLS) | Supported | Reuses `SolidSyslogStreamSender` (RFC 6587 framing is identical) for both adapters | +| 5.5 | TLS close_notify handling | Supported | OpenSSL: `SSL_shutdown` in `TlsStream_Close`. Mbed TLS: `mbedtls_ssl_close_notify` in `MbedTlsStream_Close` | ## Summary From ef874667639c65d751626a7ef8c4beb11f40fbc1 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Fri, 22 May 2026 00:00:42 +0100 Subject: [PATCH 12/15] =?UTF-8?q?docs:=20DEVLOG=20=E2=80=94=20S08.07=20sli?= =?UTF-8?q?ce=206=20+=207=20FreeRTOS=20TLS=20via=20mbedTLS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Long-form narrative of the slice-6c bring-up: three-layer mbedTLS init diagnosis (heap routing → PSA / TLS-1.3 → STRONG-entropy label), dual-mode TLS/mTLS dispatcher, capacity_threshold marker over UART, the CodeRabbit-driven discovery that ctr_drbg_seed had been failing silently (producing deterministic-but-consistent pseudo-random for the client-side handshake nonce — a happy accident that masked the real bug), and the slice-7 documentation sweep that registers SolidSyslogMbedTlsStream across CLAUDE.md, iec62443.md, rfc-compliance.md, and the new integrating-mbedtls.md. --- DEVLOG.md | 220 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) diff --git a/DEVLOG.md b/DEVLOG.md index 2388970a..a94354ef 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -1,5 +1,225 @@ # Dev Log +## 2026-05-21 — S08.07 slice 6 + 7: FreeRTOS TLS via mbedTLS + +Closes S08.07 (#272). Lights up the TLS slot on the FreeRTOS QEMU BDD +target (mps2-an385) and registers the Mbed TLS adapter across the +documentation set. + +The first two pieces of slice 6 (6a CMake scaffolding, 6b real wrapper) +shipped earlier on the work branch and got CI red on `tls_transport.feature` +and `mtls_transport.feature` — every TLS scenario timed out with +"oracle received 0 of 1 messages within 10 seconds". The diagnosis went +three layers deep, and surfacing each layer required a code change in the +adapter or its integrator wrapper. The story below is the order it +happened in, not the order a clean rewrite would present. + +### Layer 1 — `mbedtls_ssl_setup` returning `MBEDTLS_ERR_SSL_ALLOC_FAILED` + +The CI failure mode was opaque: TCP connect succeeded, the BDD target +reported `Sent 1 message`, but syslog-ng never saw a TLS record. Adding +breadcrumbs into `MbedTlsStream_Open` showed `mbedtls_ssl_setup` returning +`-0x7F00`, i.e. `MBEDTLS_ERR_SSL_ALLOC_FAILED`, before any TLS bytes hit +the BIO callbacks. mbedTLS's per-context buffers — IN/OUT plus handshake +state — were being requested via libc `calloc`, which on this target goes +through newlib's `_sbrk` into the **4 KiB syscall heap** at +[Bdd/Targets/FreeRtos/Common/Syscalls.c](Bdd/Targets/FreeRtos/Common/Syscalls.c). +A single TLS context needs ~16 KiB and can't fit there. + +The standard FreeRTOS + Mbed TLS integration pattern is to route +mbedTLS's allocations to the FreeRTOS heap via +`mbedtls_platform_set_calloc_free`. That requires +`MBEDTLS_PLATFORM_MEMORY` in the integrator's mbedTLS config and a +pvPortMalloc-backed shim with calloc semantics (zero-fill on allocate, +overflow-safe nmemb × size). Both shipped in +[Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c](Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c) +and +[Bdd/Targets/FreeRtos/mbedtls_user_config.h](Bdd/Targets/FreeRtos/mbedtls_user_config.h). +Also shrank `MBEDTLS_SSL_IN_CONTENT_LEN` to 4096 and +`MBEDTLS_SSL_OUT_CONTENT_LEN` to 2048 from the 16 KiB defaults — server +cert + chain fits in 4 KiB comfortably, application messages fit in 2 KiB +comfortably, and the saving keeps the per-context working set under +control across multiple concurrent contexts. + +### Layer 2 — `mbedtls_ssl_handshake` returning `MBEDTLS_ERR_ERROR_GENERIC_ERROR` + +With ssl_setup now succeeding, the next handshake call returned +`-0x0001` at the very first state transition with no BIO `Send` ever +firing. mbedTLS 3.6's TLS 1.3 code path routes through PSA crypto, and +PSA's built-in entropy collector returns +`PSA_ERROR_INSUFFICIENT_ENTROPY` (-148) on `MBEDTLS_NO_PLATFORM_ENTROPY` +targets — which we are, by design, since there is no platform entropy +source on a Cortex-M3 QEMU image. + +Defined `MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG` in the user config and provided +`mbedtls_psa_external_get_random` that wraps the same CTR_DRBG the +classic mbedTLS API uses, so PSA and the classic API share one entropy +chain. Reordered `EnsureMbedTlsInitialised` so `psa_crypto_init()` is +called after `mbedtls_ctr_drbg_seed` — otherwise the first PSA crypto +operation pulls from an unseeded DRBG and the handshake explodes +differently. + +After this, the local QEMU run handshook successfully and the message +landed in syslog-ng's `/var/log/syslog-ng/received_tls.log`. The full +freertos behave suite went 40-pass / 0-fail (the @mtls scenario was +admitted in slice 6c via the dispatcher described below). + +### Slice 6c — dual-mode TLS/mTLS dispatch + +The FreeRTOS BDD target spawns the TLS sender once at boot from +`InteractiveTask`, before the behave harness has had a chance to send +`set transport tls|mtls` over the UART. POSIX and Windows binaries read +the choice from argv before the equivalent setup runs, so on those +platforms the same `BddTargetTlsSender_Create(resolver, mtls)` call gets +the right value. On FreeRTOS the harness UART traffic only arrives after +the prompt, by which point the sender is already wired with whatever +default was passed in. + +Resolved by sharing one TLS sender / one TLS stream / one TCP socket +across both modes and dispatching the destination port at Connect time: + +- `BddTargetSwitchConfig` (in + [Bdd/Targets/Common/](Bdd/Targets/Common/)) gains an `mtlsMode` bool + tracked by `SetByName` and exposed via + `BddTargetSwitchConfig_IsMtlsMode()`. `tls` clears it, `mtls` sets it; + both route through `BDD_TARGET_SWITCH_TLS`. +- The mbedTLS wrapper wires the client cert + key unconditionally (the + oracle's plain-TLS listener accepts an optional client cert; its mTLS + listener requires one — so the same identity works on both ports) and + plumbs `DispatchEndpoint` / `DispatchEndpointVersion` as the + StreamSender Endpoint pair. Those read `IsMtlsMode` at each Connect + call and pick between `BddTargetTlsConfig` (port 6514) and + `BddTargetMtlsConfig` (port 6515). +- `ci/docker-compose.bdd.yml` admits `@mtls` in the freertos behave + filter: `(@udp or @tcp or @tls or @mtls)`. + +The `mtls` parameter on `BddTargetTlsSender_Create` is retained for +cross-platform contract uniformity (POSIX / Windows still read it at +startup) but is ignored on FreeRTOS. + +### Slice 6c — `capacity_threshold.feature` admitted + +Closing the FreeRTOS BDD coverage gap. `capacity_threshold.feature` was +previously gated `@freertoswip` because the host behave step asserts on +`os.path.exists("/tmp/solidsyslog_threshold_marker.log")` (the Linux +binary writes that file from its threshold callback) — and the FreeRTOS +guest has no shared filesystem with the host. + +Wired a UART-based marker instead: FreeRTOS `OnThresholdCrossed` prints a +line-anchored `[THRESHOLD-CROSSED]` token, and a new +`_threshold_marker_present(context)` helper in +[Bdd/features/steps/syslog_steps.py](Bdd/features/steps/syslog_steps.py) +accepts either the host marker file (Linux / Windows binaries unchanged) +or the token in the captured stdout buffer (FreeRTOS). The behave +captured-stdout reader thread the harness already runs for the prompt +protocol provides the channel for free. Also added a +`set capacity-threshold N` handler to FreeRTOS `OnSet` and a +`--capacity-threshold` entry in the FreeRTOS UART translation table so +the existing harness flag reaches the target. + +After this the freertos suite is 42-pass / 0-fail; the only remaining +`@freertoswip` exclusion is the oversize-UTF-8 MTU scenario, which is +unreachable on this target because `SOLIDSYSLOG_MAX_MESSAGE_SIZE = 512` +keeps every BDD message under the path MTU. Correctly documented now +rather than hand-waved as "no semihosting equivalent." + +### CodeRabbit review — `mbedtls_ctr_drbg_seed` was failing silently all along + +CodeRabbit flagged the five `(void)`-discarded mbedTLS init returns +(ctr_drbg_seed, psa_crypto_init, two x509_crt_parse, pk_parse_key) as +hiding root-cause failures behind later opaque handshake errors. +Replacing the `(void)` casts with rc capture + diagnostic printf + early +return immediately broke every TLS scenario locally: `ctr_drbg_seed` now +returned `MBEDTLS_ERR_CTR_DRBG_ENTROPY_SOURCE_FAILED` (-0x0034) and the +early-return tripped, certs never got parsed, and the handshake had no +state to work with. + +The actual bug: `DemoEntropySource` was registered as +`MBEDTLS_ENTROPY_SOURCE_WEAK`. Reading +[mbedtls/library/entropy.c](https://github.com/Mbed-TLS/mbedtls/blob/v3.6.2/library/entropy.c) +shows the loop in `mbedtls_entropy_func` exits only when +`thresholds_reached && strong_size >= MBEDTLS_ENTROPY_BLOCK_SIZE` — +i.e. **at least one STRONG-tagged source must contribute**. Without +that, the loop runs to `ENTROPY_MAX_LOOP=256` and returns +`ENTROPY_SOURCE_FAILED`. ctr_drbg_seed then returns its DRBG-flavoured +wrapper of the same error. + +Pre-CR-fix, the `(void)` discard hid the error. The partially-seeded +DRBG context still produced deterministic-but-consistent bytes when +`mbedtls_ctr_drbg_random` was called against it (the AES-CTR state had +been written to, just not from validated entropy), and syslog-ng's +server-side RNG drives its half of the handshake from real entropy. So +the handshake completed and the message arrived — appearing to "work" +while actually using nothing-like-random bytes for the client nonce. + +The fix is `MBEDTLS_ENTROPY_SOURCE_STRONG` in the +`mbedtls_entropy_add_source` call. The audit-trail printf at the end of +`EnsureMbedTlsInitialised` ("demo-only entropy ... Not for production") +is the real quality assertion — the STRONG/WEAK label is mbedTLS-side +structural, not a quality claim. A real integrator on bare-metal replaces +`DemoEntropySource` with TRNG / HSM bytes before shipping; the demo +source stays for QEMU smoke runs where no real entropy exists. + +Also addressed the two other CodeRabbit findings: +- `tlsSender` was allocated at boot but never destroyed in + `TeardownAll()`. Added `BddTargetTlsSender_Destroy()` + `tlsSender = + NULL` between the `SwitchingSender_Destroy` and the inner stream + destroys, matching the (approximately reverse) Create order. +- The comment block around `tlsSender = BddTargetTlsSender_Create(...)` + still described slice 6b as "TLS-only, mTLS support is slice 6c work + and may require an extra BDD_TARGET_SWITCH_MTLS slot" — refreshed to + describe the dispatcher that shipped. + +### Slice 7 — documentation sweep + +Four edits across the doc set: + +- **New [`docs/integrating-mbedtls.md`](docs/integrating-mbedtls.md).** + First draft was generic mbedTLS-tutorial flavoured ("here's how to + init mbedTLS") and missed the actual integrator question. Rewritten + around two concrete scenarios: + - *You already have Mbed TLS in your image* — three numbered steps + (pick a `SolidSyslogStream` byte transport, fill in the config + struct with your existing handles, wire into a + `SolidSyslogStreamSender`) plus the auditable coexistence-contract + not-touched list. + - *You do not have Mbed TLS yet* — defers to upstream Mbed TLS + porting docs and then lists the SolidSyslog-specific consumption + checklist (seeded CTR_DRBG with at least one STRONG source, + psa_crypto_init after the seed, parsed CA chain, optional client + cert + key, byte transport). + FreeRTOS-specific gotchas (newlib syscall heap, PSA external RNG, + buffer sizing) moved to a dedicated section labelled as an + integrator checklist rather than baked into the body. +- **[`CLAUDE.md`](CLAUDE.md).** New row for + `SolidSyslogMbedTlsStream.h` in the Public-Header-Audiences table, + alongside the existing `SolidSyslogTlsStream.h` row. The + StreamSender row enumerates `SolidSyslogMbedTlsStream` alongside + `SolidSyslogTlsStream` as a TLS-capable Stream backend. +- **[`docs/iec62443.md`](docs/iec62443.md).** New `### Embedded SL4` + subsection mapping `SolidSyslogMbedTlsStream` to the same CR 1.5 / + CR 1.8 / CR 2.12 / CR 3.9 controls the OpenSSL substrate satisfies. + The SL4 setup-recipe at the bottom mentions both adapters as + alternatives. +- **[`docs/rfc-compliance.md`](docs/rfc-compliance.md).** RFC 5425 + section table widens from OpenSSL-only language to per-row "OpenSSL: + … / Mbed TLS: …" coverage, with an intro paragraph noting the two + adapters share the SolidSyslogStream vtable so the requirement + matrix is per-section, not per-adapter. + +### Final state + +| Job | Status | +|---|---| +| bdd-freertos-qemu | green (42 scenarios pass / 0 fail / 7 skipped — `@freertoswip` udp_mtu only) | +| integration-linux-mbedtls | green | +| bdd-linux-syslog-ng | green | +| All other jobs | green | + +PR #421 closes S08.07 (#272) and reaches the acceptance criteria laid out +in the issue body (TLS / mTLS / tcp_reconnect on FreeRTOS, doc surface +registered). + ## 2026-05-20 — S11.11: Honest MISRA suppressions + E11 close-out Closes S11.11 (#414) and **closes E11** (#29). From 4b0f67d261a6bb9cae0442fba4e5260947375165 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Fri, 22 May 2026 06:16:47 +0100 Subject: [PATCH 13/15] style: clang-format BddTargetTlsSender_MbedTls_FreeRtosTcp.c MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reflow the printf arg lists and ctr_drbg_seed call to match clang-format's preferred layout — picked up by analyze-format on the prior push. No behaviour change. --- .../BddTargetTlsSender_MbedTls_FreeRtosTcp.c | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c b/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c index 9bc6ca4a..cb0dacce 100644 --- a/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c +++ b/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c @@ -219,13 +219,12 @@ static void EnsureMbedTlsInitialised(void) mbedtls_ctr_drbg_init(&drbg); static const unsigned char personalization[] = "solidsyslog-freertos-bdd"; - int drbgSeedRc = mbedtls_ctr_drbg_seed( - &drbg, mbedtls_entropy_func, &entropy, personalization, sizeof(personalization) - 1U - ); + int drbgSeedRc = + mbedtls_ctr_drbg_seed(&drbg, mbedtls_entropy_func, &entropy, personalization, sizeof(personalization) - 1U); if (drbgSeedRc != 0) { - (void) printf("[mbedtls] ctr_drbg_seed FAILED rc=-0x%04x; TLS slot will be unusable\r\n", - (unsigned) -drbgSeedRc); + (void + ) printf("[mbedtls] ctr_drbg_seed FAILED rc=-0x%04x; TLS slot will be unusable\r\n", (unsigned) -drbgSeedRc); return; } vTaskDelay(1U); @@ -253,8 +252,8 @@ static void EnsureMbedTlsInitialised(void) int caParseRc = mbedtls_x509_crt_parse(&caChain, caPemBuf, sizeof(caPemBuf)); if (caParseRc != 0) { - (void) printf("[mbedtls] CA chain parse FAILED rc=-0x%04x; TLS slot will be unusable\r\n", - (unsigned) -caParseRc); + (void + ) printf("[mbedtls] CA chain parse FAILED rc=-0x%04x; TLS slot will be unusable\r\n", (unsigned) -caParseRc); return; } vTaskDelay(1U); @@ -264,12 +263,13 @@ static void EnsureMbedTlsInitialised(void) memcpy(clientCertPemBuf, bdd_baked_client_cert_pem, sizeof(bdd_baked_client_cert_pem)); clientCertPemBuf[sizeof(bdd_baked_client_cert_pem)] = '\0'; mbedtls_x509_crt_init(&clientCertChain); - int clientCertParseRc = - mbedtls_x509_crt_parse(&clientCertChain, clientCertPemBuf, sizeof(clientCertPemBuf)); + int clientCertParseRc = mbedtls_x509_crt_parse(&clientCertChain, clientCertPemBuf, sizeof(clientCertPemBuf)); if (clientCertParseRc != 0) { - (void) printf("[mbedtls] client cert parse FAILED rc=-0x%04x; mTLS will be unusable\r\n", - (unsigned) -clientCertParseRc); + (void) printf( + "[mbedtls] client cert parse FAILED rc=-0x%04x; mTLS will be unusable\r\n", + (unsigned) -clientCertParseRc + ); return; } vTaskDelay(1U); @@ -290,8 +290,10 @@ static void EnsureMbedTlsInitialised(void) ); if (clientKeyParseRc != 0) { - (void) printf("[mbedtls] client key parse FAILED rc=-0x%04x; mTLS will be unusable\r\n", - (unsigned) -clientKeyParseRc); + (void) printf( + "[mbedtls] client key parse FAILED rc=-0x%04x; mTLS will be unusable\r\n", + (unsigned) -clientKeyParseRc + ); return; } vTaskDelay(1U); From 37d1ebb35cdf8dc56025b0a9673dd94083bb1dbd Mon Sep 17 00:00:00 2001 From: David Cozens Date: Fri, 22 May 2026 07:01:33 +0100 Subject: [PATCH 14/15] fix: S08.07 short-circuit BddTargetTlsSender_Create on mbedTLS init failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit's follow-up review on the slice-6c CR-fix push pointed out that EnsureMbedTlsInitialised's new early-return branches log and bail but BddTargetTlsSender_Create still builds a TLS sender around whatever partial state was left in drbg / certs / key — pushing the failure into later opaque handshake / send errors rather than disabling the TLS slot cleanly at the point of init failure. Closes that loop: - BddTargetTlsSender_Create checks mbedTlsInitialised after EnsureMbedTlsInitialised. On failure it returns the shared SolidSyslogNullSender, matching the bad-setup-contract pattern used elsewhere in the library — the SwitchingSender's tls slot drops messages cleanly and the file-scope sender / tlsStream / underlyingStream statics stay NULL. - BddTargetTlsSender_Destroy now short-circuits when sender is NULL, so the early-return-from-Create path doesn't try to release pool slots that were never acquired. No behaviour change on the happy path: 42/42 freertos BDD scenarios still pass locally. Refs CodeRabbit comment 3286071197 on PR #421. --- .../BddTargetTlsSender_MbedTls_FreeRtosTcp.c | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c b/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c index cb0dacce..4564b569 100644 --- a/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c +++ b/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c @@ -27,6 +27,7 @@ #include "BddTargetTlsConfig.h" #include "SolidSyslogFreeRtosTcpStream.h" #include "SolidSyslogMbedTlsStream.h" +#include "SolidSyslogNullSender.h" #include "SolidSyslogStream.h" #include "SolidSyslogStreamSender.h" @@ -347,6 +348,17 @@ struct SolidSyslogSender* BddTargetTlsSender_Create(struct SolidSyslogResolver* (void) mtls; EnsureMbedTlsInitialised(); + if (!mbedTlsInitialised) + { + /* EnsureMbedTlsInitialised already printed a [mbedtls] ... FAILED + * diagnostic explaining which step tripped. Returning the shared + * NullSender here keeps the bad-setup contract intact — the + * SwitchingSender's tls slot drops messages cleanly rather than + * failing opaquely later inside MbedTlsStream_Open, and the + * statics below stay NULL so BddTargetTlsSender_Destroy can + * detect the short-circuit. */ + return SolidSyslogNullSender_Get(); + } underlyingStream = SolidSyslogFreeRtosTcpStream_Create(); @@ -377,6 +389,14 @@ struct SolidSyslogSender* BddTargetTlsSender_Create(struct SolidSyslogResolver* void BddTargetTlsSender_Destroy(void) { + /* If EnsureMbedTlsInitialised failed, Create short-circuited to the + * shared NullSender and never assigned the file-scope statics — there + * is nothing to release. The pool-backed Destroy helpers tolerate + * a NULL handle but skipping makes the no-op explicit. */ + if (sender == NULL) + { + return; + } SolidSyslogStreamSender_Destroy(sender); SolidSyslogMbedTlsStream_Destroy(tlsStream); SolidSyslogFreeRtosTcpStream_Destroy(underlyingStream); From b8e311317bb45c99ac0a5749c12460d6a4c1ddcd Mon Sep 17 00:00:00 2001 From: David Cozens Date: Fri, 22 May 2026 07:01:42 +0100 Subject: [PATCH 15/15] chore: S08.07 address CodeRabbit nitpicks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three small CodeRabbit nitpicks on PR #421: - ci/docker-compose.bdd.yml — drop the ambiguous "(this commit)" phrasing; "S08.07 slice 6c" survives squash-merge correctly. - Bdd/Targets/FreeRtos/main.c — move OnStoreFull, GetCapacityThreshold, and OnThresholdCrossed below RebuildWithFileStore, their first caller. Matches the file-ordering rule in CLAUDE.md ("Helper functions are forward-declared at the top of the file ... and defined immediately beneath the function that first calls them"). The forward declarations near the top of the file are unchanged. - docs/integrating-mbedtls.md — add `text` language specifier to the pipeline-shape ASCII diagram's fenced code block so markdownlint stops warning about the missing tag. No code behaviour change. --- Bdd/Targets/FreeRtos/main.c | 62 ++++++++++++++++++------------------- ci/docker-compose.bdd.yml | 8 ++--- docs/integrating-mbedtls.md | 2 +- 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/Bdd/Targets/FreeRtos/main.c b/Bdd/Targets/FreeRtos/main.c index 8eb9ff0d..c0425974 100644 --- a/Bdd/Targets/FreeRtos/main.c +++ b/Bdd/Targets/FreeRtos/main.c @@ -549,37 +549,6 @@ static enum SolidSyslogDiscardPolicy MapDiscardPolicy(const char* policy) return SOLIDSYSLOG_DISCARD_POLICY_OLDEST; } -static void OnStoreFull(void* context) -{ - (void) context; - if (pendingHaltExit) - { - /* Semihosting SYS_EXIT — terminates QEMU with the given status so - * the BDD harness sees the run end deterministically. Mirrors the - * Linux example's _exit(2) (Bdd/Targets/Linux/main.c::OnStoreFull). */ - SemihostingExit(2); - } -} - -static size_t GetCapacityThreshold(void* context) -{ - return *(const size_t*) context; -} - -/* Stdout-based marker the behave harness watches for. Linux's equivalent - * (Bdd/Targets/Linux/main.c::OnThresholdCrossed) writes a host file at - * /tmp/solidsyslog_threshold_marker.log — that file path isn't reachable - * from the QEMU guest. Instead we print a known token to the UART, which - * the captured-stdout reader in Bdd/features/steps/syslog_steps.py buffers - * and the threshold step then scans. The token is line-anchored so a stray - * substring inside a longer message body could not accidentally trip the - * assertion. */ -static void OnThresholdCrossed(void* context) -{ - (void) context; - (void) printf("[THRESHOLD-CROSSED]\r\n"); -} - static bool RebuildWithFileStore(void) { /* Lifecycle mutex blocks the Service task from running SolidSyslog_Service @@ -637,6 +606,37 @@ static bool RebuildWithFileStore(void) return true; } +static void OnStoreFull(void* context) +{ + (void) context; + if (pendingHaltExit) + { + /* Semihosting SYS_EXIT — terminates QEMU with the given status so + * the BDD harness sees the run end deterministically. Mirrors the + * Linux example's _exit(2) (Bdd/Targets/Linux/main.c::OnStoreFull). */ + SemihostingExit(2); + } +} + +static size_t GetCapacityThreshold(void* context) +{ + return *(const size_t*) context; +} + +/* Stdout-based marker the behave harness watches for. Linux's equivalent + * (Bdd/Targets/Linux/main.c::OnThresholdCrossed) writes a host file at + * /tmp/solidsyslog_threshold_marker.log — that file path isn't reachable + * from the QEMU guest. Instead we print a known token to the UART, which + * the captured-stdout reader in Bdd/features/steps/syslog_steps.py buffers + * and the threshold step then scans. The token is line-anchored so a stray + * substring inside a longer message body could not accidentally trip the + * assertion. */ +static void OnThresholdCrossed(void* context) +{ + (void) context; + (void) printf("[THRESHOLD-CROSSED]\r\n"); +} + /* Tears down whichever store is currently installed (file-backed or null). * Shared by RebuildWithFileStore (which then re-creates) and * ShutdownGracefully (which then exits). FatFsFile_Destroy → Close → diff --git a/ci/docker-compose.bdd.yml b/ci/docker-compose.bdd.yml index 004ec4a2..9bca9e28 100644 --- a/ci/docker-compose.bdd.yml +++ b/ci/docker-compose.bdd.yml @@ -111,10 +111,10 @@ services: # threshold-marker plumbing has a semihosting equivalent. # S08.11 tags the non-@tls switching_transport scenario @udp @tcp so the # existing (@udp or @tcp) clause admits it. S08.07 slice 6b lights up - # TLS on FreeRTOS, and slice 6c (this commit) lights up mTLS via the - # dual-port dispatcher on the same Switching slot — admit both @tls - # and @mtls now. Slice 6c also wires the capacity_threshold marker - # over UART so capacity_threshold.feature runs here unmodified — the + # TLS on FreeRTOS, and S08.07 slice 6c lights up mTLS via the dual-port + # dispatcher on the same Switching slot — admit both @tls and @mtls + # now. S08.07 slice 6c also wires the capacity_threshold marker over + # UART so capacity_threshold.feature runs here unmodified — the # @freertoswip exclusion that used to gate it has been dropped. # @windows_wip excludes the Windows-only TCP singletask variant. # Tunable-driven gates like @requires_message_size_1500 are not listed diff --git a/docs/integrating-mbedtls.md b/docs/integrating-mbedtls.md index 45f3b9fa..d18449c7 100644 --- a/docs/integrating-mbedtls.md +++ b/docs/integrating-mbedtls.md @@ -16,7 +16,7 @@ re-teach mbedTLS — for that, see the ## The shape -``` +```text SolidSyslog_Log ─▶ Buffer ─▶ Sender ─▶ SolidSyslogStreamSender │ ▼