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/.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/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 new file mode 100644 index 00000000..4564b569 --- /dev/null +++ b/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_FreeRtosTcp.c @@ -0,0 +1,407 @@ +/* MbedTLS-over-FreeRtosTcpStream BDD TLS sender (slice 6b). + * + * Composes: + * - SolidSyslogFreeRtosTcpStream inner TCP transport + * - SolidSyslogMbedTlsStream TLS over the injected Stream + * - SolidSyslogStreamSender RFC 6587 octet-counting framing + * + * 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. + * + * 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 "BddTargetMtlsConfig.h" +#include "BddTargetSwitchConfig.h" +#include "BddTargetTlsConfig.h" +#include "SolidSyslogFreeRtosTcpStream.h" +#include "SolidSyslogMbedTlsStream.h" +#include "SolidSyslogNullSender.h" +#include "SolidSyslogStream.h" +#include "SolidSyslogStreamSender.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include "BddBakedCaPem.h" +#include "BddBakedClientCertPem.h" +#include "BddBakedClientKeyPem.h" + +struct SolidSyslogResolver; + +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; + +/* 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 + * "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; + 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 RtosSleep(int milliseconds) +{ + /* 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); +} + +/* 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); + + /* 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); + /* 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_STRONG + ); + + 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); + 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() + * 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. */ + 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); + 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"); + + 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)); + 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"); + + 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); + int clientKeyParseRc = mbedtls_pk_parse_key( + &clientKey, + clientKeyPemBuf, + sizeof(clientKeyPemBuf), + NULL, + 0U, + 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. 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"); + + 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(); + 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(); + + static struct SolidSyslogMbedTlsStreamConfig tlsStreamConfig; + tlsStreamConfig = (struct SolidSyslogMbedTlsStreamConfig) {0}; + tlsStreamConfig.Transport = underlyingStream; + tlsStreamConfig.Sleep = RtosSleep; + tlsStreamConfig.Rng = &drbg; + tlsStreamConfig.CaChain = &caChain; + /* 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 = DispatchEndpoint; + senderConfig.EndpointVersion = DispatchEndpointVersion; + sender = SolidSyslogStreamSender_Create(&senderConfig); + + return sender; +} + +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); + + /* 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 45faf05c..a9282429 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,85 @@ 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() + +# --- 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 @@ -185,10 +271,16 @@ 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/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} $ ) @@ -210,7 +302,10 @@ 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 + ${BAKED_PEMS_DIR} # BddBaked*.h (xxd -i output) ${FREERTOS_KERNEL_PATH}/include ${FREERTOS_PORT_DIR} # portmacro.h ${PLUS_TCP_SRC_DIR}/include @@ -226,6 +321,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/main.c b/Bdd/Targets/FreeRtos/main.c index 267ea5e9..c0425974 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; @@ -263,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) @@ -457,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 @@ -536,23 +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; -} - static bool RebuildWithFileStore(void) { /* Lifecycle mutex blocks the Service task from running SolidSyslog_Service @@ -593,7 +589,7 @@ static bool RebuildWithFileStore(void) .OnStoreFull = OnStoreFull, .StoreFullContext = NULL, .GetCapacityThreshold = GetCapacityThreshold, - .OnThresholdCrossed = NULL, + .OnThresholdCrossed = OnThresholdCrossed, .ThresholdContext = &pendingCapacityThreshold, }; currentStore = SolidSyslogBlockStore_Create(&storeConfig); @@ -610,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 → @@ -665,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); @@ -793,16 +827,29 @@ 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. 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|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; + 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"); diff --git a/Bdd/Targets/FreeRtos/mbedtls_user_config.h b/Bdd/Targets/FreeRtos/mbedtls_user_config.h new file mode 100644 index 00000000..7cd2029c --- /dev/null +++ b/Bdd/Targets/FreeRtos/mbedtls_user_config.h @@ -0,0 +1,99 @@ +/* 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 + +/* 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 */ 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 e7143677..54d6fd98 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) @@ -613,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/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/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). diff --git a/ci/docker-compose.bdd.yml b/ci/docker-compose.bdd.yml index c0235964..9bca9e28 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 @@ -110,14 +110,18 @@ 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, 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 # 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 or @mtls)' Bdd/features/ volumes: bdd-output-linux: 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). | 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/integrating-mbedtls.md b/docs/integrating-mbedtls.md new file mode 100644 index 00000000..d18449c7 --- /dev/null +++ b/docs/integrating-mbedtls.md @@ -0,0 +1,216 @@ +# 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 + +```text +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` | 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 + 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); + ``` +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 + +`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. 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